システムプログラム(第4回)

情報学群情報科学類
大山 恵弘

理解を深めるために

目次

  1. プロセスの概念と機能
  2. プロセスに関する基本操作: プロセス生成,プログラム実行,プロセス終了
  3. プロセスに関する他の操作
  4. リダイレクション
  5. パイプ

プロセスの概念と機能

プログラムとプロセス

実行形式のプログラムには,CPU が実行できる機械語命令とそれにより処理されるデータの集合(実行形式,ロードモジュール)が格納されている. 他に,ヘッダと呼ばれる部分には機械語命令やデータなどの各領域についての情報が含まれる.また,共有ライブラリを動的リンクするように作成された実行形式には,実行に必要な共有ライブラリの情報が含まれる.デバッグ情報付きでコンパイルされた実行形式には,デバッグ情報なども含まれる.

一方,実行中のプログラムがプロセスである. プロセスに入っている実行に必要な情報には,プログラムに含まれている情報もあれば,プログラムに含まれていない情報もある. 初期値が0(初期値なし)のデータ領域はプログラムに含める必要性はなく,そのデータ領域がプロセス中のどこにどれだけの大きさで必要かという情報だけがプログラムに含まれていればよい. しかし,プログラムの実行にはそのデータ領域が必要であるため,プロセスではそのデータ領域が実際に確保されている必要がある. 初期値があるデータ領域の初期値はプログラムに含める必要がある. 実行が進めばデータ領域のデータは書き換えられたり,追加されたりする. 機械語命令を格納するためのテキスト領域の確保も必要である. テキスト領域は通常書き換わらない. プログラムには含まれていない,実行の履歴や状態(関数呼び出し履歴や局所変数の値)を格納するためのデータ領域(スタック領域)も必要である. ヘッダやデバッグ情報は必要があればプログラムファイルから読み込めばよい.

プログラムとプロセスの関係

プロセスの機能

プロセスの重要な機能として資源割り当てと保護がある.

資源割り当て

UNIX におけるプロセスは,プロセッサ時間,メモリ,ファイル,キーボードやディスプレイ,プリンタなどのデバイスといった,処理を行うにあたって必要となる計算資源の割り当て単位である. ユーザがプログラムを起動すると,それはプロセスにより実行される. OS カーネルによりプロセスに対しプロセッサ時間やメモリが割り当てられプロセスの実行が始まる. プロセスは,プログラムに記述された通りにファイルをアクセスし,キーボードやディスプレイなどのデバイスを使用する. ファイルをオープンした時に得るファイルディスクリプタは各プロセス固有のものになる. ファイルのアクセス権はユーザに対して与えられるが,計算資源はプロセスに対して与えられる.

基本的には,OSカーネルは,プロセッサ時間やメモリを各プロセスにほぼ平等に割り当てる. ファイルやデバイスは先着順で割り当てる.

保護

プロセスは,割り当てられた資源が保護される単位でもある. あるプロセスは他のプロセスに割り当てられた資源に対し許可なくアクセスすることはできない. たとえ同じユーザが生成したプロセス間であってもである. 保護の機能により,例えばある暴走したプロセスが他のプロセスの実行内容を破壊するようなことは起こらない.

プロセスのメモリマップ

プロセスのメモリマップを詳しく見ると以下のようになる.

メモリマップ

それぞれの領域の用途は以下の通りである:
テキスト領域 プログラムの機械語命令が置かれる領域.この領域は読み出し専用になっており,同じプログラムを実行しているプロセスの間で共有可能になっている.
データ領域(初期値あり) 0以外の初期値を持つ大域変数や,static と指定された局所変数が置かれる領域.
データ領域(初期値なし) プログラムの BSS (Block Started by Symbol) と呼ばれる部分に対応する領域. 初期値を持たないまたは初期値が0の大域変数や,static と指定された局所変数が置かれる領域.プロセス生成時に確保され,0に初期化される.
ヒープ領域 malloc 関数などにより,プロセスの実行中に確保されるデータ領域.
共有ライブラリ 共有ライブラリを配置するための領域. ヒープとスタックの間にとられる. テキスト領域と同じく読み出し専用で,他のプロセスと共有される.
スタック領域 static と指定されていない局所変数,関数引数,関数呼び出し時の戻り番地などが置かれる領域.
引数,環境変数 プログラムに渡される引数と環境変数は,スタック領域の最上位部分に格納されている.

上記のメモリマップは,以下の簡単なプログラムで確かめることができる. 環境変数が格納されている文字列へのポインタは environ という変数に格納されている. 初期値が設定されない data0 はデータ領域(初期値なし)に配置され,初期値を持つ data1 はデータ領域(初期値あり)に配置される.

memorymap.c
     1  #include <stdio.h>
     2
     3  extern char **environ;
     4
     5  int data0;
     6  int data1 = 10;
     7
     8  int main(int argc, char *argv[])
     9  {
    10          char c;
    11
    12          printf("environ:\t%p\n", environ);
    13          printf("argv:\t\t%p\n", argv);
    14          printf("stack:\t\t%p\n", &c);
    15
    16          printf("bss:\t\t%p\n", &data0);
    17          printf("data:\t\t%p\n", &data1);
    18
    19          return EXIT_SUCCESS;
    20  }

このプログラムをコンパイル,実行すると,以下のような結果が得られる. 環境変数の文字列が置かれたアドレスが一番大きく,次にコマンドライン引数の文字列の配列,その次がスタックであることがわかる. 実際にはスタックのアドレスとして表示している値は,スタック内に確保される変数 c の領域のアドレスである. また,データ領域(初期値なし)が配置されるアドレスが,データ領域(初期値あり)が配置されるアドレスよりも大きいこともわかる.

$ ./a.out[←]
environ:        0x7ffee7dc5950
argv:           0x7ffee7dc5940
stack:          0x7ffee7dc590f
bss:            0x107e3b024
data:           0x107e3b020
$

プロセスの属性

プロセスの属性には,主に以下のようなものがある.これらの多くはpsコマンドで参照できる.また./proc の下には,psによる出力よりも詳しい情報が書かれたファイルがある.

PID(プロセス ID) それぞれのプロセスにつけられる,プロセスを識別するための番号.Linuxでは 0〜32767 の範囲の整数.
PPID(親プロセス ID) そのプロセスを生成したプロセス(親プロセス)のプロセスID.
PGID(プロセスグループ ID) 所属するプロセスグループの ID.プロセスグループは,複数プロセスにまとめてシグナルを送る場合などに使用される.
UID(ユーザ ID) プロセスを実行したユーザの ID.これとは別にアクセス権限を表す実効ユーザ ID もある.
GID(グループ ID) プロセスを実行したグループの ID.これとは別にアクセス権限を表す実効グループ ID もある.
ファイルディスクリプタ オープンしたファイルの表.
umask ファイル作成時のモードを決める時にマスク値として使用される値.
制御端末 シグナルを受け取る端末.
カレントディレクトリ 現在のディレクトリ.相対パス名を使う場合の出発点となる.current working directory とも言う.
ルートディレクトリ ルートディレクトリはプロセスごとに決めることができる.通常,アクセスできるファイルを制限するために使用する.
実行ステート 実行中か,停止中か,ゾンビ状態か,などのプロセスの実行状態.
優先順位 プロセスの実行優先順位.
シグナル制御情報 シグナルに対応してどの処理が行われるかの情報.
利用可能資源量 プロセスの使える資源の上限.
実行統計情報 これまでの資源利用量の統計情報.

環境変数

メモリマップを確かめるプログラムに出てきたが,環境変数はプロセスに文字列で渡される. 環境変数を構成する文字列群の構造は argv の構造と同じであり,文字列の配列である. C言語では通常,その文字列の場所は外部変数 environ により指し示される. environはその配列を指すポインタ(従って型は char **)である.

環境変数を(便利に)操作するために以下のライブラリ関数が用意されている.

char *getenv(const char *name);                                   /* 環境変数の値の取得 */
int   putenv(char *string);                                       /* 環境変数の追加または修正 */
int   setenv(const char *name, const char *value, int overwrite); /* 環境変数の追加または修正 */
void  unsetenv(const char *name);                                 /* 環境変数の削除 */

プロセスを操作するコマンド

プロセスに対して操作を行うプログラムにはいろいろある. 以下は一部のよく使用するコマンドである.

シェル(sh, ksh, bash, zsh, csh, tcsh) コマンドの実行(プロセス生成,プログラム実行),リダイレクション,パイプなど
ps, pstree, pgrep, top プロセスの観察
kill, pkill プロセスの(強制)終了
nice プロセスの実行優先順位の制御
limit プロセスの利用可能資源の制御
gdb デバッグツール

これらのコマンドは他のプログラムと同様に,様々なシステムコールを呼び出すことによってその機能を実現している.

プロセスに関する基本操作: プロセス生成,プログラム実行,プロセス終了

プロセスの生成とプログラムの実行

概要

ユーザのプログラムはプロセスにより実行される. 通常,ユーザがシェルのコマンドプロンプトの後ろにコマンド名を打ち込むと,そのコマンドが実行される. この処理をもう少し詳しく見てみると,

  1. シェルを実行するプロセスは,コマンドを実行するプロセスを生成する
  2. 生成されたプロセスは,指定されたコマンドを実行する
  3. シェルは,コマンドを実行するプロセスが終了するのを待つ
  4. コマンドが終了すると,シェルの実行が再開される
というステップになっている.これらのステップにはそれぞれ以下のシステムコールが対応している.

pid_t fork(void);
int   execve(const char *filename, char *const argv[], char *const envp[]);
pid_t wait(int *status)
void  exit(int status);

fork システムコールは,fork システムコールを呼んだプロセスの複製を生成する. fork システムコールを呼んだプロセスが親プロセスとなり,複製され新たに生成されたプロセスが子プロセスになる. execve システムコールは,execve システムコールを呼んだプロセスに,指定されたプログラムをロードして実行する. wait システムコールは,子プロセスの終了を待つ. exit システムコールは,呼び出したプロセスを終了させる.

fork, execve, wait, exit によるプロセスの生成,プログラムの実行,プロセスの終了の待機,プロセスの終了を図示すると以下のようになる.

プロセスの生成

UNIX では,プロセスの生成は fork,プログラムの実行は execve でしかできない(Linux では clone というシステムコールでプロセスを生成することもできるが,これは元々スレッドをサポートするために入れたものであり,やや邪道である).

fork によるプロセスの生成と execve によるプログラムの実行は,一見不可解な組み合わせかもしれない. プロセスを生成してプログラムを実行するところまで,1つのシステムコールでやってしまえば良いと思うかもしれない. しかし,次のリダイレクションやパイプを見てみると,fork と execve の組み合わせになっている謎が(少しは)解けるかもしれない.

プログラム例(fork)

以下のプログラム例では簡単のために,execve を使わず,fork, wait, exitだけを使っている.

fork.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <sys/types.h>
     5  #include <sys/wait.h>
     6
     7  void do_child(void)
     8  {
     9          printf("This is child(pid=%d).\n", getpid());
    10          exit(2);
    11  }
    12
    13  int main(void)
    14  {
    15          int child, status;
    16
    17          if ((child = fork()) < 0) {
    18                  perror("fork");
    19                  exit(1);
    20          }
    21
    22          if (child == 0) {
    23                  do_child();
    24          } else {
    25                  if (wait(&status) < 0) {
    26                          perror("wait");
    27                          exit(1);
    28                  }
    29                  printf("The child (pid=%d) exited with status(%d).\n",
    30                         child, WEXITSTATUS(status));
    31          }
    32
    33          return EXIT_SUCCESS;
    34  }

fork は自分をコピーして子プロセスを作る. 現在実行中の機械語命令の場所もコピーする. 従って,子プロセスは fork から戻るところからプロセスの実行が始まる(17行目). fork を呼び出した親プロセスも fork から戻る(同じく17行目). どちらが親でどちらが子かは,fork の戻り値から判別することができる(22行目). fork は戻り値として,親プロセスには子プロセスのプロセス ID を返し,子プロセスには0を返す. fork が失敗した時には負の値を返す.

親プロセスは fork の後 wait を呼び出し,子プロセスが終了するまで実行を一時停止する(25行目).

子プロセスは getpid で取得したプロセス ID と共にメッセージを表示した後,exit を呼んでプロセスを終了する(10行目). exit の引数として 2 を与えており,この値は wait で待っている親プロセスに渡される.

子プロセスが終了すると,wait で待っている親プロセスの実行は再開され,wait の呼び出しから戻る. 戻るときに status の値がセットされる(25行目). セットされる値は,子プロセスが exit を呼び出した時の引数の値である. その値を取り出すために WEXITSTATUS というマクロが使われる(30行目).

以下は,このプログラムをコンパイル,実行した結果である. 親プロセスが受け取る fork の戻り値と,子プロセスが getpid で取得するプロセスIDが同じであること,また子プロセスでの exit の引数が,親プロセスの wait に渡されていることがわかる.

$ ./a.out[←]
This is child(pid=69978).
The child (pid=69978) exited with status(2).
$

プログラム例(execve)

以下は execve を使用した簡単なプログラム例である. このプログラムは /bin/ls を引数なしで実行する. このプログラムを実行すると,そのプロセスに /bin/ls のプログラムが上書きされるようにロードされ /bin/ls プログラムの main 関数から実行が始まる. すなわち,このプログラムを実行していたプロセスは /bin/ls を実行するプロセスに変わってしまう.

execve には引数と環境変数をプロセスが必要とする形式(argv 形式)で渡す必要がある. 引数を格納するための配列 argv を宣言し(7行目),配列に値をセットしている(9, 10行目). 新しいプログラムで使う環境変数がある場所として,現在使用している環境変数がある場所をそのまま渡している(3, 12行目).

execve は,実行するプログラムを,シェルのようにパスをサーチしてみつけてはくれない. 従って,実行するプログラムは絶対パスで指定する必要がある(9行目).

execve0.c
     1  #include <unistd.h>
     2
     3  extern char **environ;
     4
     5  int main(void)
     6  {
     7          char *argv[2];
     8
     9          argv[0] = "/bin/ls";
    10          argv[1] = NULL;
    11
    12          execve(argv[0], argv, environ);
    13
    14          return EXIT_FAILURE;
    15  }

実際に execve が実行するプログラムは,execve の第1引数で指定されたプログラムである. そのプロセスに,execve の第2引数に指定された文字列の配列が渡される. 従って,以下のように書いても,/bin/ls は実行できる.

execve1.c
     1  #include <unistd.h>
     2
     3  extern char **environ;
     4
     5  int main(void)
     6  {
     7          char *argv[2];
     8
     9          argv[0] = "ls";
    10          argv[1] = NULL;
    11
    12          execve("/bin/ls", argv, environ);
    13
    14          return EXIT_FAILURE; 
    15  }

execve で実行するプログラムに引数を与えたい場合は,argv[1] 以降に指定すれば良い. 以下は /bin/ls / を実行するプログラムの例である.

execve2.c
     1  #include <unistd.h>
     2
     3  extern char **environ;
     4
     5  int main(void)
     6  {
     7          char *argv[3];
     8
     9          argv[0] = "/bin/ls";
    10          argv[1] = "/";
    11          argv[2] = NULL;
    12
    13          execve(argv[0], argv, environ);
    13
    14          return EXIT_FAILURE; 
    15  }

実行してみると,ルートディレクトリの内容が表示され,正しく引数が渡されていることがわかる.

$ ./a.out[←]
Applications    System          dev             net             tmp
Library         Users           etc             opt             traces.log
Network         Volumes         home            private         usr
Pictures        bin             mach_kernel     sbin            var
$

ここまで読んで,鋭い人は argv[0] は何のためにあるのかと疑問を持つかもしれない. argv[0] に与える文字列はマニュアルでは

... by custom, the first element should be the name of the executed program (for example, the last component of path).
などと規定されている.この文字列は,たとえば ps コマンドによる表示で利用される.

プログラム例(fork&execve)

上記の fork のプログラムと execve のプログラムを組み合わせると,以下のようになる.

fork_exec.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <sys/types.h>
     5  #include <sys/wait.h>
     6
     7  extern char **environ;
     8
     9  void do_child(void)
    10  {
    11          char *argv[3];
    12
    13          printf("This is child (pid=%d).\n", getpid());
    14
    15          argv[0] = "/bin/ls";
    16          argv[1] = "/";
    17          argv[2] = NULL;
    18
    19          execve(argv[0], argv, environ);
    20  }
    21
    22  int main(void)
    23  {
    24          int child, status;
    25
    26          if ((child = fork()) < 0) {
    27                  perror("fork");
    28                  exit(1);
    29          }
    30
    31          if (child == 0) {
    32                  do_child();
    33          } else {
    34                  if (wait(&status) < 0) {
    35                          perror("wait");
    36                          exit(1);
    37                  }
    38                  printf("The child (pid=%d) exited with status(%d).\n",
    39                         child, WEXITSTATUS(status));
    40          }
    41
    42          return 100;
    43  }

実行結果には変わりはないが,子プロセスの exit status は /bin/ls が設定した 0 になっている.

$ ./a.out[←]
This is child (pid=1398).
Applications    System          dev             net             tmp
Library         Users           etc             opt             traces.log
Network         Volumes         home            private         usr
Pictures        bin             mach_kernel     sbin            var
The child (pid=1398) exited with status(0).
$

execve を使ったプログラムを書くときには,execve が失敗する可能性も考えなければならない. execve はプログラムの実行に失敗すると負の整数の戻り値を返す. 以下は,失敗に対応するための処理を追加し(18~21行目),存在しないプログラムを実行しようとした例である.

execve_fail.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <sys/types.h>
     5  #include <sys/wait.h>
     6
     7  extern char **environ;
     8
     9  void do_child(void)
    10  {
    11          char *argv[2];
    12
    13          printf("This is child (pid=%d).\n", getpid());
    14
    15          argv[0] = "/bin/xxxxx";
    16          argv[1] = NULL;
    17
    18          if (execve(argv[0], argv, environ) < 0) {
    19                  perror("execve");
    20                  exit(1);
    21          }
    22  }
    23
    24  int main(void)
    25  {
    26          int child, status;
    27
    28          if ((child = fork()) < 0) {
    29                  perror("fork");
    30                  exit(1);
    31          }
    32
    33          if (child == 0) {
    34                  do_child();
    35          } else {
    36                  if (wait(&status) < 0) {
    37                          perror("wait");
    38                          exit(1);
    39                  }
    40                  printf("The child (pid=%d) exited with status(%d).\n",
    41                         child, WEXITSTATUS(status));
    42          }
    43
    44          return 100;
    45  }

上記のプログラムを実行すると,execve は失敗し,perror の出力するエラーメッセージが表示される.

$ ./a.out[←]
This is child (pid=1409).
execve: No such file or directory
The child (pid=1409) exited with status(1).
$

その他のシステムコール

exit は現在はライブラリ関数となっており,_exit がシステムコールである. なぜ exit がライブラリ関数になっているのかは,atexit 関数のマニュアルを見るとわかる.

特定のプロセスの終了を待つには waitpid を使用する.

void  _exit(int status);
pid_t waitpid(pid_t pid, int *status, int options);

プログラム実行のためのライブラリ関数

execve には引数や環境変数を argv 形式で渡す必要があるが,そのための処理を記述するのが面倒であることがある. また,execve はプログラムのサーチをしてくれない. さらに,環境変数の情報を明示的に与える必要がある. そこで,もう少し簡単に使えるようにと,以下のライブラリ関数が用意されている.

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

execl, execlp, execle ではコマンドライン引数の渡し方が execve とは異なる. これらのライブラリ関数には,コマンドライン引数のように文字列を並べて引数を渡す.

     1  #include <unistd.h>
     2
     3  int main(void)
     4  {
     5          execl("/bin/ls", "ls", "/", NULL);
     6          return EXIT_FAILURE;
     7  }

また,execlp, execvp は環境変数 PATH を使用してプログラムをサーチしてくれる. よって,それらの関数の第一引数に渡すプログラムは絶対パスで書かなくても良い.

     1  #include <unistd.h>
     2
     3  int main(void)
     4  {
     5          execlp("ls", "ls", "/", NULL);
     6          return EXIT_FAILURE;
     7  }

プロセスに関する他の操作

プロセスの強制終了

プロセスを終了するために通常使用される kill コマンドは,kill システムコールを呼ぶことで実現される. kill システムコールは,指定されたプロセスにシグナルと呼ばれるイベントを送るためのものである. シグナルについては第5回目の講義で詳しく述べる.

int kill(pid_t pid, int sig);

メモリ領域の確保

malloc は動的に確保され free により解放されたメモリ領域をできるだけ使いまわすように設計されているが,現在のヒープ領域だけではメモリ領域が足りなくなった時にヒープ領域を拡大するために,システムが暗黙に以下のシステムコールを呼び出すことがある. 呼び出し後はヒープ領域が拡大され,新たなメモリ領域確保ができるようになる. これらのシステムコールをアプリケーションプログラムが呼び出す機会は少ない.

int brk(void *end_data_segment);
void *sbrk(ptrdiff_t increment);

簡単なプログラム実行

以下のライブラリ関数は,外部プログラムを簡便に実行できるように用意されている. popen や system を呼び出すと子プロセスが生成され,その子プロセスがシェルを起動し,そのシェルが command の引数に与えられたコマンド行を実行する. popen を使うと,子プロセスの標準入力や標準出力を親プロセスのファイルポインタに関係づけることができる.

FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);
int system(const char *command);

その他のシステムコール

以下は,プロセスの実行優先順位や利用可能資源の制御,デバッグ等を行うためのシステムコールである.

int nice(int incr);
int getrlimit(int resource, struct rlimit *rlp);
int getrusage(int who, struct rusage *r_usage);
int setrlimit(int resource, const struct rlimit *rlp);
int ptrace(int request, pid_t pid, caddr_t addr, int data);

リダイレクション

概要

リダイレクションは標準入出力をファイルに切り替える機能である. 以下では,ls の結果をファイルに出力したり,wc への入力をファイルにしたりしている.

$ ls *.c > a.txt[←]
$ wc < a.txt[←]
     38      38     374
$

リダイレクションにより入出力をファイルに対して行うという処理は ls, wc とは無関係に行われている. プロセスの標準入出力は,ファイルディスクリプタの0, 1, 2に割り当てられている. これらのファイルディスクリプタの先が何であるかは,プロセスは関知しない. プロセスは0は標準入力,1は標準出力,2は標準エラー出力であるとして処理を行う.

execve はプログラムを実行するためにプロセスのメモリ空間を上書きし,そのプログラムの main 関数から実行を始めるが,ファイルディスクリプタを含む他の属性は変更しない. よって,ファイルディスクリプタ0の先をファイルに切り替えてから execve を呼ぶと,execve によって起動されるプログラムにおける標準入力はそのファイルになる. 従って,標準入力からの読み込みを行うと,そのファイルの内容が読み込まれることになる. また,ファイルディスクリプタ1の先をファイルに切り替えてから execve を呼ぶと,execve によって起動されるプログラムにおける標準出力への書き込みは,そのファイルへの書き込みとなる.

ファイルディスクリプタ0, 1, 2によってファイルの入出力を行うためには,ファイルディスクリプタの付け替えをしてくれるシステムコールである dup か dup2 を呼ぶ. どちらも指定されたファイルディスクリプタの複製を作成する. 呼び出し後は,古いファイルディスクリプタも,dup や dup2 によって作成された新しいファイルディスクリプタも,同じものとして使用できる状態になっている. 古いファイルディスクリプタを close しなければ,古いファイルディスクリプタは有効なままである. 古いファイルディスクリプタを close しても,新しいファイルディスクリプタは有効なままである.

int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup は新しいファイルディスクリプタとして,その時点で使用されていない最小のファイルディスクリプタの値を使用し,dup2 は newfd として指定された値を使用するという点だけが異なる.

dup2 を使用したリダイレクションの設定を図示すると,以下のようになる.

リダイレクションの処理

  1. (1) ファイルをオープンし,(例えば)ファイルディスクリプタ3を得る.
  2. (2) 標準入力(ファイルディスクリプタ0)をクローズする.
  3. (3) dup2 により,ファイルディスクリプタ3の複製をファイルディスクリプタ0として作成する.
  4. (4) ファイルディスクリプタ3をクローズすることにより,ファイルはファイルディスクリプタ0からのみアクセスできるようになる.

プログラム例

以下は,第1引数に指定されたファイルを標準入力として wc を実行するプログラムである. 15~19行目で第1引数に指定されたファイルをオープンする処理を行っている. 21行目でファイルディスクリプタ 0 をクローズし,再利用可能な状態にしている. 22行目で第2引数を0として dup2 を呼び出し,15行目でオープンしたファイルをファイルディスクリプタ 0 からもアクセスできるようにしている. 27行目で,15行目のオープンで取得したファイルディスクリプタの方をクローズしている. これにより,オープンしたファイルはファイルディスクリプタ0からのみアクセスできる状態になる. 29行目で execlp を呼び,wc を起動している. ファイルディスクリプタ0は15行目でオープンしたファイルとつながっているため,wc の標準入力はそのファイルから読み込まれることになる.

redirect.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <fcntl.h>
     5
     6  int main(int argc, char *argv[])
     7  {
     8          int file_fd;
     9
    10          if (argc != 2) {
    11                  fprintf(stderr, "Usage: %s file_name\n", argv[0]);
    12                  exit(1);
    13          }
    14
    15          file_fd = open(argv[1], O_RDONLY);
    16          if (file_fd < 0) {
    17                  perror("open");
    18                  exit(1);
    19          }
    20
    21          close(0);
    22          if (dup2(file_fd, 0) < 0) {
    23                  perror("dup2");
    24                  close(file_fd);
    25                  exit(1);
    26          }
    27          close(file_fd);
    28
    29          execlp("wc", "wc", NULL);
    30
    31          return EXIT_FAILURE;
    32  }

以下は,上記のプログラムの実行結果である.シェルでリダイレクションを使った場合と同じ結果が得られている.

$ ./a.out a.txt[←]
      33      59     667
$ wc < a.txt
      33      59     667
$

パイプ

概要

シェルでコマンドを実行する場合,パイプ機能を用いることで,あるコマンドの出力を別のコマンドの入力とすることができる. 例えば以下のように ls の結果を wc の入力にすることによって,現在のディレクトリにあるファイルやディレクトリの数を数えることができる. この例では .c で終わるファイルの数を数えている.

$ ls *.c | wc[←]
      38      38     374
$

パイプはプロセス間でデータをやり取りするための通信路を表現するデータ構造であり,入口と出口をプロセスに提供する.プロセスがパイプの入口から書いたデータを,別のプロセスが出口から読み出すことができる. パイプをもう少し詳しく見ると下図のようになる. データの流れの方向は,入口から出口の単方向であり,入口に書いた順番で出口から読み出される.

パイプ

このパイプを実現するのが,その名の通り pipe システムコールである.

int pipe(int fildes[2]);

pipe システムコールを呼ぶと,入力と出力それぞれの口(ファイルディスクリプタ)を持つパイプが1本作られる. ファイルディスクリプタは,システムコールの引数に受け取った fildes 配列に格納され,fildes[0] がパイプの出口,fildes[1] がパイプの入口のファイルディスクリプタになる. すなわち,以後は fildes[1] にデータを書き,fildes[0] からデータを読む. この関係は,ファイルディスクリプタ 0 が標準入力,ファイルディスクリプタ 1 が標準出力であることと対比させるとわかりやすい.

プロセス間のパイプの作成を図示すると以下のようになる.

パイプの作成

  1. (1) pipe システムコールを呼ぶと,入力と出力それぞれの口(ファイルディスクリプタ)を持つパイプが1本作られる.
  2. (2) この状態で fork を呼ぶと,親プロセスと子プロセスがパイプ(入出力の口)を共有した状態になる.
  3. (3) 一方のプロセスが入力を,もう一方が出力の口を閉じると,プロセスからプロセスへデータを送れる(プロセス間通信ができる)状態になる.

プログラム例

以下のプログラムは,パイプを作成した後 fork を実行し,子プロセスから親プロセスへ文字列を送る.

pipe.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <sys/types.h>
     5  #include <sys/uio.h>
     6  #include <sys/wait.h>
     7
     8  int pipe_fd[2];
     9
    10  void do_child(void)
    11  {
    12          char *p = "Hello, dad or mam!\n";
    13
    14          printf("This is child.\n");
    15
    16          close(pipe_fd[0]);
    17
    18          while (*p != '\0') {
    19                  if (write(pipe_fd[1], p, 1) < 0) {
    20                          perror("write");
    21                          exit(1);
    22                  }
    23                  p++;
    24          }
    25  }
    26
    27  void do_parent(void)
    28  {
    29          char c;
    30          int count, status;
    31
    32          printf("This is parent.\n");
    33
    34          close(pipe_fd[1]);
    35
    36          while ((count = read(pipe_fd[0], &c, 1)) > 0) {
    37                  putchar(c);
    38          }
    39
    40          if (count < 0) {
    41                  perror("read");
    42                  exit(1);
    43          }
    44
    45          if (wait(&status) < 0) {
    46                  perror("wait");
    47                  exit(1);
    48          }
    49  }
    50
    51  int main(void)
    52  {
    53          int child;
    54
    55          if (pipe(pipe_fd) < 0) {
    56                  perror("pipe");
    57                  exit(1);
    58          }
    59
    60          if ((child = fork()) < 0) {
    61                  perror("fork");
    62                  exit(1);
    63          }
    64
    65          if (child) {
    66                  do_parent();
    67          } else {
    68                  do_child();
    69          }
    70
    71          return EXIT_SUCCESS;
    72  }

このプログラムをコンパイル,実行すると以下のような結果が得られる.

$ ./a.out[←]
This is parent.
This is child.
Hello, dad or mam!
$

上記のプログラムを dup2 を使用し,標準入出力をパイプで接続するように修正すると,以下のようになる. パイプの不要な出入口のクローズ(13, 34行目)と,dup2 によるリダイレクションのためのクローズ(15, 20, 36, 41行目)と,たくさんクローズを呼ぶ必要がある.

pipe2.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4
     5  int pipe_fd[2];
     6
     7  void do_child(void)
     8  {
     9          char *p = "Hello, dad or mam!\n";
    10
    11          printf("This is child.\n");
    12
    13          close(pipe_fd[0]);
    14
    15          close(1);
    16          if (dup2(pipe_fd[1], 1) < 0) {
    17                  perror("dup2 (child)");
    18                  exit(1);
    19          }
    20          close(pipe_fd[1]);
    21
    22          while (*p != '\0') {
    23                  putchar(*p++);
    24          }
    25  }
    26
    27  void do_parent(void)
    28  {
    29          char c;
    30          int status;
    31
    32          printf("This is parent.\n");
    33
    34          close(pipe_fd[1]);
    35
    36          close(0);
    37          if (dup2(pipe_fd[0], 0) < 0) {
    38                  perror("dup2 (parent)");
    39                  exit(1);
    40          }
    41          close(pipe_fd[0]);
    42
    43          while ((c = getchar()) != EOF) {
    44                  putchar(c);
    45          }
    46
    47          if (wait(&status) < 0) {
    48                  perror("wait");
    49                  exit(1);
    50          }
    51  }
    52
    53  int main(void)
    54  {
    55          int child;
    56
    57          if (pipe(pipe_fd) < 0) {
    58                  perror("pipe");
    59                  exit(1);
    60          }
    61
    62          if ((child = fork()) < 0) {
    63                  perror("fork");
    64                  exit(1);
    65          }
    66
    67          if (child) {
    68                  do_parent();
    69          } else {
    70                  do_child();
    71          }
    72
    73          return EXIT_SUCCESS;
    74  }

ところで,パイプの不要な出入り口をクローズする必要があるのはなぜだろうか? 最も大きな理由は,パイプの入口に対応するファイルディスクリプタがすべてクローズされるまで,そのパイプの出口には EOF が送られないからである. EOF が送られない結果,そのパイプの出口からデータを読み出そうとしているプロセスは,読み出し用の関数(getcharなど)からいつまでたってもリターンしないことになる.その結果,プログラムが固まるなどの影響が出る.

fork と execve がなぜ分かれているかという話に戻ろう. プロセスの生成と実行を一度に行うシステムコールがあったとして,同様のプロセス間通信を実現しようとしたら,プログラムはうまく書けるだろうか? そのシステムコールにいろいろなオプションや引数を渡せるようにして実現するのであろうか. それとも,より汎用のプロセス間通信機能を使うのであろうか. 少なくとも,fork と execve が分かれていることにより,パイプはより使いやすくなり,リダイレクションはより実現しやすくなっている.

練習問題

練習問題(401)

printenv コマンドは,現在のプロセスの環境変数を表示してくれる. 同様の表示をしてくれるコマンドを,C 言語のプログラムとして記述せよ.

練習問題(402)

メモリマップを確かめるプログラムを Linux 上でコンパイルし,繰り返し実行したところ,以下のようにスタックのアドレスが毎回若干変化することがわかった. この現象が起きることを実際にプログラムを実行して確認しなさい.さらに,実行するごとにスタックの場所が変わる理由を予想した後,調査しなさい.

$ ./a.out[←]
environ:        0x7ffc47977e08
argv:           0x7ffc47977df8
stack:          0x7ffc47977d0f
bss:            0x6009d8
data:           0x6009a4
$ ./a.out[←]
environ:        0x7ffd5246bc08
argv:           0x7ffd5246bbf8
stack:          0x7ffd5246bb0f
bss:            0x6009d8
data:           0x6009a4
$ ./a.out[←]
environ:        0x7ffd3db44b48
argv:           0x7ffd3db44b38
stack:          0x7ffd3db44a4f
bss:            0x6009d8
data:           0x6009a4
$

練習問題(403)

以下のプログラムをコンパイル,実行すると Hello. が8回表示される. 8回表示される理由を説明しなさい.

     1  #include <stdio.h>
     2  #include <unistd.h>
     3  #include <sys/wait.h>
     4
     5  int main(void)
     6  {
     7          fork();
     8          fork();
     9          fork();
    10          printf("Hello.\n");
    11          wait(NULL);
    12          return EXIT_SUCCESS;
    13  }

練習問題(404)

system は,与えられたコマンドを sh というシェルを別プロセスで起動して実行させるライブラリ関数である. 以下のように,コマンド行を文字列で渡すとそれを実行してくれる. fork, execve,wait などのシステムコール,または相当のライブラリ関数を使用して,system 相当の機能を持つ関数 mysystem を作りなさい.

     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4
     5  int main(void)
     6  {
     7          system("ls *.c | wc");
     8          return EXIT_SUCCESS;
     9  }

system の引数の文字列(リダイレクションやパイプやワイルドカードを意味する記号を含む)は system ではなく sh が解釈することに注意せよ. すなわち,引数の文字列の処理は sh に丸投げされる.
system のマニュアルページに記述されているシグナルについての仕様は無視してよい.すなわち,コマンドの実行でシグナルが発生することは考えなくてよい.
コード例にあるように、system の返り値は捨てられると仮定してよい.すなわち,mysystem は0や1などの適当な返り値を返しておけばよい.
systemの引数に NULL が与えられることも考えなくてよい.

練習問題(405)

リダイレクションによって標準入力をファイルに切り替えるプログラムを変更し,標準出力への書き込みもファイルに対して行うようにしたプログラムを作成しなさい. 書き込み先のファイルはプログラムの第2引数として受け取るものとする.

練習問題(406)

第1引数で与えられるコマンドの実行結果(exit status)に応じて,第2引数または第3引数で与えられるコマンドを実行するプログラム if-then-else を作りなさい.例えば

$ ./if-then-else "test 1 -eq 1" "echo yes" "echo no"[←]
yes
$ ./if-then-else "test 1 -eq 2" "echo yes" "echo no"[←]
no
$

となるようなプログラムである. fork, execve, wait などを使う練習のために,system 関数やシェルを使わずにプログラムを記述すること.

ダブルクオーテーションで囲った3つの文字列を与えるのではなく,コマンドの区切りとなる単語をあらかじめ決めておき,それによって区切りをプログラムに伝えるようにしても良い. 例えば以下は then, else を区切りとなる単語に決めたときのコマンド列である. execve を使用する場合には,if-then-else が受け取った文字列の配列の then, else に対応する要素を NULL で上書きすれば,その文字列の配列を execve にそのまま引数として渡せるので,以下の仕様のほうが,プログラムを作りやすいかもしれない.

$ ./if-then-else test 1 -eq 1 then echo yes else echo no

練習問題(407)

fork で子プロセスを作り,子プロセスと親プロセスが交互に実行を繰り返すプログラムを作りなさい.パイプによってプロセス切り替えを発生させるようにしなさい. 具体的には,以下に示すように,子プロセスが数字1文字を,親プロセスがアルファベットの小文字1文字を出力する処理を繰り返すようにしなさい.

% ./a.out 60 [←]
0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d
%
合計何文字出力するかはプログラムへの引数で指定できるようにしなさい.実行結果は偶数の引数を指定した場合の出力と奇数の引数を指定した場合の出力の両方を含むようにすること.

子プロセスが1文字(最初は0)を出力した後,実行を親プロセスに戻し,親プロセスが1文字(最初はa)を出力した後,また実行を子プロセスに移し,子プロセスが1文字(1)を出力し...と進む. 具体的には以下の処理を実行すればよいだろう.

  1. まず,親プロセスはパイプからデータが読めるようになるのを待ち,子プロセスは自身の処理を実行する.
  2. 子プロセスはパイプにデータを書き込む.その後,パイプからデータが読めるようになるのを待つ.
  3. 親プロセスはパイプからデータを読めたら,自身の処理を実行する.
  4. 親プロセスはパイプにデータを書き込む.その後,パイプからデータが読めるようになるのを待つ.
  5. 子プロセスはパイプからデータを読めたら,自身の処理を実行する.
  6. 2.に戻る.
親プロセスが終了するまでに fork で作る子プロセスは1つのみとする.すなわち,親プロセス,子プロセスともに,プロセス ID はプログラムの実行が終了するまで変わらないものとする(fork された子プロセスが1文字出力後にすぐに exit するようなプログラムは正解にはならない).

練習問題(408)

練習問題(407)のプログラムを変更し,子プロセスの数をプログラムへの引数で与えられるようにしたプログラムを作りなさい.

練習問題(409)

3個のプロセスの標準入出力を2つのパイプで結び,それらのプロセスがデータをパイプでやりとりするプログラムを作りなさい.

先にパイプを2つ作ってから2回 fork する方法と,1つパイプを作り fork し,もう1つパイプを作りまた fork する方法があるが,どちらでも良い.

練習問題(410)

popen, pclose は,指定されたプログラムを実行するプロセスを生成し,そのプロセスと現在のプロセスをパイプにより接続し,そのプロセスからの入力またはそのプロセスへの出力に使えるファイルポインタを返すライブラリ関数である.

popen, pclose 相当の機能を持つ関数 mypopen, mypclose を作りなさい. 最初は,popenで実行するプログラムからの出力を受け取る機能だけを実装するところから始めるとよい.

練習問題(411)

パイプを使用するプログラムを,以下のように,親プロセスからの出力を子プロセスの入力にするように書き換えたところ,プログラムが終了しなくなってしまった. その理由を考え,プログラムが終了するようにプログラムを修正せよ.

pipe_reverse.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <sys/types.h>
     5  #include <sys/uio.h>
     6  #include <sys/wait.h>
     7
     8  int pipe_fd[2];
     9
    10  void do_parent(void)
    11  {
    12          char *p = "Hello, my kid.";
    13          int status;
    14
    15          printf("This is parent.\n");
    16
    17          close(pipe_fd[0]);
    18
    19          while (*p != '\0') {
    20                  if (write(pipe_fd[1], p, 1) < 0) {
    21                          perror("write");
    22                          exit(1);
    23                  }
    24                  p++;
    25          }
    26
    27          if (wait(&status) < 0) {
    28                  perror("wait");
    29                  exit(1);
    30          }
    31  }
    32
    33  void do_child(void)
    34  {
    35          char c;
    36          int count;
    37
    38          printf("This is child.\n");
    39
    40          close(pipe_fd[1]);
    41
    42          while ((count = read(pipe_fd[0], &c, 1)) > 0) {
    43                  putchar(c);
    44          }
    45
    46          if (count < 0) {
    47                  perror("read");
    48                  exit(1);
    49          }
    50  }
    51
    52  int main(void)
    53  {
    54          int child;
    55
    56          if (pipe(pipe_fd) < 0) {
    57                  perror("pipe");
    58                  exit(1);
    59          }
    60
    61          if ((child = fork()) < 0) {
    62                  perror("fork");
    63                  exit(1);
    64          }
    65
    66          if (child) {
    67                  do_parent();
    68          } else {
    69                  do_child();
    70          }
    71
    72          return EXIT_SUCCESS;
    73  }

Yoshihiro Oyama / <oyama@cs.tsukuba.ac.jp>