프로세스란 실행 중인 프로그램을 의미하며, 프로그램 자체는 그저 디스크에 있는 명령어와 정적 데이터의 집합이다. OS는 이들을 가져와 실행 가능한 형태로 만들어 프로그램을 유용하게 사용할 수 있게 한다.
하지만 하나의 프로그램만 실행하는 것이 아니라 여러 개의 프로그램을 동시에 실행해야 하는 경우가 많다. 이를 위해 OS는 CPU를 가상화하여 많은 가상 CPU가 존재하는 것처럼 보이게 한다. 이를 위해 시간 분할(time sharing)이라는 기술을 사용하는데, 하나의 프로세스를 실행한 후 중지시키고 다른 프로세스를 실행하는 방식으로 CPU를 시간적으로 분할하여 가상 CPU가 존재하는 것처럼 보이게 한다.
CPU를 가상화하기 위해서는 낮은 수준에서의 매커니즘이 필요하며, 이를 위해 매커니즘이라는 용어를 사용한다. 매커니즘은 필요한 기능을 구현하는데 사용되는 낮은 수준의 메소드나 프로토콜을 의미한다. 이러한 매커니즘 위에는 OS에서의 여러 가지 결정을 하는 정책(policies)이 존재한다. 이러한 정책은 OS에서 결정을 내리는 알고리즘으로, CPU에서 실행 가능한 여러 프로그램 중 어떤 프로그램을 실행할지 결정하는 등의 의사 결정을 수행한다.
1. The Abstraction: A Process
OS에서 제공하는 프로세스 추상화는 실행 중인 프로그램을 의미한다. 프로세스는 실행 중인 프로그램으로, 실행 과정에서 해당 시스템의 다양한 구성 요소에 접근하거나 영향을 미치는 것으로 요약할 수 있다.
프로세스를 이해하려면 프로그램이 실행될 때 읽거나 업데이트할 수 있는 기계 상태(machine state)를 이해해야 한다. 어떤 시점에서 프로그램 실행에 중요한 기계 부분은 무엇일까? 이것이 프로세스로 구성되는 것이다.
프로세스로 구성되는 기계 상태의 한 가지 예는 메모리이다. 명령어는 메모리에 있으며, 실행 중인 프로그램이 읽거나 쓰는 데이터도 메모리에 있다. 따라서 프로세스가 접근할 수 있는 메모리(주소 공간)는 프로세스의 일부이다.
레지스터도 프로세스의 기계 상태 중 하나이다. 많은 명령어가 레지스터를 명시적으로 읽거나 업데이트하기 때문에 레지스터는 프로세스 실행에 중요하다.
그리고 프로세스의 기계 상태 중 특히 중요한 레지스터도 있다. 예를 들어, 프로그램 카운터(PC)는 다음에 실행될 프로그램 명령어를 알려준다. 또한 스택 포인터와 관련된 프레임 포인터는 함수 매개변수, 지역 변수, 반환 주소 등을 위한 스택을 관리하는 데 사용된다.
마지막으로, 프로그램은 종종 지속적인 저장 장치에도 액세스한다. 이러한 I/O 정보는 프로세스가 현재 열어둔 파일 목록과 같은 것을 포함할 수 있다.
2. Process API
• 생성(Create): 운영 체제는 새로운 프로세스를 생성하는 방법이 포함되어야 한다. 셸에 명령어를 입력하거나 응용 프로그램 아이콘을 더블 클릭하면 OS가 호출되어 지정한 프로그램을 실행할 새 프로세스를 생성한다.
• 종료(Destroy): 프로세스 생성 인터페이스가 있듯이, 시스템은 프로세스를 강제로 종료시키는 인터페이스도 제공한다. 물론 많은 프로세스는 완료되면 그 자체로 종료되지만, 종료되지 않는 프로세스를 종료하려는 경우에는 장애 발생 프로세스를 중단시키는 인터페이스가 매우 유용하다.
• 대기(Wait): 때로는 프로세스가 실행을 멈출 때까지 기다리는 것이 유용하므로, 어떤 종류의 대기 인터페이스가 자주 제공된다.
• 기타 제어(Miscellaneous Control): 종료하거나 프로세스를 대기시키는 것 외에도, 때로는 다른 제어가 가능하다. 예를 들어 대부분의 운영 체제에서는 프로세스를 일시 중지하고(일시적으로 실행 중지) 다시 시작할 수 있는 방법이 제공된다.
• 상태(Status): 일반적으로 프로세스에 대한 상태 정보를 얻을 수 있는 인터페이스도 제공된다. 예를 들어, 프로세스가 실행된 시간이나 어떤 상태에 있는지와 같은 정보를 얻을 수 있다.
3. Process Creation: A Little More Detail
프로그램을 프로세스로 바꾸려면 운영 체제가 작업을 수행해야 한다. 이를 위해서 먼저 프로그램 코드와 정적 데이터(초기화된 변수 등)를 메모리에 로드해야 한다. 초기에는 프로그램이 디스크에 있고, 운영 체제는 실행 가능한 형식으로 로드하여 메모리에 적재한다. 이로 인해 디스크에서 해당 바이트를 읽어서 메모리에 어딘가에 놓는 과정이 필요하다. 이후에는 프로그램의 런타임 스택(혹은 스택)을 위해 메모리를 할당해야 한다. C 프로그램에서 스택은 지역 변수, 함수 매개 변수, 반환 주소 등을 위해 사용된다. 운영 체제는 이를 할당하고, main() 함수의 인수로 전달되는 argc 및 argv 배열을 초기화해야 한다.
또한 운영 체제는 프로그램의 힙(heap)을 위한 메모리도 할당할 수 있다. C 프로그램에서 힙은 명시적으로 요청된 동적으로 할당된 데이터를 위해 사용된다. 프로그램은 malloc()을 호출하여 이를 요청하고, free()를 호출하여 명시적으로 해제한다. 힙은 연결 리스트, 해시 테이블, 트리 등과 같은 데이터 구조에 필요하다. 힙은 처음에는 작을 것이다. 프로그램이 실행되면서 malloc() 라이브러리 API를 통해 메모리를 더 요청하면 운영 체제가 프로세스에 더 많은 메모리를 할당하여 이를 지원할 수 있다.
또한, 운영 체제는 입력/출력(I/O)과 관련된 초기화 작업도 수행한다. 예를 들어, UNIX 시스템에서 각 프로세스는 기본적으로 표준 입력, 출력 및 오류를 위한 세 개의 개방된 파일 기술자(file descriptor)를 갖는다. 이러한 기술자는 프로그램이 터미널에서 입력을 읽고 화면에 출력할 수 있도록 한다.
프로그램 코드와 정적 데이터를 메모리에 로드하고 스택과 힙을 초기화하고 I/O 설정과 관련된 작업을 수행하는 등의 일련의 작업 이후에, 운영 체제는 프로그램 실행의 시작점인 main() 함수로 점프하여 CPU 제어를 새로 생성된 프로세스에게 이전한다. 이로써 프로그램이 실행되며 프로세스가 생성된다.
4. Process States
프로세스가 가질 수 있는 세 가지 상태에 대해 알아보자. 프로세스가 실행 중인 Running 상태, 실행 준비가 되어있지만 현재는 실행되지 않는 Ready 상태, 그리고 어떤 작업이 완료되기 전까지 실행되지 못하고 대기 중인 Blocked 상태이다. 이러한 상태들은 운영 체제(OS)가 프로세스를 관리하는 과정에서 상호 전환할 수 있다. Running 상태에서 Ready 상태로 전환될 경우, 운영 체제는 이전에 실행하던 프로세스를 중단시켜야 하며, Blocked 상태에서 Ready 상태로 전환될 경우, 어떤 이벤트가 발생할 때까지 해당 프로세스는 계속해서 대기 상태로 유지된다.
이러한 상태 전이를 그래프로 나타내면, 그림 4.2와 같이 나타낼 수 있다. 이 그래프를 보면, 프로세스가 Ready 상태와 Running 상태 사이를 왔다갔다 할 수 있으며, Blocked 상태에서는 다른 프로세스가 CPU를 사용할 수 있다는 것을 알 수 있다. 이와 비슷하게, CPU를 사용하는 두 개의 프로세스가 있다고 가정해보자. 이 경우, 각 프로세스의 상태를 추적해보면 그림 4.3과 같이 나타낼 수 있다. 또 다른 예시로는, 한 프로세스가 I/O 작업을 요청하면, 해당 프로세스는 Blocked 상태가 된다. 이 때, 운영 체제는 다른 프로세스를 실행시켜 자원 이용률을 향상시키고, I/O 작업이 완료되면 다시 Ready 상태로 전환시키는 등의 다양한 결정을 내려야 한다. 이러한 결정은 운영 체제 스케줄러에 의해 수행되며, 이에 대해서는 이후 몇 개의 장에서 논의할 예정이다.