Linuxシステムコール、プロセスの生成

UNIX / Linux システムコール・プログラミング

プロセスの生成

Linuxのプロセスとは?

Linuxでは、プログラムの実行単位がプロセスとして定義されています。
これまで、このシリーズでもプロセス間通信を取り上げてきましたが、そもそもプロセスとは何でしょうか?

今回は、このプロセスについて取り上げてみたいと思います。

さて、先程プログラムの実行単位がプロセスだと書きました。通常、我々はgccなどを使って作成された”実行ファイル”をメモリに読み込んでプログラムを実行します。これがプロセスです。

簡単な”Hello World”を例題に説明します。

hello.c
  1. #include <stdio.h>
  2.  
  3. int main(int argc, char** argv) {
  4.  
  5. printf("Hello World\n");
  6. }

このプログラムをコンパイルします。
    $ gcc hello.c -o hello
とすると、実行ファイル hello が出来上がります。
実行するには、
    $ ./hello
とすれば、実行されます。
当然ですが、このときにナニが起きているのか?
今まで気にしたことは無いかもしれませんが、詳しく見てみましょう。

”$ ./hello”では、実行ファイルをコマンドとして、シェルから実行しています。
このシェルというユーザーインターフェイスも1つのプロセスとしてメモリ空間に存在しています。

ユーザーが、”$ ./hello”と、入力するとシェルは、fork() というシステムコールを呼び出して別のプロセスのためのユーザー空間を生成します。

そして、exec()というシステムコールを使ってそのメモリ空間に実行ファイル”hello”を読み出して展開し、main()にジャンプすることで、実行ファイル”hello”が実行され、コンソールに"Hello World"が表示されます。

UNIX系のOSでは伝統的に fork()/exec() という2段階のシステムコールによってプロセスの生成を行います。

まず、fork()ですが、このシステムコールは呼び出したプロセスのコピーを作り出します。
実際のコードを見てみましょう。
  1. childID = fork();
  2.  
  3. if(childID != 0) {
  4. // 親プロセスの処理
  5. }
  6. else {
  7. // 子プロセスの処理
  8. }

fork()システムコールにより親プロセスをコピーした別のプロセス空間が生成され、fork()システムコールの戻り値で自分が親プロセスなのか子プロセスなのかを判断します。
つまり、この時点で子プロセスはfork()の戻りから開始されることになります。

上記のコードで、親プロセス側には生成した子プロセスのプロセスIDが戻ります。子プロセス側は0が戻ります。(エラーの場合は、<0)
この子プロセス側の処理でexec()を行って、自分自身のプログラムイメージを別の実行ファイルの内容に置き換えます。

また、親プロセス側は子プロセスの終了を待ち合わせるため、wait()システムコールで子プロセスの終了ま待ち合わせ、子プロセスの完了状態を知ることができるようになっています。

なぜ、イキナリexec()でメモリ確保して実行しないのか?

実は、この部分こそ先人たちの知恵の賜物と言える部分です。

先程の hello を実行するとき、下記のように実行すると、どうなるでしょうか?
    $ ./hello > HELLO
HELLOというファイルが作られ、内容は"Hello World\n"となっているはずです。

もともとの hello には何も手を加えていません。
リダイレクト、という機能を使っているからですね。
我々がUnix/Linuxを利用するときに普通にリダイレクトやパイプを使っていますが、fork()とexec()を分けることによってこの仕掛が簡単にできるようになっています。

まず、リダイレクト先のファイルを作ります。

それから、fork()システムコールで子プロセスと分離します。この時、子プロセス側でdup2()システムコールを使って標準出力をリダイレクト先のファイルのファイルディスクリプタに入れ替えてしまいます。

そして、exec()で hello を実行しますが、子プロセス側の標準出力は親プロセス側で生成されたファイルになっているので子プロセス側では出力ファイルの実態を意識することなくリダイレクト先のファイルに出力することが可能になります。

親プロセス側では、リダイレクト先のファイルは使わないので閉じてしまいます。そして、wait()で子プロセスの完了を待ち合わせます。

それでは、実際にプロセス生成してみましょう!

実際に親プロセス側から、子プロセスを起動し、子プロセスの完了を待ち合わせるプログラムを作成します。また、子プロセスに渡す引数によって子プロセス側からエラーを返すようにして親プロセス側で子プロセスの完了状態を判断できるようにしてみましょう。

親プロセス側ソース(forkexec.c)
  1. #include
  2. #include
  3. #include
  4. #include
  5. #include
  6. static pid_t childID;
  7. int main(int argc, char** argv) {
  8. if((childID = fork()) < 0 ) {
  9. perror("fork()");
  10. exit(-1);
  11. }
  12. else {
  13. if(childID != 0) { // 親プロセス
  14. int status;
  15. char ret; // 終了コードは8ビットである(注意!)
  16. waitpid(childID, &status, 0); // 子プロセスの待ち合わせ
  17. if(WIFEXITED(status)) { // 子プロセスは完了したか?
  18. ret = WEXITSTATUS(status);
  19. if(ret != 0) { // 子プロセスの完了コードがエラーである
  20. printf("Error from child process [%d]\n", ret);
  21. }
  22. else { // 子プロセスの完了コードは正常である
  23. printf("Child process was normal end.\n");
  24. }
  25. }
  26. else { // 子プロセスが異常終了した(セグメンテーション違反とか)
  27. printf("Child process was fail stopped.\n");
  28. }
  29. }
  30. else { // 子プロセス
  31. char *exec_argv[] = {"hello", argv[1], NULL};
  32. char *exec_env[] = { NULL };
  33. execve("hello", exec_argv, exec_env);
  34. perror("execve()");
  35. _exit(2);
  36. }
  37. }
  38. return(0);
  39. }
親プロセスは、まず fork() で子プロセスと分離します。子プロセス側では hello をexecve()で起動しますがこの時に親プロセスを起動した時の第1引数を子プロセスに渡しています。
親プロセス側では、子プロセスの状態変化をwaitpid()で待ち合わせます。子プロセスが完了したか異常終了したかをWIFEXITED()マクロで判定します。このWIFEXITED()マクロはプロセスが正常に完了した場合は真を、セグメンテーション違反などの異常終了した場合は偽を返します。この場合の正常とは、main()関数のreturnなど、プロセスの終了ポイントまで実行された状態を正常としています。
子プロセスの処理自体が正常であったかを判定するには、終了コードをWEXITSTATUS()マクロで取得しますが、このとき有効なデータサイズは8ビットになっていますので注意してください。
このサンプルでは、正常完了を0、エラー終了を0以外としています。

子プロセス側ソース(hello.c)
  1. #include
  2. #include
  3. int main(int argc, char** argv) {
  4. int ret = 0;
  5. if(argc == 2) {
  6. printf("Hello :%s\n", argv[1]);
  7. }
  8. else {
  9. ret = -9;
  10. }
  11. _exit(ret);
  12. }
子プロセス側では、起動されたときの第1引数を ”Hello :"の文字列に続けて標準出力に出力していますが、起動引数が2以外の場合は表示処理を行わず、終了コードを"-9"として終了します。
終了の_exit()システムコールは、呼んだ時点でそのプロセスを直ちに終了させるためのシステムコールです。位置的には、return()としても等価となりますが、明示的なプロセス終了としてこのコードでは_exit()としています。

実際に実行すると以下のようになります。
まずは、第1引数を指定して実行すると、
  $ ./forkexec LightningBrains
  Hello :LightningBrains
  Child process was normal end.

次に引数を入れないで実行すると、
  $ ./forkexec
  Error from child process [-9]


wait()やexec()のシステムコールがいくつか存在します。
基本的に動作する内容は同じです。
今回使用したexecve()が基本で、使い勝手を考慮したいくつかのフロントエンド関数がexecXXX()のような名前で実装されています。自分が使いやすいものを利用されるといいでしょう。

プロセスの制御のために使うシステムコールがいくつかありますが、原理を理解してしまえば問題ありません。
動的にプロセスを制御するプログラミンができることで、システムリソースをフルに活用するようなプログラミングも可能になります。



Have a Happy Hucking!!

Lightning Brains

コメント

このブログの人気の投稿

Linuxシステムコール、メッセージキューの使い方

Linuxシステムコール、共有メモリの使い方

Linuxシステムコール、セマフォの使い方