プロセス

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro-2002/2002-05-13
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.is.tsukuba.ac.jp/~yas/index-j.html

■正誤表

◆C言語のポインタと配列、2次元配列で 「*(p+1) と p[1] も同じ」の最初の「*」が抜けていた。 ◆キーによるプロセスの強制終了で、 gdb の対象が /bin/ls になっていた。正しくは、/bin/catである。 core を作る実験をする時には、limit コマンドでcoredumpsize を unlimited にする必要がある。標準では、0 kbytes になっている。
% limit coredumpsize [←]
coredumpsize    0 kbytes
% limit coredumpsize unlimited [←]
% limit coredumpsize [←]
coredumpsize    unlimited
% []
また、標準の端末(gnome-terminal) では、^\ では、漢字変換機能の on/off に割り当てられている。core を作る実験をする時には、 gnome-terminal ではなく、kterm を実行するとよい。
% kterm & [←]
% []

■今日の重要な話

プロセスの考え方と操作

■プロセスとは

◆プログラムとプロセス

プロセッサ(CPU)が実行できる機械命令の列がプログラムである。 プロセスとは、プログラムがオペレーティング・システムによってメモリに読 み込まれ、やはりオペレーティング・システムの管理下にあるプロセッサによっ て実行の対象になったものである。

メモリにプロセスが3つ、UNIXカーネル、ハードディスク、CPU

図1 プログラムとプロセス

プロセスには、保護、資源割り当てのような機能がある。

◆保護

あるプロセスが 暴走 して他のプロセスを破壊したり、あるいは、意図的に他のプロセスからデータ を盗もうとしたとしても、それを禁止してプロセスを守ることをプロセスの保 護という。プロセスを保護するためには、プロセスが直接他のプロセスのメモ リなどにアクセスできないようにプロセスとプロセスを隔離する。Unix では、 基本的にはプロセスごとに独立した論理アドレス空間を割り当てることで実現 している。

◆資源割当て

資源とは、 メモリ、ディスク、プリンタ、ディスプレイ、キーボードなど コンピュータが処理を進める上で利用価値のあるものを総称である。 プロセスは、資源を公平に配分するための単位としての役割がある。

◆プロセスの操作

プロセスの操作には次のようなものがある。

◆プロセスを作ろう==プログラムを実行しよう

シェルにコマンドを与えると、それに対応したプロセスが作られ、 その結果コマンドが実行される。 (ただし、cdやsetなどのシェルの内部コマンドではプロセスは作られない。) 例えば、lsコマンドを与えると、 新しいプロセスが作られ、ls というプログラム ( /bin/ls/usr/bin/ls などのファイルに保存されている ) がメモリに読み込まれて実行される。 Xウインドウでメニューから ktermを起動すると、ktermのプロセスとその端末 の中で動くシェルのプロセスの2つが作られる。

◆プロセスの観察

◆プロセスの属性

ps(process) コマンドを実行すると、プロセスの一覧を表示する。 psコマンドの表示の例を示す。
----------------------------------------------------------------------
% ps [←]
  PID TTY          TIME CMD
23788 pts/0    00:00:00 tcsh
23819 pts/0    00:00:00 emacs
23822 pts/0    00:00:00 cat
23825 pts/0    00:00:00 ps
% []
----------------------------------------------------------------------
psコマンドの実行結果は1行が1プロセスである。 左から、以下のような意味がある。
PID (プロセス識別子)
プロセスを区別するための整数。 多くのシステムで16ビットで、 0〜65535、または、0〜32767の範囲をとる。
TTY (端末名)
プロセスがどの端末と結びつけられているかを示している。 端末 pts/0/dev/pts/0の意味。
TIME
CPU時間 (CPUがそのプロセスを実行するために費やした時間)。
COMMAND
そのプロセスを起動した時のコマンド。

----------------------------------------------------------------------
% ls -l /dev/pts/0 [←]
crw--w----    1 yas      tty      136,   0  5月 12 21:23 /dev/pts/0
% []
----------------------------------------------------------------------

◆プロセスと資源

プロセスにはメモリやCPUなどの資源が割り当てられる。 これはpsコマンドに-lオプションや -uをつけると表示される。
----------------------------------------------------------------------
% ps -l [←]
  F S   UID   PID  PPID  C PRI  NI ADDR    SZ WCHAN  TTY          TIME CMD
100 S  1013 23788 23786  0  72   0    -  1006 rt_sig pts/0    00:00:00 tcsh
000 T  1013 23819 23788  0  69   0    -  2210 do_sig pts/0    00:00:00 emacs
000 T  1013 23822 23788  0  69   0    -   585 do_sig pts/0    00:00:00 cat
000 R  1013 23863 23788  0  70   0    -   818 -      pts/0    00:00:00 ps
% ps -u [←]
USER       PID %CPU %MEM   VSZ  RSS TTY      STAT START   TIME COMMAND
yas      23788  0.0  0.8  4024 2044 pts/0    S    21:21   0:00 -tcsh
yas      23819  0.1  1.7  8840 4540 pts/0    T    21:21   0:00 emacs -nw
yas      23822  0.0  0.2  2340  552 pts/0    T    21:21   0:00 cat
yas      23864  0.0  0.3  2840  896 pts/0    R    21:29   0:00 ps -u
% []
----------------------------------------------------------------------
TIME は過去に利用した CPU 時間の割合、 SZ は、プロセスが確保しているメモリ、 RSSは、そのうちメインメモリに入っている部分である。

S は、 プロセスの 状態 (STATe) であり、次のようなものがある。

R (runnable)
実行可能な状態。CPUが空いていれば実行できる。
D (Disk)
ディスク入出力を行ない、その完了を待っている状態。
S (Sleep)
キーボードや他のプロセスからの入力を待っている状態。
Z (Zombie)
既に終了していて、終了処理の完了を待っている。
T (Traced)
一時的に停止しているか、デバッグの対象になっている。
Linux には、そのほかに W がある。BSD系には、 S の他に、I (Idle) (20秒以上sleep している) がある。

◆プロセスの親子関係

Unix では、後で説明する fork() システムコールを 発行すると、新しくプロセスが作られる。 (Unix では、これ以外の方法ではプロセスは作られない。) この時、「もとのプロセス」の「親プロセス」という。 プロセスの親プロセスのプロセス識別子は、psコマンドに「-l」オプションを つけるとPPIDのところに表示されている。

■プロセスの資源と属性

プロセスには、 図?のような属性と資源がある。

図? プロセスの資源と構成要素

図2 プロセスの資源と属性

◆資源

プロセスは、資源を割当てる対象である。逆にいえば 割当てられた資源がまとまったプロセスを構成するといえる。
メモリ(memory)
プロセスの重要な資源はメモリ(主記憶、main memory, main storage) である。 メモリのうち主記憶に入っている部分を レジデント・セット(resident set) という。
スワップ領域
スワップ領域とは、主記憶(main memory)が不足した時に、プロセスのメモリ・ イメージを格納するための、二次記憶(secondary storage)上の領域である。 二次記憶としては、ハードディスクがよくつかわれる。
レジスタ・セット
プロセスが実行されている時はCPUのレジスタに格納されている データは、プロセスが待ち状態や待機状態の時は、 メモリに保存される。
ファイル記述子の表
プロセスごとにファイル記述子の表(開いたファイルの表)がある。通常、0 番目がキーボード、1番目と2番目がディスプレイを指している。
制御端末
プロセスごとにキーボードからのシグナルを受け取る 端末が一つ定められてる。この端末を 制御端末 とう。
カレント・ワーキング・ディレクトリ
相対パス名を解釈する時に使う出発点となるディレクトリである。 cd コマンド、chdirシステムコールで変更できる。
ルート・ディレクトリ
プロセス毎にルート・ディレクトリを設定することができる。 匿名ftp(anonymous ftp)など、 特に高いセキュリティが求められる時に プロセスがアクセスできる ディレクトリの範囲を制限するために使われる。 変更するには、chrootシステムコールを使う。

◆属性

UNIXのプロセスが持っている重要な属性には、 次の様なものがある。
PID(プロセス識別子)
プロセスを区別するための16ビットの整数。0〜65535、または、0〜32767の範囲。
PPID
親プロセスのプロセス識別子
PGID(Process Group ID)
プロセスにはプロセス・グループ識別子(PGID)と呼ばれる属性がある。プロセ ス・グループ識別子を指定してシグナルを送る機能(シグナルのマルチキャス ト機能)を使うと、同じプロセス・グループIDを持つプロセスにまとめてシグ ナルを送ることができる。これはプロセスの一時停止、再開のためのジョブ制 御で使われる。
UID(User ID)
ユーザ(user, 利用者)を区別するための整数。 多くのシステムで16ビット。
GID(group ID)
(複数のユーザが所属する)グループを区別するための整数。 多くのシステムで16ビット。
マスク(mask)
新たにファイルを作成する時のモードを決定する数。多くのシステムで16ビット。
状態
上述した R,D,S,Z,Tの区別。
優先順位(priority)
優先順位は、CPU資源を割当てる時に使われる。 優先順位は0から127の数で表わされ、 数字が小さい方が優先順位が高いことを意味する。
制限資源量
計算機上のメモリ資源や同時に開けるファイルの数は有限である。 この有限の資源を有効に分配するために、 プロセスごとに使える資源量の上限が決められている。 これを制限資源量とう。 プロセスは制限資源量を越えて資源を使用することはできない。 メモリの制限資源量を越えた場合はメモリ割り当てに失敗し、 ファイルを開く場合は、ファイルの開くことに失敗する。 ただし、CPU時間が制限値に達した場合は、 プロセスは強制終了させられる。 制限できる資源には次のようなものがある ( 制限できる資源の種類はかなりシステムに依存する) ()中はlimitコマンドで使うキーワード。 tcsh,csh では limitコマンドで、制限資源量を設定できる。制限を解除する には、unlimitコマンドで制限を解除することができる。bash では、ulimit コマンドがある。
その他
その他の属性としては、 シグナル制御関連の属性 や 統計情報 がある

■psコマンド(Linux)

Linux の ps コマンドは、BSD 系 Unix のものに似ているが、 System V 系 Unix (Unix 98標準)のオプションも受け付ける。 BSD 系のものは、オプションの前に(-)を付けない。

ps

Linuxの ps(process) コマンドは、引数を付けないで実行すると、 プロセス識別子、端末名、 CPU時間、コマンド名を表示する。
----------------------------------------------------------------------
% ps [←]
  PID TTY          TIME CMD
23788 pts/0    00:00:00 tcsh
23819 pts/0    00:00:00 emacs
23822 pts/0    00:00:00 cat
23958 pts/0    00:00:00 ps
% []
----------------------------------------------------------------------
ただし、そのままでは、psコマンドを実行した端末と結びつけられているプロ セスしか表示しない。

◆ps x

「-x」オプションを使うと、他の端末に結び付いているプロセスも表示する。
----------------------------------------------------------------------
% ps x [←]
  PID TTY      STAT   TIME COMMAND
23788 pts/0    S      0:00 -tcsh
23819 pts/0    T      0:00 emacs -nw
23822 pts/0    T      0:00 cat
23967 pts/1    S      0:00 -tcsh
23996 pts/1    S      0:00 /bin/sh /usr/local/netscape/run-mozilla.sh /usr/local
24000 pts/1    S      0:03 /usr/local/netscape/mozilla-bin
24002 pts/1    S      0:00 /usr/local/netscape/mozilla-bin
24003 pts/1    S      0:00 /usr/local/netscape/mozilla-bin
24004 pts/1    S      0:00 /usr/local/netscape/mozilla-bin
24013 pts/0    R      0:00 ps x
% []
----------------------------------------------------------------------

◆ps -l, ps l (long)

プロセスが使っているメモリの大きさ (SZVSZRSS)、親プロセスの PID (PPIID(Parent PID))優先順位(PRI(priority)) などの プロセスの資源と属性を表示する。
----------------------------------------------------------------------
% ps -l [←]
  F S   UID   PID  PPID  C PRI  NI ADDR    SZ WCHAN  TTY          TIME CMD
100 S  1013 23788 23786  0  71   0    -  1025 rt_sig pts/0    00:00:00 tcsh
000 T  1013 23819 23788  0  69   0    -  2210 do_sig pts/0    00:00:00 emacs
000 T  1013 23822 23788  0  69   0    -   585 do_sig pts/0    00:00:00 cat
000 R  1013 24029 23788  0  74   0    -   817 -      pts/0    00:00:00 ps
% ps l [←]
  F   UID   PID  PPID PRI  NI   VSZ  RSS WCHAN  STAT TTY        TIME COMMAND
100  1013 23788 23786  13   0  4100 2152 rt_sig S    pts/0      0:00 -tcsh
000  1013 23819 23788   9   0  8840 4540 do_sig T    pts/0      0:00 emacs -nw
000  1013 23822 23788   9   0  2340  552 do_sig T    pts/0      0:00 cat
000  1013 24030 23788  12   0  3268 1304 -      R    pts/0      0:00 ps l
% []
----------------------------------------------------------------------
-l と l で表示が微妙に違う。

◆ps u

ps l, ps -l と同様に詳しい表示をする。
----------------------------------------------------------------------
% ps u [←]
USER       PID %CPU %MEM   VSZ  RSS TTY      STAT START   TIME COMMAND
yas      23788  0.0  0.8  4100 2152 pts/0    S    21:21   0:00 -tcsh
yas      23819  0.0  1.7  8840 4540 pts/0    T    21:21   0:00 emacs -nw
yas      23822  0.0  0.2  2340  552 pts/0    T    21:21   0:00 cat
yas      24047  0.0  0.3  2840  904 pts/0    R    22:04   0:00 ps u
% []
----------------------------------------------------------------------

◆ps -e (every)

全てのプロセスを表示する。System V 風。

◆ps a (all)

端末と結び付いている全てのプロセスを表示する。 x も付けた方がよい。

◆ps ax (all)

全てのプロセスを表示する。BSD風。

◆ps aux

a,u, x の組合わせ。全てのプ ロセスを表示する時に、よく使われる。

プロセスを殺そう

プロセスには、ls のように、勝手に終了するプログラムもあるが、エディタ emacs や X ウインドウ関連のプログラムのように、終了のための操作をして 始めて終了するプログラムもある。 場合によっては、プロセスが自主的に終了する前に、 強制的に終了させなければならないことがある。 これを、プロセスを 殺す(kill) という。

プロセスを殺す方法には、次のような方法がある。

どちらの方法でも、裏ではシグナル(ソフトウェア割込み) という仕掛けが働く。

◆キーによるプロセスの強制終了

次は、cat のプロセスを^Cで強制終了している。
----------------------------------------------------------------------
% cat [←]
Line 1[←]
Line 1
Line 2[←]
Line 2
^C
% []
----------------------------------------------------------------------
^Dを打つと、キーボードから EOF (End Of File) が送られ、cat プ ログラム自ら自発的に終了する(キーボードには、普通のファイルと違い、終 わりとう考え方はないが、OS がそういう仕組みを提供している)。 ^Cを打つと、cat は、自発的に終了するのではなくて、強制終了さ せれている。

^Cと同様に ^\ もあるが、 デバッグ用にcore という名前のファイルができることが異なる。 coreは、実行中のプロセスのメモリの内容をファイルに保存したものである。

----------------------------------------------------------------------
% ls -l core [←]
ls: core: そのようなファイルやディレクトリはありません
% cat [←]
Line 1[←]
Line 1
Line 2[←]
Line 2
^\
Quit (core dumped)
% ls -l core [←]
-rw-------    1 yas      lab         94208  5月 12 22:14 core
% []
----------------------------------------------------------------------
coreファイルはプログラムのデバッグに使える。

----------------------------------------------------------------------
% gdb /bin/cat core [←]
GNU gdb Red Hat Linux (5.1-0.71)
Copyright 2001 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(no debugging symbols found)...
Core was generated by cat.
Program terminated with signal 3, Quit.
Reading symbols from /lib/i686/libc.so.6...done.
Loaded symbols for /lib/i686/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0  0x4010c964 in __libc_read () from /lib/i686/libc.so.6
(gdb) where[←]
#0  0x4010c964 in __libc_read () from /lib/i686/libc.so.6
#1  0x401626b4 in __DTOR_END__ () from /lib/i686/libc.so.6
#2  0x08048e9c in __fxstat64 ()
#3  0x08049902 in __fxstat64 ()
#4  0x40048627 in __libc_start_main (main=0x8049330 <__fxstat64+1756>, argc=1, 
    ubp_av=0xbffff5c4, init=0x80489ac, fini=0x804a93c <__fxstat64+7400>, 
    rtld_fini=0x4000dcc4 <_dl_fini>, stack_end=0xbffff5bc)
    at ../sysdeps/generic/libc-start.c:129
(gdb) 
----------------------------------------------------------------------
どのような関数(この例では番地だけ)が呼び出された結果、プログラムが終了 したかがわかる。

普通のデバッグ(自分が作ったプログラム)では、いちいち core を作る必要は ない。単にデバッガの中からプログラムを実行すればよい。強制的に core を 作るのは、普通、既に実行している他人が作ったプログラムを解析する時など に限られる。

◆kill コマンドによるプロセスの強制終了

^C^\ で死なないプロセス を殺すには、kill コマンドを使う。
% kill pid [←]
プロセス識別子pidのプロセスを殺す。 オプションなしでkill コマンドを使っても死なないプロセスがある。 その場合はオプション -KILLをつけてkillコマンドを 実行する。
% kill -KILL pid [←]
これでほとんどのプロセスは死ぬ。(OSの都合上(ディスクのI/O完了の 報告など)で、死なないプロセスもある。)

◆kill システムコールによるプロセスの強制終了

kill コマンドは、内部では、kill システムコールを使っている。kill シス テムコールは、「ソフトウェア割込み」に分類される仕組みを提供する。ソフ トウェア割込みは、ハードウェアの割込みをソフトウェアによりエミュレート したようなものである。詳しくは、後半の講義で述べる。

■mainの引数

既にfile-copy.cでも出 てきたが、プログラムを実行する時、コマンドラインから引数を渡すことがで きる。

----------------------------------------------------------------------
   1:	/*
   2:	        arg-print.c -- mainの引数を表示するプログラム
   3:	        ~yas/syspro/proc/arg-print.c
   4:	        Start: 1997/04/21 18:23:13
   5:	*/
   6:	
   7:	main( int argc, char *argv[], char *envp[] )
   8:	{
   9:	    int i ;
  10:	        printf("&argc == 0x%x, argc == %d\n", &argc, argc );
  11:	        printf("&argv == 0x%x, argv == 0x%x\n",&argv, argv );
  12:	        for( i=0 ; argv[i] ; i++ )
  13:	            printf("argv[%d]==0x%x, \"%s\"\n",i,argv[i],argv[i] );
  14:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/arg-print.c . [←]
% make arg-print [←]
cc     arg-print.c   -o arg-print
% ./arg-print  [←]
&argc == 0xbfffe7c0, argc == 1
&argv == 0xbfffe7c4, argv == 0xbfffe824
argv[0]==0xbffffa81, "./arg-print"
% ./arg-print who am i [←]
&argc == 0xbfffe740, argc == 4
&argv == 0xbfffe744, argv == 0xbfffe7a4
argv[0]==0xbffffa78, "./arg-print"
argv[1]==0xbffffa84, "who"
argv[2]==0xbffffa88, "am"
argv[3]==0xbffffa8b, "i"
% []
----------------------------------------------------------------------
argv[0] には、プログラムの名前が含まれている。argv[1] 以降に、普通の意 味での引数が含まれている。argc には、プログラムの名前まで含めての引数 の数が含まれている。argv[0] からargv[argc-1] まで参照できる。 argv[argc] は、参照してはいけない(0が入っているはずではあるが)。

◆argvの構造

argvは、2次元配列ではない。 char へのポインタの配列の先頭番地を入れた変数。

argvの構造、3つのメモリ

図3 argvの構造

◆printf()の%xと%s

printf() の %x, %s の意味に注意する。
%x
4バイトの値を数だと思って、16進表記の文字コードの並び変換して write() する。
%s
4バイトの値を番地だと思って、その番地から始まる 内容を、0が来るまで write() する。

◆C言語のポインタと配列、2次元配列

C言語で char *argv[] は、*argv[0] と書いたら char 型(8ビットの 整数)という意味である。

C言語で char *p と宣言した時、*p と p[0] は、同じである。 (p+1) と p[1] も同じである。

2次元配列は、C言語では配列の配列として表される。

	char array[10][20];
これで、全部で 10*20*1 == 200 バイトのメモリが確保される。array[i][j] の番地を計算するには、次のようになる。

	(1) arrayの先頭番地を求める。
	(2) (1)の値に + i*20 を加える。
	(3) (2)の値に j を加える。
array[10][20] のうち、最初の [10] は、番地の計算には使われない。

main() の引数で char *argv[] と char **argv は、同じ意味になる。 argv[i] は、 *(argv+i)、 argv[i][j] は、*(*(argv+i)+j) という意味になる。

char *argv[] で、argv[i][j] と書いた時の番地の計算の仕方は、次のように なる。

	(1) argv の番地の内容を load する。
	(2) (1)の値に + i*4 を加え、その値の番地の内容を load する。
	(3) (2)の値に j を加える。
2次元配列と違って、番地を求めるだけで、間に load が2回入る所に注意す る。2資源配列だと番地を計算する時には、load は出てこない。

(main の)引数で、char *argv[] と char **argv は、よいが、char argv[][] は、伝統的にはエラーになる。argv[i][j] を計算しようとすると、i を何倍 してよいのか計算できないので。しかし、受け付けるコンパイラもある。

C 言語で、char a[10] とした時には、10 バイトの領域が確保される。しかし、 char *p 宣言しただけでは、ポインタ自身(4バイト)の領域は確保されるが、 その先の領域は確保されない。

ポインタは、必ず番地をセットしてから使う。次のプログラムは、誤りである。

main()
{
    char *p ;
	*p = 'A' ;
}

■環境変数

main の引数以外にプログラムの動きを変えるものに、環境変数がある。環境 変数は、csh (tcsh) からは、setenv コマンドで設定できる。

----------------------------------------------------------------------
% printenv LANG [←]
ja_JP.eucJP
% echo $LANG [←]
ja_JP.eucJP
% date [←]
日  5月 12 22:57:44 JST 2002
% setenv LANG C [←]
% date [←]
Sun May 12 22:57:53 JST 2002
% []
----------------------------------------------------------------------
環境変数は、プログラムからは、main() の3番目の引数、外部変数 environ 、 ライブラリ関数 getenv() でアクセスできる。メモリ中の構造は、argv と同 じである。各文字列は、「変数名=値」の形式になっている。

----------------------------------------------------------------------
   1:	/*
   2:	        env-print.c -- 環境変数を表示するプログラム
   3:	        ~yas/syspro/proc/env-print.c
   4:	        Start: 1997/05/05 16:42:22
   5:	*/
   6:	extern char **environ ;
   7:	
   8:	main( int argc, char *argv[], char *envp[] )
   9:	{
  10:	    int i ;
  11:	        printf("envp == 0x%x\n",envp );
  12:	        printf("environ == 0x%x\n",environ );
  13:	        for( i=0 ; envp[i] ; i++ )
  14:	            printf("envp[%d]==0x%x, \"%s\"\n",i,envp[i],envp[i] );
  15:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/env-print.c . [←]
% make env-print [←]
cc     env-print.c   -o env-print
% ./env-print  [←]
envp == 0xbfffe5bc
environ == 0xbfffe5bc
envp[0]==0xbffffa93, "USER=yas"
envp[1]==0xbffffa9c, "LOGNAME=yas"
envp[2]==0xbffffaa8, "HOME=/home/lab/Denjo/yas"
envp[3]==0xbffffac1, "PATH=/home/lab/Denjo/yas/bin:/bin:/usr/local/bin:/usr/local3/bin:/usr/java/jdk1.3.1_01/bin:/usr/bin:/usr/bin/X11:/usr/X11R6/bin:/usr/local/Acrobat4/bin:/usr/lib/ICAClient:/usr/pgi/linux86/bin"
<中略>
envp[44]==0xbfffff9c, "NAME=Yasushi Shinjo"
envp[45]==0xbfffffb0, "RHOST=bridge.hlla.is.tsukuba.ac.jp Wed"
envp[46]==0xbfffffd7, "SIGNATURE=Yasushi Shinjo"
% printenv | head -4 [←]
USER=yas
LOGNAME=yas
HOME=/home/lab/Denjo/yas
PATH=/home/lab/Denjo/yas/bin:/bin:/usr/local/bin:/usr/local3/bin:/usr/java/jdk1.3.1_01/bin:/usr/bin:/usr/bin/X11:/usr/X11R6/bin:/usr/local/Acrobat4/bin:/usr/lib/ICAClient:/usr/pgi/linux86/bin
% []
----------------------------------------------------------------------
main の第3引数 envp と大域変数 environ は、同じ値である。環境変数には、 ホーム・ディレクトリの名前を保持している HOME、コマンドを検索するディ レクトリの名前のリストを表す PATH、標準的に使われるエディタを表す EDITOR などがある。

◆ライブラリ関数 getenv()

環境変数は、main() の引数や 外部変数 environ を見るよりも、ライブラリ 関数 getenv() を使ってアクセスした方が簡単である。

----------------------------------------------------------------------
   1:	/*
   2:	        home-print.c -- 環境変数 HOME を表示するプログラム
   3:	        ~yas/syspro/proc/home-print.c
   4:	        Start: 1997/05/05 16:42:22
   5:	*/
   6:	
   7:	#include <string.h>     /* strncmp() */
   8:	#include <stdlib.h>     /* getenv() */
   9:	
  10:	extern char **environ ;
  11:	
  12:	main()
  13:	{
  14:	    int i ;
  15:	    char *homedir ;
  16:	        for( i=0 ; environ[i] ; i++ )
  17:	        {
  18:	            if( strncmp(environ[i],"HOME=",5)==0 )
  19:	            {
  20:	                homedir = environ[i] ;
  21:	                printf("0x%x, [%s]\n", homedir, homedir );
  22:	                break;
  23:	            }
  24:	        }
  25:	        homedir = getenv("HOME");
  26:	        printf("getenv(\"HOME\")== 0x%x, [%s]\n", homedir, homedir);
  27:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/home-print.c . [←]
% make home-print [←]
cc     home-print.c   -o home-print
% ./home-print [←]
0xbffffa9d, [HOME=/home/lab/Denjo/yas]
getenv("HOME")== 0xbffffaa2, [/home/lab/Denjo/yas]
% []
----------------------------------------------------------------------
環境変数 HOME の値を調べるは、外部変数 environ の中から "HOME=" という 5 文字から始まるものを探す。これには、 str"n"cmp() を使うと便利である。

自分でループで探さなくても、getenv() ライブラリ関数を使う方法がある。 この場合、"HOME=" の 5 文字がスキップされた値(0xbffffa9d+5== 0xbffffaa2)が得られる。

ホーム・ディレクトリにあるファイル、たとえば、.cshrc をアクセスするに は、この環境変数の値の後ろに "/.cshrc" を追加すればよい。ただし、単に strcat() してはいけない。別の長さが十分なバッファを用意して、それにコ ピーする。

    homedir = getenv("HOME");
    p = malloc( strlen(homedir)+strlen("/.cshrc")+1 );
    strcpy( p, homedir );
    strcat( p, "/.cshrc" );

◆環境変数とシェル変数

シェル変数と環境変数とは違う。 csh では、シェル変数は、set コマンドで設定し、環境変数はsetenv コマン ドで設定する。シェル変数は、そのシェルの中だけで参照できるが、環境変数 は、そのシェルから実行されるプロセスで参照できる。

csh の特殊な機能として、シェル変数 path (小文字)を変更すると、環境変数 PATH (大文字)が変化する。シェル変数と環境変数が連動しているものには、 他に、term, user, home などがある。

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

UNIX では、新たにプロセスを作る時に、自分のコピーしか作れない(fork()シ ステム・コール)。元のプロセスを親プロセス、作られたプロセスを子プロセ スという。

現在のプログラムの実行はそのまま続けて、新しくプログラムを実行するには、 次のようにする。

execve() システム・コールの引数は、 main() の引数と関連している。

◆fork

fork() は、プロセスをコピーする。
----------------------------------------------------------------------
   1:	/*
   2:	        fork-hello.c -- 画面に文字列を表示するプログラム
   3:	        ~yas/syspro/proc/fork-hello.c
   4:	        Start: 2001/05/13 23:19:01
   5:	*/
   6:	
   7:	main()
   8:	{
   9:	        fork();
  10:	        fork();
  11:	        fork();
  12:	        printf("hello\n");
  13:	}
----------------------------------------------------------------------

実行結果


----------------------------------------------------------------------
% cp ~yas/syspro/proc/fork-hello.c . [←]
% make fork-hello [←]
cc     fork-hello.c   -o fork-hello
% ./fork-hello  [←]
hello
hello
hello
hello
hello
hello
hello
hello
% []
----------------------------------------------------------------------

fork() がなければ、1回しか表示されないはずである。1回の fork() でプ ロセスが2つに分かれる(新しいプロセスが1つ増える。)fork() した先の プロセスもさらに分かれるので、全部で2の3乗個のプロセスに分かれる。

◆execve()

execve() は、プロセスはコピーしないが、プログラムを切替える。
----------------------------------------------------------------------
   1:	/*
   2:	        exec-date.c -- 実行中のプロセスのプログラムをdateプログラムに切替える
   3:	        ~yas/syspro/proc/exec-date.c
   4:	        Start: 2001/05/13 23:25:49
   5:	*/
   6:	
   7:	#include <unistd.h>
   8:	
   9:	extern char **environ;
  10:	
  11:	main()
  12:	{
  13:	    char *argv[2] ;
  14:	        argv[0] = "/bin/date" ;
  15:	        argv[1] = 0 ;
  16:	        execve( argv[0],argv,environ );
  17:	        execve( argv[0],argv,environ );
  18:	        execve( argv[0],argv,environ );
  19:	        execve( argv[0],argv,environ );
  20:	}
----------------------------------------------------------------------

実行結果


----------------------------------------------------------------------
% cp ~yas/syspro/proc/exec-date.c . [←]
% make exec-date [←]
cc     exec-date.c   -o exec-date
% ./exec-date  [←]
月  5月 13 00:59:25 JST 2002
% []
----------------------------------------------------------------------

execve() は、最初の1回しか有効ではない。execve() に成功すると、現在の プログラムがメモリから消され、新しいプログラム(/bin/date)がメモリに読 込まれる。/bin/date は、仕事が終ると exit() する。元のプログラムにもど ることはない。

◆fork()、execve()、wait() の標準的な使い方


----------------------------------------------------------------------
   1:	/*
   2:	        proc-create.c -- calプログラムよりプロセスを作る
   3:	        ~yas/syspro/proc/proc-create.c
   4:	        Start: 1995/02/27 15:27:54
   5:	*/
   6:	
   7:	#include <unistd.h>     /* fork(), execve(), pid_t */
   8:	#include <sys/types.h>  /* wait() */
   9:	#include <sys/wait.h>   /* wait() */
  10:	
  11:	extern char **environ;
  12:	
  13:	main()
  14:	{
  15:	   pid_t child_pid ;
  16:	   int status ;
  17:	        if( (child_pid=fork()) == 0 )
  18:	        {
  19:	            char *argv[4] ;
  20:	            printf("child: pid == %d, ppid == %d\n", getpid(), getppid() );
  21:	            argv[0] = "cal" ;
  22:	            argv[1] = "5" ;
  23:	            argv[2] = "2002" ;
  24:	            argv[3] = 0 ;
  25:	            printf("child: 3\n");sleep( 1 );
  26:	            printf("child: 2\n");sleep( 1 );
  27:	            printf("child: 1\n");sleep( 1 );
  28:	            printf("child: 0\n");
  29:	            execve( "/usr/bin/cal", argv, environ );
  30:	            perror( "execve" );
  31:	            /* exec に失敗したら exit() を忘れないこと */
  32:	            exit( 1 );
  33:	        }
  34:	        else if( child_pid > 0 )
  35:	        {
  36:	            printf("parent: pid == %d, ppid == %d, child_pid == %d\n",
  37:	                    getpid(), getppid(),child_pid );
  38:	            printf("parent: waiting ...\n");
  39:	            wait( &status );
  40:	            printf("status == %d\n",status );
  41:	        }
  42:	        else
  43:	        {
  44:	            perror("fork");
  45:	        }
  46:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/proc-create.c . [←]
% make proc-create [←]
cc     proc-create.c   -o proc-create
% ./proc-create  [←]
parent: pid == 24619, ppid == 23788, child_pid == 24620
child: pid == 24620, ppid == 24619
child: 3
parent: waiting ...
child: 2
child: 1
child: 0
      5月 2002
日 月 火 水 木 金 土 
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

status == 0
% []
----------------------------------------------------------------------

fork() でプロセスが2つに分かれる。つまり、元のプロセスのコピーにより、 新しいプロセスが1つできる。コピーにより、fork() の結果は、違う。親プ ロセス(もとともあったプロセス)では、fork() の結果は、正の値が返され る。この正の値は、子プロセスの PID である。子プロセス(コピーの方)は、 0 が返される。

まれに、fork() が失敗することがある。その時には、子プロセスは作られず、 親プロセスに -1 が返される。

親プロセスと子プロセスは、独立に動作する。どちらが先に実行されるかとい う保証はない。本来は、独立に仕事をさせたい時に、プロセスを作るものであ り、1つのひとつ順序よくやりたい時にはプロセスを作らない。実行の順序を 制御するには、プロセス間で「同期」と呼ばれる操作を行う必要がある。

Unix の場合、別のプログラムをサブルーチンのように使いたいことがある。 この場合、子プロセスを fork() して、同期として、子供の終了(exit()する) を親が待つ(wait()する)。

それ以外の同期の方法は、少々複雑で、ファイルに対するロックやパイプなど のプロセス間通信を通じて行う。

sleep() は、引数で指定された秒数だけプロセスの実行を休止するライブラリ 関数である。

一度できたプロセスは、exit() しない限り、終了しない。main() がから抜け ると、プロセスが終了するのは、main() を呼び出した関数が exit() システ ムコールを実行しているからである。

start()
{
    ....
    ret = main(argc, argv, envp);
    exit( ret );
}
正常に終了した時には、exit(0)する。そうでない時には、0 以外の値で exit する。exit で返した値は、wait() で受け取ることができる。ただし、下位 8 ビットだけしか渡されない。WEXITSTATUS(status)で受け取る。

wait() で得られる status を調べると、exit() の値の他に、kill で殺され たかどうか、kill や ptrace() で停止したかどうかも調べられる。詳しくは、 man 2 wait を参照しなさい。

execve() は、システム・コールである。これを使いやすくするために、次の ようなライブラリ関数が用意されている。

     int execl (const char *path, const char *arg0, ..., const char *argn,
          (char *)0);

     int execv (const char *path, char *const *argv);

     int execle (const char *path, const char *arg0, ..., const char *argn,
          (char *0), const char *envp[]);

     int execve (const char *path, char *const *argv, char *const *envp);

     int execlp (const char *file, const char *arg0, ..., const char *argn,
          (char *)0);

     int execvp (const char *file, char *const *argv);

     int system(const char *string);

     FILE *popen(const char *command, const char *type);

     int pclose (FILE *stream);

親プロセスが子プロセスの終了を待ちたい場合には、wait() システムコール を使う必要がある。
pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait3(int *status, int options,struct rusage *rusage)
pid_t wait4(pid_t pid, int *status, int options,struct rusage *rusage)
Linux の wait() には、必ず引数を与える。status が不要の時には、0 を与 える。

◆ライブラリ関数 system()

プログラムの中で他のプログラムをサブルーチンのようにして実行するには、 system() というライブラリ関数(システムコールではない)が便利である。

main()
{
    system("ls -l");
    system("wc <file.c");
}

引数の文字列では、/bin/sh の命令が記述可能である。

■練習問題

★練習問題 14 echoコマンド

echo コマンドと似た動きをするプログラムをつくりなさい。

★練習問題 15 printenvコマンド

printenv コマンドと似た動きをするプログラムを作りなさい。

★練習問題 16 getenv()の利用

getenv() ライブラリ関数を利用して環境変数 HOME を取得し利用しなさい。 たとえば、~/.cshrc など、ホーム・ディレクトリにあるファイルを画面に表 示するプログラムを作りなさい。

★練習問題 17 putenv()の利用

putenv()、または、setenv() ライブラリ関数を利用して環境変数を変え、プ ログラムの動き(ライブラリ関数の動き)が変ることを確かめなさい。

★練習問題 18 getenv()の実現

getenv() ライブラリ関数と似た動きをする関数を作りなさい。

★練習問題 19 putenv()の利用

putenv() または、setenv() ライブラリ関数と似た動きをする関数を作りなさ い。既存の同じ名前の環境変数を上書きする時には、メモリが足りないことが ある点に注意する。単にその同じ番地に上書きすれば、次の環境変数が潰され る。

新しい環境変数を追加するには、environ の先の領域もコピーして増やす。

★練習問題 20 ライブラリ関数 system() の利用

ライブラリ関数 system() を使って、 proc-create.c を書き直しなさい。

★練習問題 21 ライブラリ関数 system()の内容

fork(), execve() (あるいは execl() など)、wait() などを使って、ライブ ラリ関数 system() と同じような関数を作りなさい。元々の system() と区別 するために、関数の名前は別のもの(たとえば、mysystem())としなさい。

シグナルについては何もしなくてもよい。

★練習問題 22 n回実行

指定された引数の回数だけプログラムを実行するようなプログラムをつくりな さい。たとえば、次の例では、date -u を3回実行している。
% ./run-n 3 date -u [←]
日  5月 12 16:18:53 UTC 2002
日  5月 12 16:18:54 UTC 2002
日  5月 12 16:18:55 UTC 2002
% []
argv[1] の文字列を、atoi() などで処理して数を得る。 その回数だけ、fork(),execve(),wait() をする。 &argv[2] 以降を、execve() にそのまま与えるとよい。

ライブラリ関数 system() も使ってもよいが、使わない方が簡単であろう。

★練習問題 23 カウントダウン

次のようにカウントダウンするプログラムを、execve() (あるいは execl() など)を使って作りなさい。
% ./countdown 3 [←]
3
2
1
0
% []
このプログラムでは、単に表示がこのようになるのではなくて、カウント・ダ ウンするたびに必ず execve() などでプログラムを切替えなさい。次のような プログラムは、「不可」である。
main(int argc, char *argv[])
{
    int n, i ;
    n = atoi( argv[1] );
    for( i=n ; i>=0 ; i-- )
    {
        printf("%d\n",i);
    }
}
カウント・ダウンするたびに、execve() などでプログラムを切替えること。
main(int argc, char *argv[])
{
    ...
    n = atoi( argv[1] );
    if( n <= 0 )
       exit( 0 );
    else
    {
	n -- ;
	...
	execve(....);
	perror("execve");
	exit( 1 );
    }
}
このプログラムでは、fork() も wait() も、不要である。

★練習問題 24 if-then-else

プログラムを実行し、その結果に応じて別のプログラムを実行するようなプロ グラムを作りなさい。たとえば、次のように実行する。

----------------------------------------------------------------------
% ./if-then-else "cc file1.c" "play happy" "play sad" [←]
----------------------------------------------------------------------
argv[1] で与えられたプログラムを実行し、成功すれば、argv[2] を、実行す る。失敗すれば、argv[3] を実行する。この場合、ライブラリ関数 system() を使ってもよい。

コマンドの区切りは、別のもでもよい。


----------------------------------------------------------------------
% ./if-then-else cc file1.c then play happy else play sad [←]
----------------------------------------------------------------------
この場合、argv[3] の then や argv[6] の else を 0 で潰して execvp() な どで実行するとよい。この場合は、system() よりも、execvp() を使った方が 簡単であろう。まず最初の条件判定をする部分を fork-exec で実行し、wait する。wait の結果、成功していたら、then 以下の部分をexec し、そうでな ければ、else 以下の部分を exec する。then 以下や else 以下を実行する時 には、もどってくる必要がないので、fork しなくてもよい。

★練習問題 25 並列wget

wget コマンドは、引数で与えられた URL の WWW 資源をローカル・ファイル に落とすプログラムである。

----------------------------------------------------------------------
% wget http://www/ [←]
--02:00:20--  http://www/
           => index.html
www:80 に接続しています... 接続しました!
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 2,443 [text/html]

    0K -> ..                                                     [100%]

02:00:20 (2.33 MB/s) - index.html を保存しました [2443/2443]

% []
----------------------------------------------------------------------
複数の wget コマンドを同時に実行して、複数の WWW 資源を同時にコピーす るプログラムをつくりなさい。たとえば、次のように実行すると、最大3個の wget コマンドを同時に動かし、同時にコピーを行う。

----------------------------------------------------------------------
% ./run-wget-n 3 url1 url2 url3 url4 url5 url6 [←]
----------------------------------------------------------------------
このプログラムでは、ライブラリ関数 system() を使ってもよい(使わなくて もよい)。

必ずしも同数に分割する必要はない。高度なプログラムを記述すれば、終了し 次第、次の URL に進むようにすることができる。wget コマンドにはもともと も複数の URL を取る機能もあるので、それを使ってもよい。