システム・プログラム 電子・情報工学系 新城 靖 <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro-2001/2001-05-07
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.is.tsukuba.ac.jp/~yas/index-j.html
キーボードで打てるのは、ASCII 文字の並びである。キーボードから int を 読みたい時には、文字の並びとしてまず読み込み(fgets())、次に、 文字の並びから整数に変換する(atoi(), strtol(), sscanf())
scanf() は、人間が打つ、間違い(意図的な攻撃)を含む可能性がある場所で 使うべきではない。
機械語序論の spim (xspim) のシステムコールは、特殊で、キーボードからは、 ASCII 文字の並びを打ち込んでも、OSの中で整数に変換して、返す機能があ る。Unix のシステムコールでは、そのようなことはできない。
プロセッサ(CPU)が実行できる機械命令の列がプログラムである。 プロセスとは、プログラムがオペレーティング・システムによってメモリに読 み込まれ、やはりオペレーティング・システムの管理下にあるプロセッサによっ て実行の対象になったものである。
図1 プログラムとプロセス
プロセスには、保護、資源割り当てのような機能がある。 あるプロセスが 暴走 して他のプロセスを破壊したり、あるいは、意図的に他のプロセスからデータ を盗もうとしたとしても、それを禁止してプロセスを守ることをプロセスの保 護という。プロセスを保護するためには、プロセスが直接他のプロセスのメモ リなどにアクセスできないようにプロセスとプロセスを隔離する。Unix では、 基本的にはプロセスごとに独立した論理アドレス空間を割り当てることで実現 している。 資源とは、 メモリ、ディスク、プリンタ、ディスプレイ、キーボードなど コンピュータが処理を進める上で利用価値のあるものを総称である。 プロセスは、資源を公平に配分するための単位としての役割がある。/bin/ls
や/usr/bin/ls
などのファイルに保存されている
)
がメモリに読み込まれて実行される。
Xウインドウでメニューから ktermを起動すると、ktermのプロセスとその端末
の中で動くシェルのプロセスの2つが作られる。
ps(process)
コマンドを実行すると、プロセスの一覧を表示する。
psコマンドの表示の例を示す。
psコマンドの実行結果は1行が1プロセスである。 左から、以下のような意味がある。---------------------------------------------------------------------- % psPID TTY TIME CMD 9086 ttyq1 0:00 cat 9084 ttyq1 0:00 emacs 9123 ttyq1 0:00 ps 9072 ttyq1 0:01 tcsh %
----------------------------------------------------------------------
/dev/ttyq1
は
ttyq1
の意味。
TIME
COMMAND
---------------------------------------------------------------------- % ps -lF S UID PID PPID C PRI NI P SZ:RSS WCHAN TTY TIME CMD b0 T 1231 9155 9153 0 60 20 * 462:106 - ttyq1 0:00 man b0 T 1231 9086 9072 0 60 20 * 50:28 - ttyq1 0:00 cat b0 T 1231 9084 9072 0 60 20 * 2114:849 - ttyq1 0:00 emacs b0 T 1231 9153 9072 0 60 20 * 462:108 - ttyq1 0:01 man b0 R 1231 9161 9072 4 62 20 0 405:160 - ttyq1 0:00 ps b0 T 1231 9156 9155 0 60 20 * 78:44 - ttyq1 0:00 sh b0 T 1231 9157 9156 0 60 20 * 519:210 - ttyq1 0:00 less b0 S 1231 9072 9070 1 39 20 * 703:324 8027eb80 ttyq1 0:01 tcsh %
----------------------------------------------------------------------
TIME
は過去に利用した CPU 時間の割合、
SZ
は、プロセスが確保しているメモリ、
RSS
は、そのうちメインメモリに入っている部分である。
S
は、
プロセスの
状態
(STATe)
であり、次のようなものがある。
R
(runnable)
D
(Disk)
S
(Sleep)、I
(I)
Z
(Zombie)
T
(Traced)
0
, X
などがある。
Unix では、後で説明する fork() システムコールを 発行すると、新しくプロセスが作られる。 (Unix では、これ以外の方法ではプロセスは作られない。) この時、「もとのプロセス」の「親プロセス」という。 プロセスの親プロセスのプロセス識別子は、psコマンドに「-l」オプションを つけるとPPIDのところに表示されている。
図2 プロセスの資源と属性
ps(process)
コマンドは、引数を付けないで実行すると、
プロセス識別子、端末名、
CPU時間、コマンド名を表示する。
ただし、次に説明する「-u」オプションを使わないと、ps コマンドを実行した端末と結びつけられているプロセスしか表示しない。 特定のユーザのプロセスだけを表示する。たとえば、 ユーザ名が---------------------------------------------------------------------- % psPID TTY TIME CMD 11614 pts/2 0:01 mule 11481 pts/2 0:00 tcsh 11616 pts/2 0:00 cat 11615 pts/2 0:01 mule %
----------------------------------------------------------------------
root
のユーザのプロセスだけを表示さたせい時には、次のよ
うに打つ。
自分のプロセスだけを表示したいなら、次のようにする。% ps -u root![]()
% ps -u $user![]()
$user
は、csh()や tcsh()のシェル変数で、自分自身
のユーザ名が入っている。
プロセスの大きさ(
SZ
(size))、親プロセスの PID
(PPIID
(Parent PID))優先順位(PRI
(priority))
プロセスの資源と属性を表示する。
コマンド名を引数まで含めて全部(the full cmmand name)を表示する。---------------------------------------------------------------------- % ps -lF S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 8 R 1231 18864 18862 1 51 20 50804680 164 pts/1 0:00 tcsh %
----------------------------------------------------------------------
全てのプロセスを表示する。---------------------------------------------------------------------- % ps -fUID PID PPID C STIME TTY TIME CMD yas 18864 18862 0 23:18:49 pts/1 0:00 -tcsh %
----------------------------------------------------------------------
-e
, -f
の組合わせ。全てのプロセスを
表示する時に、よく使われる。
-f
, -u
の組合わせ。自分のプロセスを
表示する時に、よく使われる。
プロセスを殺す方法には、次のような方法がある。
---------------------------------------------------------------------- % catLine 1
Line 1 Line 2
Line 2 ^C %
----------------------------------------------------------------------
^C
と同様に
^\
もあるが、
デバッグ用にcore
という名前のファイルができることが異なる。
coreは、実行中のプロセスのメモリの内容をファイルに保存したものである。
---------------------------------------------------------------------- % catLine 1
Line 1 Line 2
Line 2 ^\ Quit (core dumped) % ls -l core
-rw-r--r-- 1 yas 8430000 Sep 7 03:28 core %
----------------------------------------------------------------------
core
ファイルはプログラムのデバッグに使える。
^C
や ^\
で死なないプロセス
を殺すには、kill コマンドを使う。
プロセス識別子pidのプロセスを殺す。 オプションなしでkill コマンドを使っても死なないプロセスがある。 その場合はオプション% kill pid![]()
-KILL
をつけてkillコマンドを
実行する。
これでほとんどのプロセスは死ぬ。(OSの都合上(ディスクのI/O完了の 報告など)で、死なないプロセスもある。)% kill -KILL pid![]()
---------------------------------------------------------------------- 1: /* 2: arg-print.c -- mainの引数を表示するプログラム 3: ~yas/syspro/cc/arg-print.c 4: $Header: /home/lab2/OS/yas/syspro/proc/RCS/arg-print.c,v 1.3 2001/05/06 12:10:02 yas Exp $ 5: Start: 1997/04/21 18:23:13 6: */ 7: 8: main( int argc, char *argv[], char *envp[] ) 9: { 10: int i ; 11: printf("&argc == 0x%x, argc == %d\n", &argc, argc ); 12: printf("&argv == 0x%x, argv == 0x%x\n",&argv, argv ); 13: for( i=0 ; argv[i] ; i++ ) 14: printf("argv[%d]==0x%x, \"%s\"\n",i,argv[i],argv[i] ); 15: } ----------------------------------------------------------------------実行例。
---------------------------------------------------------------------- % cp ~yas/syspro1/cc/arg-print.c .% make arg-print
cc arg-print.c -o arg-print % ./arg-print
argc == 1 argv == 0x7fff2ef4 argv[0]==0x7fff3000, "./arg-print" % ./arg-print who am i
argc == 4 argv == 0x7fff2ee4 argv[0]==0x7fff3000, "./arg-print" argv[1]==0x7fff300c, "who" argv[2]==0x7fff3010, "am" argv[3]==0x7fff3013, "i" %
----------------------------------------------------------------------
図3 argvの構造
プログラムからは、main() の3番目の引数、外部変数 environ 、ライブラリ 関数 getenv() でアクセスできる。メモリ中の構造は、argv と同じである。 各文字列は、「変数名=値」の形式になっている。---------------------------------------------------------------------- % printenv LANGja_JP.EUC % echo $LANG
ja_JP.EUC % date
2001年 5月 6日(日曜日) 21時55分02秒 JST % setenv LANG C
% date
Sun May 6 21:55:09 JST 2001 %
----------------------------------------------------------------------
---------------------------------------------------------------------- 1: /* 2: env-print.c -- 環境変数を表示するプログラム 3: ~yas/syspro/proc/env-print.c 4: $Header: /home/lab2/OS/yas/syspro/proc/RCS/env-print.c,v 1.4 2001/05/06 11:24:46 yas Exp $ 5: Start: 1997/05/05 16:42:22 6: */ 7: extern char **environ ; 8: 9: main( int argc, char *argv[], char *envp[] ) 10: { 11: int i ; 12: printf("envp == 0x%x\n",envp ); 13: printf("environ == 0x%x\n",environ ); 14: for( i=0 ; envp[i] ; i++ ) 15: printf("envp[%d]==0x%x, \"%s\"\n",i,envp[i],envp[i] ); 16: } ----------------------------------------------------------------------実行例。
---------------------------------------------------------------------- % make env-printcc env-print.c -o env-print % ./env-print | head
envp == 0x7fff2efc environ == 0x7fff2efc envp[0]==0x7fff300c, "HOME=/home/lab2/OS/yas" envp[1]==0x7fff3023, "USER=yas" envp[2]==0x7fff302c, "LOGNAME=yas" envp[3]==0x7fff3038, "PATH=/home/lab2/OS/yas/bin:/usr/local/bin:/usr/local2/bin:/usr/local2/X11/bin:/usr/local/gnu/bin:/usr/local/X11/bin:/usr/local/tex/bin:/usr/java/bin:/usr/sbin:/usr/bsd:/sbin:/usr/bin:/usr/bin/X11:/usr/etc:/etc:/usr/freeware/bin" envp[4]==0x7fff311c, "MAIL=/var/mail/yas" envp[5]==0x7fff312f, "SHELL=/usr/bin/tcsh" envp[6]==0x7fff3143, "TZ=JST-9" envp[7]==0x7fff314c, "HZ=100" % printenv | head
HOME=/home/lab2/OS/yas USER=yas LOGNAME=yas PATH=/home/lab2/OS/yas/bin:/usr/local/bin:/usr/local2/bin:/usr/local2/X11/bin:/usr/local/gnu/bin:/usr/local/X11/bin:/usr/local/tex/bin:/usr/java/bin:/usr/sbin:/usr/bsd:/sbin:/usr/bin:/usr/bin/X11:/usr/etc:/etc:/usr/freeware/bin MAIL=/var/mail/yas SHELL=/usr/bin/tcsh TZ=JST-9 HZ=100 SSH_CLIENT=130.158.85.130 51726 22 SSH_TTY=/dev/ttyq0 %
----------------------------------------------------------------------
UNIX では、新たにプロセスを作る時に、自分のコピーしか作れない(fork()シ ステム・コール)。元のプロセスを親プロセス、作られたプロセスを子プロセ スという。
現在のプログラムの実行はそのまま続けて、新しくプログラムを実行するには、 次のようにする。
---------------------------------------------------------------------- 1: /* 2: proc-create.c -- calプログラムよりプロセスを作る 3: ~yas/syspro/proc/proc-create.c 4: $Header: /home/lab2/OS/yas/syspro/proc/RCS/proc-create.c,v 1.4 2001/05/06 13:00:24 yas Exp $ 5: Start: 1995/02/27 15:27:54 6: */ 7: 8: #include <unistd.h> /* pid_t */ 9: 10: extern char **environ; 11: 12: main() 13: { 14: pid_t child_pid ; 15: if( (child_pid=fork()) == 0 ) 16: { 17: char *argv[4] ; 18: printf("child: pid == %d, ppid == %d\n", getpid(), getppid() ); 19: argv[0] = "cal" ; 20: argv[1] = "5" ; 21: argv[2] = "2001" ; 22: argv[3] = 0 ; 23: execve( "/usr/bin/cal", argv, environ ); 24: perror( "execve" ); 25: /* exec に失敗したら exit() を忘れないこと */ 26: exit( 1 ); 27: } 28: else if( child_pid > 0 ) 29: { 30: printf("parent: pid == %d, ppid == %d, child_pid == %d\n", 31: getpid(), getppid(),child_pid ); 32: } 33: else 34: { 35: perror("fork"); 36: } 37: } ----------------------------------------------------------------------実行例。
execve() は、システム・コールである。これを使いやすくするために、次の ようなライブラリ関数が用意されている。---------------------------------------------------------------------- % ./proc-createchild: pid == 13625, ppid == 13624 2001 年 5 月 日 月 火 水 木 金 土 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 parent: pid == 13624, ppid == 13070, child_pid == 13625 %
----------------------------------------------------------------------
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 *statptr); pid_t waitpid (pid_t pid, int *statptr, int options); pid_t wait3 (int *statptr, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *statusp, int options, struct rusage *rusage); /* SGI では使えない */
この例では、echo のプロセスと、tr のプロセスは、パイプで接続されている。 パイプは、open() したファイルと同じようにread() したり write() したり することがでる。しかし実際には、ファイルではなく、プロセスとプロセスが データを交換する仕組(プロセス間通信の仕組)の1つである。---------------------------------------------------------------------- % echo "hello,world" | tr '[a-z]' '[A-Z]'HELLO,WORLD %
----------------------------------------------------------------------
パイプを作るには、pipe() システム・コールを使う。これで、パイプ が1本作られ、2つのファイル記述子(読込み用と書込み用)が返される。
---------------------------------------------------------------------- 1: /* 2: pipe-rw.c -- pipe() を使ったプログラム 3: ~yas/syspro/proc/pipe-rw-nodup.c 4: $Header: /home/lab2/OS/yas/syspro/proc/RCS/pipe-rw-nodup.c,v 1.1 2001/05/06 13:13:22 yas Exp $ 5: Start: 1997/05/26 20:43:29 6: */ 7: 8: #include <stdio.h> 9: #include <unistd.h> 10: 11: extern void parent( int fildes[2] ); 12: extern void child( int fildes[2] ); 13: 14: main() 15: { 16: int fildes[2] ; 17: pid_t pid ; 18: 19: if( pipe(fildes) == -1) 20: { 21: perror("pipe"); 22: exit( 1 ); 23: } 24: /* fildes[0] -- 読み込み用 25: * fildes[1] -- 書き込み用 26: */ 27: if( (pid=fork()) == 0 ) 28: { 29: child( fildes ); 30: } 31: else if( pid > 0 ) 32: { 33: parent( fildes ); 34: } 35: else 36: { 37: perror("fork"); 38: exit( 1 ); 39: } 40: } 41: 42: void parent( int fildes[2] ) 43: { 44: char *p, c ; 45: close( fildes[0] ); 46: p = "hello,world\n" ; 47: while( *p ) 48: { 49: c = *p ++ ; 50: write( fildes[1],&c,1 ); 51: } 52: close( fildes[1] ); 53: } 54: 55: void child( int fildes[2] ) 56: { 57: char c, c2 ; 58: close( fildes[1] ); 59: while( read(fildes[0],&c,1) >0 ) 60: { 61: c2 = toupper(c); 62: write( 1, &c2, 1 ); 63: } 64: close( fildes[0] ); 65: } ----------------------------------------------------------------------実行例。
---------------------------------------------------------------------- % ./pipe-rw-nodupHELLO,WORLD %
----------------------------------------------------------------------
図4−1 pipe() システムコール実行前
図4−2 pipe() システムコール実行後
図4−3 fork() システムコール実行後
図4−4 親子で close() システムコール実行後
dup() よりも、dup2() の方が便利である。
---------------------------------------------------------------------- 1: /* 2: pipe-rw.c -- pipe() と dup() を使ったプログラム 3: ~yas/syspro/proc/pipe-rw.c 4: $Header: /home/lab2/OS/yas/syspro/proc/RCS/pipe-rw-dup.c,v 1.7 2001/05/06 13:13:22 yas Exp $ 5: Start: 1997/05/26 20:43:29 6: */ 7: 8: #include <stdio.h> 9: #include <unistd.h> 10: 11: extern void parent( int fildes[2] ); 12: extern void child( int fildes[2] ); 13: 14: main() 15: { 16: int fildes[2] ; 17: pid_t pid ; 18: 19: if( pipe(fildes) == -1) 20: { 21: perror("pipe"); 22: exit( 1 ); 23: } 24: /* fildes[0] -- 読み込み用 25: * fildes[1] -- 書き込み用 26: */ 27: if( (pid=fork()) == 0 ) 28: { 29: child( fildes ); 30: } 31: else if( pid > 0 ) 32: { 33: parent( fildes ); 34: } 35: else 36: { 37: perror("fork"); 38: exit( 1 ); 39: } 40: } 41: 42: void parent( int fildes[2] ) 43: { 44: char *p, c ; 45: close( fildes[0] ); 46: close( 1 ); /* 標準出力をパイプに切替える */ 47: dup( fildes[1] ); 48: close( fildes[1] ); 49: 50: p = "hello,world\n" ; 51: while( *p ) 52: { 53: c = *p ++ ; 54: write( 1,&c,1 ); 55: } 56: close( 1 ); 57: } 58: 59: void child( int fildes[2] ) 60: { 61: char c, c2 ; 62: close( fildes[1] ); 63: close( 0 ); /* 標準入力をパイプに切替える */ 64: dup( fildes[0] ); 65: close( fildes[0] ); 66: 67: while( read(0,&c,1) >0 ) 68: { 69: c2 = toupper(c); 70: write( 1, &c2, 1 ); 71: } 72: close( 0 ); 73: } ----------------------------------------------------------------------実行例。
---------------------------------------------------------------------- % ./pipe-rw-dupHELLO,WORLD %
----------------------------------------------------------------------
図4−5 親子で close(),dup(),close() システムコール実行後
proc-create
を実行すると、cal
の出力より先に「%」が表示されたり、親プロセスの出力と子プロセスの出力
が入り交じることがある。この理由を考えなさい。
この問題を解決しなさい。そのためには、wait() システム・コール (waitpid(),wait3(),wait4()など)を用いて、同期を行えばよい。すなわち、 子プロセスが表示する可能性がある間は、親プロセスを待ち状態にしなさい。
先にパイプを2つ作ってから2回 fork() してもよい。パイプを1つ作って fork() してから もう1つパイプを作って fork() するという方法でもよい。
ヒント: 使わないパイプのファイル記述子は、全部 close() すること。パイプの書き 込み側のファイル記述子が開いている間は、read() しても EOF (End Of File) にならない。自分自身で write() する可能性もある。
ヒント:書き手がいないパイプは、全ての書込み側のファイル記述子を closeすると作れる。
ヒント:プロセスを fork() しなくても、パイプに書くことはできる。プロセ スが1個の状態で、バッファの大きさ以上のデータを書き込むと、プロセスが ブロックされる(先に進まなくなる)。
---------------------------------------------------------------------- FILE *popen(const char *command, const char *type); int pclose (FILE *stream); ----------------------------------------------------------------------これを利用して、プロセスを作成し、プロセスにデータを与えたり、あるいは、 プロセスからデータを受け取るプログラムをつくりなさい。
たとえば、expr コマンドを実行して、その結果を受け取るプログラムをつく りなさい。expr は、次のように引数で与えられた式を評価し、結果を標準出 力に返すものである。
この課題では、expr コマンドに似たようなプログラム作るのではなく、それ をそのまま利用して、結果を受け取るプログラムを作る。実行するプログラム としては、expr 以外に次のようなものが考えられる。---------------------------------------------------------------------- % expr 1 + 23 %
----------------------------------------------------------------------