이번 챕터에서는 OS에서 제공해주는 Process란 무엇인지 살펴볼 것이다.
Process란, 단순히 '실행중인 프로그램'을 의미한다. 프로그램 그 자체는 디스크 위에 올라가 있는 명령어(+ 정적 데이터) 뭉치일 뿐, OS가 프로그램을 메모리로 load 해주어야 비로소 실제로 작동하는 프로세스가 되는 것이다.
우리가 보통 컴퓨터로 무언가를 할 때 수십 수백 개의 프로세스를 동시에 실행하는데, CPU가 하나, 또는 적어도 프로세스 개수보다는 훨씬 적은 수만큼만 있더라도 그걸 딱히 걱정하면서 컴퓨터를 사용하지는 않는다. 이건 앞에서 간단히 배운 CPU의 가상화 때문에 가능한 것인데, OS는 이걸 Time sharing(시분할) 기법을 사용해서 구현한다. (뒤에서 배운다!)
이러한 가상화를 구현하기 위한 규칙? 방법? 들을 low/high level 중 어느 영역에 가까운지에 따라 아래 두 유형으로 나눠서 부른다고 한다.
다시, 프로세스는 위에서 말했듯 OS에서 제공되는 '실행중인 프로그램'의 추상화된 개념이다.
이 프로세스의 구성 요소를 이해하려면, Machine state를 먼저 알아야 한다. machine state는 '프로그램이 실행될 때 읽거나 갱신할 수 있는 무언가'를 의미한다.
Memory
메모리 역시 machine state인데, 실행해야 할 명령이 메모리에 상주해 있어야 하고, 데이터를 읽고 쓰는 것도 메모리 위에서 수행되기 때문이다.
Address space
프로세스가 접근할 수 있는 주소 공간(virtual addrss space, 뒤에서 배운다.) 역시 프로세스의 일부이다.
기타 특수 레지스터
Introduction에서 배웠다시피, OS는 사용자가 OS의 기능을 사용할 수 있도록 API를 제공해야 한다.
Create
OS는 사용자가 새 프로세스를 생성할 수 있도록 지원해야 한다.
Destroy
보통 프로그램들은 작업을 마치면 알아서 종료되지만, 그렇지 않은 프로그램의 경우 사용자가 임의로 프로세스를 종료할 수 있도록 지원해야 한다.
Wait
종종 프로세스가 잠시 실행을 중단해야 할 일이 있다. I/O라든지, 전에 짧게 배운 Concurrency 문제를 해결하기 위해서라든지.
Miscellaneous Control
프로세스를 생성하고, 소멸시키고, 대기시키는 것 이외에도 프로세스를 '잠깐 멈추었다 다시 실행'하도록 하는 기능 따위를 제공할 수도 있다.
Status
프로세스가 얼마나 오래 실행되고 있는지, 어떤 상태값을 가지고 있는지를 확인할 수 있는 기능을 제공해야 한다.
프로세스가 생성 가능한 무언가라는 것도 알겠고, OS가 API로 프로세스의 생성을 지원한다는 것도 알겠는데, 프로그램이 어떻게 프로세스가 되는지는 아직 배우지 않았다. 천천히 살펴보자.
Load
프로세스를 생성하려면, 일단 OS가 disk 어딘가에 들어 있는 실행 파일의 '코드'와 '정적 데이터'를 메모리에, 그리고 프로세스의 주소 공간에 load 해야 한다. 예전에는 프로그램을 실행하기 전에 실행 파일 전체를 한 번에 load 했는데(eagerly), 요즘은 Paging, Swapping과 같은 방법을 통해 실행에 필요한 데이터만 load 한다고 한다(lazily).
Stack Allocation
코드와 정적 데이터가 메모리에 load 되면, 그 다음으로 프로그램에 따라 메모리로부터 run-time stack(=stack)을 할당받아야 할 수도 있다. C로 작성된 프로그램에서 이 stack 영역에는 지역 변수, 함수에 전달할 매개 변수, 반환 주소(return address)가 저장되는데, 이 값들은 main 함수에 주어지는 argc, argv에 의해 다르게 조정할 수 있다.
Heap Allocation
Stack과 함께 자주 언급되는 것 같은데, 메모리로부터 heap 또한 할당받아야 한다. C로 작성된 프로그램에서 heap 영역에는 동적으로 할당된(dynamically-allocated) 데이터가 저장된다. 즉, heap에는 연결 리스트, 해시 테이블, 트리 등 원시 자료형이 아닌 것들을 malloc()을 호출해 메모리 공간을 할당 받아 저장하고, free()를 통해 다시 명시적으로 할당된 공간을 돌려주는 것이다.
Initialization
이외에도 여러 초기화 작업을 해주어야 하는데, 특히 I/O에 관련된 프로그램의 경우 UNIX 시스템에선 기본적으로 표준 입력, 표준 출력, 에러를 위한 3개의 file descriptor를 가지도록 초기화된다. 이 file descriptor는 프로그램이 터미널로부터 들어오는 입력과 화면으로의 출력을 쉽게 수행할 수 있도록 하기 위한 요소인데, 뒤쪽의 persistence 쪽에서 자세히 배운다.
여기까지! 이런 저런 작업들을 다 마치고 나면 프로그램을 프로세스로서 실행할 준비가 다 된 것이다. 이제 C 프로그램의 진입점(entry point)인 main() 함수로 점프해서, OS가 CPU의 사용권을 넘겨주기를 기다리면 된다.
모든 프로세스는 세 가지의 상태(state) 중 반드시 하나를 가지도록 되어 있다.
드디어 위에서 줄기차게 이야기 하던 schedule이 정확히 무엇인지 알 수 있게 되었다. schedule이란, 프로세스가 ready 상태에서 running 상태로 전환되는 것을 의미한다. 이와는 반대로, deschedule은 프로세스가 running 상태에서 ready 상태로 전환되는 것을 의미한다.
Blocked는 논외인게, 만약 CPU를 사용하고 있지는 않은데 running 상태로 되어 있다면 실제로 CPU를 써야 하는 프로세스들이 있는데도 CPU가 놀고 있게 되고, ready 상태도 어차피 스케줄러에 의해 곧 running 상태로 schedule 될테니 같다. 이 때문에 blocked 상태가 있는 것이고, blocked 상태를 유발한 어떤 작업이 끝나면 프로세스가 다시 ready 상태로 전환되어 CPU에게 schedule 되기를 기다리게 되는 것이다!
이러한 규칙이 적용된 두 가지 예제를 살펴보자.
위 예제의 경우 Process 0(이하 p0)이 실행되고 있다가(running), 대기중(ready)이던 Process 1(이하 p1)이 스케줄러로부터 간택당하.. 기도 전에 p0이 종료되어 p1에게로 CPU의 사용권이 넘어간 것 같다. 아마도?
위 예제는 조금 더 복잡해 보이는데, 시점 3까지 p0가 잘 실행되고 있다가 I/O 요청이 발생해 CPU를 더이상 사용하지 않게 된 상황이다.
때문에 CPU를 더 굴려먹기 위해서 대기중이던 p1에게 CPU 사용권을 넘겨주고 있다가, 시점 4부터 시점 8까지 p1이 CPU를 적당히 잘 써먹고 작업을 완료해 시점 7부터 대기중이던 p0에게 CPU 사용권을 다시 넘겨준 것이다.
시점 7에서 p0이 I/O가 끝났다고 해서 바로 running 상태로 넘겨주는게 아니라, 일단 ready 상태로 전환해 놓고 스케줄러의 선택을 받을 때까지 기다리고 있다는게 중요하다.
OS도 하나의 프로그램이므로, 다른 프로그램과 마찬가지로 다양한 정보들을 추적하기 위한 데이터 구조를 가지고 있다.
예를 들자면, OS가 각각의 프로세스의 상태를 추적하기 위해, ready 상태의 모든 프로세스들에 대한 정보와 현재 실행중인 프로세스의 상태(아래서 언급할 PCB)들을 기록하는 Process list를 가지고 있기도 하고, blocked 상태인 프로세스의 상태를 추적하고 있다가, I/O가 끝나면 다시 적절한 과정을 거쳐 ready 상태로 바꿔주기도 한다.
아래는 xv6 커널에서 프로세스의 상태를 추적하기 위한 구조체로, Linux, Mac OS, widnows 등의 운영 체제에서도 비슷한 방법으로 이를 수행한다고 한다.
코드에서 context로 구현된 구조체는 Register context를 의미하는데, 프로세스가 중단되면 해당 메모리 공간에 중단 당시에 CPU에 쓰여져 있던 레지스터를 저장하기 위한 것이다.
해당 구조체를 사용해 다시 running 상태로 전환될 때 이전의 상태(register)를 CPU로 다시 복구함으로써 중단되었던 시점과 같은 상태에서 프로그램을 다시 실행할 수 있게 되는데, 자세한 내용은 뒤의 context switch 관련 내용에서 배운다.
그 아래에 있는 proc_state는 위에서 배운 running(RUNNIG), ready(RUNNABLE), blocked(SLEEPING)을 포함한 6가지의 process state를 enum으로 표현했다.
3개 상태 이외의 나머지는 아마.. 프로세스가 막 생성되었음을 표시하기 위한 상태(UNUSED, EMBRYO?)인 것 같고, 마지막 ZOMBIE status는 실행은 종료되었으나 아직 프로세스가 점유하고 있던 메모리 등이 다시 반환되지 않은 상태를 의미하는 것 같다. 읽어 보니 부모 프로세스가 자식 프로세스가 끝날 때까지 기다리고 있음을 표현하기 위해 사용할 수도 있다고 한다. 여하튼 이 3개는 그냥 훌렁훌렁 넘어가서.. 패스.
그 아래의 context 구조체와 proc_state enum을 member로 가지는 proc_state가 위에서 언급한 'OS가 프로세스의 상태를 추적하기 위한' 데이터 구조체인데, 개념적으로는 이걸 PCB(Process Control Block)이라고 부른다. 자세히 살펴보니 OS가 process를 unique하게 구분하기 위한 값인 PID도 member로 속해 있다.
프로세스란 무엇이고, 어떤 것으로 구성되어 있으며, 어떤 방법으로 OS에 의해 추적/관리 되는지 공부했다.
이제 프로세스가 뭔지는 대강 알 것 같은데, C언어 종속적인 개념들에서 조금씩 멈칫했다.
구글.. 구글링이.. 필요해..