今回、アプリケーション開発をする中で、Linuxプロセスについて、少し気になったことがあったので、Linuxのプロセスについて調べてみました。Linuxのプロセスの生成から消滅、さらにゾンビプロセスなどLinuxプロセスについてまとめました。
実行環境
- 実機:raspberrypi
- OS:raspbian 5.4.83-v7
参考にしたテキスト
プロセスとは
Linuxは、いくつものプログラムが同時に働く、マルチオペレーティングシステムです。それらの実行中のプログラムのことをプロセス(process)といいます。プロセスは様々な種類があり、ユーザーが実行するプログラムや、ユーザーのシェルのプロセス、各種デーモン(daemon)プロセスなどがあります。そして常にLinuxは数十個のプロセスが同時に動いています。
プログラムは通常1つのプロセスとして動作しています。しかし、プログラムの処理が複雑な場合、プロセスを複数使用し処理を実行する方がいいこともあります。この時使用するのがリダイレクト処理と呼ばれるものです。リダイレクトは複数のプロセスをつなぎ合わせることができる機能です。リダイレクト処理を使用し、複数のプロセスを組み合わせることで、個々のプログラムは簡単なもので済みます。
プロセスとプログラム
Linuxでは以下のように現在実行されているプロセスを確認できます。ターミナル上から、psコマンドにオプション[-ef]をつけて実行すると、以下のように現在実行されているプロセスが全て表時されます。それぞれのプロセスは、一時的に一意なプロセスID(PID)と呼ばれる値が与えられます。
ropi@raspberrypi:~/Documents $ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 00:04 ? 00:00:06 /sbin/init splas
pi 10665 10211 0 09:39 pts/2 00:00:00 /bin/bash
root 12035 2 0 10:17 ? 00:00:00 [kworker/2:0H]
root 12501 2 0 10:29 ? 00:00:00 [kworker/3:0+events]
root 13371 2 0 10:50 ? 00:00:00 [kworker/u8:0-events_unbound]
root 13390 2 0 10:50 ? 00:00:00 [kworker/2:2-events]
root 13492 2 0 10:53 ? 00:00:00 [kworker/0:0-mm_percpu_wq]
root 13591 2 0 10:55 ? 00:00:00 [kworker/1:0H]
root 13624 2 0 10:56 ? 00:00:00 [kworker/u8:1-cfg80211]
root 13668 2 0 10:57 ? 00:00:00 [kworker/2:3-events_freezable]
root 13784 2 0 11:00 ? 00:00:00 [kworker/0:1-events]
root 13807 2 0 11:00 ? 00:00:00 [kworker/1:2-mm_percpu_wq]
root 13810 2 0 11:01 ? 00:00:00 [kworker/3:1-events_power_efficient]
root 13887 2 0 11:02 ? 00:00:00 [kworker/0:0H]
root 13924 2 0 11:03 ? 00:00:00 [kworker/2:0-events]
pi 14012 10466 0 11:05 ? 00:00:00 sleep 180
root 14023 2 0 11:05 ? 00:00:00 [kworker/0:2-events]
root 14030 2 0 11:06 ? 00:00:00 [kworker/3:2-mm_percpu_wq]
root 14040 2 0 11:06 ? 00:00:00 [kworker/u8:2-events_unbound]
pi 14129 10665 0 11:08 pts/2 00:00:00 ps -ef
プロセスはプログラムから生成され、プログラムの処理が終了するとすぐに消えます。また、プログラムは全てファイルシステムの中に実行可能ファイルとして存在します。しかしXWindowなどのプログラムは、ウィンドウで作業している間はずっとプロセスが生存されます。
次に、C言語プログラムを自分で作成し実行したときの動きを確認します。以下のような流れでプロセスが生成されます。まず、ファイルシステム内のa.outを実行します。プログラムの実行は、プログラムの実行は、main関数の先頭が呼び出されたとき、プロセスが生成し、プログラムの実行が終了したときにプロセスが消滅します。
C言語で、自分自身のPIDとプロセス情報を確認してみます。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void){
pid_t mypid;
char command[1024];
mypid = getpid();
printf("pid = %d\n", mypid);
if (snprintf(command, sizeof(command), "ps -l -p %d", mypid) >= sizeof(command))
{
fprintf(stderr, "too long command line (pid = %d)\n", mypid);
exit(1);
}
system(command);
exit(0);
}
実行結果は以下のようになります。
pi@raspberrypi:~/Documents/process_c/training $ ./mypinfo
pid = 18987
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 18987 10665 0 80 0 - 463 do_wai pts/2 00:00:00 mypinfo
このPIDは一意の値であれば何でもいいので、プロセスが生成されるたびに、新しいIDを順に割り当てます。通常、新しく生成されたプロセスのPIDは、その直前に生成されたプロセスのPIDに+1したものが割り当てられます。そして現在PIDの最大値は32768となっており、最大値に達するとカーネルは使用されていないPIDの再利用を開始します。
プロセスの一生 ~生成と消滅~
プロセスの生成は、①プロセスの複製・生成、②プログラムの実行、2つの方法があります。
①プロセスの生成
プロセスを作り出すことをプロセスの生成といいます。そしてLinuxにおいて、プロセスは自分の複製を作ることもでき、この動作をfork(フォーク)といいます。この時のプロセスの生成元を親プロセス(parent process)といい、新しくできたプロセスを子プロセス(child process)といいます。
ここでfork(2)を使用して実際に、子プロセスを生成し、子プロセスから親プロセスのPIDを取得するというプログラムを実行してみます。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void){
pid_t pid;
if ((pid = fork()) == 0){
printf("child: parent PID = %d, my PID = %d\n", getppid(), getpid());
exit(0);
}else{
printf("parent: my PID = %d, child PID = %d\n", getpid(), pid);
for(;;)
sleep(1);
}
}
実行結果
parent: my PID = 21106, child PID = 21107
child: parent PID = 21106, my PID = 21107
基本的に子プロセスのPIDは、親プロセス+1したものもしくは、直前に生成されたプロセス+1したPIDが割り当てられます。上記の結果から、親プロセスから生成された子プロセスは、ちゃんと子プロセスのPIDを取得でき、親子関係が成立しています。
子プロセスが親プロセスから引き継ぐ情報
子プロセスは以下の情報を子プロセスから引き継ぎます。
- 実行状態
- プロセスのメモリ領域の内容
- ユーザーID・グループID
- 環境
- 作業ディレクトリ
- umask値
- シグナルマスクとシグナルハンドラ
- ファイル記述子
- ディレクトリストリーム
また、PIDやPPID、CPUの利用状況は引き継がれません。
②プログラムの実行
実行可能なプログラムからプロセスを実行するためにexec関数群(exec family)があります。この関数は自分のプロセス新しいプロセスで上書きするシステムコールです。詳しい関数の説明は、manページを参照ください。
このexecを実行した時点で、現在実行しているプログラムが消滅し、自分のプロセス上に新しいプログラムをロードして実行します。この時、PIDは変更されません。新しいプログラムに上書きされるため、元のプログラムには戻りません。
execをC言語で実行してみます。以下のプログラムをsample_execとし実行すると、lsコマンドにプログラムが上書きされます。
#include <stdio.h>
#include <unistd.h>
int main(void){
printf("Invoking ls . \n");
fflush(stdout);
execlp("ls", "ls", (char *)0);
printf("Done\n");
return 0;
}
実行結果
pi@raspberrypi:~/Documents/process_c/training $ ./sample_exec
Invoking ls .
cat2 cddo debug.txt exec0.c fork0.c fork1.c getpath getpid.c head.c mypinfo mysh0.c myvec.h prochan sample.txt sample_exec.c showenv.c showppid.c spawn.c tostio.c writestdio.c
cat2.c cddo.c exec0 fork0 fork1 getPID getpath.c head input.file mypinfo.c mysub.h output.file prochan.c sample_exec showenv showppid spawn tostio writestdio
実行結果は、lsコマンドの結果が出力されています。元々のプログラムsample_execは上書きされ、printf(‘Done\n’);は実行されていないのがわかります。
exec前後で引き継がれる情報
execはforkと違い、プログラムやデータが上書きされるため、プロセスのメモリ内容や実行状態は異なります。しかし、execの実行は同じプロセス内で処理されるため、PIDも引き継がれます。
- PIDやPPID
- ユーザーIDやグループID
- 作業ディレクトリ
- umask値
- シグナルマスク
- ファイル記述子
- プロセスがそれまで使用していた資源
ゾンビプロセスとwait()処理
ゾンビプロセスとはいつまでの回収されないプロセスのことです。(参考:https://en.wikipedia.org/wiki/Zombie_process)
ゾンビプロセスとは親プロセスが子プロセスをほっといてしまい、いつまで経っても終了できない子プロセスのことです
引用:【C言語】ゾンビプロセスの作成、回避、成仏方法
では、どのような時にゾンビプロセスが生成されるのか、シェルの動作から確認します。
プロセスの生成とプログラムの実行を繰り返す代表的なプロセスがシェルです。シェルはユーザーからのコマンド入力を受け取り、子プロセスの生成と指定されたプログラムの実行を繰り返す。この処理の流れの中で、ゾンビプロセスはどこで生成されるのでしょうか。答えは、子プロセスがexitを呼び出した後です。
シェルはユーザーからのコマンド入力を受け取ると、フォークして子プロセスを生成します。この時点では、子プロセスはまだシェルのプログラムを実行しています。この後、子プロセスは、実行するファイル名と引数を受け取って、プログラムを実行します(exec関数)。すると、子プロセスが実行していた親プロセスの処理は、新しいプログラムに上書きされ、プログラムを実行し、処理が終了するとexitを呼び出し、処理を終了させます。実は、この時点ではまだ子プロセスは消滅しません。全ての子プロセスは、exitを呼ぶと必ずゾンビ(zombi)状態になります。ゾンビ状態のプロセスは処理を実行しません。
ゾンビプロセスが終了する順序は、子プロセスがexitしゾンビプロセス状態になった時、wait()というシステムコールを呼びだします。このシステムコールで親プロセスは子プロセスの終了状態を受け取ります。それと同時に子プロセスは完全に消滅(キレイさっぱり成仏)されます。
ここで、先ほど使用した子プロセスから親プロセスのPIDとPPIDを取得するプログラムを使用して、fork()して子プロセスを生成後から、子プロセスがexit()するまでに10秒間のsleepを入れ、その間のプロセスがゾンビかされているか確認してみます。まず、コードは以下のようになります。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
if (fork() == 0){
/*child*/
printf("\nmy parent is now pid %d \n", getppid());
exit(0);
}
printf("parent process, pid %d\n", getpid());
sleep(10); /*fork()を実行後、10秒間のスリープ*/
exit(0);
}
実行中に、psコマンドを実行すると、
pi@raspberrypi:~/Documents/$ parent process, pid 682
my parent is now pid 682
ps // 実行途中にpsコマンドを実行する
PID TTY TIME CMD
313 pts/1 00:00:00 bash
682 pts/1 00:00:00 sample_proc
683 pts/1 00:00:00 sample_proc <defunct> //ここで<defunct>が確認
684 pts/1 00:00:00 ps
プロセスがゾンビ化すると、「zombie」や「defunct」と表示されます。これは、プログラムの生成が終了していますが、まだexit()が実行されていないので、ゾンビ状態が確認できます。
参考:ゾンビプロセスの回避方法
ゾンビプロセスを回避する方法は3つあります。(引用:ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道)
- fork()したらwait()する
- ダブルfork()する
- sigaction()を使用する
一番最初の方法を実行するのは、親の務めです。fork()したら、wait()しましょう。2つ目のダブルフォークとは、子プロセスの途中でfork()する方法です。
この状態になると、孫プロセスの親、つまり「子プロセス」はいなくなります。wait()する権利があるのは、直接の親しかありませんので、「子プロセス」がいなくなった時点で、wait()する権利は「親プロセス」となり、ゾンビとならず、すぐに終了してくれます。
最後に、sigaction()は、「自分はwait()しない」とシグナルをカーネルに伝えることで、ゾンビぷrセスを回避できます。詳細は、別の記事でまとめたいと思います。
プロセスの一生のまとめ
プロセスの一生は以下の2パターンにまとめることができます。
① fork() → exec() → exit()
② fork() → exit()
プロセスを任意のタイミングで終了させる
killコマンドを用いたプロセスの終了
killコマンドはPIDを指定してプロセスにシグナルを送るコマンド。
kill [オプション] [プロセス番号(PID)]
例えばsleepコマンドを終了させるコマンドは、以下のように実行する。
pi@raspberrypi:~ $ ps
PID TTY TIME CMD
24702 pts/0 00:00:00 bash
24818 pts/0 00:00:00 sleep
24819 pts/0 00:00:00 ps
pi@raspberrypi:~ $ kill 24818
pi@raspberrypi:~ $ ps
PID TTY TIME CMD
24702 pts/0 00:00:00 bash
24821 pts/0 00:00:00 ps
[1]+ Terminated sleep 180
デフォルトのシグナルは、15のSIGTERMが実行される。主なシグナルは以下の通り。
1 | SIGHUP | 制御端末・プロセスのハングアップ |
2 | INT | キーボードからの割り込み(Ctrl + C) |
3 | QUIT | キーボードからの中止(Ctrl + \) |
15 | TERM | プロセスの終了命令(デフォルト) |
9 | KILL | プロセスの強制終了 |
19 | STOP | プロセスの停止命令 |
18 | CONT | プロセスの再開 |
しかし、毎回プロセス番号を調べてコマンドを実行するのは、かなり手間。そこで、プロセス名を指定してkillコマンドを実行できるpkillコマンドの出番。
pgrepとpkillコマンドを用いたプロセスの終了
pkillコマンドは、プロセス名を指定してプロセスに終了シグナルを送ることができる。
pkill [オプション] [プロセス名]
参考:https://www.atmarkit.co.jp/ait/articles/1708/03/news014.html
例えば、バックグラウンドで実行するvmstatコマンドをpkillで指定してプロセスを終了します。
pi@raspberrypi:~ $ ps
PID TTY TIME CMD
24702 pts/0 00:00:00 bash
24741 pts/0 00:00:00 vmstat
24742 pts/0 00:00:00 ps
pi@raspberrypi:~ $ pkill vmstat
[1]+ Terminated vmstat 60
pi@raspberrypi:~ $ ps
PID TTY TIME CMD
24702 pts/0 00:00:00 bash
24744 pts/0 00:00:00 ps
pkillで注意する点は、プロセス名が正規表現で指定されるため、「vmstat」と名前がつくプロセスは全て終了されてしまいます。
pi@raspberrypi:~
ps
PID TTY TIME CMD
24702 pts/0 00:00:00 bash
24752 pts/0 00:00:00 vmstat
24753 pts/0 00:00:00 vmstat
24754 pts/0 00:00:00 vmstat
24756 pts/0 00:00:00 ps
pi@raspberrypi:~ $ pkill vmstat
[1] Terminated vmstat 60
[2]- Terminated vmstat 60
[3]+ Terminated vmstat 60
そこで、同じ名前のプロセスがある場合、pgrepコマンドを使用してあらかじめプロセス名を確認してから、pkillを実行することをおススメします。
pgrep [オプション] [プロセス名]
参考:https://www.atmarkit.co.jp/ait/articles/1707/28/news010.html
pi@raspberrypi:~ $ pgrep -l vmstat
24775 vmstat
24776 vmstat
24777 vmstat
親がいない子プロセスの生存について
上記で説明した通り、基本的に親プロセスから複製されたプロセスが子プロセスであり、親子関係が成立している。そして、子プロセスの終了を確認するのが親プロセスの役目。しかし、この時親プロセスが先に終了してしまった場合どのようなふるまいをするのか、C言語を使って試す。
以下のコードは、子プロセスを生成して3秒後に親プロセスの処理を終了させる。親が終了したときの子プロセスの自分のPIDと親PIDを出力する。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
if (fork() == 0){
/*child*/
sleep(3);
printf("\nmy parent is now pid %d \n", getppid());
exit(0);
}
printf("parent process, pid %d\n", getpid());
exit(0);
}
出力結果
pi@raspberrypi:~/Documents/process_c/training $ ./prochan
parent process, pid 25236
my parent is now pid 1
子プロセスが処理を実行中に、親プロセスが終了すると、子プロセスは親プロセスのさらに親である、PID1と親子関係を持つ。つまり継母に受け継がれる。この場合は、PIDは1になっているが、環境によっては、PIDは別のIDを持つ。
また、親プロセスを正常に終了させたい場合は、コマンドライン上からpgrepコマンドとpkillコマンドを実行して、以下のように実行するといい。(参考:https://orebibou.com/ja/home/201703/20170322_001/)
pkill -TERM -P $(pgrep 終了させたいプロセス名)
まとめ
今回は、アプリケーションを作っていて、プロセスの生成と終了が気になったので少しまとめました。かなり参考書の情報が多いので、気になる方は、以下に今回使用した参考書を載せておくので、そちらでより理解を深めてください。
コメント