Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.
프로세스는 실행 중인 프로그램이다. 프로그램은 그 자체로는 lifeless한, 그냥 디스크에 있는 명령어 덩어리이지만, OS는 이러한 프로그램을 실행하고 사용할 수 있도록 한다. 실제로 보통 시스템들은 수십에서 수백 개의 프로세스들을 동시에 실행하고 있는데, 우리는 OS 덕에 CPU가 사용 가능한지를 신경쓰지 않고도 시스템을 쉽게 사용할 수 있다.
물리 CPU는 한정되어 있는데, OS는 어떻게 무한히 많은 CPU들을 제공하고 있다는 환상을 제공할 수 있을까?
OS는 CPU 가상화를 통해 이런 환상을 제공하는데, 한 프로세스를 실행하고 멈추고 다른 것을 실행하고 멈춰, 많은 가상 CPU들이 있는 것처럼 보이게 하는 것이다. 이를 가리켜 CPU 시분할(time sharing)이라 하며, 이를 통해 사용자들은 동시에 사용하고자 하는 여러 프로세스들을 사용할 수 있게 한다.
CPU 가상화를 잘 구현하기 위해, OS는 low-level machinery와 high-level intelligence를 모두 필요로 한다. 여기서 low-level machinery를 가리켜 메커니즘(mechanism) 이라 하고, high-level intelligence를 가리켜 정책(policy) 라고 한다.
메커니즘이란 특정한 기능성을 구현하기 위한 low-level의 메서드나 프로토콜을 가리키는데, 예를 들면 문맥 전환(context switch)이 그렇다. 문맥 전환이란 OS가 한 프로세스를 멈추고 다른 프로세스를 실행할 수 있도록 하는 일련의 기술이다.
이러한 메커니즘들 위에 정책이 있는데, 정책이란 어떤 일이 일어나지를 결정하는 알고리즘이라 할 수 있다. 예를 들어 CPU에서 돌아갈 수 있는 프로그램들의 수가 주어졌을 때 어떤 프로그램을 실행할지는 스케줄링 정책에 의해 결정된다.
프로세스는 실행 중인 프로그램이며, 이러한 프로세스를 구성하는 게 무엇인지 이해하기 위해서는 machine state를, 프로그램이 실행 중일 때 어떤 것을 읽거나 쓸 수 있는지를 이해해야 한다.
machine state의 중요한 구성 요소 중 하나는 메모리다. 메모리에는 명령어와 실행 중인 프로그램이 읽고 쓰는 데이터들이 있다.
많은 명령어들은 레지스터를 읽고 쓰기 때문에, 이 레지스터들도 프로세스 실행에 있어 중요하다. 이런 machine state를 이루는 레지스터들 중에는 특별한 것들이 있다.
프로그램은 때로 하드 드라이브나 SSD와 같은 영구 저장소에도 접근한다.
프로세스 API에 대해서는 나중에 자세하게 다루게 되겠지만, 여기서는 모든 OS의 인터페이스에 포함되어야 할 기본적인 아이디어들을 소개한다.
OS는 어떻게 프로그램을 프로세스로 만들까? 프로세스의 생성은 어떻게 이루어질까?
초기의 간단한 OS에서 프로세스 로딩은 eager하게 이루어졌다. 즉 프로그램이 실행되기 전에 모두 올리는 방식이었다.
이와 달리 현대 OS는 lazy하게, 즉 코드 조각과 데이터들을 프로그램 실행에 필요한 시점에 로딩한다. 어떻게 이렇게 lazy한 로딩이 가능한지를 이해하기 위해서는 paging과 swapping에 대해 이해해야 하며, 관련 내용은 나중에 다뤄진다.
코드와 정적 데이터가 메모리에 올라가고 나서, OS는 다음의 작업들을 한다.
main()
함수의 argc
, argv
배열 파라미터를 주어진 인자로 채운다.OS는 프로그램의 힙에 해당하는 메모리를 할당한다.
malloc()
으로 할당하고, free()
로 해제한다.malloc()
에 의해 메모리가 할당됨에 따라 점점 커진다.OS는 다른 초기화 작업들, 특히 I/O와 관련된 초기화 작업들을 수행한다.
위의 과정들을 통해 OS는 프로그램 실행을 위한 단계를 설정한다. 마지막 남은 작업은 프로그램 실행의 진입점인 main()
함수를 실행하는 것이다. main(
) 루틴을 실행함으로써 OS는 CPU의 소유권을 새롭게 만들어진 프로세스에 넘기고, 프로그램이 실행되기 시작한다.
프로세스는 한 시점에 여러 상태들 중 하나의 상태를 가진다.
프로세스는 OS의 재량에 따라 Ready와 Running 상태를 오간다. 프로세스가 Running 상태에서 Ready 상태로 옮겨가면 프로세스가 Descheduled 되었다고 하고, 반대로 프로세스가 Ready에서 Running 상태로 옮겨가면 Scheduled 됐다고 한다.
Running 상태의 프로세스는 한 번 블럭되면, 특정 이벤트가 일어날 때까지 해당 상태를 유지한다. 만약 이벤트가 발생하면 프로세스는 Ready 상태로 가고, OS의 결정에 따라 Running 상태로 간다.
OS도 프로그램이고, 따라서 다른 프로그램드과 같이 관련있는 여러 정보 조각들을 추적하기 위한 자료 구조를 가지고 있다. 예를 들어 각 프로세스의 상태를 추적하기 위해 OS는 Ready 상태의 모든 프로세스들과 현재 어떤 프로세스가 Running 중인지에 대한 추가 정보를 담은 일종의 프로세스 리스트를 가지고 있다. 또한 OS는 Blocked 상태의 프로세스들도 추적해야 한다.
아래는 xv6 커널에서 프로세스 추적을 위해 사용하기 위해 OS에 필요한 정보들을 보여준다. 실제 OS에서도 비슷한 프로세스 구조들을 사용한다.
xv6는 MIT PDOS lab에서 만든 x86 및 RISC-V를 위한 교육용 운영체제다.
// xv6에서 프로세스를 중단하거나 재시작하기 위해 사용하는 레지스터들
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// 프로세스가 속할 수 있는 상태들
enum proc_state { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// xv6에서 추적하는 각 프로세스의 정보들.
struct proc {
char *mem; // 프로세스 메모리의 시작점
uint sz; // 프로세스 메모리의 크기
char *kstack; // 이 프로세스의 커널 스택의 bottom
enum proc_state state; // 프로세스 상태
int pid; // PID
struct proc *parent; // 부모 프로세스
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // 열린 파일들
struct inode *cwd; // 현재 디렉토리
struct context context; // 프로세스 실행을 위한 컨텍스트
struct trapframe *tf; // 현재 interrupe를 위한 트랩 프레임
};
register context는 멈춘 프로세스들의 레지스터 내용들을 담고 있다. 프로세스가 정지하면 각 레지스터는 메모리 위치에 저장되고, 이 값들을 다시 불러와 레지스터에 옮김으로써 OS는 프로세스를 다시 실행할 수 있게 된다.
코드에서 볼 수 있듯 프로세스는 위의 Running, Ready, Blocked가 아닌 다른 상태들도 가질 수 있다.
wait()
)을 하고 자식 프로세스의 완료를 대기하고, OS에 사라질 프로세스와 관련된 자료 구조들을 클린업 해도 좋다고 알린다.