Process
오늘날 우리는 컴퓨터로 여러가지 일을 동시에 합니다. 지금도 웹브라우저로 필요한 정보를 찾는 동시에 화면을 반으로 나누어 한쪽에 초고를 띄워놓고 나머지 반쪽 화면을 통해 이 글을 적고 있습니다. 어떻게 이런 일이 가능한 걸까요? 그 비밀은 프로세스Process 라는 개념에 있습니다. 이번 글에서는 프로세스의 내부보다는 프로세스 그 자체에 대한 내용을 중점적으로 살펴봅니다.
프로세스의 탄생
컴퓨터라는 물건이 탄생한지 얼마 되지 않은 초창기 시절, 컴퓨터는 지금과 달리 한 번에 하나의 프로그램만 실행할 수 있는 스페셜리스트였다. 운영체제라는 개념이 존재하지 않았고, 여러 사람이 같이 써야 할 만큼 비쌌다. 그 시절 프로그램은 주로 천공 카드punched card 였고, 컴퓨터는 주변기기를 통해 프로그램에 기록된 명령을 순서대로 읽고 그에 맞는 명령을 실행했다. 컴퓨터를 사용하고자 하는 사람은 자신의 프로그램과 데이터를 걸어놓고 먼저 신청한 사람의 작업이 끝날 때까지 기다려야 했다. 이때 한 자리를 차지한 프로그램과 데이터을 합쳐 작업job 이라 불렀고, 이를 컴퓨터가 실행할 때 작업의 최소 단위를 태스크task 라 불렀다.[1][2][3]
컴퓨터가 발전하면서 이제는 저장 장치에 프로그램을 기억해 두었다가 실행될 때 메모리 위에 올려 실행하게 되는데, 상술한 개념들이 이때 오늘날 사용되는 프로그램과 프로세스의 개념으로 변화하게 된다. 프로그램은 이제 파일 단위로 저장된 명령어들의 집합이며, 프로세스는 실행 중인 프로그램의 상태, 컴퓨터에서 연속적으로 실행되고 있는 프로그램의 '동적인 상태'다.
프로그램에서 프로세스로
그렇다면 프로그램은 어떻게 프로세스가 됐을까? 프로세스가 프로그램으로부터 생성되는 과정을 자세히 들여다보자.
프로세스 생성하기
우선 프로세스가 실행되기 위해서는 하드웨어에 접근할 수 있어야 한다. 그런데 이 하드웨어는 운영체제가 통제권을 쥐고 있어서 하드웨어에 접근하기 위해서는 운영체제의 허락이 있어야 한다. 그렇기 때문에 프로세스는 커널, 특히 모놀리식 커널Monolithic Kernel [4]에서 생성된다. 사용자의 프로세스가 새 프로세스나 새 스레드를 생성하면, 시스템 호출 인터페이스가 요청을 받아서 커널로 전달하고, 커널에서는 이 요청에 대해 새 작업을 생성한다. 이 작업은 스케쥴러Scheduler 라는 모듈의 대기열에 배치되며 때가 되면 CPU 등 컴퓨터의 자원을 사용할 수 있게 된다.
커널이 새 프로세스나 스레드를 스폰하라는 요청을 받았을 때, 커널에서는 특정 실행 파일에 대한 핸들러Handler 를 종류에 따라 찾고, 핸들러에 맞는 실행 파일을 주 기억 장치(보통 RAM)로 불러올 로더 프로그램Loader Program 을 사용한다. 그리고 프로세스를 실행하기 위해 로더 프로그램은 다음과 같은 작업을 수행한다.
- 실행 맥락과 실행을 요청한 사용자의 권한을 확인한다.
- 메인 메모리로부터 새 프로세스에 대한 메모리를 할당한다.
- 실행 파일의 이진 파일 내용을 할당된 메모리로 복사한다.
- 이는 대부분 데이터 및 텍스트 세그먼트를 포함한다.
- 스택 세그먼트에 메모리 영역을 할당하고 초기 메모리 매핑을 준비한다.
- 메인 스레드[5] 및 스택 메모리 영역이 생성된다.
- 커맨드 라인 인수를 메인 스레드의 스택 영역 최상단에 스택 프레임으로 복제한다.
- 실행에 필요한 필수 레지스터를 초기화한다.
- 프로그램의 진입점에 대한 첫번째 명령어를 실행한다.
운영체제는 이렇게 만들어진 프로세스에 각각의 번호(Process Identification Number, PID)를 붙여 관리한다. 프로세스를 만들 때는 현재 실행 중인 프로세스에서 만들게 되는데, 이때 현재 실행 중인 프로세스를 부모 프로세스Parent Process, 새로 생성되는 프로세스를 자식 프로세스Child Process 라고 부른다. 부모 프로세스로부터 자식 프로세스가 생겨나기 때문에 프로세스의 의존을 나타내는 그래프는 트리의 형태를 띤다.
또한 새로운 프로세스를 만들 때는 세 가지 옵션을 설정할 수 있다.
- Resource Sharing Option: 프로세스 간의 자원 공유resource sharing에 대한 옵션이다.
- 부모와 자식 프로세스가 모든 자원을 공유.
- 자식 프로세스가 부모 프로세스의 자원 중 일부만 공유.
- 부모 와 자식 프로세스가 서로 자원을 공유하지 않는다.
- Execution Option: 프로세스의 동시 실행execution에 대한 옵션이다.
- 부모와 자식 프로세스가 번갈아 가며 동시에 실행될 수 있다.
- 자식 프로세스가 종료되어야 부모 프로세스가 실행된다.
- Address Space Option: 프로세스의 주소 공간Address Space 에 대한 옵션이다.
- 자식이 부모와 동일한 데이터를 가진다.
- 자식 프로세스가 새로운 데이터를 가진다.
실제로 프로세스를 생성하는 과정은 분량 상의 문제로 여기서 다루지 않고 다른 글에서 자세히 다루게 될 것이다.
프로세스의 구조
이렇게 만들어진 프로세스는 프로그램의 동적 인스턴스이며, 그 상태가 시시각각 변화하는 역동적인 과정이다. 그렇지만 당연히 고정된 골격이 있고, 이제 그 골격을 알아볼 차례다.
내부 구조
운영체제의 관점에서, 일반적인 프로세스의 내부 구조는 다음과 같다.[6]
프로세스에는 크게 네 개의 구역이 존재하는데, 각각 다음과 같다:
- Code(Text) Section
- 프로세스의 코드가 실행 가능한 상태로 저장되어 있다.
- 크기가 고정되어 있으며, 실행 도중에 그 크기가 변하지 않는다.
- Data Section
- 전역 변수들이 저장되어 있다.
- 크기가 고정되어 있으며, 실행 도중에 그 크기가 변하지 않는다.
- Stack Section
- 지역 변수들, 반환 주소, 함수 인자 등 일시적인 수명을 가진 변수들이 저장되어 있다.
- 실행 중에 그 크기가 변한다.
- Heap Section
- 실행 중에 동적으로 할당된 메모리가 저장되어 있다.
- 실행 중에 그 크기가 변한다.
프로세스 내부를 직접 만져보고 자세하게 파헤치는 과정은 분량 문제로 여기서 다루지 않고 다른 글에서 자세히 다루게 될 것이다.
Process Control Block
운영체제는 프로세스를 주기적으로 바꿔주면서 CPU가 최대한 쉬지 않고 일하게 만들어야 하기 때문에, 프로세스의 내부 구조와는 별개로 프로세스를 하나의 물체로 다룰 필요가 있다.
그래서 운영체제는 프로세스를 하나의 물체처럼 추상적으로 취급하는데, 이때 하나의 프로세스를 Process Control Block(이하 PCB)이라는 하나의 객체로 취급된다.
PCB는 프로세스마다 가지고 있는, 프로세스를 통제하기 위한 모든 정보들의 집합으로, Task Control Block라 부르기도 한다. 여기에는 다음의 정보가 포함되어 있다.
- Process State
- Process Number: Degree of Multiprogramming
- Program Counter
- CPU Registers
- CPU Scheduling Information
- Memory-Management Information
- Accounting Information
- I/O Status Information
PCB에 대한 자세한 내용은 분량 상의 문제로 다른 글에서 자세히 다루게 될 것이다.
메모리 위의 프로세스
앞서 프로세스의 레이아웃을 살펴보았다. 그러나 실제로 프로세스가 메인 메모리에 저장되어 있는 모습을 이해하기 위해서는 소프트웨어의 관점에서 레이아웃을 볼 때와는 다른 관점으로 접근할 필요가 있다. 실제 프로세스는 메인 메모리 내 여기저기에 흩어져 있으며, 운영체제는 메모리 가상화Memory Virtualization 를 통해 실제로는 흩어져 있는 프로세스를 하나의 통합된 주소 공간으로 파악한다.
하드웨어의 관점에서 보는 프로세스는 분량 상의 문제로 다른 글에서 자세히 다루게 될 것이다.
프로세스 활용하기
현실에서는 프로세스를 수십 개 다루는 것 정도 쯤이야 기본이다. 그래서 그 수십 개의 프로세스를 조화롭게 사용하기 위한 여러가지 기술이 있다. 그 중 대표적인 것이 바로 스케줄러와 프로세스 간 소통(Inter-Process Communication, IPC)인데, 분량 상 여기서 다루지는 않을 것이다.
프로세스 종료하기
프로세스가 모든 일을 마치고 종료될 때, 부모 프로세스는 상태 값을 받을 수 있다. 또한 자식 프로세스에게 할당됐던 자원들이 운영체제에 의해 해제된다. 시스템 호출System Call 이나 코드에 의해 자식 프로세스가 강제로 종료될 수 있는데, 이때 운영체제에 따라 부모 프로세스가 종료되면 자식 프로세스가 모두 멈추거나Cascading Termination , 모든 프로세스의 부모인 최초의 프로세스를 부모로 다시 정하기도 한다.
이 과정에서 실제 프로세스는 종료했지만 부모 프로세스가 그 상태를 확인하지 못했을 때(ex. POSIX API를 사용하는 환경에서 wait()
함수를 호출하기 전에 자식 프로세스가 먼저 종료됨) 이 프로세스를 좀비 프로세스Zombie Process라고 부른다. 실제로는 종료된 프로세스지만, 이들을 관찰하는 부모 프로세스의 입장에서는 아직 종료된 프로세스가 아니다.
반대로 부모 프로세스가 종료를 위해 시스템 호출을 사용하지 못하고 먼저 종료된 경우, 자식 프로세스는 고아 프로세스Orphan Process가 되어 홀로 남겨진다. 종료 후 상태에 대한 정보를 넘길 부모 프로세스가 없는 상황을 해결하기 위해 POSIX 환경에서는 init
프로세스를 이들의 새로운 부모 프로세스로 지정한다.
실제로 프로세스를 종료시키는 과정은 분량 상의 문제로 여기서 다루지 않고 다른 글에서 자세히 다루게 될 것이다.
참고 자료 & 더보기
참고 자료
- Amini, K. (2022). 전문가를 위한 C (박지윤, Trans.; 1st ed.). 한빛미디어.
- Silberschatz, A., Galvin, P. B., & Gagne, G. (2018). Operating system concepts (10th ed.). Wiley.
- Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. (2014). Operating Systems: Three Easy Pieces (Version 0.8). Arpaci-Dusseau Books, Inc., http://www.ostep.org
더보기
케임브리지 대학 수학 연구소의 컴퓨터 EDSAC에 대한 1951년 영상을 통해 초창기 컴퓨터가 어떻게 사용되었는지 엿볼 수 있다. ↩︎
자세한 내용은 이 링크를 통해 확인할 수 있다. 오늘날 Job과 Task는 구분없이 사용되기도 하며 그 정의가 모호하다. ↩︎
Kamran, A. (2022)에 의하면, 커널 수준에서 프로세스나 스레드를 다룰 때 작업job 이라는 용어를 프로세스나 스레드 대신 사용하기도 한다. ↩︎
하나의 주소 공간을 갖는 커널 프로세스 한 개로 구성되는 커널이며, 이 커널 프로세스는 커널이 제공하는 모든 서비스를 포함한다. ↩︎
멀티스레드 프로그램에서 가장 중요한 스레드다. 실행 흐름의 측면에서 가장 처음 실행되고 가장 마지막으로 종료되는 스레드이며, 프로세스의 다른 모든 스레드가 시작되는 기원이다. ↩︎
이 레이아웃은 운영체제가 프로세스를 인식하는 형태이며, 이 형태를 가상 주소 공간Virtual Address Space 이라 부르기도 한다. ↩︎