부모 프로세스가 자식 프로세스를 생성하는 것을 프로세스 생성이라고 한다. 리눅스, 유닉스 계열의 OS에서는 최초의 프로세스 init이 존재하고, 그 프로세스가 다른 자식 프로세스를 생성할 수 있고 그 자식 프로세스는 또 다른 자식 프로세스를 생성할 수 있다. 이 때 프로세스의 생성에는 사용자 프로그램이 아니라, 운영체제의 시스템 콜을 통해서만 가능하다. 이렇게 생성된 프로세스는 트리 구조를 형성하고 각자 고유한 번호인 pid를 가지게 된다.
프로세스의 생성은 두 가지 단계로 나뉘어진다.
1. 자식 프로세스가 부모 프로세스의 code, data, stack 그리고 cpu 문맥(프로그램 카운터), 운영체제 데이터, pcb등 문맥을 모두 복사한다.
2. 자식 프로세스는 복제 후 공간에 새로운 프로그램을 올린다.
유닉스에서는 위 단계와 같이 fork() 시스템 콜이 새 프로세스를 생성하고, 그 다음 exec() 시스템 콜을 통해 새로운 프로그램을 메모리에 올린다.
프로세스는 자원을 필요로 하는데, 운영체제로부터 받거나 부모와 공유한다. 이에 대한 프로세스의 모델은 세 가지가 있다.
보통 전혀 공유하지 않는 모델이 일반적이다.
리눅스같은 운영체제에서는, 프로세스를 생성할 때 부모의 주소 공간을 완전히 복사하지 않고 공유하고 있다. 그렇게 공유하고 있다가, 새로운 변경점(내용이 바뀐다던지)이 생기면 그 때 일부분을 복사하게 된다.(copy-on-wirte)
프로세스 실행에는 두 가지가 존재한다.
프로세스의 종료에는 두 가지가 존재한다.
자발적 종료
부모 프로세스가 자식의 수행을 종료시킨다. -> abort
자식이 할당 자원의 한계치를 넘어서거나 자식에게 할당된 태스크가 더 이상 필요하지 않은 경우
부모가 종료(exit)하는 경우
-> 운영체제는 부모 프로세스가 종료하는 경우 자식이 더 이상 수행하도록 두지 않는다.
-> 맨 밑에 자식부터 차례로 전부 종료한다. (단계적인 종료)
-> 이 때 종료되지 못하고 남아버린 자식 프로세스는 고아 프로세스라고 한다.
fork() 시스템 콜은 부모 프로세스로부터 자식 프로세스를 만드는 시스템 콜이다.
int main() {
int pid;
pid = fork();
if (pid == 0) {
printf("i am child");
} else if (pid > 0) {
printf("i am parent");
}
}
위 코드는 fork() 시스템 콜을 이용해 새 프로세스를 생성하는 코드다. fork()를 실행하면, 자식 프로세스가 생성되게 되고, 똑같이 복사된 위 코드를 실행하게 된다. 하지만 위에서 설명했듯 자식 프로세스는 부모 프로세스의 cpu 문맥(프로그램 카운터)도 복사하므로, 코드의 진행상황도 복사하게 된다. 따라서 처음부터 코드를 실행하게 되는 것이 아니라 pid = fork(); 이후의 코드부터 진행하게 된다.
fork() 시스템 콜은 부모 프로세스의 경우 양수 pid를 return하고, 자식 프로세스의 경우 0을 return하므로 그것을 if문을 통하여 처리한 예제다.
exec() 시스템 콜은 어떤 프로그램을 새로운 프로그램으로 덮어 씌워버리는 시스템 콜이다.
int main() {
int pid;
pid = fork();
if (pid == 0) {
execlp("/bin/date", "bin/date", (char*)0);
} else if (pid > 0) {
printf("i am parent");
}
}
위 코드에서 fork() 시스템 콜을 통해 새 자식 프로세스가 만들어지게 되면, 자식 프로세스는 execlp() 시스템 콜을 실행하게 된다. 이 시스템 콜은 "/bin/date" 위치에 존재하는 프로그램을 실행하라는 뜻으로, 코드를 수행하다 이 시스템 콜을 만나면 기존 코드들은 전부 사라지고 시스템 콜로 설정한 새 프로그램(코드)으로 덮어 씌워지게 된다.
wait() 시스템 콜은 위에서 설명한 "자식이 종료될 때까지 부모가 wait하는 모델"에 사용된다.
프로세스 A가 wait() 시스템 콜을 호출하면, 커널은 자식 프로세스가 종료될 때까지 프로세스 A를 Sleep(Block)시킨다.
그 후 자식 프로세스가 종료되면 커널은 프로세스 A를 깨운다.(Ready)
int main() {
int pid;
pid = fork();
if (pid == 0) {
/* child process doing */
} else if (pid > 0) {
wait();
}
}
프로세스를 종료하는 시스템 콜이다.
위에서 설명했듯 프로세스는 두 가지 종류의 종료가 있다.
하나는 자발적 종료다. 우리가 짠 코드는 거의 자발적 종료인데, 우리가 exit() 시스템 콜을 넣지 않아도 컴파일러는 main함수가 return되는 위치에 exit() 시스템 콜을 넣어준다.
나머지는 비자발적 종료다. 비자발적 종료는 외부에서 프로세스를 강제 종료시킨 경우를 말하며 예시로 부모 프로세스가 자식 프로세스를 강제종료시킨 경우와 추가로 키보드로 kill, break 등을 친 경우에도 강제 종료할 수 있다.
프로세스는 두 가지로 구분된다.
독립적 프로세스
협력 프로세스
협력 프로세스에서 설명하는 프로세스 협력 메커니즘을 IPC(interprocess communication)이라고 한다. IPC에는 두 가지 종류가 있다.
하나는 메시지를 전달하는 방법(message passing)으로, 이 방법은 각 프로세스가 커널을 통해 메시지를 전달하여 통신한다. 이 방법에는 두 가지 종류가 있다.
direct communication
indirect communicaiton
direct communication은 해당 프로세스에게 메시지가 바로 전달될 수 있지만, indirect communicaiton은 딱히 통신하려는 프로세스의 이름을 명시하지 않으므로, 모든 프로세스에게 메시지가 전달될 수 있다.
나머지는 주소 공간을 공유하는 방법으로, 각 프로세스가 독립적인 주소 공간을 가지고 있는 것이 아니라, 일부는 독립적이고 일부는 공유하게 한다. 이 방법에도 명확한 구분은 아니지만, 두 가지 종류가 존재한다.
sharded memory
thread
메시지 전달 방법과 주소 공간 공유 방법을 그림으로 나타낸 것이다.
반효경 교수님의 운영체제 강의를 바탕으로 작성했습니다