Process의 생성과 소멸의 과정을 프로그래밍 코드와 함께 알아보자.
프로세스의 생성의 이유는 다음과 같다.
- New Batch Job: disk 또는 tape에 있는 JCL와 같은 job을 처리하기 위한 process
- Interactive log-on: 사용자가 운영체제에서 로그인을 하면 권한이 부여되며 이는 process 생성과 같다.
- Created by OS to provoke a service: 커널이 서비스를 지원하기 위해 process를 생성한다.
- Spawned by existing process: 존재하는 프로세스에 의해서 새로운 프로세스가 만들어진다.
- 새로운 메모리 공간을 할당: PCB와 a.out을 저장한다.
- a.out을 메모리에 저장: code, data를 저장하고 procedure call을 위한 stack 할다
- PCB 초기화: PCB를 생성하고 값(e.g. pid, state)을 초기화한다.
- read-list에 PCB를 저장한다.
유닉스 계열에선 위와 같은 과정을 첫 번째 process만 수행하고, 이후에 생성되는 자식 프로세스들은 fork를 통해서 새로운 프로세스를 만들 수 있다. 이 때, 부모 프로세스의 정보의 일부분을 cloning한다.
그렇다면 위와 같은 tree 형태로 나타낼 수 있다. 이 때 root의 프로세스를 init process 또는 Super parent라고도 불리운다.
fork()를 통해 프로세스를 만들 때, 위에서 본 과정을 그대로 수행하지 않고 부모 프로세스의 정보를 복사한다.
그리고 pid와 같은 일부 필수 정보만 수정한다.
bash -ps 는 프로세스의 관계를 확인할 수 있는 유닉스 계열의 명령어이다.
한 프로세스에서 fork() system call을 호출하면 커널은 sys_fork() system handler를 호출한다.
이 system handler는 부모, 자식 프로세스에 두 번 리턴한다.
(리턴값은 각각 자식 프로세스의 pid, 0이다.)
이후 프로세스들을 다른 프로그램으로 교체될 수 있도록 execve() 함수를 사용할 수 있다.
인자로 경로를 전달하며 여기엔 다른 프로그램이 존재한다.
getty는 유닉스의 interactive log-on 부분이다. 터미널 상에서 커맨드로 로그인을 하고, 쉘을 실행한다.
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int g_variable = 100; void main(){ int l_variable = 0; int pid; printf("[%d] Hello \n", getpid()); g_variable++; l_variable++; pid = fork(); if ( pid > 0) { printf("Parent: created a child(pid = %d) \n", pid); g_variable++; l_variable++; printf("Child: l_variable = %d, g_variable:%d \n", l_variable, g_variable); } else if (pid == 0) { printf("Child: pid = %d, ppid = %d\n", getpid(), getppid()); g_variable++; l_variable++; printf("Child: l_variable = %d, g_variable:%d \n", l_variable, g_variable); } printf("[%d] Bye \n", getpid()); }
$ gcc fork1.c
[3265] Hello
Child: pid = 3266, ppid = 3265
Child: l_variable = 2, g_variable:102
[3266] Bye
Parent: created a child(pid = 3266)
Child: l_variable = 2, g_variable:102
[3265] Bye
위의 결과값을 토대로 process는 data, stack segment는 다르다. 하지만 code는 read-only 속성을 가지고 있기 때문에 공유하여 관리한다.
리눅스와 같은 현대 OS에세 프로세스를 생성할 때 전부 복사하는 것이 아니라 일단 공유한다.
데이터의 수정이 필요할 때만 page 단위로 복사한다.
(저 page 단위의 table 부분은 가상 메모리 때 배운다.)
속도, 공간 모두 효율적인 방법이다.
프로세스의 종료는 자발적, 비자발적 종류 두 가지이다.
- 자발적 종료로 exit() system call을 커널에게 요청 or main 함수의 끝(exit 자동 호출)
프로세스의 I/O 버퍼, open file, 메모리 자원들이 모두 할당 해제된다.- 비자발적 종료로 부모 프로세스나 OS에 의해서 종료될 수 있다.
kill(pid, signal) system call 또는 abort() system call을 통해 수행할 수 있다.
부모프로세스가 자식 프로세스를 대개 삭제하는 일이 있는데, 자식 프로세스가 더 이상 필요하지 않거나, 버그가 있거나, OS에 따라 부모 프로세스가 종료될 때 소멸된다.
Zombie process란 zombie 상태의 프로세스를 의미한다.
프로세스가 소멸되었는 데, 커널이 프로세스의 PCB와 같은 정보를 가지고 있는 상태를 의미한다.
exit status란 return 0과 같이 0이 의미하는 것은 종료가 잘 되었다는 것이다.
이는 부모 프로세스에게 전달되는데, 이러한 이유는 종료 상태값을 통해서 shell script에서 활용할 수 있다.
이를 위해 zombie state가 존재한다. 부모 프로세스가 종료 상태값을 가져가야 완전히 소멸되며, 부모가 가져갈 때까지 이 상태가 유지된다.
자식 프로세스가 zombie process가 되면 signal을 전송하며 이를 wait 함수를 사용하여 값을 가져간다. 이를 reaping이라 부른다
자식 프로세스가 exit 하고 reaping할 때까지 또는 부모 프로세스가 소멸될 때까지를 zombie state라 부른다
pid = wait(&status); pid = waitpid(-1, &status, WNOHANG)) //non blocking
부모 프로세스가 자신보다 일찍 종료되면 고아 프로세스가 되고 init process가 부모 프로세스로 교체된다. 이를 reparent라 부른다.
exit system call이 호출되면 cleanup handler가 호출이 된다. 이후 zombie state가 되고 exit status가 커널에 저장된다.
시스템 자원들이 해제되고 부모 프로세스에게 종료되었다는 signal을 전송한다.
이로 인해 자식 프로세스들이 고아가되면 init process로 교체된다.