[Operating System] – “프로세스(Process) 란?”
[Operating System] – “프로세스(Process) 란?”
이 글은 도서 Operating Systems: Three Easy Pieces 를 읽고, 제가 이해한 방식대로 정리한 것임을 밝힙니다.
blog-driven-development 나 blog-driven-learning 은 위험합니다. 저의 글을 끊임없이 의심하고, 검증하고, 혹시라도 틀린 내용이 있거나, 논리적 비약이 있다면 가감없이 알려주시면 감사하겠습니다 :)
운영체제가 제공하는 환상: CPU 가상화(CPU Virtualization
)
가상화(假像化, virtualization)는 컴퓨터에서 컴퓨터 리소스의 추상화를 일컫는 광범위한 용어이다. “물리적인 컴퓨터 리소스의 특징을 다른 시스템, 응용 프로그램, 최종 사용자들이 리소스와 상호 작용하는 방식으로부터 감추는 기술”로 정의할 수 있다.
이전 글 에서 살펴보았듯이, 운영체제는 적은 개수의 CPU 가 존재하더라도 마치 엄청나게 많은 수의 CPU 가 존재하는 것만 같은 환상을 만들어내, 여러 개의 프로그램을 동시에 실행시킵니다. 마치, 필자가 현재 블로그 글 작성을 위한 웹 브라우저와 코드 작성을 위한 VSCode 에디터를 동시에 띄워 두고, 백그라운드에는 음악 플레이어를 실행시켜 흥겨운 기분이 나도록 만드는 것처럼 말이죠.
운영체제는 컴퓨터가 알아들을 수 있는 명령어 덩어리와 정적 데이터의 묶음인 프로그램이라는 것을, 실제 살아숨쉬는 프로세스라는 것으로 바꿔 올바르게, 오류 없이, 프로세스들이 서로 방해받지 않게 실행시킬 책임이 있습니다. 이전 글에서 힌트를 얻으셨겠지만, 운영체제는 CPU를 가상화하여 CPU가 무한 개에 가깝게 존재하는 것 같은 환상을 만들어냅니다.
cpu
process 1 - - --
process 2 - - -
process 3 - - -
위처럼, 운영체제는 적은 수의 CPU 를 여러 개로 나누어, 하나의 프로세스를 시작하고 중지하고 – 다른 프로세스를 실행하는 작업을 반복해 여러 개의 CPU가 존재하는 것처럼 보이게 합니다(마치, 위의 그림에서 process 1 이 한 순간 CPU 를 사용하고, 중지한 다음 process 2가 사용하도록 하고, 중지한 다음 process 3이 사용하도록 하듯이 말입니다).이를 시분할(time sharing
) 이라고 합니다.
운영체제는 CPU 가상화를 효율적으로 구현하기 위해서, 아래의 두 가지 요소를 활용합니다. “무엇을(Policy)” 과 “어떻게(mechanism)” 을 분리하는 것이죠.
- 필요한 기능을 구현하는 방법이나 규칙을 의미하는
mechanism
(예컨대, 시분할 시스템은 어떻게 구현될 수 있을까요?) - 운영체제 내에서 어떤 결정을 내리기 위한 알고리즘인
policy
(예컨대, 시분할 기법을 활용해 동시에 여러 프로그램을 실행해야 한다면, 한 순간에 어떤 프로그램을 실행시키는 것이 마땅할까요?)
운영체제는 CPU를 가상화하여 여러 개의 프로세스를 동시에 제공한다고 했습니다. 그리고 그 가상화라는 것을 효율적으로 구현하기 위해서 mechanism & policy
라는 개념을 이용한다는 것도 알았습니다. 그러면, 프로세스란 무엇인지 알아봐야 할 차례입니다.
프로세스(Process) 의 개념
프로세스(process)는 컴퓨터에서 연속적으로 실행되고 있는 프로그램이다. 종종 스케줄링의 대상이 되는 작업(task)이라는 용어와 거의 같은 의미로 쓰인다. 여러 개의 프로세서를 사용하는 것을 멀티프로세싱이라고 하며 같은 시간에 여러 개의 프로그램을 띄우는 시분할 방식을 멀티태스킹이라고 한다. 프로세스 관리는 운영 체제의 중요한 부분이 되었다.
프로그램은 일반적으로 하드 디스크 등에 저장되어 있는 실행코드를 뜻하고, 프로세스는 프로그램을 구동하여 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업 단위를 지칭한다. 예를 들어, 하나의 프로그램을 여러 번 구동하면 여러 개의 프로세스가 메모리 상에서 실행된다.
https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4
운영체제는 프로세스라는, 실행 중인 프로그램이라는 개념을 제공합니다. 그 실행 중인 프로그램(프로세스)은 우리의 시스템의 자원에 이곳저곳 접근하여 영향을 끼치겠죠. 어떤 파일을 쓴다던지, CPU 를 사용하여 연산을 수행하는 것처럼 말이에요. 이처럼 프로세스는 machine state
(하드웨어 상태) 를 읽거나, 갱신합니다. 그렇다면 어떤 하드웨어 자원에 접근할까요? 프로세스는 어떻게 표현될 수 있을까요?
- 가장 중요한 구성 요소는 메모리(
address space
)입니다. 프로세서가 실행해야 할 명령어, 프로세스가 사용하는 데이터들은 모두 메모리에 저장되죠. 프로세스가 접근할 수 있는 메모리는 프로세스의Machine State
를 구성하는 요소 중 하나입니다. - 프로그램의 어느 명령어가 실행 중인지를 알려주는 프로그램 카운터(
Program Counter
), 함수의 변수 및 리턴 주소를 저장하는 스택을 관리할 때 사용되는 스택 포인터(Stack Pointer
), 프레임 포인터(Frame Pointer
) 와 같은 레지스터들 또한Machine State
를 구성하는 요소입니다. - 또한, 실행 중인 프로그램은 이전 글에서 소개드렸던 영구 저장장치들에 접근할 수도 있죠.
정리하자면, 프로세스는 실행 중인 프로그램을 의미하며, 특정 순간의 프로세스를 표현하기 위해서 프로세스가 실행하는 동안 갱신한 시스템의 자원의 목록, 즉 하드웨어 상태(Machine State
) 를 기술함으로서 표현할 수 있다는 것입니다.
좋아요, 프로세스가 뭐고, 시스템의 어떤 부분에 영향을 끼치고, 특정 순간의 프로세스는 어떻게 표현될 수 있는지까지 알게 되었습니다. 그렇다면 운영체제는 어떻게 명령어와 정적 데이터 덩어리들을 프로세스로 만드는지 알아보아야 쓰겠습니다.
프로세스 생성(Process Creation
)
프로그램이 실행될 때 여전히 우리가 알고 있는 것은 “프로그램을 읽고, 명령어를 반입하고, 명령어를 해석하고, 명령어를 실행한다” 입니다. 맞아요, 운영체제는 먼저 프로그램 실행을 위해 프로그램 코드와 정적 데이터들을 프로세스가 접근할 수 있는 주소 공간(address space
) 에 로드합니다. 디스크 어딘가에 죽어있던 프로그램이라는 것에 생명을 불어넣기 위해서, 가장 첫 번째로 하는 일은 그것들을 메모리로 올린다는 것이죠.
-------------------------------------
| 코드 영역 (Code) | --> 프로그램의 기계어 코드
-------------------------------------
| 데이터 영역 (Data - Initialized) | --> 초기화된 전역 변수 및 정적 변수
-------------------------------------
| 데이터 영역 (BSS - Uninitialized) | --> 초기화되지 않은 전역 변수 및 정적 변수
-------------------------------------
| 힙 영역 (Heap) | --> 동적 메모리 할당 공간
-------------------------------------
| |
| |
| (Unused/Free Memory) | --> 힙과 스택 사이의 여유 공간
| |
| |
-------------------------------------
| 스택 영역 (Stack) | --> 함수 호출 시 스택 프레임 저장
-------------------------------------
위의 그림은 프로세스의 메모리 구조를 나타낸 것입니다. 맞아요! 프로그램 명령어들은 위의 코드 영역에 로드됩니다.
다음으로 운영체제가 수행하는 일은 스택 영역(Stack
) 영역에 대한 메모리를 할당하고, 힙(Heap
) 영역을 위한 메모리를 할당하고, 입출력과 관련된 초기화 작업을 합니다. 결과적으로, 아래와 같은 순서가 되는 거죠.
- 디스크 어딘가에 죽어있던 명령어들을
Code
영역에 할당, 정적 데이터를 할당하기 위해Data
,BSS
영역 할당 - 함수 호출, 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위해 일정 크기의
Stack
영역 할당 - 동적으로 할당되고 해제되기 위한
Heap
영역을 위한 공간 할당 - 입출력과 관련된 초기화 작업 수행
- 새로 생성된 프로세스를 CPU 에게 전달, 프로그램 실행
프로세스 상태(Process State
)
프로세스는 운영체제 안에서 만들어지고 나서 여러 상태(State
) 를 가집니다. 실제로 CPU
를 점유하여 실행(Running
) 중일 수도 있고,CPU
를 사용할 준비가 되었지만, 운영체제가 다른 프로세스를 사용하고 있는 등의 이유로 준비(Ready
) 중일 수도 있고, I/O
와 같이 다른 사건을 기다리는 동안 CPU
가 할당되어도 실행될 수 없는 대기(Blocked
) 상태가 될 수도 있죠. 운영체제는 이와 같은 상태를, 스케쥴링 정책에 따라 조절합니다.
운영체제의 스케쥴링 정책에 따라서, 프로세스의 상태는 위의 세 가지 상태에서 각각 전이될 수 있습니다. 예컨대, 프로세스1이 디스크를 읽거나 네트워크로부터 패킷을 기다릴 때에 Running
상태에서 Ready
상태로 전이되고, 그 때에 운영체제는 프로세스2가 CPU 를 사용하지 않고 있는 것을 감지한 다음 프로세스2를 실행시킵니다. 이후 프로세스1의 입출력이 완료된다면, 다시금 프로세스2는 Ready
상태로 전이되고 프로세스1이 실행되어 완료될 수도 있죠. 그림으로 표현해보자면, 아래와 같습니다.
Process-1 |Process-2 |
--------------------------|
(Running) |(Ready) |
(Running) |(Ready) |
(Running) |(Ready) | // Process-1 says: "네트워크 입출력좀 할게요~"
(Blocked) |(Running) | // Process-2 says: "CPU 안 쓰실 동안 좀 쓰겠습니다~"
(Blocked) |(Running) |
(Blocked) |(Running) |
(Ready) |(Running) | // Process-1 says: "입출력 끝이에요~"
(Ready) |(Running) | // Process-2 says: "저는 할 일 다 끝났습니다~!"
(Running) | |
(Running) | | // Process-1 says: "저도 할일 끝이요~ Goodbye, World!"
위처럼, 운영체제는 프로세스1이 대기상태에 있는 동안 CPU 를 놀리지 않고 프로세스2를 실행시킴으로서, CPU 를 계속 동작시키며 자원의 이용률을 높일 수 있습니다. 하지만 위처럼 프로세스1의 입출력이 끝났을 때 프로세스1에게 CPU를 점유하도록 하지 않고 프로세스2가 끝날 때까지 프로세스1을 Ready
상태로 두는 것은 과연 잘한 일이었을까요? 이것이 잘 한 일이었는지, 아닌지는 추후 운영체제의 스케쥴러에 대해 자세히 다루며 알아볼 것입니다.
MacOS
의 프로세스 자료구조
실제로 운영 체제 또한 프로그램의 한 종류입니다. 필자가 사용하고 있는 MacOS
의 경우, 아래와 같이 process
를 나타내는 코드가 존재하죠.
원본 코드는 https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/sys/proc_internal.h 에 위치해 있습니다. 아래의 코드는 이해를 위해 주석을 한글로 번역하고, 상당량의 코드를 제거한 것입니다.
/*
* 프로세스에 대한 설명.
*
* 이 구조체는 UN*X에서 프로세스로 알려진 제어 스레드를 관리하는 데 필요한 정보를 포함합니다.
* 이 구조체는 프로세스가 사용하는 하위 구조체에 대한 참조를 포함하고 있으며, 관련된 프로세스와 공유할 수 있습니다.
* 프로세스 구조체와 하위 구조체는 항상 접근 가능하지만, 아래 "(PROC ONLY)"로 표시된 항목은 프로세스가 실행 중인 프로세서에서만 접근 가능할 수 있습니다.
*/
struct proc
{
LIST_ENTRY(proc)
p_list; /* 모든 프로세스의 리스트. */
void *XNU_PTRAUTH_SIGNED_PTR("proc.task") task; /* 해당하는 태스크 (정적) */
struct proc *XNU_PTRAUTH_SIGNED_PTR("proc.p_pptr") p_pptr; /* 부모 프로세스를 가리키는 포인터 (LL) */
pid_t p_ppid; /* 프로세스의 부모 PID 번호 */
pid_t p_original_ppid; /* 프로세스의 원래 부모 PID 번호, 재부모화되더라도 변경되지 않음 */
pid_t p_pgrpid; /* 프로세스 그룹 ID (LL) */
uid_t p_uid; /* 사용자 ID */
gid_t p_gid; /* 그룹 ID */
uid_t p_ruid; /* 실 사용자 ID */
gid_t p_rgid; /* 실 그룹 ID */
uid_t p_svuid; /* 저장된 사용자 ID */
gid_t p_svgid; /* 저장된 그룹 ID */
uint64_t p_uniqueid; /* 프로세스 고유 ID - fork/spawn/vfork 시 증가하며, exec 동안에는 동일하게 유지됨 */
uint64_t p_puniqueid; /* 부모의 고유 ID - fork/spawn/vfork 시 설정되며, 재부모화되더라도 변경되지 않음 */
lck_mtx_t p_mlock; /* 프로세스를 위한 뮤텍스 락 */
pid_t p_pid; /* 프로세스 식별자 (정적) */
char p_stat; /* 프로세스 상태 (PL) */
char p_shutdownstate; /* 종료 상태 */
char p_kdebug; /* 커널 디버그 상태 (CC) */
char p_btrace; /* 블록 추적 상태 (CC) */
////////////////
// 엄청난 생략.. //
////////////////
};
구조체의 멤버 변수 중, p_stat
가 보이는데, 이는 우리가 위에서 배웠던 프로세스의 상태를 나타냅니다. 또 다른 코드에서는, 우리가 위에서 배웠던 프로세스의 상태를 나타내고 있네요.
원본 코드 위치는 https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/sys/proc.h.auto.html 에 있습니다.
/* 상태 값들. */
#define SIDL 1 /* fork에 의해 생성 중인 프로세스. */
#define SRUN 2 /* 현재 실행 가능 상태. */
#define SSLEEP 3 /* 특정 주소에서 대기(잠자고) 중인 프로세스. */
#define SSTOP 4 /* 디버깅 중이거나 일시 정지된 프로세스. */
#define SZOMB 5 /* 부모 프로세스에 의해 수집(회수)되기를 기다리는 좀비 상태. */
위에서 확인하실 수 있듯이, 프로세스의 상태의 종류가 배웠던 RUNNING / READY / BLOCKED
보다 많은 것을 확인할 수 있습니다. 프로세스가 종료되었지만, 메모리에는 남아있는 상태를 Unix
기반 시스템에서는 Zombie
상태라고 부른다고 하네요.
Linux
의 프로세스 자료구조
linux
의 소스코드에도, process 를 나타내는 곳이 존재합니다.
원본 코드는 https://github.com/torvalds/linux/blob/master/include/linux/sched.h 에 있습니다. 마찬가지로 엄청난 수의 코드가 생략된 것입니다.
struct task_struct
{
////////////////
// 엄청난 생략.. //
////////////////
unsigned long atomic_flags; /* 원자적 접근이 필요한 플래그. */
struct restart_block restart_block;
/* 프로세스 ID: */
pid_t pid;
/* 프로세스 그룹 ID: */
pid_t tgid;
/* 실제 부모 프로세스: */
struct task_struct __rcu *real_parent;
#ifdef CONFIG_KEYS
/* 캐시된 요청 키. */
struct key *cached_requested_key;
#endif
////////////////
// 엄청난 생략.. //
////////////////
/*
* 실행 파일 이름, 경로 제외.
*
* - 일반적으로 setup_new_exec()에서 초기화됨
* - [gs]et_task_comm()으로 접근
* - task_lock()으로 잠금
*/
char comm[TASK_COMM_LEN];
////////////////
// 엄청난 생략.. //
////////////////
};
프로세스의 고유 아이디를 나타내는 pid
, 프로세스의 이름을 나타내는 comm
과 같은 변수들로 프로세스의 상태를 표현하고 있는 것을 확인할 수 있습니다.
요약
우리는 이번 포스트에서 프로세스의 개념에 대해 배웠습니다. 프로세스는 아주 간단히 말해 실행 중인 프로그램이었고, 운영체제는 일련의 과정을 통해 죽어 있는, 정적인 데이터와 명령어인 프로그램이라는 것에 생명을 불어넣어 프로세스라는 것을 만든다는 것도 배웠죠. 이제, 다음 포스트에서 우리는 운영체제가 프로세스를 구현하기 위해 어떤 기법을 사용하는지, 운영체제는 여러 개의 프로세스를 어떻게 스케쥴링하는지에 대해 알아볼 것입니다.