青木 峰郎 SBクリエイティブ 2017-09-22
こんにちは。
最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。
このエントリは完全な個人のメモです。
お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。
11章の内容はこちら。
amistad06-a.hatenablog.com
プロセスに関わるAPI
この章で説明すること
プロセスを作るAPIや終了するAPI、パイプに関するAPI、プロセスの親子関係、プロセスグループとセッションなどについて説明します。
fork(2)
自プロセスを複製して新しいプロセスを作るシステムコールです。
forkを呼び出した直後の状態のプロセスが複製されます。
forkを呼び出したプロセスを親プロセス、forkで新しく作られたプロセスを子プロセスと呼びます。
親プロセスと子プロセスはforkの返り値で判断します。
親プロセスには子プロセスのプロセスIDが返却され、子プロセスには0が返却されます。
forkに失敗した場合は子プロセスは作られず、-1が返却されます。
exec
自プロセスを新しいプログラムで上書きするシステムコールです。
いくつかのライブラリ関数とシステムコールが存在するので詳細はmanを参照してください。
wait(2), waitpid(2)
forkしたプロセスの終了を待ちます。
waitは子プロセスのうちどれか1つが終了するのを待ちます。
waitpidはpidで終了待ちするプロセスを選択できます。
プロセスをforkしてプログラムを実行してみる
fork, exec, waitを使ったプログラムを作成してみます。
コマンドライン引数でプログラム(コマンド)を受け取ってそれを実行するプログラムです。
#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);
}
pid = fork();
if(pid < 0){
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0){
// child process
execl(argv[1], argv[1], argv[2], NULL);
perror(argv[1]);
exit(99);
}else{
// parent process
int status;
waitpid(pid, &status, 0);
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);
}
}
実行結果は以下。
$ ./a.out /bin/echo OK
OK
child (PID=20501) finished; exit status=0
$ ./a.out /bin/ls ~/
Android share ダウンロード デスクトップ ビデオ ミュージック
prj tmp テンプレート ドキュメント ピクチャ 公開
child (PID=20503) finished; exit status=0
$
_exit(2)
プロセスを終了するシステムコールです。
引数に指定した整数値が終了ステータスになります。
exit(3)
プロセスを終了するライブラリ関数です。
引数に指定した整数値が終了ステータスになります。
終了時にstdioのバッファを全てフラッシュします。
また、atexit()で登録した処理の実行も行います。
終了ステータス
0が成功、1が失敗というのはLinux(UNIX)の決まり事です。
それぞれEXIT_SUCCESS, EXIT_FAILUREというマクロが定義されています。
ゾンビ
プロセスをfork()したときに親プロセスがwait()を呼ばない場合、子プロセスはずっと終了ステータスを保持したまま残り続けます。
この状態になったプロセスのことをゾンビプロセスと呼びます。
ゾンビプロセスが増えるとカーネルのリソースを食いつぶすので注意が必要です。
ゾンビプロセスになるのを防ぐには3つの方法があります。
・fork()したらwait()する
・ダブルfork
・sigaction()
パイプ
第3章で少し触れましたが、プロセスとプロセスを繋ぐストリームがパイプでした。
パイプもファイルディスクリプタで表現されます。
amistad06-a.hatenablog.com
pipe(2)
両端とも自プロセスに繋がったパイプを作成するシステムコールです。
パイプがある状態でforkをすると、inとoutがどちらも親プロセスと子プロセスに繋がったパイプができます。
例えば、この状態で親プロセスのout側のパイプをclose()し、子プロセスのin側のパイプをclose()すると親から子へのパイプができます。
dup(2), dup2(2)
第5章で軽く触れたシステムコールです。
ファイルディスクリプタを複製します。
このシステムコールとclose()を使用したら狙ったファイルディスクリプタにパイプを繋げることができます。
amistad06-a.hatenablog.com
popen(3)
プロセスをオープンし、それにつながるパイプストリームを返す関数です。
パイプは書き込みか読み込みのどちらか一方だけを指定できます。
pclose(3)
popenでfork()した子プロセスをwait()し、パイプストリームを閉じます。
プロセスの親子関係
Linuxではfork()またはそれに類するAPIでプロセスが作成されます。
pstreeコマンドでプロセスの親子関係を見ることができます。
systemdプロセス(またはinitプロセス)がLinux起動時にカーネルが直接起動するプログラムであり、すべてのプロセスの親になっています。
getpid(2), getppid(2)
getpidは自プロセスのプロセスIDを、getppid()は親プロセスのプロセスIDを取得するシステムコールです。
/proc
特定のプロセスの情報が欲しいときは/procを見ます。
psコマンドやpstreeコマンドもここから情報を取得しています。
プロセスグループとセッション
全てのプロセスは必ず1つのプロセスグループに属します。
パイプで繋がれたプロセスは同じプロセスグループに属します。
ちなみに、プロセスグループの中で最初に起動されたプロセスはプロセスグループリーダーと呼ばれ、PIDとPGIDが等しくなります。
セッションは端末(ターミナル)ごとに起動されているプロセスをまとめたものです。
PIDとSIDが等しいものはセッションリーダーと呼ばれます。
あるセッションが実行されている端末のことをそのセッションの制御端末と言います。
デーモンプロセス
制御端末がないプロセスのことをデーモンプロセスまたはデーモンと言います。
デーモンプロセスはプロセスを起動した人がログアウトした後も動き続けます。
setpgid(2)
新しいプロセスグループを作るためのシステムコールです。
setsid(2)
新しいセッションを作るためのシステムコールです。
setsidで作成したセッションは制御端末を持ちません。
つまり、デーモンプロセスになります。
演習問題
1. fork()したらプロセスが使うメモリは倍になるのでしょうか。調べなさい。
以下のプログラムで検証
#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);
}
// ★1
for(int i = 0; i < 5; i++){
sleep(3);
fprintf(stdout, "count = %d\n", i);
}
pid = fork();
if(pid < 0){
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0){
// child process
// ★2
for(int i = 0; i < 5; i++){
sleep(3);
fprintf(stdout, "count = %d\n", i);
}
execl(argv[1], argv[1], argv[2], NULL);
perror(argv[1]);
exit(99);
}else{
// parent process
int status;
waitpid(pid, &status, 0);
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);
}
}
forkする前のforループ(★1)と、子プロセスの中のforループ(★2)のタイミングでpsコマンドを実行したら以下の結果になった。
user@VirtualBox:/proc$ ps u -u user | grep 4770(★1のタイミングで実行)
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
user 4770 0.0 0.0 4508 748 pts/0 S+ 10:48 0:00 ./a.out /bin/ls .
user 4778 0.0 0.0 22560 1008 pts/1 R+ 10:48 0:00 grep --color=auto 4770
user@VirtualBox:/proc$ ps u -u user | grep 4770(★2のタイミングで実行)
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
user 4770 0.0 0.0 4508 748 pts/0 S+ 10:48 0:00 ./a.out /bin/ls .
user 4780 0.0 0.0 22560 1032 pts/1 S+ 10:48 0:00 grep --color=auto 4770
forkするだけではメモリの使用量は増えない。
※-uオプションはユーザー指定のオプション
※VSZが仮想メモリの量、RSSが物理メモリの量
2. fork()とexec()を使ってプログラムを起動する簡単なシェルを書きなさい
実装したプログラムは以下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define STR_MAX 100
int count_words(char str[]){
int cnt = 1;
for(int i = 0; i < STR_MAX; i++){
if(str[i] == ' '){
cnt++;
}
}
// fprintf(stdout, "cnt = %d\n", cnt);
return cnt;
}
int exec_cmd(char cmd[STR_MAX]){
pid_t pid;
char *tmp;
int num = count_words(cmd);
char *words[num + 1];
tmp = strtok(cmd, " ");
int i = 0;
while(tmp != NULL){
words[i] = tmp;
tmp = strtok(NULL, " ");
i++;
}
words[num] = NULL;
// for debug
// for(int j = 0; j < num + 1; j++){
// if(words[j] != NULL){
// fprintf(stdout, "words[%d] = %s\n", j, words[j]);
// }else{
// fprintf(stdout, "words[%d] = NULL\n", j);
// }
// }
pid = fork();
if(pid < 0){
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0){
// child process
execv(words[0], words);
perror(words[0]);
exit(99);
}else{
// parent process
int status;
waitpid(pid, &status, 0);
if(WIFEXITED(status)){
// 正常終了(何もしない)
}else if(WIFSIGNALED(status)){
// シグナル受信で終了
printf("signal, sig=%d\n", WTERMSIG(status));
}else{
printf("abnormal exit\n");
}
return status;
}
}
int main(int argc, char *argv[]){
char cmd[STR_MAX];
while(1){
fprintf(stdout, "$: ");
fgets(cmd, sizeof(cmd), stdin);
// fgetsで取得した文字列の最後の改行コードを削除
int len = strlen(cmd);
cmd[len-1] = '\0';
exec_cmd(cmd);
}
}
}
3. 問題2で作ったシェルにパイプとリダイレクトを実装しなさい。(難しい)
目下作成中。
まとめ
12章の内容はこんな感じです。
13章はまた今度。
おわり。
青木 峰郎 SBクリエイティブ 2017-09-22