[Operating System] Process
이번 아티클에서는 Process에 관해 정리한다.
* 새롭게 알게 되는 내용이 있다면 지속적으로 업데이트합니다.
Program vs. Process
프로그램은 디스크에 저장되어 있는 실행 가능한 파일을 의미한다. 이는 커널에 의해 메모리로 로드되어 실행될 수 있는데, 이 경우 프로세스라고 한다. 따라서 프로세스를 프로그램의 '인스턴스'라고 표현하기도 한다.
프로세스는 실행과 스케줄링의 가장 기본적인 단위이다. 다만 스레드라는 개념이 도입되기 시작하면서, 스레드에 대한 스케쥴링도 가능해졌다. 하지만 스레드는 독립적인 실행 메모리를 가지지 않기 때문에, 실행의 기본 단위라고는 이야기하기 어렵다.
Process ID
프로세스는 Process ID(PID)로 식별되는데, 이를 통해 프로세스를 식별할 수 있다. 유닉스 계열 운영체제에서는 fork() 시스템 콜을 통해 PID가 반환되며, 이렇게 반환된 PID를 통해 자식 프로세스를 기다리거나 종료시키는 등의 작업을 수행할 수 있다.
보통 PID는 0 혹은 양의 정수 범위에서 순차적으로 증가하면서 지정되는 경우가 대부분이다. 또한, 미리 정의된 PID를 가지고 있는 프로세스도 있는데, UNIX 계열에서 0번의 경우 메모리 페이징을 담당하는 swapper process, 1번의 경우 시스템을 시작하고 종료하는 데 사용되는 init process가 상주한다.
Process Address Space
프로세스의 주소 공간은 위와 같은 구조를 띤다. 메모리 주소는 0번지부터 시작하는데, 이렇게 모든 프로세스가 0번지부터 시작하는 메모리 주소를 가질 수 있는 이유는 가상 메모리와 관련이 있다.
각각의 세부 영역을 조금 더 자세히 설명하면 아래와 같다.
- code (text segment)
- 말 그대로 code, 즉 기계어 명령어들이 저장되는 위치
- read-only 영역
- static data (data segment)
- 프로그램이 실행되는 동안 항상 메모리를 차지하고 있는 변수들이 저장되는 곳
- 초기화된 데이터 구역과 초기화가 되지 않은 데이터 구역(BSS) 으로 나뉨 (BSS 구역 데이터들은 디스크에 따로 저장하지 않은 상태로 프로세스를 시작할 수 있음 = 메모리 최적화)
- 프로그램의 라이프사이클과 동일하게 맞춰 생성 및 소멸됨
- heap (dynamically allocated memory)
- 사용자에 의해 동적으로 할당되는 메모리가 위치하는 곳 (= 런타임에 크기가 결정)
- 하위 주소에서 상위 주소로 메모리 공간을 사용
- stack (dynmically allocated memory)
- 지역변수, 매개변수 등이 저장되는 영역
- stack 영역의 값은 함수 호출 시 할당되며 호출 완료 시 소멸
- 상위 주소에서 하위 주소로 메모리 공간을 사용
stack이나 heap은 동적으로 메모리를 할당하기 때문에, 미리 지정한 크기를 벗어나는 경우가 생길 수 있다. 그리고 이러한 경우를 stack overflow 혹은 heap overflow라고 부른다.
유의해야 할 점은, 스레드가 생성되는 경우, 스레드는 프로세스의 stack을 공유하지 않고 새로운 stack을 만들어내는데, 이는 race condition 등을 해결하기 위함이다. 논리적으로 생각해보아도 스레드마다 지역변수나 파라미터를 공유한다면 신경써야 할 일이 많아질 것이다. 그렇지만 text 영역, data 영역, heap은 공유한다.
Stack Pointer
stack 영역에서는 SP(Stack Pointer)를 가지는데, 이는 스택 영역에서의 마지막 요소 혹은 다음으로 사용될 메모리 주소를 가리키는 레지스터이다. stack 영역을 정의하는 방식은 프로세서마다 다를 수 있고, 정해진 표준도 없으나 대개 따르는 디자인은 아래와 같다.
함수가 호출 될 때 스택 프레임이 생성되어 쌓이고, 호출이 완료되면 제거된다. 구체적인 스택 프레임의 구성요소는 아래와 같다.
- Local variables -> 함수 내에서 사용되는 지역변수
- Base Pointer -> 스택 프레임의 기준점을 정의하는 포인터
- Return address -> 호출한 위치로 돌아가기 위한 주소
- Parameters -> 함수에 전달된 인자들
Process State
프로세스는 여러 상태를 가진다. 각각의 상태에 대한 정의는 아래와 같다.
- new -> 프로세스가 생성되는 단계
- ready -> 실행이 가능한 상태
- ready 상태의 프로세스를 running 상태로 전환하는 작업을 dispatch라고 하고, 이 작업은 스케쥴러가 수행
- running -> 프로세스가 실행되고 있는 상태
- 이 상태에서 인터럽트가 발생하면 ready 상태로 전환
- wating -> I/O 혹은 이벤트가 발생했을 때 대기하는 상태
- I/O 작업 혹은 이벤트 처리 작업이 완료되면 ready 상태로 전환
- terminated -> 프로세스가 종료된 상태
PCB(Process Control Block)
프로세스들을 관리하기 위해서는 메타정보가 필요한데, 운영체제는 PCB라는 자료구조를 통해 이를 관리한다.
프로세스 상태, PID, PC(Program Counter), 레지스터, 메모리 관리 정보, 입출력 상태 정보, 스케줄링 정보 등을 가진다. 프로세스는 running과 ready 상태 사이에서 전이를 반복하는데, 이 때 PCB를 통해 진행 상황에 대한 정보를 복구할 수 있다. 즉 인터럽트가 발생하면 PCB에 메타데이터를 저장하고, dispatch가 되면 PCB로부터 관련 정보를 복구한다. 즉 한마디로 말하면, Context Switching 과정에서 필요한 정보다. *참고로, 리눅스에서는 task_struct 구조체가 PCB(=TCB)에 해당한다.
Context Swtich
실행중인 프로세스는 지속적으로 전환된다. 이 과정에서 앞서 이야기한 PCB에 상태 Save, Reload를 한다. 다만 이러한 과정을 거치게 되면 레지스터, 메모리 맵, 캐시 등과 같은 자료구조들을 변경시켜야 하기 때문에 Administrative Overhead가 지속적으로 발생한다(운영체제 구조에 따라 Overhead 양은 다르다).
그럼에도 Context Switch를 수행해야만 하는 이유는, 빠른 응답을 기대할 수 있기 때문이다. CPU는 한 번에 하나의 작업만 처리할 수 있기 때문에, Context Switch를 통한 '여러 프로세스를 실행하는 것처럼 보이게 해주는' 작업이 필요하다.
통상 Context Switch는 초당 100번에서 1000번 발생한다고 한다. (운영체제에 따라 다르겠지만)
Schedulers
스케쥴러의 종류는 크게 세 가지가 있는데, Long-term, Medium-term, 그리고 Short-term이다. 각각의 책임은 아래와 같다.
- Long-term scheduler(job scheduler) -> ready queue로 들어올 프로세스를 선별함
- Medium-term scheduler(swapper) -> 메모리가 부족한 경우, ready queue에서 어떠한 프로세스를 제거할지 결정함 (swap in & swap out)
- Short-term scheduler(CPU scheduler) -> ready queue에서 어떠한 프로세스를 실행시킬지 선별함(dispatch)
다만 가상 메모리의 등장으로 Long-term, Medium-term scheduler는 time sharing system에서 그 중요도가 하락하게 되었는데, 실제 메모리량에 관계 없이 프로세스를 실행시킬 수 있기 때문이다.
Queue and PCB
운영체제는 PCB를 통해 프로세스의 논리적 식별을 수행한다. 즉 프로세스가 어떠한 상태에 접어들었다는 것을 PCB를 Queue에 집어넣음으로서 표현한다. 프로세스 상태(Ready, Waiting, New)에 따라 각각의 큐가 존재한다. 단, Running 상태의 경우는 하나의 프로세스만 존재하므로 별도의 큐가 존재하지는 않는다.
Process Creation in Linux/Unix
fork()
유닉스 및 리눅스 운영체제에서는 fork() 라는 시스템 콜을 통해 프로세스를 생성한다. 프로세스를 생성한다는 의미는 PCB를 생성한다는 의미이다. 또한, 프로세스 주소 공간 역시 생성한다.
다만 특이한 점은 fork()를 call한 Parent Process와 완전히 동일한 주소 공간을 생성한다는 점이다. 또한, PCB에서도 열려 있는 파일들과 같이 리소스들에 대한 정보를 그대로 복사한다(부모 프로세스와 동일한 환경에서 시작하기 위함).
다만 실제로는 진짜 복사를 하는게 아니라, CoW(Copy-on-Write)라는 지연 복사 방식을 사용한다. 우선은 동일한 페이지 테이블을 참조하고 있다가 쓰기가 발생하면 복사하는 방식이다. 이를 통해 메모리 최적화를 이뤄낼 수 있다. 심지어는 읽기만 발생하는 경우라면 추가적인 프로세스 주소 공간을 사용하지 않는다.
fork()는 부모 프로세스와 협력할 때나, 부모 프로세스의 데이터를 통해 작업을 처리할 때 유용하다. 예시로는 웹서버가 있는데, 부모 프로세스는 TCP 커넥션이 생성되면 이를 fork()를 통해 자식 프로세스에 맡길 수 있다.
exec()
프로그램을 실행시켜 프로세스로 만들고 싶다면 exec() 시스템 콜을 활용한다. 다만 이는 프로세스를 만드는(PCB를 만드는) 작업은 아니다. exec()을 실행한 프로세스는 타겟 프로세스로 완전히 대체되는데, 그렇기 때문에 fork() -> exec() 구조를 많이 사용한다.
zombie & orphan process
만약 부모 프로세스보다 자식 프로세스가 먼저 종료되었고, 부모 프로세스가 wait 혹은 waitpid (non-blocking wait)을 수행하지 않으면 좀비 프로세스가 된다. 좀비 프로세스는 별다른 메모리나 CPU를 점유하지는 않지만 엔트리로서 프로세스 테이블에 남아 시스템 리소스를 낭비한다. 즉, PCB가 그대로 유지된다.
고아 프로세스는 부모 프로세스가 자식 프로세스보다 먼저 종료된 경우를 일컫는다. 자식 프로세스가 고아 상태가 되면, 커널은 init 프로세스(PID 1)에 고아 프로세스를 넘겨준다. init 프로세스는 고아 프로세스가 종료될 때까지 기다리고, 상태를 수집한다.
IPC(Inter-Process Communication)
여러 프로세스가 통신해야 할 일이 있다면 IPC 기법을 사용한다. 대표적인 커뮤니케이션 방식은 크게 1. 메세지 전달 방식(message passing), 2. 공유 메모리(shared memory)가 있다. 이외에도 Half-Duplex 방식인 pipe, 네트워크를 타는 socket이 IPC 기법으로 사용된다.
이러한 기법들을 사용하는 이유는 운영체제의 책임 중 하나인 protection과 관련이 있는데, 한 프로세스가 다른 프로세스의 메모리 영역을 마음대로 침범해서는 안되기 때문이다.
Shared memory
공유 메모리 방식은 프로세스 간 메모리를 공유하는 방식이다. 이 방식은 단순한 메모리 참조이기 때문에 접근 속도가 빠르다. 또한, 메모리 사용을 효율적으로 할 수 있다. 그렇지만 공유를 한다는 특성 상, 쓰기가 발생하는 경우 동기화 장치(세마포어, 뮤텍스)가 필요하다. Shared Memory를 사용하는 대표적인 예시로는 Bounded Buffer Problem(Producer-Consumer Problem)이 있다.
BSD 계열의 운영체제에서는 메모리맵(mmap)이 비슷한 기능을 수행한다.
Message queue
메시지 큐 방식은 메세지를 FIFO로 교환할 수 있게 해주는 방식으로, 동기화는 필요하지 않지만 공유 메모리 방식보다 느리다는 단점이 있다. 또한 이러한 메세지 큐는 커널에 의해 관리되는 큐다.
참고자료
https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4_%EC%8B%9D%EB%B3%84%EC%9E%90
https://www.geeksforgeeks.org/heap-overflow-stack-overflow/
https://www.techtarget.com/whatis/definition/stack-pointer
https://www.geeksforgeeks.org/difference-between-short-term-medium-term-and-long-term-scheduler/
https://www.geeksforgeeks.org/process-scheduler-pcbs-and-queueing/
Abraham Silberschatz, Peter B. Galvin, and Greg Gagne, Operating System Principles, 10th Edition, John Wiley & Sons, Inc. 2019.