프로세스 구조 분해 조립

프로세스의 모든 것 (상태 전이도, PCB, 문맥 교환, 프로세스 구조)

목차


01. 프로세스를 이해해야 하는 이유

프로세스를 이해함으로써 부하 문제나 메모리 누수, 성능 분석 등의 기술적 업무를 수행할 능력을 키울 수 있다.


02. 프로세스란 ? (실행 중인 프로그램보다 정확한 표현)

흔히 프로세스에 대한 정의를 ‘실행 중인 프로그램’이라고 소개한다. 틀린 표현은 아니지만 좀 더 정확한 표현을 하자면 실행 중인 프로그램의 인스턴스(복제본) 이라는 표현이 더 올바르다.
단어 하나만 붙을 뿐이지만 이 표현이 주는 의미는 매우 크다.

예를 들어 카카오톡과 메모장을 생각해 보도록 하자. 두 프로그램 모두 실행 전에는 디스크에 저장되어 있는 파일 상태 일 것이다. 이를 실행하게 되면 메모리로 올라가서 실행하게 된다.
이때 카카오톡의 경우 다중 실행을 할 수 없도록 개발되어 하나만이 실행될 것인데, 이러한 프로그램에는 그냥 ‘실행 중인 프로그램’이라고 정의해도 무방하다.

그러나 우린 좀 더 범용적이고 제대로 된 프로세스의 이해를 위해 메모장을 여러 개 띄워 볼 것이다.

아래 이미지는 메모장을 여러 개 띄우고 ‘작업 관리자’에서 확인한 프로세스 상태이다. 메모장 프로그램을 실행할 때마다 해당 프로그램의 인스턴스가 생성되는 것을 확인할 수 있다.

메모장이 동시에 여러개의 창이 띄워짐
메모장이 동시에 여러개의 창이 띄워짐
프로세스를 확인하여도 메모장은 각자 생성됨
프로세스를 확인하여도 메모장은 각자 생성됨

리눅스에서도 마찬가지로 vi를 여러 개 실행해보자. 여러 개의 vi 프로세스가 각자 고유의 PID를 가지고 생성되었다.

vi가 각자 프로세스로 생성됨
vi가 각자 프로세스로 생성됨

즉, 프로세스란 디스크에 저장된 프로그램 파일(명령 코드와 데이터의 집합)이 실행되어 운영체제에 의해 복제되고 관리되어지는 단위라고 이해할 수 있다. 
그리고 프로세스는 물리 메모리에 적재되고 CPU에 의해 실행되게 될 것이다.

프로그램이 프로세스 형태로 메모리에 바로 적재되어 실행될까 ?

단순히 용량만 생각해도 메모리의 용량은 디스크에 비해 매우 작고 프로그램의 크기를 생각하면 수 많은 프로세스가 어떻게 메모리에 다 들어가나 생각이 들 수 있.

간단히 설명하자면 이는 운영체제의 가상 메모리라는 기능을 이용하여 프로세스를 관리하기 때문이다. 이를 통해 필요한 주소(코드)만을 물리 메모리에 매핑하여 실행하게 된다.

03. 프로세스 상태와 다이어그램

여러 프로세스는 CPU를 할당 받아 빠르게 번갈아가면서 실행된다. 그러한 과정에서 프로세스는 여러 상태로 변화하며 실행된다.
간단한 예시로 ‘실행 중’ 상태나 ‘일시 중단됨’ 상태 등의 여러 상태가 존재한다. 

바로 다음 내용에서 배우겠지만 운영체제는 이런 프로세스의 상태를 PCB (Process Control Block / 프로세스 제어 블록) 라는 곳에 기록하고 넘겨주는 과정을 문맥 교환(Context Switching) 이라고 한다.

PCB는 내가 상태가 전환 되어도 어디까지 실행되었는지 기록하여 다음 실행 상태에서도 이전 위치부터 실행하기 위해 사용하는 목적이 있으며, 그 외에도 여러 정보를 담고 있다. (바로 다음 내용에서 자세히 다뤄보자)

아래 이미지에서 top 도구로 프로세스의 상태를 보면 여러가지가 있다. 

프로세스의 상태 확인
프로세스의 상태 확인

아래는 ‘프로세스 상태 다이어그램(Process status diagram)’으로 ‘상태 전이도’라고도 부르는 그림이다.
이 흐름을 이해하는 것이 프로세스의 부하 또는 여러 문제를 찾을 수 있는 지식이 되기에 반드시 이해하고 넘어가는 것을 권장한다.

프로세스 상태 다이어그램 혹은 상태 전이도
프로세스 상태 다이어그램 혹은 상태 전이도

자. 그럼 간략하게 프로세스의 상태와 다이어그램에 대해 보았으니 이제 상세한 내용을 다뤄보도록 하자.
(위의 2개의 이미지를 참고하여 학습하자)


주요 프로세스 상태

W 혹은 Ready (Watting / Ready / Runnable)

  • 준비 상태
  • CPU를 할당 받기 위해 기다리고 있는 상태이다. 이후 자신의 차례가 되면 CPU를 할당 받아 실행 상태가 된다.
  • 이때 실행 상태로 전환 되는 것을 디스패치(Dispatch)라고 한다.

R (Running)

  • 실행 상태
  • CPU를  할당 받아 자원을 소모하고 있는 프로세스
  • 프로세스는 할당된 일정 시간 동안만 CPU를 사용할 수 있다. 그리고 할당 시간을 모두 사용하게 되면 타이머 인터럽트가 발생하면서 다시 준비 상태가 된다.
  • 만약 실행 도중 I/O의 요청이 발생한다면 입출력 장치의 작업이 끝날 때까지 기다려야 한다. 이게 다음에 나오는 대기 상태(D)가 된다.

D (Disk sleep / block / Uninterruptible Sleep)

  • 대기 상태
  • 블록 된 프로세스 상태로 ‘block’ 으로도 표현하다. 
  • 디스크 혹은 네트워크 I/O를 대기하고 있는 프로세스를 의미한다.
  • 이 상태의 프로세스들은 대기하는 동안 Run Queue에서 빠져나와 Wait Queue에 들어가게 된다.
  • 매우 중요한 상태이기에 반드시 기억하자. 왜 중요하냐면 I/O 대기 상태의 프로세스가 많으면 특정 요청이 끝나기를 기다리는 프로세스가 많다는 의미이고, 결국 CPU 부하가 발생하기 때문이다.
  • 확인 방법 중 하나는 vmstat 에서 b 로 표시된 상태를 보면 된다.

S (Sleeping / Interruptible Sleep)

  • D 상태와의 가장 큰 차이점은 요청한 리소스를 즉시 사용할 수 있다는 점이다
  • sleep() 시스템 콜 등을 호출해서 타이머를 작동 시키거나 콘솔 입력을 기다리는 프로세스 상태이다.
  • 이 상태의 프로세는 요청에 대한 응답을 기다리는 상태가 아니기에 언제 어떻게 시그널이 들어올지 모르기 때문에 언제든 시그널을 받아서 처리할 수 있도록 마킹하고 대기 상태로 빠진다.

Z (Zombie)

  • 부모 프로세스가 wait() 시스템 콜로 자식 프로세스의 종료 상태를 수집하지 못하는 경우 발생한다. 
  • 시스템 자원을 사용하지 않지만 PID 고갈을 일으킬 수 있다.
    • 실제 사례를 예로 든다. 리눅스는 열 수 있는 프로세스의 개수에 제한이 있다. 그런데 Application 버그로 인하여 좀비 프로세스가 계속해서 발생할 경우 프로세스 제한에 막혀 더 이상 프로세스를 생성하지 못하여 SSH 원격 접속이 불가능하는 등의 장애가 발생한다.

종료 상태 (terminated)

  • 프로세스가 종료된 상태
  • PCB와 프로세스가 사용한 메모리를 정리한다.

04. PCB (Process Control Block) / 프로세스 제어 블록

프로세스는 CPU에 의해 한 번에 하나만 실행될 수 있다. 그리고 자신의 사용 시간이 끝나면 타이머 인터럽트에 의해 자신의 상태를 저장하고 다음 프로세스에게 넘겨주게 된다.

이를 위해 프로세스는 자신이 누구인지, 어디까지 실행되었는지 등의 여러 정보를 담고 있는 ‘프로세스 제어 블록(PCB)’를 사용하게 된다.

쉽게 말하면 상품 정보(ex. 옷, 신발 등)를 가지고 있는 Tag와 유사하게 생각하면 된다.

그리고 프로세스는 생성과 동시에 커널 영역에 PCB가 생성이 된다. 프로세스가 종료되면 PCB가 폐기된다. 즉, PCB는 프로세스의 시작과 종료를 함께한다.

PCB에 담긴 정보 (커널 영역)

  • PID (프로세스 ID) – 프로세스를 식별하기 위함
  • 프로세스 상태 – 현재 프로세스의 상태를 표시 (실행 중, 대기 중, 정지 등)
  • 프로그램 카운터 및 레지스터 값 – 프로그램 카운터로 다음에 실행할 명령어 주소를 가리키는 레지스터 값을 가지고 저장
  • 파일 디스크립터 – 프로세스가 열고 있는 파일 또는 소켓 정보
  • CPU 스케줄링 정보 – 프로세스가 언제, 어떤 순서로 CPU를 할당 받을지에 대한 정보
  • 프로세스 우선 순위 
  • 사용한 파일과 입/출력 장치 목록
커널 영역에 있는 PCB
커널 영역에 있는 PCB

05. 문맥 교환 (Context Switching)

프로세스 A가 프로세스 B에게 CPU를 넘겨줄 때 프로세스 A는 자신이 다시 실행될 때를 위해 기억해야 할 정보들이 있다.
이를 Context(문맥)이라고 하며, PCB(Process Control Block)에 저장되어 있다.

Context(문맥)에는 프로세스 A는 B에게 CPU 사용권을 넘겨주기 전에 프로그램 카운터, 여러 레지스터 값, 메모리 정보, 실행을 위해 열었던 파일이나 입/출력 장치 등의 실행 중간의 정보를 백업한다.
그래야 다음 차례가 왔을 때 이전까지 실행했던 내용을 이어서 실행을 할 수 있기 때문이다.

이처럼 기존 프로세스의 문맥을 PCB에 백업하고 새로운 프로세스를 실행하기 위해 문맥을 PCB로 부터 복구하여 새로운 프로세스를 실행하는 것을 문맥 교환(Context Switching)이라고 한다.
이 과정을 여러 프로세스가 빠르게 번갈아가면서 수행하기에 우리가 느끼기에는 프로세스가 동시에 실행되는 것처럼 보이게 된다.

프로세스의 실제 실행 과정
프로세스의 실제 실행 과정

다만 문맥 교환이 자주 발생하게 되면 오버헤드가 발생할 수 있기에 성능 저하가 일어나는 부하 지점이 될 수 있다.

문맥 교환 시 오버헤드 발생 예시

  • 캐시 메모리 플러시
    • CPU 캐시는 현재 실행 중인 프로세스의 데이터를 저장한다. 문맥 교환 시 새로운 프로세스가 실행되면 기존 프로세스의 캐시 데이터는 무효화되고 새로운 프로세스의 데이터가 캐시에 로드된다. 이로 인해 캐시 미스(cache miss)가 발생하며, 데이터가 캐시에 존재하지 않으면 메모리에서 CPU 캐시로 데이터를 가져와야 하므로 메모리 접근 시간이 증가하게 되서 성능 저하가 발생할 수 있다.
  • 스케줄러 오버헤드
    • 운영체제의 스케줄러가 어떤 프로세스를 실행할지 결정하는 데 소요되는 시간과 자원도 오버헤드에 포함된다. 이 과정은 프로세스의 우선순위, 현재 상태 등을 고려하여 다음에 실행될 프로세스를 선택하는데, 이러한 결정 과정이 CPU 시간을 소모하며 전체 시스템 성능에 영향을 미칠 수 있다.
  • 레지스터 저장 및 복원
    • 현재 실행 중인 프로세스의 레지스터 상태를 저장하고 새로 실행될 프로세스의 레지스터 상태를 복원하는 작업이 필요하다. 이 과정은 프로세서의 상태를 유지하고 정확한 실행을 보장하는 데 필수적이지만, 추가적인 CPU 시간을 소비하여 오버헤드를 발생시킨다.
  • 메모리 관리 오버헤드
    • 새로운 프로세스의 페이지 테이블과 관련된 작업이 필요하다. 이는 프로세스의 가상 메모리 주소를 물리 메모리 주소로 변환하기 위해 필요한 정보로, 이 작업은 메모리 관리 단위에서 추가적인 오버헤드를 발생시킨다. 페이지 테이블의 설정 및 변환 과정은 CPU 시간을 소모하며 시스템 성능에 영향을 미칠 수 있다.

그리고 이런 문맥 교환 오버헤드를 줄이기 위해 다양한 기술이 사용된다. 예를 들어, 멀티스레딩(multithreading)이 있다. 멀티스레딩은 단일 프로세스 내에서 여러 스레드를 사용하여 병렬 처리를 수행한다. 스레드 간의 문맥 교환은 프로세스 간의 문맥 교환보다 빠르기 때문에 오버헤드를 줄일 수 있다. 스레드는 동일한 메모리 공간을 공유하므로, 페이지 테이블 전환 등의 메모리 관리 오버헤드도 줄어들게 된다.

그 외에도 아래와 같이 문맥 교환 시 발생하는 오버헤드를 줄이는 기술들이 있다. 많은 기술이 있지만 예시로 몇 가지만 소개한다.

  • Lightweight Kernel Threads
    • 커널 수준에서 스레드를 효율적으로 관리하여, 사용자 모드와 커널 모드 간의 전환을 최소화한다. 이로 인해 문맥 교환 시 발생하는 오버헤드를 줄이고 시스템 성능을 향상시킬 수 있다.
    • 커널의 스레드 관리 전략의 일환으로 커널 컴파일 시 이 기능을 포함시킬 수 있다.
    • 이 외에도 커널 레벨에서 문맥 교환 오버헤드를 줄이는 기능은 다양하게 있다.
  • Coroutines
    • 코루틴은 비동기 프로그래밍에서 사용되며, 스레드보다 가벼운 단위로 작동한다. 함수 호출을 중단하고 재개할 수 있어 문맥 교환에 드는 오버헤드가 적다. 이를 통해 효율적으로 비동기 작업을 처리할 수 있다.
    • 프로그래밍 언어에서 지원하는 기능으로 개발자가 코드로 작성한다.
  • 프로세서 친화성(Processor Affinity)
    • 특정 프로세스를 특정 CPU 코어에 고정하여 캐시의 데이터 접근 효율을 높이고 문맥 교환 빈도를 줄이는 방법이다. 이렇게 하면 프로세스가 항상 같은 코어에서 실행되기 때문에 캐시가 더 자주 활용되어 성능이 향상된다.
    • 운영체제에서 지원하는 것으로 리눅스에서는 taskset으로 설정 가능하고 윈도우는 작업 관리자에서 설정 가능하다
  • 가상화 최적화
    • 하이퍼바이저는 가상 머신 간의 문맥 교환을 효율적으로 관리하여 오버헤드를 최소화한다. 이를 통해 가상화 환경에서 성능을 향상시킬 수 있다.
    • VMware, Hyper-V, KVM 등 가상화 솔루션의 설정 메뉴에서 최적화 옵션을 사용할 수 있다.
윈도우의 프로세스 친화성(Processor Affinity) 설정 방법

작업 관리자에서 프로세스의 ‘선호도 설정’을 선택한다

원하는 CPU Core를 선택한다

특정 CPU에 고정하여 실행 시키는 방법은 프로세스가 항상 같은 Core에서 실행하기에 캐시 데이터를 더 자주 사용하고 문맥 교환이 줄어들고 해당 작업의 성능이 향상될 수 있다. 그러나 유연성이 감소하여 자원 분배가 비효율적일 수 있으며, 고정된 Core를 사용하다보니 과부하 문제도 신경 써야한다.


06. 프로세스의 메모리 영역

이번에 배운 PCB는 커널 영역에 생성된다고 하였는데, 그럼 사용자 영역에는 프로세스가 어떻게 존재하게 되는지 배워보자

사용자 영역 프로세스 하나에는 코드 영역, 데이터 영역, 힙 영역, 스택 영역 총 4가지로 구분되어 메모리에 저장된다.

프로세스의 메모리 영역

커널 영역

“05. PCB (Process Control Block) / 프로세스 제어 블록” 에서 학습한 프로세스의 PCB가 저장되는 공간이 커널이다.

위 그림에서 커널 영역과 사용자 영역이 나뉘는 것에 대한 이해는 ‘시스템 콜(System Call)‘ 문서를 참조하자.

스택 영역 (Stack)

데이터를 일시적으로 사용할 데이터를 저장하는 공간이다. (데이터 영역과 달리 잠깐 쓰고 말 값들이 저장되는 공간)

예를 들면 함수의 실행이 끝나면 사라지는 매개 변수나 지역 변수가 대표적이다.

(동적 할당 영역 (실시간으로 크기가 변할 수 있음) : 메모리의 높은 주소부터 낮은 주소로 할당)

스택 영역에서는 메모리 누수(memory leak)이 발생하지 않는다.

함수 호출 시 자동으로 메모리가 할당되고, 함수가 종료되면 자동으로 해제되는 구조이기 때문이다. 이러한 자동 할당 및 해제 덕분에 프로그래머가 메모리 관리를 직접하지 않아도 메모리 누수가 발생할 걱정 할 필요가 없다.

힙 영역 (Heap)

프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간으로 동적으로 메모리를 할당하고 해제하는 사용된다.

프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가 해당 공간을 반환해야 한다. 이 의미는 ‘더 이상 해당 메모리 공간을 사용하지 않겠다’고 운영체제에 말해주는 것이다.

힙 영역에서 메모리 공간을 반환하지 않으면 메모리 누수가 발생한다.

메모리 누수 (memory leak)
메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래한다. 이를 메모리 누수라고 한다.

개발자 입장에서는 개발 중 마주칠 수 있는 문제이며, 운영자 입장에서는 메모리 누수로 인해 사용 메모리 사용률이 커지게 되어 시스템 성능 저하를 유발 시킬 수 있기에 운영 중 장애가 발생하는 요인이 된다.

커널 또한 프로그램이기에 메모리 누수가 발생할 수 있다. 만약 메모리 사용률이 높은데 프로세스 사용률이 메모리 사용률 만큼 되지 않는다면 meminfo를 이용해 커널 영역의 사용률을 확인하면 된다.
(동적 할당 영역 (실시간으로 크기가 변할 수 있음) : 메모리의 낮은 주소부터 높은 주소로 할당)

데이터 영역 (Data)

프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간으로 잠깐 껐다가 없앨 데이터가 아닌 것들을 저장한다.

대표적인 데이터로 ‘전역 변수’를 들 수 있으며 프로그램이 실행되는 동안 유지되고 프로그램 전체에서 접근할 수 있는 변수이다.

(정적 할당 영역 (크기가 변하지 않는 영역) : 저장될 내용이 프로그램이 실행되는 동안에만 유지될 데이터)

코드 영역 (Text)

실행할 수 있는 코드, 즉 기계어로 이루어진 명령어가 저장되는 영역

코드 영역에는 데이터가 아닌 CPU가 실행할 명령어가 담겨 있기 때문에 쓰기가 금지되어 있다. (Read Only 공간)

(정적 할당 영역 (크기가 변하지 않는 영역) : 프로그램 구성 명령들이 갑자기 바뀔 일이 없음)

프로세스의 모든 것 (상태 전이도, PCB, 문맥 교환, 프로세스 구조)에 대한 2개 댓글

  1. 준

    와.. 너무 잘 정리되어 있네요.
    도움되는 글 잘 읽었어요!
    무더위 조심하세요ㅎㅎㅎ

    1. Su-hyeon Jo
      Su-hyeon Jo 님의 말:

      잘 읽으셨다니 다행입니다. 날 더운데 건강 유념하세요 ~ ㅎㅎ

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다