OS의 메모리 할당과 이벤트 루프, 콜스택, 태스크큐

Hansu Kim·2021년 12월 30일
0

개발 Must-know

목록 보기
4/9

프로세스 실행을 위해서는 OS가 프로그램의 정보를 메모리에 로드해야 한다.

OS의 메모리 할당

프로그램 실행시 OS의 동작

프로세스 실행시, 위 이미지와 같이 RAM에는 코드, 데이터, 힙, 스택 영역이 할당되며 CPU가 메모리의 영역들을 오가며 프로세스의 작업을 수행하게 된다.

단순한 코더가 아닌 개발자라면, 메모리의 어느 영역에 어떤 데이터가 저장되는지, 본인이 다루는 언어의 가비지 컬렉팅 동작이 어떻게 이루어지는지, 메모리풀은 쓰레드별인지 공통 풀인지 등 코드로부터 이루어지는 IO에 대해 반드시 숙지하고 있어야 한다.

본 포스트에서는 OS 입장에서 프로그램 실행시 프로세스가 메모리를 어떻게 할당하는지에 대해 작성됐다.

RAM의 메모리 할당

RAM은 크게 4가지 영역으로 나뉜다.

1. 코드 영역

말 그대로 실행할 프로그램의 코드가 저장되는 곳이다.
프로그램 시작부터 종료까지 메모리에서 유지된다.

2. 데이터 영역

전역 변수와 정적 변수가 저장되는 곳이다.
프로그램 시작부터 종료까지 메모리에서 유지된다.

3. 힙 영역

동적 할당, 해제가 이루어지는 공간이다.
힙은 Queue 구조로, FIFO 방식이다.
메모리의 낮은 주소부터 할당된다.
스택 영역과 메모리 풀을 공유한다.

4. 스택 영역

매개 변수와 지역 변수가 저장되는 곳이다.
함수 호출이 완료되면 사라진다.
메모리의 높은 주소부터 할당된다.
스택은 말 그대로 Stack 구조로, LIFO 방식이다.
(런타임에서 함수 내에서 다른 함수들이 호출되는 것을 상상하면 이해하기 쉽다.)
힙 영역과 메모리 풀을 공유한다.

힙과 스택의 메모리풀 공유

힙은 낮은 주소부터, 스택은 높은 주소부터 메모리를 할당하며 메모리 풀을 공유한다.
어느 한쪽의 공간이 부족하면 오버플로우가 발생하게 되며
힙이 스택을 침범하면 힙 오버 플로우, 스택이 힙을 침범하면 스택 오버플로우라 칭한다.

쓰레드와 이벤트 루프

프로세스와 쓰레드


운영체제가 생성하는 단위를 process라고 한다.
이 process 안에서 공유되는 메모리를 바탕으로 또 여러 작업을 생성할 수 있는데, 이 때의 작업 단위를 Thread라고 한다.

따라서 각 thread 마다 할당된 개인적인 메모리가 있으면서, thread가 속한 process가 가지는 메모리에도 접근할 수 있다.

일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다.

파이썬은 기본적으로 단일 쓰레드로 실행되며
Java는 멀티 쓰레드로 실행된다. 다만 Java의 경우, 쓰레드 생성은 리소스가 많이 필요하기에 JVM에서 Application 구동 시점에 정의한 스레드 개수만큼 미리 OS에 쓰레드 풀을 생성해놓고 개발자가 필요에 따라 해당 쓰레드들을 가져다 쓴다.
Java Spring 기본 방식에서는 쓰레드 내 IO발생시 해당 동작이 끝날 때까지 대기하게 되는데, 해당 상태를 Thread Blocking 상태라고 칭한다.
블로킹 상태의 쓰래드 수가 쓰래드 풀의 허용 개수를 초과하면 가용한 쓰레드가 없어서 서버 장애가 발생하게 된다.

이벤트 루프

이벤트루프는 기본적으로 Javascript의 레퍼런스들을 참고해서 공부했으며, 각 언어별 동작은 다를 수 있습니다.

이벤트 처리 방식은 다음 2가지이다.

  • Event listner + Event handle thread (java의 spring+tomcat?)
  • Event queue + Event loop

이 중 Event loop 방식은 이벤트루프는 Call stack, Task queue, 백그라운드를 오가며 CPU가 할 작업들을 할당해주는 역할을 한다.

Call stack은 현재 CPU가 작업 중인 실행흐름이라고 보면 되고, Task queue는 다음에 할 작업들의 리스트이다.

이벤트 루프는 Call stack이 비면 Task queue에서 할당되어있는 작업을 Call stack에 할당해준다.

예시 코드를 통해 알아보자

def C():
	pass
def B():
	print("B is executing")
def A():
	B()
    print("A is executing")
    
if __name__="__main__":
	A()    #---- 1번
	B()    #---- 2번
   	C()    #---- 3번

프로그램의 실행흐름 번호별로 콜스택과 테스크큐의 상태를 설명하겠다.

1번

런타임 최초의 실행 흐름이므로, 이벤트루프의 콜스택과 테스크큐는 모두 비어있다. 이벤트루프는 A()를 테스크큐에 보낸 후, 콜스택이 비어있는 상태이기에 테스크큐의 A()를 콜스택으로 보내고, CPU는 콜스택의 작업을 수행한다.
그와 동시에, 테스크큐는 다시 비어있는 상태가 되기에 이벤트루프는 B()를 테스크 큐에 올린다.

콜스택에서 실행 중인 A 함수는 내부에서 B 함수가 다시 호출됐기에, 콜스택 A()위에 B()가 쌓이게 되며, B를 수행한 후 A를 수행하고 콜스택은 비어있는 상태가 된다.
(해당 메모리가 스택 구조로 동작하는 이유이기도 하다.)

2번

A() 함수가 콜스택에서 수행완료 됐기에, 이벤트 루프는 테스크 큐에 있는 B()를 콜스택으로 옮기고, CPU는 B()를 수행하며, 이벤트루프는 비어있는 테스크큐에 C()를 할당한다.

3번

B()가 콜스택에서 모두 수행되면, 이벤트루프는 테스크큐의 C()를 콜스택으로 옮기게 되며, 테스크큐에 다음 할당된 작업이 없으므로 CPU가 C()를 모두 완료하면 프로세스는 종료된다.

이벤트루프의 종류

  • Single Thread Event Loop (Python, node.js)

    1. event handle thread가 1개
    2. event loop 구현 간단. 예측 가능한 동작 수행
    3. event 발생 순서에 따라 처리
    4. 작업 수행 시간이 길어질 수 있음
  • Multi Thread Event Loop (Vert.x)

    1. event handle thread 여러 개
    2. event 발생 순서와 작업 수행 순서가 일치하지 않음
    3. multi core CPU 효율적으로 사용
    4. 작업 수행 시간이 상대적으로 짧아짐
    5. Thread 갯수가 지나치게 많아지면 문제 발생
      1. OOM 에러 발생
      2. 과도한 GC 유발
      3. Context switching 비용
        • 하나의 core는 한번에 한 thread만 실행 가능
        • Thread의 상태 Run, Waiting, Ready, Sleep, Blocked 중 Waiting, Sleep, Blocked 상태인 Thread를 골라 실행하는데 이 때, thread가 가지고 있는 stack 정보를 core의 register로 복사하는 비용 발생
      4. thread 경합
        • 여러 thread 간 공유되는 자원이 있을 때, 단일 접근 권한을 얻기 위해서 thread 끼리 경쟁이 일어나며 여기에 CPU 자원 사용

참조URL: https://all-young.tistory.com/17
https://dgkim5360.tistory.com/entry/understanding-the-global-interpreter-lock-of-cpython
https://shortstories.gitbooks.io/studybook/content/c774_bca4_d2b8_baa8_b378.html
https://m.blog.naver.com/4roring/221155283089
너무 좋은 블로그 작성글들

0개의 댓글