[시스템] 2. 멀티 프로세스 프로그래밍
1. Zombies
Zombie는 프로세스가 종료되었음에도 불구하고 여전히 시스템 자원을 소비하고 있는 것을 말한다. 좀비가 많아지면 pid를 다 써서 새로운 프로세스를 못 만들게 되는 상황이 생길 수 있다. 좀비를 제거하는 행위는 Reaping 이라 하며, 종료된 자식의 부모가 처리한다. 만약 부모가 자식을 Reaping 하지 않고 종료될 경우, 모든 프로세스의 조상 프로세스인 init 프로세스가 자식을 Reaping 한다. 서버 프로그램처럼 부모 프로세스가 오랫동안 종료되지 않아야 한다면 init 프로세스가 Reaping 하지 않으므로, 부모 내에서 자식 프로세스가 좀비가 되지 않도록 꼭 처리해야 한다.
좀비 프로세스 : 자식 프로세스가 종료되었지만, 부모 프로세스가 아직 그 종료를 확인하지 않는 프로세스
고아 프로세스 : 자식보다 먼저 부모프로세스가 죽었을 경우의 자식 프로세스
아래 코드를 실행한 뒤 ps 명령어를 통해 프로세스 상태를 확인해보면, <defunct> 가 뜨는 것을 보아 자식 프로세스가 좀비임을 알 수 있다. 이는 자식 프로세스가 종료되었지만 PID는 할당된채 (프로세스로서 존재하기 위한 가장 기존적인 자원을 붙들고 있는 채)로 시스템 내에 계속 남아있다는 뜻이다. 부모는 background 에서 계속 실행중이기 때문에, kill 을 통해 부모를 강제 종료시키면 init 프로세스가 좀비를 죽일 수 있게 된다.
#include <stdio.h>
#include <unistd.h>
void fork7()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1); /* Infinite loop */
}
}
2. wait: Reaping Children
$\verb|int wait(int *child_status)|$
임의의 자식 프로세스를 Reaping 해주는 함수이다. 자식 프로세스들 중 어떤 하나가 종료될 때까지 실행을 중지하는 역할을 한다. 리턴하는 값은 종료된 자식 프로세스의 pid 이고, 이때 자식을 Reaping을 하도록 되어 있다. 만약 여러 자식 프로세스들이 있을 경우 종료 순서와 Reaping 순서를 모두 보장할 수 없다.
$\verb|int waitpid(pid_t pid, int *status, int options)|$
특정 자식 프로세스를 Reaping 해주는 함수. pid 를 입력해줘서 해당 프로세스가 Reaping 될 때까지 기다린다. 여러 자식 프로세스들이 있을 경우 이 함수가 종료 순서는 보장할 수 없지만, Reaping 순서는 보장해준다. Return 값은 정상적으로 종료될 경우 자식의 pid를, 에러가 발생하면 -1을, WNOHANG 옵션이 걸린 상태에서 자식 프로세스가 종료되지 않았는데 리턴해야 할 경우 0을 리턴한다.
아래 코드는 wait을 사용한 예제로, 어떤 자식 프로세스가 먼저 종료되고 Reaping 될지는 알 수 없다. wait의 인자인 child_status 가 가리키는 곳에는 자식 프로세스가 exit 될 때의 인자 값이 들어가게 된다. 이 child_status 값을 바로 해석하기는 어려워서 아래와 같은 매크로 함수들을 사용해야 한다.
- WIFEXITED(child_status): 자식이 정상적으로 종료되었으면 true 를 리턴한다.
- WEXITSTATUS(child_status): 자식의 exit status 를 리턴한다. (exit 할 때 들어갔던 인자 값)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 5
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);
}
}
/*
Child 2210 terminated with exit status 103
Child 2209 terminated with exit status 102
Child 2208 terminated with exit status 101
Child 2207 terminated with exit status 100
Child 2211 terminated with exit status 104
*/
3. exec: Running New Program
$\verb|int execl(char *path, char *arg0, char *arg1, ..., 0)|$
path 경로의 프로그램을 arg0, arg1, ...의 인자값을 줘서 실행한다. arg0은 일반적으로 실행 파일명이고, 실제 인자값들은 arg1 부터 주면 된다. 해당 프로그램에서 에러가 발생하면 -1을 리턴하고, 정상적으로 작동하면 아무 값도 리턴하지 않는다.
fork의 경우 부모와 똑같이 생긴 자식이 생기고, fork가 일어난 부분부터 진행한다. 하지만 execl의 경우 그 아래 있는 코드는 무시하고 완전히 새로운 프로그램이 덮어씌워진다. 프로그램 카운터 (PC)도 새로운 프로그램의 시작점부터 출발한다.
프로세스 $A$를 진행하다가 $B$도 같이 진행해야 하는 경우가 많지, $B$로 완전히 바꿔야 하는 경우는 잘 없다. 따라서 일반적으로 exec을 사용할 때 fork 와 함께 쓰는 경우가 많다. fork를 만나면 자식 프로세스가 생성되고, 이 자식 프로세스 내에서 exec 를 실행하게 된다. 즉, 부모는 기존 프로그램으로 존재하지만, 자식은 새로운 프로그램을 실행하는 것이다.
아래 코드에서 fork로 생성된 자식 프로세스는 execl을 만나 완전히 새로운 프로그램으로 덮어씌워진다. 부모 프로세스는 그 아래의 코드를 실행하게 된다. 이때 wait함수에 의해 자식 프로세스가 끝날 때까지 (exec을 했어도 부모 자식 관계는 유지됨) 기다리게 된다.
#include <stdio.h>
#include <unistd.h>
int main() {
if (fork() == 0) {
execl("/usr/bin/cp", "cp", "foo", "bar", 0);
}
wait(NULL);
printf("copy completed\n");
exit();
}
4. exec 함수군
$\verb|int execl(char *path, char *arg0, char *arg1, ..., 0)|$
$\verb|int execlp(char *file, char *arg0, char *arg1, ..., 0)|$
$\verb|int execle(char *file, char *arg0, char *arg1, ..., char* envp[])|$
$\verb|int execv(char *path, char* argv[])|$
$\verb|int execvp(char *file, char* argv[])|$
$\verb|int execvpe(char *file, char* argv[], char* envp[])|$
- l: arg 인자들을 하나하나 대입할 수 있다.
- v: arg 인자들을 문자열 배열로 전달한다.
- p: p가 없으면 path 경로를, p가 있으면 파일명을 입력받는다. 경로는 운영체제가 알아서 찾는다.
- e: envp를 통해 환경변수를 설정할 수 있다.