システムプログラム(第3週)

電子・情報工学系
追川 修一
<shui @ cs.tsukuba.ac.jp>

このページは,次の URL にあります.
http://www.coins.tsukuba.ac.jp/~syspro/2005/No3.html
システムプログラムのホームページ(2005年度)
http://www.coins.tsukuba.ac.jp/~syspro/2005/
からもリンクが張ってあります.

今日の内容

プロセスの概念と機能

プログラムとプロセス

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

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

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

プロセスの機能

プロセスの重要な機能として以下の2つがある.

資源割り当て

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

基本的には,OSカーネルは,プロセッサ時間やメモリに関しては,それぞれのプロセスに平等になるように割り当てる. ファイルやデバイスに関しては先着順である.

保護

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

プロセスのメモリマップ

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

メモリマップ

それぞれの領域の用途は以下の通りである:
テキスト領域 プログラムの機械語命令が置かれる.この領域は読み出し専用になっており,同じプログラムから起動されるプロセスの間で共有可能になっている.
データ領域(初期値あり) プログラム中に指定された 0 以外の初期値を持つ大域 (global) 変数,静的局所 (static local) 変数が置かれる.
データ領域(初期値なし) 通称 BSS (Block Started by Symbol) セグメント.初期値を持たない,又は初期値が 0 の大域変数,静的局所変数が置かれる.プロセス作成時に確保され,0 に初期化される. 変数の名前(シンボル)だけなのが名前の由来.
ヒープ領域 malloc()などにより,プロセス実行時に確保されるデータ領域.
共有ライブラリ 共有ライブラリのため領域はヒープとスタックの間にとられる.テキスト領域と同じく読み出し専用で,他のプログラムと共有される.
スタック領域 C言語の自動変数(staticでないローカル変数)や,引数,関数呼び出し時の戻り番地などが置かれる.
引数,環境変数 コマンドに渡される引数,環境変数は,スタック領域の最上位部分に格納されている.

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

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

このプログラムをコンパイル実行してみると,以下のような結果が得られ,環境変数の文字列が最上位アドレスにあり,次にコマンド引数の文字列,そしてスタックの順であることがわかる. また,BSSセクションがデータセクションよりも上位アドレスに来ていることもわかる.

% ./a.out [←]
environ:        0xbfffdc5c
argv:           0xbfffdc54
stack:          0xbfffdbe7
bss:            0x080496e8
data:           0x080495f0
%

プロセスの属性

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

環境変数

メモリマップを確かめるプログラムに既に出てきたが,プロセスには環境変数は文字列で渡され,その文字列の場所は外部変数 environ により指し示される. 環境変数を構成する文字列は,構造的には argv と同じであり,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, top プロセスの観察
kill プロセスの(強制)終了
nice プロセスの実行優先順位の制御
limit プロセスの利用可能資源の制御
gdb, strace デバッグツール

これらのコマンドは,他のプログラムと何ら変わることが無く,様々なシステムコールを呼び出すことで,その機能を実現している.

プロセス作成,プログラム実行,プロセス終了

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

概要

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

  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 システムコールは,exit システムコールを呼んだプロセスを終了させる.

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

プロセスの作成

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

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

プログラム例(fork)

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

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

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

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

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

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

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

% ./a.out [←]
This is child(pid=23656).
The child (pid=23656) existed with status(2).
%

プログラム例(execve)

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

execve には,引数,環境変数はそれぞれプロセスが必要とする形式(argv形式)で渡す必要がある. argv は,char* 型の配列を宣言し(7行目),要素に値をセットしている(9, 10行目). 環境変数は,現在の環境変数の値をそのまま渡している.

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

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

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

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

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

     1  #include <stdio.h>
     2
     3  extern char **environ;
     4
     5  main()
     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);
    14  }

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

% ./a.out [←]
bin   dev  home    lhome  lost+found  mnt  proc  sbin  usr  work
boot  etc  initrd  lib    misc        opt  root  tmp   var
%

プログラム例(fork&execve)

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

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

実行結果としては,何ら変わりはないが,exit status は /bin/ls が設定しているため 0 になっている.

% ./a.out [←]
This is child(pid=23774)
bin   dev  home    lhome  lost+found  mnt  proc  sbin  usr  work
boot  etc  initrd  lib    misc        opt  root  tmp   var
The child (pid=23774) existed with status(0).
%

実際に execve を使ったプログラムを書く場合,execve が失敗する可能性も考えなければならない. execve はプログラムの実行に失敗すると負の整数の戻り値を返す. 以下は,その処理を追加し(17〜20行目),存在しないプログラムを実行しようとしてみた例である.

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

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

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

その他のシステムコール

exit は現在はライブラリ関数となっており,_exit がシステムコールである. 特定のプロセスの終了を待つには waitpid を使用する.

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

なぜ exit はライブラリ関数になっているのかは,atexit(3)やon_exit(3)を見てみるとわかる.

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

execve は引数や環境変数を argv 形式で渡す必要がある,またプログラムのサーチしてくれないため,もう少し簡単に使えるようにと,以下のライブラリ関数が用意されている.

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 <stdio.h>
     2
     3  void
     4  main()
     5  {
     6          execl("/bin/ls", "ls", "/", NULL);
     7  }

また,execlp, execvp は環境変数 PATH を使用してコマンドをサーチしてくれる.

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

リダイレクション,パイプ

リダイレクション

概要

リダイレクションは標準入出力をファイルにする機能である. 以下は,シェルから 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. ファイルをオープンし,(例えば)ファイルディスクリプタ 3 を得る.
  2. 標準入力(ファイルディスクリプタ 0)をクローズする.
  3. dup2 により,ファイルディスクリプタ 3 の複製をファイルディスクリプタ 0 として作成する.
  4. ファイルディスクリプタ 3 をクローズすることにより,ファイルはファイルディスクリプタ 0 からのみアクセスできるようになる.

プログラム例

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

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

以下は,上記プログラムの実行結果である.

% ./a.out a.txt [←]
     38      38     374
%

パイプ

概要

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

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

パイプをもう少し詳しく見てみると,下図のようになる. パイプには入口と出口があり,入口から書いたデータを出口から読み出すことができる. データの流れの方向は,入口から出口の単方向であり,入口から書いた順番で出口からは読み出される.

パイプ

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

int pipe(int filedes[2]);

pipe システムコールを呼ぶと,入力と出力それぞれの口(ファイルディスクリプタ)を持つパイプが1本作られる. ファイルディスクリプタは,上記の例だと filedes 配列に格納され,filedes[0] がパイプの出口,filedes[1] がパイプの入口のファイルディスクリプタになる. この関係は,ファイルディスクリプタ 0 が標準入力,ファイルディスクリプタ 1 が標準出力であることと対比させるとわかりやすい. 即ち,filedes[0] から読み,filedes[1] に書く.

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

パイプの作成

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

プログラム例

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

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

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

% ./a.out [←]
this is parent.
this is child.
Hello, dad!!.
%

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

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

プロセスの作成と実行を1度に行うシステムコールがあったとして,同じこと(別に全く同じである必要はないがプロセス間の通信)を実現しようとしたらどうなるであろうか? そのシステムコールにいろいろなオプションや引数を渡せるようにして実現するのであろうか. それとも,より汎用のプロセス間通信機能を使うのであろうか(現在のUNIXにはそのような機能が備わっているが). 少なくとも,forkとexecveが分かれていることにより,リダイレクションやパイプが簡単に実現できることがわかって貰えただろうか.

その他のプロセス操作のためのシステムコール,ライブラリ関数

プロセス強制終了

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

int kill(pid_t pid, int sig);

メモリ領域確保

動的メモリ領域確保のためには,通常ライブラリ関数であるmalloc又はcallocを使用する. malloc により確保される領域の初期値は不定である. calloc は確保した領域を 0 に初期化する. 動的なメモリ領域は確保と開放を繰り返すことが多い. そのため,malloc又はcallocは何らかのアルゴリズムを用いて効率よくメモリ領域の再利用を可能にしている.

malloc, calloc で確保した領域が不要になったら,free により開放する. malloc, calloc で領域確保をして,不要になっても開放しないままで放置しておくと,最終的には使用可能なメモリ量を超えて,プログラムは以上終了してしまう. 特に,確保した領域へのポインタを失ってしまい開放できなくなってしまうバグはメモリーリークと呼ばれ,発見し難いバグの一つである.

void *calloc(size_t nmemb, size_t size);
void *malloc(size_t size);
void free(void *ptr);
void *realloc(void *ptr, size_t size);

現在のヒープ領域だけではメモリ領域が足りなくなった時に,以下のヒープ領域を拡大するためのシステムコールが呼び出される. 呼出し後はヒープ領域が拡大され,新たなメモリ領域確保ができるようになる.

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

簡単なプログラム実行

以下のライブラリ関数は,単純なプロセスの実行や,パイプを使用してのプロセスの実行を簡単に行えるように用意されている.

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

その他のシステムコール

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

int nice(int inc);
int getrlimit(int resource, struct rlimit *rlim);
int getrusage(int who, struct rusage *usage);
int setrlimit(int resource, const struct rlimit *rlim);
int ptrace(int request, int pid, int addr, int data);

その他のライブラリ関数

練習問題

練習問題(23)

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

練習問題(24)

メモリマップを確かめるプログラムをコンパイルし,繰り返し実行してみたところ,以下のようにスタックのアドレスが毎回若干変化していることがわかった. これを確かめ,なぜスタックの場所が変わるか,その理由について考えてみなさい.

% ./a.out [←]
environ:        0xbfffe8dc
argv:           0xbfffe8d4
stack:          0xbfffe867
bss:            0x080496e8
data:           0x080495f0
% ./a.out [←]
environ:        0xbfffe6dc
argv:           0xbfffe6d4
stack:          0xbfffe667
bss:            0x080496e8
data:           0x080495f0
% ./a.out [←]
environ:        0xbfffe7dc
argv:           0xbfffe7d4
stack:          0xbfffe767
bss:            0x080496e8
data:           0x080495f0
%

練習問題(25)

以下のプログラムをコンパイル,実行すると hello が8回表示される. その理由について考えよ.

     1  #include <stdio.h>
     2
     3  main()
     4  {
     5          fork();
     6          fork();
     7          fork();
     8          printf("hello\n");
     9  }

練習問題(26)

system(3) は,シェルによるコマンドの実行を行ってくれるライブラリ関数である. 以下のように,シェル(/bin/sh)のコマンドラインを文字列で渡すと実行してくれる.

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

fork, execve(又は相当のライブラリ関数),wait などを使用して,system(3)相当の機能を持つ関数 mysystem を作りなさい.

マニュアルページに記述してあるシグナルについては,無視してよい.

練習問題(27)

標準入力をファイルからリダイレクションするプログラムをもとに,標準出力もファイルに対しての書き込みとなるように変更せよ. 書き込むファイルは,プログラムの第2引数としてとるものとする.

練習問題(28)

第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
%

となるようなものをである.

コマンドの区切りは別のものでも良い. 以下は then, else が区切りになっている. execve を使用する場合は,argv の then, else の部分を NULL にすると execve にそのまま渡せるので,以下のような区切りのほうが,プログラムは作りやすいかもしれない.

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

練習問題(29)

fork でプロセスを作り,親プロセスと子プロセスが交互に文字を出力するプログラムを作りなさい. また,何文字出力するかはプログラムへの引数で指定できるようにしなさい.

以下は,子プロセスは数字を親プロセスはアルファベットの大文字を出力するようにしてみたプログラムでの実行例である.

% ./a.out 30 [←]
0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D
%

練習問題(30)

3個のプロセスの標準入出力を2つのパイプで結びなさい.

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

練習問題(31)

popen(3), pclose(3)は,パイプにより接続された指定されたプログラムを実行するプロセスを生成し,そのプロセスに対する入力又は出力のどちらか一方を提供するライブラリ関数である.

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

練習問題(32)

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

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