Unix는 C 프로그램에서 프로세스를 제어하기 위한 많은 System Call을 제공한다.
Obtaining Process IDs
각 프로세스는 고유한 양의 정수인 Process ID(PID)를 가지고 있다.
- pid_t getpid(void)
- 현재 프로세스의 PID를 반환한다.
- pid_t getppid(void)
- 부모의 PID를 반환한다.
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
getpid와 getppid는 type pid_t의 정수를 반환한다. Linux 시스템에서 pid_t는 type.h에 int로 정의되어 있다.
Process Status
프로그래머의 관점에서 프로세스는 세 가지 상태 중 하나라고 생각할 수 있다.
Running
프로세스는 CPU에서 실행 중이거나 대기 중이며 커널에 의해 최종적으로 스케줄된다.
Stopped
프로세가 일시 정지되어 스케줄링되지 못하는 상태이다.
SIGSTP, SIGTSTP, SIGTTIN, SIGTTOU 신호를 받아 프로세스가 정지하고 SIGCONT 신호를 받을 때까지 프로세스가 정지된 상태로 유지되며 SIGCONT 신호를 받으면 프로세스가 다시 실행된다.
Terminated
프로세스가 영구적으로 중지된 상태이다.
Terminating Processes
프로세스 세 가지 이유 중 하나로 종료된다.
- 프로세스의 종료 신호를 받는 것.
- main 루틴에서 복귀하는 것.
- 종료 함수를 호출하는 것.
void exit(int status)
#include <stdlib.h>
void exit(int status);
- 프로세스의 상태를 종료 상태로 하여 프로세스를 종료시킨다.
- 정상적으로 종료되면 status를 0으로 반환, 에러 발생 시 0이 아닌 값을 반환
- 종료 상태를 설정하는 다른 방법은 main 루틴에서 정수 값을 반환하는 것이다.
ex) int main(){ return 0;}
exit 함수는 한 번만 호출되고 반환되지 않는다.
Creating Processes
부모 프로세스는 fork 함수를 호출하여 실행 중인 새로운 자식 프로세스를 만든다.
int fork(void)
- 자식 프로세스에 0을 반환하고 부모 프로세스에 자식 프로세스의 PID를 반환한다.
- 자식 프로세스는 부모 프로세스와 거의 동일하다.
- 자식 프로세스는 부모 프로세스와 같은 가상 주소를 메모리의 부모와 다른 공간에 복사한다.
- 자식 프로세스는 부모 프로세스와 같은 File descriptors를 가진다.
- 자식 프로세스와 부모 프로세스의 PID는 다르다.
새로 생성된 자식 프로세스는 거의 동일하지만 완전히 동일하지 않다.
자식 프로세스는 Code와 Data Segment, Heap, 공유 라이브러리 및 User Stack을 포함하여 부모 프로세스의 가상 주소 공간을 복사하여 가져온다.
자식 프로세스는 부모 프로세스와 같은 File Descriptor를 얻었기 때문에 자식 프로세스는 부모 프로세스에서 열려 있던 파일을 읽고 쓸 수 있다.
fork 함수는 한 번 호출되지만 호출한 프로세스(부모)와 새로 생성된 자식 프로세스에서 반환하기 때문에 복잡하다.
상위 프로세스에서 fork는 하위 프로세스의 PID를 반환하고, 자식 프로세스에서 fork는 0을 반환한다.
하위 프로세스의 PID는 항상 0이 아니기 때문에 반환 값은 프로그램이 상위 프로세스와 하위 프로세스 중 어느 쪽에서 실행되고 있는지 알 수 있는 방법이다.
fork Example
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x);
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x);
exit(0);
}
linux> ./fork
parent: x=0
child : x=2
pid = Fork()에서 자식 프로세스가 생성되고 부모 프로세스는 pid에 자식 프로세스의 PID를 반환받고 자식 프로세스는 pid에 0을 반환받는다. 부모 프로세스가 먼저 실행된다면 pid가 0이 아니므로 parent: x=0을 출력하고 종료된다.
다음으로 자식 프로세스가 실행되면 if 조건문에서 child : x = 2를 출력하고 종료된다.
이 간단한 예시에 몇 가지 미묘한 측면이 있다.
Call Once, return twice
fork 함수는 부모 프로세스에 의해 한 번 호출되지만 부모 프로세스에 한 번, 새로 생성된 자식 프로세스에게 한번, 총 두 번 반환한다. 자식 프로세스가 한 개인 프로그램에서 매우 간단하지만, fork를 여러 번 호출하는 프로그램은 혼란스러울 수 있다.
Concurrent Execution
상위 프로세스와 하위 프로세스는 동시에 실행된다. Logical Control Flow의 명령은 커널에 의해 임의로 끼워질 수 있다. 시스템에서 프로그램을 실행하면 부모 프로세스가 먼저 printf 문을 완료하고 다음으로 자식의 printf문을 완료한다.
단, 다른 시스템에서 반대의 경우도 있다.
부모 프로세스가 먼저 실행될지 자식 프로세스가 먼저 실행될지 예측할 수 없다.
Duplicate but sepaate address space
각 프로세스에서 fork 함수가 반환된 직후에 부모와 자식 프로세스 모두 정지할 수 있다면 각 프로세스의 주소 공간이 동일한 것을 발견할 수 있다.
부모 프로세스와 자식 프로세스는 동일한 User Stack, 지역 변수 값, heap, 전역 변수 및 동일한 코드를 가진다.
따라서 예제 프로그램에서는 fork 함수가 반환될 때 로컬 변수는 부모 프로세스와 자식 프로세스에서 모두 1이 된다.
단, 부모와 자식 프로세스는 별개의 프로세스이기 때문에 각각 독립적인 private 주소 공간을 가진다.
부모 또는 자식 프로세스에서 x에 대한 변경은 private 하고 각 프로세스는 다른 메모리 주소 공간을 가지기 때문에 각 프로세스의 메모리에 반영되지 않는다.
Shared files
예시 프로그램에서 부모 프로세스와 자식 프로세스는 모두 출력 결과를 화면에 출력한다. 그 이유는 자식 프로세스가 부모 프로세스의 열린 파일을 모두 상속하기 때문이다. 부모가 fork를 호출하면 stdout 파일이 열려 화면으로 이동한다.
자식 프로세스가 이 파일을 상속하므로 출력도 화면으로 향한다.
Modeling fork with Process Graphs
프로세스 그래프는 concurrent 프로그램에서 부분적인 순서를 확인하는 데 유용한 도구이다.
- 각 vertex는 프로그램 statement의 실행에 대응된다.
- a->b는 b가 발생하기 전에 a가 발생한다는 의미이다.
- Edges는 변수의 현재 값으로 label 될 수 있다.
- 각 그래프는 main을 호출하는 상위 프로세스에 대응하는 vertex로 시작한다.
- printf vertice는 출력으로 label될 수 있다.
- 각 그래프는 inedge가 없는 vertex로 시작한다.
- 프로세스의 vertex는 종료 호출에 대응하는 vertex로 끝난다.
Process Graph Example
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x);
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x);
exit(0);
}
Reaping Child Processes
Idea
- 프로세스가 종료될 때, 시스템 리소스는 여전히 사용되고 있다.
- Ex) Exit Status, various OS tables
- 이러한 프로세스를 Zombie 프로세스라고 한다.
Reaping
- 종료된 자식 프로세스의 부모 프로세스에 의해서 수행된다. 이때 wait, waitpid 함수를 사용한다.
- 부모 프로세스는 종료 상태에 대한 정보를 받는다.
- 그다음 커널이 좀비 프로세스를 삭제한다.
What if parent doesn't reap?
자식 프로세스를 reaping 하지 않고 부모 프로세스가 종료되면 남은 자식 프로세스는 PID가 1인 init 프로세스에 의해 reap 된다. 셸이나 서버와 같은 장기 실행 프로그램은 항상 좀비 프로세스를 받아야 한다. 좀비 프로세스는 실행되고 있지 않지만 시스템 메모리 리소스를 소비한다.
init 프로세스는 시스템 부팅 시 커널에 의해 생성되며 종료되지 않는다. 또한 모든 프로세스의 상위 프로세스다.
Zobie Example
wait: Synchronizing with Children
자식 프로세스를 reaping 하기 위해 wait 함수를 호출한다.
wait을 호출하는 프로세스는 자식 프로세스가 종료될 때까지 중지한다.
int wait(int *child_status)
- 자식 프로세스 중 하나가 종료될 때 까지 현재 프로세스를 중지한다.
- 종료된 자식 프로세스의 pid를 반환한다.
- child_status!= NULL 이면 자녀가 종료된 이유와 종료 상태를 나타내는 값을 설정한다.
- wait.h에 정의된 매크로를 사용하여 종료 상태와 이유를 설정한다.
- WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG, WIFSTOPPED, WSTOPSIG, WIFCONTINUED
- wait.h에 정의된 매크로를 사용하여 종료 상태와 이유를 설정한다.
parent에서 fork -> child -> child exit -> child가 parent한테 signal 보냄 -> parent는 wait호출 -> os한테 child를 reaping 하라고 시그널을 보냄.
child가 exit 할 때 signal을 보내는데 parent가 signal을 알아채야 한다. 그래야 os가 child의 리소스를 reaping 할 수 있다.
Example
HP: hello from parent
HC: hello from child
CT: child has terminated
Bye
fork() 하여 자식 프로세스를 생성하고 부모 프로세스에서 wait함수를 호출하여 자식 프로세스가 종료될 때까지 부모 프로세스를 중지한다.
void fork10() {
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
자식 프로세스가 여러 개 생성되었다면, 순서가 임의로 정해진다. (interrupt, 스케줄러 등에 의해)
WIFEXITED와 WEXITSTATUS 매크로를 사용하여 exit status를 가져올 수 있다.
waitpid: Waiting for a Specific Process
프로세스는 waitpid 함수를 호출하여 하위 프로세스가 종료되거나 중지되기를 기다린다.
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
Returns: PID of child if OK, 0 (if WNOHANG), or −1 on error
기본적으로 (옵션 = 0인 경우) waitpid는 자식 프로세스가 종료될 때까지 호출 프로세스의 실행을 중지한다.
자식 프로세스가 이미 종료되어 있는 경우 waitpid는 즉시 반환된다.
waitpid는 종료된 프로세스의 PID를 반환한다.
종료된 자식 프로세스는 커널에 의해 시스템에서 모든 흔적이 삭제된다.
execve : Loading and Running Programs
execve 함수는 현재 프로그램의 context에서 새 프로그램을 load 하고 실행한다.
#include <unistd.h>
int execve(const char *filename, const char *argv[],
const char *envp[]);
Does not return if OK; returns −1 on error
execve 함수는 argument list인 arg와 환경 변수 목록을 가진 envp를 사용하여 실행 가능한 파일 이름을 load하고 실행한다. execve는 파일명을 찾을 수 없는 등, 에러가 발생했을 경우에만 호출한 프로그램으로 돌아온다.
- argv 변수는 null로 끝나는 포인터의 배열을 가리킨다.
각 포인터는 argument 문자열을 가리킨다.
관례상 argv [0]은 실행 가능한 오브젝트 파일의 이름이다. - envp 변수는 환경 변수 문자열에 대한 포인터의 null로 끝나는 배열을 가리킨다.
각 포인터는 name=value 형식의 이름과 값을 가지는 쌍이다.
> ls -al
shell에서 위와 같은 명령어를 적으면 어떤 일이 발생할까?
shell이라는 프로세스에서 ls -al이라는 프로세스를 fork()를 호출하여 생성하고 생성된 자식 프로세스에서 execve(ls -al)을 실행함으로써 ls -al 프로세스를 실행할 수 있다.
execve를 호출한 자식 프로세스는 code, data, stack을 덮어쓴다.
Structure of the stack when a new program starts
main이 실행되면 user stack은 위와 같은 구조로 되어 있다. stack의 맨 아래(가장 높은 주소)에서 맨 위(가장 낮은 주소)로 이동한다. 첫 번째는 argument와 envp 문자열이다. 이 포인터는 stack 상부에서 끝나는 포인터 배열에 의해 연속된다. 각 배열은 stack 상의 envp 문자열을 가리킨다. 전역 변수 환경은 이러한 포인터 중 첫 번째 envp [0]을 가리킨다.
envp 뒤에 null로 끝나는 argv [] 배열이 이어지며 각 요소는 stack 상의 argument 문자열을 가리킨다.
execve Example
"/bin/ls -lt /usr/include"를 자식 프로세스에게 실행한다면 user stack에는 command와 argument가 들어간다.
위 프로세스를 실행하면 user stack은 아래 구조를 가진다.
if ((pid = Fork()) == 0) { /* Child runs program */
if (execve(myargv[0], myargv, environ) < 0) {
printf("%s: Command not found.\n", myargv[0]);
exit(1);
}
}
어떻게 실행하냐면 Fork()하고 자식 프로세스에서 execve를 호출하고 실행하고자 하는 argument를 넣고 실행한다.