Zombies
프로세스가 종료됐지만, 여전히 시스템의 자원을 소비하는것.
이때, 시스템의 자원은 PCB(* Process Control Block)이다. 해당 프로세스를 관리할 수 있는 정보들을 담고 있는 정보들로 운영체제 커널의 자료구조입니다.
https://jwprogramming.tistory.com/16
해당 PCB에는 PID가 있기 때문에, Zombie가 계속 쌓이게되면 PID가 부족해져 더 이상 프로세스를 만들 수 없는 상황이 발생할 수 있습니다. 따라서 PCB등의 정보들까지 전부 반납하는 Reaping을 해줘야합니다.
Reaping
종료되는 자식 프로세스의 부모 프로세스에 의해 수행되는 작업.
부모 프로세스는 exit status를 받고, 커널은 그 프로세스(* PCB까지)를 버려버립니다. 즉, 자식 프로세스의 Reaping의 책임은 1차적으로 부모에게 있습니다.
만약 부모가 Reaping을 하지 않는다면 어떻게 될까요?
자식 프로세스를 Reaping하지 않고 부모 프로세스가 종료된다면. Reaping의 책임이 부모 프로세스에서 init process로 넘어갑니다.
이때, init process는 컴퓨터가 시작할 때, 가장 먼저 만들어지는 process입니다. 그 후 생성되는 process들은 init process를 복사해서 만들어진다 볼 수 있습니다. 따라서 init process는 모든 process의 부모 process라고 볼 수 있습니다.
shells들이나 servers들은 계속 켜져있는 processes들이고 이 processes들 위에서 생성되는 processes들은 shells나 severs의 자식 processes가 되어 Reaping의 책임을 갖습니다.
(* Reaping까지 직접 해줘야하는 Zombie를 왜 만들었을까? Zombie를 통해 종료된 process에 대한 정보를 얻을 수 있기 때문임. 만약 Window처럼(* Zombie는 UNIX계열에서만 만들어지고 Window에서는 만들어지지 않음) Zombie가 없다면 종료된 process에 대한 정보를 다시 찾을 수 없게됨.)
Zombie Examples #1
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 */
}
/*
linux> ./forks 7 &
[1] 6639
Running Parent, PID = 6639
Terminating Child, PID = 6640
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6639 ttyp9 00:00:03 forks
6640 ttyp9 00:00:00 forks <defunct>
6641 ttyp9 00:00:00 ps
linux> kill 6639
[1] Terminated
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6642 ttyp9 00:00:00 ps
*/
ps: System안에 있는 process list를 출력해주는 명령어
<defunct>: Zombie process라는 뜻
CMD tsch: shell, 현재 입력을 받고 있는 shell을 의미함
코드를 보면 자식 프로세스는 exit을 통해 종료하고 부모 프로세스는 무한 루프를 통해 Reaping을 수행하지 않습니다. 그렇다면 부모가 Reaping을 해주지 않았기 때문에 종료된 자식 프로세스는 Zombie가 되어 남아있는 모습을 볼 수 있습니다.
그 후 kill을 통해 부모 프로세스를 죽여주면, 부모가 Reaping을 하지 않고 종료됐기 때문에 Reaping의 책임은 부모 프로세스의 부모인 init이나 shell이 갖게 됩니다.(* init이냐 shell이냐는 os가 어떻게 정했냐에 따라 다름)
따라서 부모를 kill을 통해 종료시키고 좀비상태가 된 PID 6639(* child)와 6640(* parent)는 init이나 shell에서 Reaped됩니다.
Zombie Examples #2
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n", getpid());
exit(0);
}
}
/*
linux> ./forks 8
Terminating Parent, PID = 6675
Running Child, PID = 6676
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6676 ttyp9 00:00:06 forks
6677 ttyp9 00:00:00 ps
linux> kill 6676
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6678 ttyp9 00:00:00 ps
*/
부모가 종료된 상태고 자식은 무한 루프를 통해 계속 돌아갈 수 있는지를 확인하는 예제입니다.
결과적으로는 가능합니다. 자식 노드의 Reaping권한이 부모 프로세스의 부모 프로세스(* 아마 shell)로 넘어가기만 하지 부모가 종료된다고 자식이 종료될 필요는 없습니다.
int wait(int *child_status)
자식들 중의 하나가 종료될때까지 프로세스의 흐름을 중단하는 명령어.
즉 자식 프로세스와 싱크를 맞추는 명령어입니다. 종료되는 자식 프로세스의 PID를 리턴받습니다.
child_status가 NULL이 아니라면 그 객체(* child_staus는 어떤 숫자가 아닌 child process에 대한 정보가 encoding된 객체임)는 자식이 어떻게 종료되었는지를 나타냅니다.
wait Examples #1
void fork9() {
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
}
else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
exit();
}
wait은 자식의 Reaping을 해주는 메소드입니다.
따라서 자식이 종료될때까지 부모의 프로세스 진행을 멈추고,
자식이 종료된다면 자식을 Reaping을 하고 부모 프로세스의 진행을 다시 시작합니다.
wait Examples #2
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++) {
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);
}
}
WIFEXITED(wstatus): 정상적인 종료라면 True값을 리턴하는 메소드. exit()이나 _exit(), main()에서 return을 받은 경우를 정상적인 종료로 간주, Null Pointer Exception등을 통해 종료되면 비정상적인 종료로 간주.
WEXITSTATUS(wstatus): 자식의 exit status를 리턴
부모에서 자식 프로세스를 100개를 만들고 만들자마자 종료해주면서 Zombie를 100개를 만들어줍니다.
그 후 만들어진 프로세스의 개수만큼 wait을 해주면서 Reaping을 수행해줍니다.
이때 Reaping이 되는 순서는 자식 프로세스가 종료되는 순서로, 자식 프로세스가 종료되는 순서는 만들어지는 순서가 아닌 컴퓨터의 상태에 따라서 달라집니다. 즉, 랜덤입니다.
int waitpid(pid, &status, options)
특정한 프로세스를 기다리는 메소드.
option 파라미터를 통해 다양한 옵션을 수행할 수 있습니다.
void fork11()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = 0; i < N; i++) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
waitpid의 파라미터의 자식 프로세스의 pid를 넣어주면서 특정 pid에 해당하는 프로세스를 기다릴 수 있습니다.
fork10같은 경우 그냥 wait을 사용했기때문에 만들어지는 순서와 상관없이 랜덤으로 Reaping이 되는것을 볼 수 있습니다.(* 사실 먼저 종료되는 순서이지만 종료되는 순서는 컴퓨터의 상태에 따라서 달라지기 때문에 랜덤으로 볼 수 있습니다.)
하지만 fork11의 경우 만들어지는 순서로 pid list를 채워넣었고, pid list순서대로 Reaping을 진행했기때문에 만들어진 순서대로 Reaping되는것을 볼 수 있습니다.
(* waitpid를 잘못하면 계속 기다릴 수도 있음. 그 pid에 해당하는 프로세스가 종료될때까지 기다려야하니까.)
int execl(char *path, char *arg0, char *arg1, ..., 0)
path에 존재하는 실행가능한 프로그램을 가져와서 실행하는 메소드.
path는 실행가능한 완전한 path입니다. 또한 arg0은 해당 process의 이름이 됩니다. 일반적으로 arg0, 즉 프로세스의 이름은 path의 이름과 동일하게 합니다.
실질적인 파라미터는 arg1부터 시작합니다. 그리고 파라미터의 가장 마지막은 항상 0으로, 파라미터 리스트의 종료를 0을 통해 나타내야합니다.
만약 -1을 리턴한다면 에러가 발생한것이고, 실행에 성공했다면 아무값도 리턴하지 않습니다.
fork vs exec
fork는 부모와 똑같은 프로세스를 만들고, 그 위치에서부터 두 프로세스가 진행되지만,
exec는 path에 해당하는 .exe file을 기존의 process에 "덮어쓰기"하는것이기 때문에, 부모 프로세스와는 전혀 다른 프로세스가 됩니다.
따라서 특정 프로세스에서 exec이 실행된다면 exec메소드 아래의 코드는 실행될 수 없는 코드가 됩니다.(* 만약 fork라면 프로세스가 만들어지고 그 위치에서부터 다시 시작하기 때문에 아래 코드들이 실행될 수 있지만, exec의 경우 path에 해당하는 프로세스로 덮어써지기 때문에 원래의 프로세스에서 exec 아래의 코드들은 실행될 수 없습니다.)
이런 특징들로 인해 일반적으로는 fork와 exec를 같이 사용합니다.
fork를 통해 부모와 똑같은 프로세스를 만들고, 그 자식 프로세스에서 exec를 실행함으로써 아에 다른 프로세스를 진행시킵니다.
exec Example
main() {
if (fork() == 0) {
execl("/usr/bin/cp", "cp", "foo", "bar", 0);
}
wait(NULL);
printf("copy completed\n");
exit();
}
fork를 해서 자식 프로세스를 만들고,
자식 프로세스에서 exec를 통해 cp(* copy 프로그램)을 실행시킵니다.
부모 프로세스에서는 wait을 통해 cp가 종료될때까지 기다리고,
자식 프로세스인 cp가 끝나면, 부모는 자식을 Reaping한 후
"copy completed" 를 출력하고 부모도 종료됩니다.
exec Family
man page를 통해 execl등의 메소드를 보면 번호가 3번임을 알 수 있습니다.
하지만 execl은 분명 System call입니다. 즉 번호가 2번이어야 합니다.
이는 execl과 같은 메소드는 execve라는 2번 메소드를 감싸고 있는 Wrapper library function이기 때문입니다.
이와 같이 execve를 감싸고 있는 메소드는 정말 많습니다.
https://www.geeksforgeeks.org/exec-family-of-functions-in-c/
'[학교 수업] > [학교 수업] 시스템 프로그래밍' 카테고리의 다른 글
[시스템 프로그래밍] Signal Programming (0) | 2024.09.24 |
---|---|
[시스템 프로그래밍] Signals (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Commands (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Installation (0) | 2024.09.13 |
[시스템 프로그래밍] Processes (0) | 2024.09.05 |