Edited at

【C言語】ゾンビプロセスの作成、回避、成仏方法


【概要】

製薬企業アンブレラ社によって開発されたT-ウィルスを使ってゾンビを作っていきます。

というのは冗談で、C言語でゾンビプロセスの作成、回避、成仏方法を紹介します。


【簡単にゾンビプロセスとは】

ゾンビプロセスとは親プロセスが子プロセスをほっといてしまい、いつまで経っても終了できない子プロセスのことです。

※もちろん厳密な定義は違いますが、ゾンビプロセス知らない人がイメージ出来ればいいかな


【環境】

[vagrant@vagrant-centos65 ~]$ cat /etc/redhat-release 

CentOS release 6.5 (Final)


【ゾンビの作成方法】


まずはゾンビを作成するソース


zombie.c

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
pid_t pid;

// 引数チェック
if (argc != 3) {
fprintf(stderr, "Usage: %s <command> <arg>\n", argv[0]);
exit(1);
}

// forkして子プロセスを作成する
// fork以降の処理は親プロセスと子プロセスの2つのプロセスが同時に実行されている
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}

// 子プロセスのforkの戻り値は0
if (pid == 0) { /* 子プロセスが実行する処理 */
execl(argv[1], argv[1], argv[2], NULL);
/* execl()は成功したら戻らないので、戻ったらすべて失敗 */
perror(argv[1]);
exit(99);
}
// 親プロセスのforkの戻り値は子プロセスのプロセスID
else { /* 親プロセスが実行する処理 */
// ゾンビの生存時間は30秒
// ここでは30秒にしていますが、while(1)などの無限ループの場合はずっとゾンビが存在することになります
sleep(30);
printf("child (PID=%d) finished;\n", pid);
exit(0);
}
}



ゾンビの作り方に注意

引数1:実行するコマンドのフルパス(今回は/bin/echo

引数2:引数1のコマンドに対するパラメータ(今回はThis is zombie

下記のように実行してしまうと30秒間プロンプトが戻らなくなってしまい、同一ターミナルでゾンビの存在確認ができなくなってしまいます。

(もう一つターミナルをあげてゾンビの確認をする場合は下記方法でも問題ないです)

[vagrant@vagrant-centos65 tmp]$ gcc -o zombie ./zombie.c 

[vagrant@vagrant-centos65 tmp]$ ./zombie /bin/echo "This is zombie"
This is zombie // → この状態で30秒待たなければならない、他の作業ができないなどの影響がある
child (PID=24579) finished; // → 30秒後に表示される
[vagrant@vagrant-centos65 tmp]$

そのため下記のようにコマンドの最後に&をつけて、バックグランドプロセスで実行します。

ゾンビが存在している30秒の間にpsコマンドでゾンビが存在することの確認も行っています。

psコマンドの結果にdefunctが表示されているため、ゾンビだと確認できます。

[vagrant@vagrant-centos65 tmp]$ ./zombie /bin/echo "This is zombie" &

[1] 24601
[vagrant@vagrant-centos65 tmp]$ This is zombie
// → エンター押さないとプロンプトが返ってこないから、エンター押す
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ ps aux | grep defunct | grep -v grep
vagrant 24602 0.0 0.0 0 0 pts/0 Z 23:06 0:00 [echo] <defunct>
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ child (PID=24602) finished; // → 30秒後に表示される
// → エンター押さないとプロンプトが返ってこないから、エンター押す
[1]+ Done ./zombie /bin/echo "This is zombie"
[vagrant@vagrant-centos65 tmp]$


【ゾンビの回避方法】


方法①fork()したらwaitpid()する

forkしたら親プロセスがwaitpidを使用して、子プロセスの終了をキャッチします。

ゾンビが発生しないようにするのが親の責任ですね。


zombie_avoid1.c

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
pid_t pid;

// 引数チェック
if (argc != 3) {
fprintf(stderr, "Usage: %s <command> <arg>\n", argv[0]);
exit(1);
}

// forkして子プロセスを作成する
// fork以降の処理は親プロセスと子プロセスの2つのプロセスが同時に実行されている
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}

// 子プロセスのforkの戻り値は0
if (pid == 0) { /* 子プロセスが実行する処理 */
execl(argv[1], argv[1], argv[2], NULL);
/* execl()は成功したら戻らないので、戻ったらすべて失敗 */
perror(argv[1]);
exit(99);
}
// 親プロセスのforkの戻り値は子プロセスのプロセスID
else { /* 親プロセスが実行する処理 */
int status;

waitpid(pid, &status, 0);
sleep(30);
printf("child (PID=%d) finished; ", pid);
if (WIFEXITED(status))
printf("exit, status=%d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("signal, sig=%d\n", WTERMSIG(status));
else
printf("abnormal exit\n");
exit(0);
}
}



【実行結果】

psコマンドでゾンビが存在していないことが確認できます。

[vagrant@vagrant-centos65 tmp]$ gcc -o zombie_avoid1 ./zombie_avoid1.c 

[vagrant@vagrant-centos65 tmp]$ ./zombie_avoid1 /bin/echo "This is zombie" &
[1] 24619
[vagrant@vagrant-centos65 tmp]$ This is zombie

[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ ps aux | grep defunct | grep -v grep
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ child (PID=24620) finished; exit, status=0

[1]+ Done ./zombie_avoid1 /bin/echo "This is zombie"
[vagrant@vagrant-centos65 tmp]$


方法②ダブルfork

親プロセスから子プロセスを作成し、子プロセスから孫プロセスを作成します。

そして、子プロセスを終了することにより、孫プロセスからの親プロセスが存在しなくなるので孫プロセスはゾンビにならないで済みます。

親プロセスと子プロセスが終了しても、孫プロセスが存在していることを確認します。

プロセス
プロセスの終了時間

親プロセス
30秒経過したら終了

子プロセス
即終了

孫プロセス
60秒経過したら終了


zombie_avoid2.c

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
pid_t pid;

// 引数チェック
if (argc != 3) {
fprintf(stderr, "Usage: %s <command> <arg>\n", argv[0]);
exit(1);
}

// forkして子プロセスを作成する
// fork以降の処理は親プロセスと子プロセスの2つのプロセスが同時に実行されている
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}

// 子プロセスのforkの戻り値は0
if (pid == 0) { /* 子プロセスが実行する処理 */
pid_t pid_child;
pid_child = fork();
if (pid_child < 0) {
fprintf(stderr, "child fork(2) failed\n");
exit(1);
}

if (pid_child == 0) { /* 孫プロセスが実行する処理 */
execl(argv[1], argv[1], argv[2], NULL);
/* execl()は成功したら戻らないので、戻ったらすべて失敗 */
perror(argv[1]);
exit(99);
} else { /* 子プロセスが実行する処理 */
printf("grandchild (PID=%d) finished; ", pid_child);
exit(0);
}
}
// 親プロセスのforkの戻り値は子プロセスのプロセスID
else { /* 親プロセスが実行する処理 */
int status;

waitpid(pid, &status, 0);
sleep(30);
printf("child (PID=%d) finished; ", pid);
if (WIFEXITED(status))
printf("exit, status=%d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("signal, sig=%d\n", WTERMSIG(status));
else
printf("abnormal exit\n");
exit(0);
}
}



【実行結果】

今回は孫プロセスの存在を確認するためにechoではなく、sleepを使用しています。

[vagrant@vagrant-centos65 tmp]$ gcc -o zombie_avoid2 ./zombie_avoid2.c

[vagrant@vagrant-centos65 tmp]$ ./zombie_avoid2 /bin/sleep 60 &
[1] 25674
[vagrant@vagrant-centos65 tmp]$ grandchild (PID=25676) finished;
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$
// ゾンビは存在していない
[vagrant@vagrant-centos65 tmp]$ ps aux | grep defunct | grep -v grep
// 孫プロセスは存在している
[vagrant@vagrant-centos65 tmp]$ ps aux | grep 25676 | grep -v grep
vagrant 25676 0.0 0.1 100924 620 pts/0 S 01:29 0:00 /bin/sleep 60
[vagrant@vagrant-centos65 tmp]$ child (PID=25675) finished; exit, status=0

[1]+ Done ./zombie_avoid2 /bin/sleep 60
[vagrant@vagrant-centos65 tmp]$
// 親プロセスが終了しても孫プロセスは存在している
[vagrant@vagrant-centos65 tmp]$ ps aux | grep 25676 | grep -v grep
vagrant 25676 0.0 0.1 100924 620 pts/0 S 01:29 0:00 /bin/sleep 60
[vagrant@vagrant-centos65 tmp]$
// 60秒経過すると孫プロセスも終了する
[vagrant@vagrant-centos65 tmp]$ ps aux | grep 25676 | grep -v grep
[vagrant@vagrant-centos65 tmp]$


方法③sigactionを使う


zombie_avoid3.c

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

static void detach_children(void);
static void noop_handler(int sig);

int
main(int argc, char *argv[])
{
pid_t pid;

// 引数チェック
if (argc != 3) {
fprintf(stderr, "Usage: %s <command> <arg>\n", argv[0]);
exit(1);
}

detach_children();

// forkして子プロセスを作成する
// fork以降の処理は親プロセスと子プロセスの2つのプロセスが同時に実行されている
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}

// 子プロセスのforkの戻り値は0
if (pid == 0) { /* 子プロセスが実行する処理 */
execl(argv[1], argv[1], argv[2], NULL);
/* execl()は成功したら戻らないので、戻ったらすべて失敗 */
perror(argv[1]);
exit(99);
}
// 親プロセスのforkの戻り値は子プロセスのプロセスID
else { /* 親プロセスが実行する処理 */
printf("child (PID=%d) finished;\n", pid);
// sleepではシグナルが補足されてしまうため、whileで無限ループする
while(1);
exit(0);
}
}

static void
detach_children(void)
{
struct sigaction act;

act.sa_handler = noop_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART | SA_NOCLDWAIT;
if (sigaction(SIGCHLD, &act, NULL) < 0) {
printf("sigaction() failed: %s", strerror(errno));
}
}

static void
noop_handler(int sig)
{
;
}



【実行結果】

[vagrant@vagrant-centos65 tmp]$ gcc -o zombie_avoid3 ./zombie_avoid3.c

[vagrant@vagrant-centos65 tmp]$ ./zombie_avoid3 /bin/echo "This is zombie" &
[1] 25895
[vagrant@vagrant-centos65 tmp]$ child (PID=25896) finished;
This is zombie

[vagrant@vagrant-centos65 tmp]$
// ゾンビは存在していない
[vagrant@vagrant-centos65 tmp]$ ps aux | grep defunct | grep -v grep
// zombie_avoid3のプロセスは存在している
[vagrant@vagrant-centos65 tmp]$ ps aux | grep zombie_avoid3 | grep -v grep
vagrant 25895 102 0.0 3924 448 pts/0 R 02:42 0:13 ./zombie_avoid3 /bin/echo This is zombie
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ kill 25895
[vagrant@vagrant-centos65 tmp]$
[1]+ Terminated ./zombie_avoid3 /bin/echo "This is zombie"
[vagrant@vagrant-centos65 tmp]$


【ゾンビの成仏方法】

zombie.cで作成したゾンビプロセスの終了方法を紹介します。


ゾンビはゾンビ自体(子プロセス)を攻撃しても倒すことができない。

[vagrant@vagrant-centos65 tmp]$ ./zombie /bin/echo "This is zombie" &

[1] 25932
[vagrant@vagrant-centos65 tmp]$ This is zombie

[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ ps aux | grep -e zombie -e defunct | grep -v grep
vagrant 25932 0.0 0.0 3920 372 pts/0 S 02:47 0:00 ./zombie /bin/echo This is zombie
vagrant 25933 0.0 0.0 0 0 pts/0 Z 02:47 0:00 [echo] <defunct>
[vagrant@vagrant-centos65 tmp]$
// ゾンビをkill
[vagrant@vagrant-centos65 tmp]$ kill 25933
// ゾンビをkillしたが、ゾンビは存在している
[vagrant@vagrant-centos65 tmp]$ ps aux | grep -e zombie -e defunct | grep -v grep
vagrant 25932 0.0 0.0 3920 372 pts/0 S 02:47 0:00 ./zombie /bin/echo This is zombie
vagrant 25933 0.0 0.0 0 0 pts/0 Z 02:47 0:00 [echo] <defunct>
[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ child (PID=25933) finished;

[1]+ Done ./zombie /bin/echo "This is zombie"
[vagrant@vagrant-centos65 tmp]$


ゾンビはそのゾンビの頭(親プロセス)を攻撃すると倒すことができる。

[vagrant@vagrant-centos65 tmp]$ ./zombie /bin/echo "This is zombie" &

[1] 25965
[vagrant@vagrant-centos65 tmp]$ This is zombie

[vagrant@vagrant-centos65 tmp]$
[vagrant@vagrant-centos65 tmp]$ ps aux | grep -e zombie -e defunct | grep -v grep
vagrant 25965 0.0 0.0 3920 372 pts/0 S 02:50 0:00 ./zombie /bin/echo This is zombie
vagrant 25966 0.0 0.0 0 0 pts/0 Z 02:50 0:00 [echo] <defunct>
[vagrant@vagrant-centos65 tmp]$
// 親プロセスをkillする
[vagrant@vagrant-centos65 tmp]$ kill 25965
[vagrant@vagrant-centos65 tmp]$
[1]+ Terminated ./zombie /bin/echo "This is zombie"
[vagrant@vagrant-centos65 tmp]$
// 親プロセスもゾンビも存在していない
[vagrant@vagrant-centos65 tmp]$ ps aux | grep -e zombie -e defunct | grep -v grep
[vagrant@vagrant-centos65 tmp]$

※ダブルforkで作成した孫プロセスはゾンビではないので、killすると終了することができます。


【参考書】

ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道


【参考サイト】

zombieプロセスの作り方

double fork to avoid zombie process