オペレーティングシステム I 資料集 資料1 マルチプロセッシング(多重処理) ・・・・・・・・・・・・・1 資料2 複数のプロセス間での会話 ・・・・・・・・・・・・・・・・9 資料3 UNIX におけるシグナル ・・・・・・・・・・・・・・・ 13 資料4 Dekker のアルゴリズム ・・・・・・・・・・・・・・・・ 14 資料5 計数型セマフォとモニタ ・・・・・・・・・・・・・・・・ 15 資料6 UNIX におけるプロセス間通信プログラミング・・・・・・・ 17 資料7 スレッドとマルチスレッドプログラミング ・・・・・・・・ 44 資料1 マルチプロセッシング(多重処理) 1. 同時に複数の仕事をこなすシステム(マルチプロセッシングシステム) 昔のパソコンの BASIC や MSDOS システムを使ったことがある人はご存知だと思いますが、 これらのシステムでは同時に、二つや三つの複数のコマンドやプログラムが動くことはあ りません。基本的には一人のユーザ(シングルユーザ)が単一のコマンドやプログラムを実 行させてその終了を待って、次のコマンドを入力するといったシングルシステムであると いえます。これらのシステムを使っていて例えば、ある文書やファイルをプリンタに書き 出している間に、別のファイルを編集したい、あるいは実行時間の長いプログラムの終了 を待つ間に、別の短いテストプログラムを走らせて、テストしてみたいなどとの要求を可 能にするためには計算機のシステム(オペレーティングシステム)が同時に複数個のプログ ラムを処理する機能を有することが必要となります。この機能のことを通常「多重処理」 とか「マルチプロセッシング」といいます。これに対して、上に述べた、同時には一つの 仕事しかこなせないシステムは「シングルプロセッシング」システムと呼ばれます。UNIX は マルチプロセッシングシステムですが、先ずこの機能を簡単に確認することから始めまし ょう。 ●ウインドウシステムを立ちあげて、ウインドウが二枚オープンされた状態にして下さい。 さらに、次のようなシェルファイル print_a をそのうちの一つのウインドウで作成した後、 % chmod +x print_a によって、必ず実行ファイルとしてください。 #! /usr/bin/tcsh set count = $1 set i = 0 if ($1 == "") then set count = 10000000 endif while ($i < $count) echo $i aaaaaaaaaaaaa @ i++ end ・print_a を次のように起動して下さい。 % print_a ・他のウインドウをアクティブにして、そのウインドウ上で次のようなシェルファイル 1 print_b を作成して、同様に実行ファイルとして下さい。 #! /usr/bin/tcsh set count = $1 set i = 0 if ($1 == "") then set count = 10000000 endif while ($i < $count) echo $i bbbbbbbbbbbbb @ i++ end ・このとき、print_a が動いており、出力結果が印字されている最中にエディタプログラ ム(emacs など) が動いていることに注意して下さい。 ・print_b を作成した後、 % print_b によって print_b を起動して下さい。やはり、print_a と print_b が同時に動いて各々の ウインドウに印字していることを確認して下さい。 ● print_a および print_b を ctrl--c を入力することによってアボートして下さい。 ・2 つのウインドウのどちらか 1 つで次のように print_a と print_b を起動して下さい。 % print_a 100 ; print_b 100 ・結果を確認して下さい。 ・つづいて、次のように print_a と print_b を起動して下さい。 % print_a 100 & print_b 100 ・結果を確認して下さい。最初の起動のし方と比べて、どのように異なっているでしょう か? 特に、2 番目の結果から、UNIX の(オペレーティング)システムが複数のプログラムを 実行する手順についてどのように推測されるでしょうか? ● プロセスとは? print_a や print_b はプログラムとして動いていない状態では、ディスク中のただの実行 可能ファイルです。プログラムとして、起動されることにより、はじめて UNIX のカーネ ルに認知されプロセスとして(プロセステーブルに)登録されます。プログラムの実行が終 了すれば、登録を外されます。したがって、プロセスとは「実行中のプログラム」と考え れば良いでしょう。 2 2. UNIX はタイムシェアリングシステムです 一般に計算機は CPU が発生する次のようなクロックパルスと呼ばれる(同期)信号にした がって動作します。このクロックパルスの時間軸を以後、CPU 時間と呼ぶことにします。 今、仮にどんなプロセスも動いていない状態でプロセス A が起動されたとすると、他のプ ロセスの起動要求がなされない限り、プロセス A は終了するまで、次のように CPU 時間を 占有し続けます。 start A stop プロセス A が実行中にプロセス B の起動要求があった場合、CPU 時間はこれら 2 つのプロ セスの間で分割されそれぞれある一定時間(この時間のことをタイムスライスといいます) 交替で割り当てられ、実行が継続されます。 A B A B A さらに、プロセス C が起動された場合、同じようにして三ともえになって実行が継続され ます。 A B C A B C A B プロセス A の実行が終了すると、プロセス B とプロセス C の実行が継続されます。 A B C A B A は終了 3. TSS シミュレータを動かしてみる 3 C B C 上で説明した複数のプロセスの動きを簡単な TSS シミュレータを作って確認してみまし ょう。 #! /usr/bin/tcsh echo scheduler started echo echo echo echo echo "" プロセス A 起動...ra プロセス B 起動...rb プロセス C 起動...rc プロセス A 終了...ta プロセス B 終了...tb プロセス C 終了...tc プロセス状態の一覧...ps コマンドを忘れた時...h シミュレータの終了...end echo "" while(1) echo input command set command = $< switch ($command) case "ra": print_a > $1 & breaksw case "rb": print_b > $1 & breaksw case "rc": print_c > $1 & breaksw case "ta": kill %print_a set dummy = $< breaksw case "tb": kill %print_b set dummy = $< breaksw case "tc": kill %print_c set dummy = $< breaksw # 標準入力からコマンド読み込み # print_a の起動、出力は別ウインドウへ # print_b の起動 # print_c の起動 # print_a の終了 # print_b の終了 # print_c の終了 case "h": echo "" echo プロセス A 起動...ra プロセス B 起動...rb プロセス C 起動...rc echo プロセス A 終了...ta プロセス B 終了...tb プロセス C 終了...tc echo プロセス状態の一覧...ps コマンドを忘れた時...h echo シミュレータの終了...end echo "" breaksw case "ps": ps -l │ grep "print_[abc]" breaksw 4 case "end": set d = `ps` if ("$d" =~ *print_a*) then kill %print_a endif if ("$d" =~ *print_b*) then kill %print_b endif if ("$d" =~ *print_c*) then kill %print_c endif exit 0 # あと始末 default: echo illegal command endsw end 上のプログラムは TSS シミュレータの一部です。3 つのプロセス print_a、print_b、print_c を任意のタイミングで起動したり停止したりするプログラムです。各々のプロセスは端末 に文字を書き続けるプログラムですが、シミュレータに対する入出力と交じり合わないよ うにシミュレータが走るウインドウとは別のウインドウに出力されます。 ● 次の手順にしたがって作業して下さい。 ・新しいウインドウ WS,WP を開いて下さい。 ・WS をシミュレータウインドウ、WP をプロセスウインドウとします。 ・WS 上で TSS プログラムファイルをエディタを使って、作成して下さい。 ・WP をアクティブにした後、次のコマンドで WP のデバイス名を知って下さい。 % tty ・tty コマンドで得られた WP のデバイス名を例えば /dev/ttyp2 とします。また、シミュ レータプログラムを tss とします。 WS 上で tss を次のように起動して下さい。 % tss /dev/ttyp2 コマンドラインから与えた WP のデバイス名は tss プログラム中の $1 に代入されます。 ・"input command" のプロンプトに従って、起動コマンドまたは停止コマンドを入力して、 WP 上の出力を観察して下さい。例えば、次のようなコマンド列を順次、入力すれば結果の 表示はどのように遷移していくでしょうか? ra, rb, rc, tb, ta, rb, tc, tb 5 ・tss プログラムを終了する時には end コマンドを入力して下さい。 ● 入出力待ちによるプロセスの中断状態 プロセスの実行が入出力命令に至った時、TSS システムのプロセスの実行制御はどのよう になるでしょうか。データの入出力に要する時間が仮に、1∼2 秒であっても計算機の CPU にとってはとてつもなく長い時間であって、その間 CPU は膨大な計算をこなすことができ ます。入出力の終了を待っている間、CPU は何もせずに遊んでいればシステムのスループ ット(処理能力)を大幅に低下させてしまいます。 TSS システムは実行中のプロセスが入出力待ちになった時、そのプロセスを中断状態にし て、他のプロセスに CPU の実行時間を与えます。入出力が終了すれば、中断しているプロ セスに対して再び、CPU の実行時間が与えられます。下図にこの様子を示します。 A B C B C A B C A は入出力中で中断 4.プロセスの中断状態 プロセスの入出力待ちなどによって生じる中断状態も考慮した、TSS のシミュレータを作 成して動かしてみましょう。中断状態のシミュレートには C シェルの stop コマンドを用 いていることに注意して下さい。 例えば次のようなコマンド列を順次入力した場合、WP ウインドウに現れる結果はどのよう になるでしょうか。 ra, rb, sa, rc, sb, ca, sc, ta, cb, cc, tb, tc #! /usr/bin/tcsh echo scheduler started echo echo echo echo echo echo echo "" プロセス A 起動...ra プロセス B 起動...rb プロセス C 起動...rc プロセス A 中断...sa プロセス B 中断...sb プロセス C 中断...sc プロセス A 再開...ca プロセス B 再開...cb プロセス C 再開...cc プロセス A 終了...ta プロセス B 終了...tb プロセス C 終了...tc プロセス状態の一覧...ps コマンドを忘れた時...h シミュレータの終了...end echo "" while(1) echo input command set command = $< # 標準入力からコマンド読み込み 6 switch ($command) case "ra": print_a > $1 & breaksw case "rb": print_b > $1 & breaksw case "rc": print_c > $1 & breaksw case "sa": stop %print_a set dummy = $< breaksw case "sb": stop %print_b set dummy = $< breaksw case "sc": stop %print_c set dummy = $< breaksw case "ca": %print_a & set dummy = $< breaksw case "cb": %print_b & set dummy = $< breaksw case "cc": %print_c & set dummy = $< breaksw case "ta": kill %print_a set dummy = $< breaksw case "tb": kill %print_b set dummy = $< breaksw case "tc": kill %print_c set dummy = $< breaksw # print_a の起動、出力は別ウインドウへ # print_b の起動 # print_c の起動 # print_a の入力待ち # print_b の入力待ち # print_c の入力待ち # print_a の再開 # print_b の再開 # print_c の再開 # print_a の終了 # print_b の終了 # print_c の終了 case "h": echo "" echo プロセス A 起動...ra プロセス B 起動...rb プロセス C 起動...rc echo プロセス A 中断...sa プロセス B 中断...sb プロセス C 中断...sc echo プロセス A 再開...ca プロセス B 再開...cb プロセス C 再開...cc echo プロセス A 終了...ta プロセス B 終了...tb プロセス C 終了...tc echo プロセス状態の一覧...ps コマンドを忘れた時...h 7 echo シミュレータの終了...end echo "" breaksw case "ps": ps -l │ grep "print_[abc]" breaksw case "end": set d = `ps` if ("$d" =~ *print_a*) then kill %print_a endif if ("$d" =~ *print_b*) then kill %print_b endif if ("$d" =~ *print_c*) then kill %print_c endif exit 0 # あと始末 default: echo illegal command endsw end ●プロセスの状態 今まで見てきたように、通常、プロセスは生まれてから(起動されてから)、死ぬまで(停止 するまで)いくつかの状態を経ることになります。この状態遷移を下図にまとめておきます。 「実行待ち状態」は、今まで説明しなかった状態です。プログラムが起動されてプロセス が誕生した時、実はいきなり実行されず、実行の順番を待つ「待ち行列」に登録されて待 機します。同様に、中断状態のプロセスが再び実行される時にも、いきなり実行されずに 同じ待行列に待機して実行の順番を待ちます。 今の単純なシミュレートの方法ではこの状態のシミュレーションは難しいので今回は実行 待ち状態は実行状態に含めて扱うことにしました。 実行状態 タイムスライス 割り当て 入出力待ち タイムスライス切れ 実行待ち状態 中断状態 入出力終了 8 資料2 複数のプロセス間での会話 TSS シミュレータのところで扱った 3 つのプロセスは互いに独立であり、連絡しあって共 同である一つの仕事をするといったことはありませんでした。個々のプロセスが独立で閉 じていようとも、tss によるマルチプロセッシングの機能は非常に便利ですが、「複数のプ ロセスが連絡しあうことができる機能」をシステムが提供してくれるならば、計算機で処 理できる対象や世界がさらに広がります。 「プロセス間通信機能」として、UNIX が提供している機能にはいくつかありますが、 ここでは「パイプ」を用いて同じマシンにログインしている 2 人のユーザが会話すること を考えましょう。 ● cat 再考 cat コマンドは今まで何度も使われたことでしょうが、このコマンドの機能について復習し てみましょう。cat コマンドは基本的には標準入力から文字列データを受け取りそれをその まま標準出力にコピーするコマンドです。したがって、最も単純な使用方法は % cat です。この場合、キーボードから入力された文字列がそのままオーム返しにディスプレー 上に表示されます。リダイレクトの機能を使った次のようなコマンドについてその動作を 確認して cat コマンドの機能を十分理解しておいて下さい。 % cat > file1 % cat file1 % cat < file1 % cat < file1 > file2 % cat file2 ● パイプ再考 パイプは 2 つのプロセスが通信するための最も単純な機能です。 例えば、 % ls -l | wc の場合、ls コマンドの出力はリダイレクトされてメモリ中に確保された「パイプバッファ」 に出力されます。また、wc プロセスの入力もリダイレクトされて、このパイプバッファか ら入力されます。 9 ls はどんどんこのパイプバッファに書き込みます。バッ ファがいっぱいになれば ls は中断します メモリ上のパ ls wc イプバッファ wc はパイプバッファからデータを取り込みます。パイプバ ッファが空になれば wc は中断します。 名前つきパイプ このパイプは非常に便利な機能ですが、他のユーザが生成したプロセスとの間で通信する ことは不可能です。これを行なうためには、 「名前つきパイプ(named pipe)」の機能があり ます。名前つきパイプの場合にはパイプバッファはメモリ中に確保されるのではなく、デ ィスク中のファイルとして確保されます。通常のパイプと同様に書き込むプロセスが書き 込んだデータの順に読み込むプロセスがデータを読み込みます。 名前つきパイプは機能的には通常のパイプの機能を包含していますが、使用にあたって はリダイレクト( > 記号)を用います。少し名前つきパイプの動きを調べてみましょう。 ● 名前つきパイプの練習 ・名前つきパイプファイルの作成には次の mknod コマンドを用います。 % mknod pfile p pfile は名前つきパイプのファイル名であり、後ろの p は通常のデバイスファイルではな くパイプとして使われるファイル(パイプも一種のデバイスですが)であることを指示しま す。"ls -l" コマンドで pfile のパーミッションの先頭に p が付いていることを確認して下 さい。 ・次のように入力して結果を確認して下さい。 % ls -l > pfile & cat < pfile % ls -l > pfile & cat < pfile > file1 % cat file1 > pfile & wc < pfile ・TSS シミュレータのところで行なったように、ウインドウを 2 枚開いて下さ い。それを WA, WB とします。WB をアクティブにして tty コマンドで WB の デバイス名を知って下さい。仮にそれを /dev/ttyp2 とします。WA 上で次の ように入力して結果を確認して下さい。 % cat > file 任意の文字列を 10 数行ほど入力し て下さい。入力の終了は ctrl-d としてください。 % cat file > pfile & cat < pfile > /dev/ttyp2 名前付パイプを使って二つのプロセス間で会話する 前項までにおいて、名前付パイプの機能を理解していただいたことと思いますが、ここで はこの名前付パイプのプロセス間通信機能を使って、同じマシンにログインしているユー ザとの間で会話することを考えます。 ・名前付パイプの練習のところでパイプファイル pfile を作成しましたが、ここではもう 一つのパイプファイル pfile1 を作成しましょう。 % mknod pfile1 p ・WB 上で次のように cat プロセス cat_b を起動して下さい。オプション -u は cat コ マンドのバッファリング機能を解除します。 % cat -u < pfile1 & cat -u > pfile さらに、WA 上で次のように cat プロセス cat_a を起動して下さい。 % cat -u < pfile & cat -u > pfile1 How are you ? 再び、WB をアクティブにして cat_a からのメッセージに答えた後、メッセージを送って 下さい。 % cat -u < pfile & cat -u > pfile1 How are you ? I am fine thank you, and how are you ? WA をアクティブにして、cat_b のメッセージに答えて下さい。 % cat -u < pfile1 & cat -u > pfile How are you ? I am fine thank you, and how are you ? I am also fine. 以下、自由に二つのプロセスの間でメッセージを通信し合って下さい。通信を終了したい 時にはそれぞれのウインドウで ctrl-d を入力して下さい。ここで、二つのプロセス、cat_a, cat_b と二つのパイプファイル pfile, pfile1 の関係を図示しておきます。 11 cat_a cat_b cat pfile1 cat cat pfile cat ● 他のログインユーザと会話する。 前項で扱ったシステムは自分のアカウントセッションで生成した二つのプロセス間での 会話システムですが、これをそのまま、同じマシンにログインしているユーザとの間で会 話するシステムに拡張することができます。 ・前項で作成した二番目のパイプファイル pfile1 はここでは使用しませんので消去してお きます。 % rm pfile1 ・次のようなシェルファイル chat を作成して下さい。 #! /usr/bin/tcsh who echo "" echo -n "おしゃべりのあいて? " set friend = $< echo "================================" > ~$friend/pfile echo "Message from $LOGNAME (`tty`) to" > ~$friend/pfile echo "$friend [ `date` ] ...^G^G^G" > ~$friend/pfile echo "================================" > ~$friend/pfile echo "" > ~$friend/pfile cat -u < ~$friend/pfile & cat -u > pfile ・同じマシンにログインしているユーザを見つけて、作成した chat コマンド で自由に会話して下さい。 12 資料3 13 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 Dekker のアルゴリズム 資料4 #define #define int int TRUE FALSE turn; c1,c2; p1( ) { 1 0 /* 共有変数:現在実行中のプロセスを示す */ /* 共有変数:TRUE or FALSE */ /*プロセス P1*/ while (1) { c1 = TRUE; /*臨界領域に入ろうとすることを宣言*/ while (c2) { if (turn == 2) { /*P2 が臨界領域に入っているか?*/ c1 = FALSE; while (turn ==2); /*P2 が臨界領域から出るまで待つ*/ c1 = TRUE; } } CS1; /* 臨界領域コード */ turn =2; /* 今度は P2 が臨界領域に入る番とする */ c1 = FALSE; NCS1; /* 非臨界領域コード */ } } P2( ) { /*プロセス P2*/ while (1) { c2 = TRUE; /*臨界領域に入ろうとすることを宣言*/ while (c1) { if (turn == 1) { /*P1 が臨界領域に入っているか?*/ c2 = FALSE; while (turn ==1); /*P1 が臨界領域から出るまで待つ*/ c2 = TRUE; } } CS2; /* 臨界領域コード */ turn = 1; /* 今度は P2 が臨界領域に入る番とする*/ c2 = FALSE; NCS2; /* 非臨界領域コード */ } } main( ) { /*初期化*/ turn =1; c1 = FALSE; c2 = FALSE; para { p1( ); p2( ); } } /* プロセス p1 と p2 を並列に実行 */ 14 資料 5 1 計数型セマフォ (生産者消費者問題) 2 3 /* 同種複数資源の排他制御に用いられる。*/ 4 5 #define N 100 //バッファの収容数 6 semaphore mutex = 1; //臨界領域の制御 7 semaphore empty = N; //バッファの空きスロット数をカウント 8 semaphore full = 0; //バッファの有効スロット数をカウント 9 10 producer() 11 { 12 int item; 13 14 while (TRUE) { 15 produce_item(&item); //1つ生産 16 P(empty); 17 P(mutex); 18 enter_item(item); 19 V(mutex); 20 V(full); 21 22 //空きスロットを1つ減らす //ブロックしているかも知れない消費者を起こす } } 23 24 consumer() 25 { 26 int item; 27 28 while (TRUE) { 29 P(full); 30 P(mutex); 31 remove_item(&item); 32 V(mutex); 33 V(empty); 34 consume_item(item); 35 36 //有効スロット数を1つ減らす //ブロックしているかも知れない生産者を起こす //1つ消費 } } 37 38 15 資料 1 モニタの使用による生産者消費者問題 2 3 #define N 100 4 int no_of_data = 0; 5 condition emty, full; 6 7 producer() 8 { 9 int item; 10 11 produce_item(&item); //1つ生産 12 if (no_of_data >= N) 13 full.wait; 14 enter_item(item); //バッファに格納 (排他制御はモニタにより行われることに注意) 15 no_of_data += 1; // 残っているデータ数をインクリメント 16 emty.signal; //ブロックしているかも知れない消費者を起こす 17 } 18 19 consumer() 20 { 21 int item; 22 if (no_of_data = 0) 23 emty.wait; 24 remove_item(&item); //バッファから除去 (排他制御はモニタにより行われる) 25 no_of_data -= 1; 26 full.signal; 27 consume(item); 28 // 残っているデータ数をデクリメント //ブロックしているかも知れない生産者を起こす //除去したデータを消費 } 16 } 資料 6 1 UNIX におけるプロセス間通信プログラミング メッセージ通信 IPC(InterProcess Communication)とはプロセス間でデー タの送受 信 を行う機能 のこと で あり、メッセージ、パイプ、セマフォ、共有メモリ等様々な形式のものが提供されている。 ここではそのうちの「メッセージ」について説明する。 1.1 メッセージ メッセージ機能は、プロセス同士で直にデータの送受信を行うのではなく、データを一度 キュー(queue) と呼ばれる FIFO バッファを経由してやりとりする方法の一例である。FIFO バッファとは色々なデータを格納しておく倉庫のようなものであるが、データを取り出す 際 に は 格 納 さ れ た 順 番 で 取 り 出 さ れ る と い う 仕 様 の 倉 庫 で あ る (こ れ が FIFO(First In First Out)の名の由来である。キューはメッセージ機能を使う時にシステムが用意してく れるので、自ら作成する必要はない。 プロセス FIFO バッファ Message (メッセージボックス) Message プロセス 1.2 システムコール メッセージ機能を扱うシステムコールには、以下の4つがある。 msgget() メッセージ ID の割り付け(キューの獲得) msgsnd() 送信(キューへの書き込み) msgrcv() 受信(キューからの読み込み) msgctl() メッセージのコントロール(制御いろいろ) また、これらの関数の使用に先立って以下のヘッダファイルを include する必要がある。 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> 具体的には、プロセス A からプロセス B へデータ転送する例の手順は以下のようになる。 1. プロセス A、B で、それぞれ同じキューを作成(入手)する。(msgget())(実際には、キ ューに対応したメッセージ ID というものを受け取る。 17 2. プロセス A がキューにデータを格納する。(msgsnd()) 3. プロセス B がキューからデータを取り出す。(msgrcv()) 4. もうそれ以上通信を行わないのなら、キューを削除する。(msgctl())) 1.2.1 msgget:メッセージ ID の割り付け int msgget( key_t key, int msgflg ) 機 能: メッセージ ID を割り付ける。 引 数: key_t key; メッセージ ID を割り当てるキー int msgflg; メッセージ ID の割り付け方の指定 戻り値: int msgid; メッセージ ID 割り当て失敗時には -1 が返り、 errno.h で定義される大域変数 errno にエラー番号が 格納される。 これは、ファイル操作における fopen() 関数とよく似ている。 fopen()では、「ファイル名」を与えて「ファイルポインタ」という識別子を受け取り、以 降のアクセスをその識別子を用いて行う。msgget()でも同様に、「キー」を与えて「メッセ ージ ID」という識別子を受け取り、以降のアクセスをその識別子を用いて行う。ファイル 名を知っていれば誰でもそのファイルをアクセスできる(permission の問題は別)のと同 様、キーさえ知っていればどのプロセスからもそのキューをアクセスできる。(もちろんこ れにも permission は指定できる(後述)。) 以下、引数の説明。 key は、文字ではなく key_t という型で指定するが、実際は key_t は整数型である。した がって、key_t 型の変数は通常の long int 型変数と同様に扱える。 キー値を幾つにするかは自由だが、重複を避けるために、誰も使わないような値(自分のユ ーザーID など)にしておくのが良い。(1つのプロセス内だけでメッセージ機能を使用した い場合には特別に、キーとして IPC_PRIVATE を指定できる。 msgflg には、「割り付けの方法」と「パーミッション」の2種類の値の論理和(OR)を設定 する(後述の使用例を参照)。 「割り付けの方法」としては、以下の3種類を選択出来る。 IPC_ALLOC 既にキューが存在する場合はそのメッセージ ID を、 存在しなければエラー(-1)を返す。 18 IPC_CREAT 既にキューが存在する場合はそのメッセージ ID を、 存在しなければ新規にキューを作成しそのメッセージ ID を返す。 IPC_CREAT │ IPC_EXCL 既にキューが存在する場合はエラー(-1)を返し、存在しなけ れば新規にキューを作成しそのメッセージ ID を返す。 「 パ ー ミ ッ シ ョ ン 」 の 値 に つ い て は 、 msgflg の 下 位 9 ビ ッ ト を 使 っ て 、 キ ュ ー の permission を指定する。 8 7 R W 6 − user 5 4 R W 3 − 2 1 R W group 0 ビット − other R/W は、対応するビットを 1 にすると read/write が可能になる。 実行ビットがない他は、だいたい UNIX のファイルシステムと同じである。 ● 使用例 if ( (id = msgget( getuid(), IPC_CREAT │ 0666 )) < 0 ) { perror( "msgget" ); exit( 1 ); } キー値として、getuid()の戻り値(自分のユーザーID)を用いている。 エラーが発生した場合は、perror() (大域変数 errno に対応したエラーメッセージを表示 する関数である。)によりエラーメッセージを表示してから終了している。なお、perror() を使用するプログラムには errno.h も忘れず #include しておくこと。 1.2.2 msgsnd:送信 int msgsnd( int msgid, void *msgp, size_t msgsz, int msgflg ) 機 能: データをキューに格納する。 引 数: int msgid; msgget()で獲得したメッセージ ID void *msgp; メッセージデータ構造体へのポインタ size_t msgsz; 送信データサイズ(size_t は long 型) int 送信条件フラグ msgflg; 19 戻り値: int err; 送信成功時には 0 が返る。送信失敗時には -1 が返り、 errno.h で定義される大域変数 errno にエラー番号が 格納される。 キューに格納したいデータは、メッセージデータ構造体の中に納めて渡す。 メッセージデータ構造体の一般形: struct msgdata { long mtype; /* メッセージのタイプ */ char mtext[]; /* メッセージ本体 */ } mb; メッセージ本体は様々に変更することができる(msgsnd()の仕様中の void *msgp に注意)。 例1)データを8バイト文字列にしたい場合: struct msgdata { long mtype; char mtext[8]; } mb; 例2)データを int 型配列(要素5個)と128バイト文字列にしたい場合: struct msgdata { long mtype; int mnum[5]; char mtext[128]; } mb; このように、メッセージデータ構造体は「先頭に long mtype を指定する」という条件(別 に変数名は mtype でなくても良い)さえ守れば、他のメンバの数や型、サイズや名前は自由 に設定して構わない。(メッセージ構造体を複数個定義した時は、送信・受信時に使うメッ セージ構造体の対応を間違えないように注意すること。) メッセージのタイプ(mtype)には、任意の自然数を自由に指定できる(0 や負数は不可)。こ の数字は、メッセージデータ構造体を複数個定義した時にそれを識別する番号として使え る。(構造体の定義が1個の場合も、構造体の内容が何を表すかを示すのに使える。) 経験上、mtype を初期化しないで msgsnd() を実行すると正常に動作しない可能性が高い 20 ので、msgsnd() を使用する際には、たとえ意味が無くても、mtype に何らかの数値を代入 しておくことを推奨する。 以下、引数の説明。 msgid には、送信するキューのメッセージ ID を指定する。 msgp には、メッセージデータ構造体へのポインタを指定する。 msgsz には、送信するデータのサイズ(bytes)を指定する。メンバ mtype の分は含まない ことに注意。変数の型が size_t となっているが、これもやはり整数型と見なして扱って 構わない。 msgflg には、0 か IPC_NOWAIT のどちらかを指定する。通常、キューがいっぱいで送信不 可能な時にはキューに空きが出来るまでスリープ状態に入る(プロセスが処理を一時停止 すること。終了することではない。)が、IPC_NOWAIT を指定しておくと、エラーとして即 座に帰って来るようになる。このとき、errno.h で定義される大域変数 errno に EAGAIN が 設定されるので、通常のエラーと区別することは可能である。 ● 使用例 メッセージデータ構造体が struct msgdata { long mtype; char data[8]; } msg; と定義されている時、 msg.mtype = 1; strcpy( msg.data, "OK." ); if ( msgsnd( id, &msg, sizeof(char)*8, 0 ) < 0 ) { perror( "msgsnd" ); exit( 1 ); } この例ではメッセージ ID id のキューに、メッセージ構造体 msg を送信している。送り たいデータは char data[8]; であるので、そのサイズ(バイト)は sizeof(char)*8 で求め られる。エラーが発生した場合は、perror()関数によりエラーメッセージを表示してから 終了している。 21 1.2.3 msgrcv:受信 int msgrcv( int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg ) 機 能: データをキューから取り出す。 引 数: int msgid; メッセージ ID void *msgp; メッセージデータ構造体へのポインタ size_t msgsz; 受信データサイズ long msgtyp メッセージタイプ(選択受信時) int msgflg; 受信条件フラグ err; 受信成功時は受信したデータ数(バイト)が返る。 戻り値: int 受信失敗時は -1 が返り、errno.h で定義される 大域変数 errno にエラー番号が格納される。 指定したキューからデータを取り出してメッセージデータ構造体に格納する。 msgid には、受信するキューのメッセージ ID を指定する。 送信プロセスで指定する msgid の値と同じ値を指定する必要がある。 msgp には、メッセージデータ構造体へのポインタを指定する。メッセージデータは msgid の キューから取り出されこの構造体内に転送されるので、この構造体の初期化は無意味であ る(しても上書きされる)。msgsnd()の所でも書いたが、メッセージ構造体を複数個定義し ている場合、"受信するメッセージデータと同じ形の構造体を指定しないと正常な動作は 期待されない"、ということに注意する。 msgsz には、受信するデータサイズ(bytes)を指定する。これも mtype の分は含まない。 msgtyp には、受信したいメッセージのタイプ(mtype)を指定する。キューは FIFO なので、 基本的には格納した順番でしかメッセージを取り出すことはできないが、これに値を設定 することにより受信したいメッセージを選択することが出来る。 msgtyp = 0 の場合: メッセージの選択は行なわず、次にキューから取り出されるメッセー ジを受け取る。 msgtyp > 0 の場合: キューの中の各メッセージについて、メッセージタイプ(mtype)が msgtyp と一致するものを検索し、最初に一致したメッセージを受け取 る。 22 msgflg には、0 か IPC_NOWAIT のどちらかを指定する。通常、キューが空だったり指定し たメッセージタイプのメッセージが見付からなかったりした時には、受信できるようにな るまでスリープ状態に入るが、IPC_NOWAIT を指定しておくと、エラーとして即座に帰って 来るようになる。このとき、error.h で定義される大域変数 errno に ENOMSG が設定され るので、通常のエラーと区別することは可能である。 ● 使用例 メッセージデータ構造体が struct msgdata { long mtype; char data[8]; } msg; と定義されている時、 if ( msgrcv( id, &msg, sizeof(char)*8, 0, 0 ) < 0 ) { perror( "msgrcv" ); exit( 1 ); } この例ではメッセージ ID id のキューから、メッセージをメッセージ構造体 msg に取り 出している。取り出したいデータは char data[8]; であるので、そのサイズ(バイト)は sizeof(char)*8 で求められる。エラーが発生した場合は、perror()関数によりエラーメッ セージを表示してから終了している。 1.2.4 msgctl:メッセージのコントロール int msgctl( int msgid, int cmd, struct msgid_ds *buf ) 機 能: メッセージ機能に関する制御を行う。 引 数: int msgid; メッセージ ID int cmd; コントロールコマンド struct msgid_ds *buf; 引数用バッファへのポインタ(struct msgid_ds は msg.h で定義されている) 戻り値: int err; 正常時には 0 が返る。エラー時には -1 が返り、errno.h で定義された大域変数 errno にエラー番号が格納さ れる。 23 メッセージ機能に関する情報の入手、設定や、メッセージ ID の解放を行なう。 msgid には、コントロールするキューのメッセージ ID を指定する。 cmd には、コントロールコマンド(IPC_STAT、IPC_SET、IPC_RMID のいずれか)を指定する。 buf は cmd により用途が異なるので、cmd の動作内容とともに次に記す。 cmd = IPC_STAT の場合: buf に、現在のキューの情報が格納される。ここでは、buf の メンバの一部を抜粋して簡単に紹介する。(詳細については man コマンドで intro(2) を参照(% man 2 intro を実行する。)のこと。) struct ipc_perm msg_perm; アクセス権 ushort msg_qnum; キュー内にあるメッセージ数 ushort msg_qbytes; キューに格納できる最大バイト数 ushort msg_lspid; msgsnd() を最後に行なった プロセスの ID ushort msg_lrpid; msgrcv() を最後に行なった プロセスの ID time_t msg_stime; msgsnd() を最後に行なった時間 time_t msg_rtime; msgrcv() を最後に行なった時間 time_t msg_ctime; 最後の変更時間 cmd = IPC_SET の場合: キューのアクセス権とキューに格納できる最大バイト数の変更を行 なう。(ただし、最大バイ数の変更はスーパーユーザしか変更できな い。) buf には、対応するメンバに設定値を格納しておく。 cmd = IPC_RMID の場合: キューを削除する。一度 msgget()により新規作成されたキューは、 呼び出しもとのプロセスが終了してもずっとシステム内に残ってい る。それが不都合な場合、このコマンドにより削除する。 ● 使用例 実際問題として、このシステムコールを使うのはメッセージ ID を削除する(cmd=IPC_RMID) 時ぐらいのものであり、引数 buf も特に覚えて使う場面も少ない。従って、このシステム コールについては 24 msgctl( id, IPC_RMID, NULL ); でメッセージ ID が id のキューを削除出来るということだけ知っていれば事足りるだろう。 1.3 練習問題 1.3.1 初級 以下のメッセージ構造体を用いて、キューに数値を1つ送るプログラム sendint と、キュ ーから数値を1つ取り出して表示するプログラム recvint の2つを作成せよ。 struct msgdata { long mtype; int data; } msg; 実行例: % sendint .... メッセージキューにメッセージ構造体を送信する(数値は 900 だと する) % recvint .... メッセージキューからメッセージ構造体を受信し、数値を表示する 900 .... 結果 % 作成条件: ・ msgget() に用いるキー値(key)には自分のユーザーID を用いる。 ・ mtype = 1 で固定とする。 方針: プログラムの流れは以下のようになる。 ・sendint 1. msgget()でキューを入手する。キー値には getuid() を使う。 2. メッセージデータ構造体に値を格納する。 ・msg.mtype ← 1 ・msg.data ← 数値 3. msgsnd()でキューにメッセージを送信する。 4. 終了。 ・rcevint 1. msgget()でキューを入手する。キー値には getuid() を使う。 25 2. msgrcv()でキューからデータを受信する。 3. 数値を表示する。 4. 終了。 1.3.2 中級 メッセージを受け取ったらその階乗を計算して返すプログラム server と、その server を 使って引数の階乗を計算し、結果を受け取って表示するプログラム client を作成せよ。 実行例: % server & .... server を常駐(バックグラウンドで動作)させる [1] 5198 .... ジョブ番号[1]で常駐した % client 5 .... client を実行し、5 の階乗を server に計算させ 120 .... 答え % kill %1 .... server(ジョブ番号[1])の常駐を解除 る [1] Terminated server % 作成条件: ・msgget() に用いるキー値(key)には自分のユーザーID を用いる。 ・mtype には以下の値を用いる。 1. client → server への送信の場合、mtype = 1 2. server → client への送信の場合、mtype = client のプロセス ID (プロセス ID の 入手には getpid()関数を使用する。) 方針: キューへ送信すべきデータは以下の通りである。 ・ client → server への送信の場合、計算する値と client のプロセス ID ・ server → client への送信の場合、計算した結果 1.3.3 上級 チャット(複数人が同時に会話を行う)を行うための、サーバプログラム chatserver と クライアントプログラム chat を作成せよ。 仕様: chat ・ 「% chat <chatserver のキー値>」で chatserver に参加申請する。このとき、指 定したキー値の chatserver が常駐していない場合はその旨を告げて終了する。 ・ chatserver のキー値は chatserver を起動する人のユーザーID ( i9301 の人なら 26 9301 ) を使用する。 ・ 以降、キーボードから文字列が入力されれば、それを chatserver へ送信する。 ・ また、chatserver から文字列が送られてくれば、それを表示する。 ・ 入力された文字列の先頭がピリオド(`.')であれば、chatserver に退室申請し、終 了する。 chatserver ・ chat からの参加申請が来れば、それを参加リストに加える。 ・ chat から文字列が送られて来れば、それを参加リストに登録されている全 chat へ 送 信する。 ・ chat から退室申請が来れば、それを参加リストから削除する。 方針: chat ではキーボードとメッセージキューからの2つの入力を受け付けなければならな い。が、gets() や scanf() などで標準入力から読み込もうとすると、文字がそろうまで(改 行キーが押されるまで)ブロックされてしまう。これでは、入力が完了するまでキューを読 みに行くことが出来ない(メッセージ機能における \verb=IPC_NOWAIT= 相当の機能を標準 入力に指定することは不可能ではないが、面倒かつ複雑である)。そこで、標準入力からす ぐにデータを受け取れる状態か否かを調べる関数 canread() を組み込むことで解決を計 る。 canread() のソースを以下に示す。 #include <sys/types.h> #include <sys/time.h> /* 標準入力が読み込み可能状態かどうか確認し、 不可能なら 0、可能なら 0 以外の値を返す。エラーは -1。*/ int canread( void ) { fd_set readfds; struct timeval timeout; FD_ZERO(&readfds); FD_SET(0,&readfds); timeout.tv_sec = timeout.tv_usec = 0L; return select( 1, &readfds, NULL, NULL, &timeout ); } 27 2. 共有メモリ IPC(Interprocess Comunication とはプロセス間でデータの送受信を行う機能のことであ り、メッセージ、パイプ、セマフォ、共有メモリ等様々な形式のものが提供されている。 ここではそのうちの「共有メモリ」について説明する。 2.1 共有メモリ メッセージ機能においては、キューという FIFO バッファのおかげでデータが正しく格納さ れ、また msgsnd(),msgrcv()という関数のおかげでデータを完全な形で送受信できた。共 有メモリ機能はこんなに親切なものではなく、「バッファは作成した。その先頭アドレスを 配るから、あとはプロセス同士で勝手にやってくれ」という機能である。バッファは当然 FIFO のような高機能なものではなく、ただの連続したメモリ空間である(malloc()関数で 作成されるものと同じ)。 2.1.1 共有メモリの特徴 共有メモリはポインタ参照(*ptr など)でアクセスするので、バッファの先頭アドレスさえ 受け取ってしまえばあとは普通のポインタ型変数と同様に扱うことが可能になり、扱いが 容易である。また、データを転送する必要がなくそれゆえ送受信用の関数もないので、ア クセスが極めて高速である。 半面、同一のバッファを扱うプロセスがたくさんあると、どのプロセスがいつどのよう にバッファに読み書きを行うか予測できない上、プロセス側にとっては、いつデータがそ ろうのか、いつデータを書き込んでいいのか(プロセス間の同期のタイミング)などを判断 することができない。 従って、共有メモリを使用するに当たっては、各プロセス間であらかじめデータのアク セスに関する協定を結んでおかなければならない。(共有メモリの最初の1バイトをフラグ と見なして、0 なら読み込み可、0 以外なら書き込み中であるとする、など。) 2.2 システムコール 共有メモリを扱うシステムコールには、以下の4つがある。 shmget() 共有メモリの割り付け(共有メモリの作成) shmat() 共有メモリのアタッチ(先頭アドレスの入手) shmdt() 共有メモリのデタッチ(使用権放棄) shmctl() 共有メモリのコントロール(制御いろいろ) また、これらの関数の使用に先立って以下のヘッダファイルを include する必要がある。 #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> 28 具体的には、以下のような手順になる。 ・ 共有メモリを確保する。(shmget()) (実際には、共有メモリに対応した共有メモリ ID というものを受け取る。) ・ その先頭アドレスを入手する。(shmat()) ・ アクセスする。 ・ 使用権を放棄する。(shmdt()) ・ もうそれ以上どのプロセスも使わないなら、共有メモリを解放する。(shmctl())) 2.2.1 shmget:共有メモリの割り付け int shmget( key_t key, int size, int shmflg ) 機 能: 共有メモリを確保し、その ID を返す。 引 数: key_t key; 共有メモリを割り当てるキー値 int size; 確保するデータサイズ int shmflg; 共有メモリの割り付け方の指定 shmid; 共有メモリ ID 戻り値: int 割 り 当 て 失 敗 時 に は -1 が 返 り 、 error.h で 定 義 さ れ る 大 域 変 数 errno にエラー番号が格納される。 key にはキーを整数で指定する。これもメッセージ機能と同じくキー値を幾つにするかは 自由だが、重複を避けるために、誰も使わないような値(自分のユーザーID など)にしてお くのがよい。(1つのプロセスだけで共有メモリを使用したい場合には特別に、キーとして IPC_PRIVATE を指定できる。)size には、共有メモリの大きさをバイト単位で指定する。 shmflg には、「割り付けの方法」と「パーミッション」の2種類の値の論理和(OR)を設定 する。 「割り付けの方法」としては、以下の3種類を選択出来る。 IPC_ALLOC 既に共有 メモリが存在する場合 はその ID を、存在し なければエラー (-1)を返す。 IPC_CREAT 既に共有メモリが存在する場合はその ID を、存在しなければ新規に共 有メモリを作成しその ID を返す。 IPC_CREAT │ IPC_EXCL 既に共有メモリが存在する場合はエラー(-1)を返し、存在しなければ新 規に共有メモリを作成しその ID を返す。 29 「 パ ー ミ ッ シ ョ ン 」 の 値 に つ い て は 、 shmflg の 下 位 9 ビ ッ ト を 使 っ て 、 共 有 メ モ リ の permission を指定する。 8 R 7 W 6 5 R − user 4 W 3 − group 2 1 R W 0 ビット − other R/W は、対応するビットを 1 にすると read/write が可能になる。実行ビットがない他は、 だいたい UNIX のファイルシステムと同じである。 ● 使用例 例えば struct bufffer { int flag; char data[10]; } *sm; と定義されているとき、 if ( (id = shmget( getuid(), sizeof(struct buffer), IPC_CREAT │ 0666 )) < 0 ) { perror( "shmget" ); exit( 1 ); } により、構造体 struct buffer のサイズ分(int\ 1 個+char\ 10 個分)の共有メモリが確 保される。キー値として、getuid()の戻り値(自分のユーザーID)を用いている。エラーが 発生した場合は、perror()関数によりエラーメッセージを表示してから終了している。 なお、perror() を使用するプログラムには errno.h も忘れず #include しておくこと。 2.2.2 shmat:先頭アドレスの入手 char *shmat( int shmid, char *shmaddr, int shmflg ) 機 能: 指定した ID の共有メモリの先頭アドレスを返す。 引 数: int shmid; 共有メモリ ID char *shmaddr; 割り付けセグメントアドレス int shmflg; オプションの設定 戻り値: 30 char *address; 共有メモリの先頭アドレス エラー時には -1 が返り、errno.h で定義される大域変数 errno に エラー番号が格納される。 shmid には、共有メモリの ID を指定する。shmaddr には、通常 0 を指定しておく。(0 以外 を指定した場合については、ここでは触れない。)shmflg には通常 0 を指定するが、共有 メモリに書き込みを行わないつもりな SHM_RDONLY を指定できる。(0 を指定した場合は読 み書き共に可能。) ● 使用例 2.2.1 で定義されたポインタ型変数 sm があるとして、 if ( (sm = shmat(id, 0, 0)) < 0 ) { perror( "shmat" ); exit( 1 ); } とすると、sm に id に対応する共有メモリの先頭アドレスが格納される。エラーが発生し た場合は、perror()関数によりエラーメッセージを表示してから終了している。 これ以降は sm を使って、 sm->flag = 1; sm->data[0] = '\0'; のようにして共有メモリをアクセスすることが出来る(sm->flag は共有メモリにアクセス するための排他制御に使用することを前提としている。2.3.2 の中級問題を参照のこと。 2.2.3 shmdt:共有メモリの使用権の放棄 int shmdt( char *shmaddr ) 機 能: 共有メモリの使用権を放棄する。 引 数: char *shmaddr; 共有メモリの先頭アドレス 戻り値: int err; 正常時には 0 が返る。エラー時には -1 が返り、errno.h で定義さ れた大域変数 errno にエラー番号が格納される。 共有メモリを使わなくなった時には、この関数により使用権を放棄しておく。(使用権を放 31 棄したのちにアクセスすると不当なアクセスと見なされ、システムによりそれなりの処置 がとられることになる。)shmaddr には、共有メモリの先頭アドレス(shmat()で得たもの) を指定する。 ● 使用例 if ( shmdt( sm ) < 0 ) { perror( "shmdt" ); exit( 1 ); } sm には、共有メモリの先頭アドレスが入っているものとする。エラーが発生した場合は、 perror()関数によりエラーメッセージを表示してから終了している。 2.2.4 shmctl:共有メモリのコントロール int shmctl( int shmid, int cmd, struct shmid_ds *buf ) 機 能: 共有メモリに関する制御を行なう。 引 数: int shmid; int cmd; 共有メモリ ID コントロールコマンド struct shmid_ds *buf; 引数用バッファへのポインタ (struct shmid_ds は shm.h で定義されている) 戻り値: int err; 正常時には 0 が返る。エラー時には -1 が返り、errno.h で定義 された大域変数 errno にエラー番号が格納される。 共有メモリに関する情報の入手、設定や、共有メモリの解放を行なう。shmid には、コン トロールする共有メモリの ID を指定する。cmd には、コントロールコマンド(IPC_STAT、 IPC_SET、IPC_RMID のいずれか)を指定する。buf は cmd により用途が異なるので、cmd の 動作内容とともに次に記す。 cmd = IPC_STAT の場合: buf に、現在の共有メモリの情報が格納される。ここでは、buf のメンバの一部を抜粋して 簡単に紹介する。(詳細については man コマンドで intro(2) を参照('% man 2 intro' を 実行)のこと。) struct ipc_perm shm_perm; アクセス権 32 int shm_segs2; セグメントのサイズ ushort shm_cpid; 共有メモリを作成したプロセスの ID ushort shm_lpid; 共有メモリを最後にアクセスしたプロセスの ID short shm_nattch; 現在共有メモリを使用しているプロセス数 time_t shm_atime; shmat() を最後に行なった時間 time_t shm_dtime; shmdt() を最後に行なった時間 time_t shm_ctime; 最後の変更時間 cmd = IPC_SET の場合: 共有メモリのアクセス権の変更を行なう。buf 内の以下のメンバに値を設定する。 shm_perm.uid ユーザーID shm_perm.gid グループ ID shm_perm.mode パーミッションモード cmd = IPC_RMID の場合: 共有メモリを解放する。一度 shmget() により新規割り付けされた共有メモリ は、呼び出し元のプロセスが終了してもずっとシステム内に残っている。それ が不都合な場合、このコマンドにより削除する。 ● 使用例 メッセージ機能と同様、このシステムコールも共有メモリの削除(cmd=IPC_RMID)を最も頻 繁に使用する。 shmctl( id, IPC_RMID, NULL ); で共有メモリ ID が id の共有メモリを削除できる。 2.3 練習問題 2.3.1 初級 int サイズ分の共有メモリに数値を書き込むプログラム writeint と、その共有メモリか ら数値を読みだして表示するプログラム readint を作成せよ。 実行例: % writeint .... 共有メモリに数値(ここでは 1000 だとする)を書き 込む % readint .... 共有メモリから数値を読みとり、表示する 33 1000 .... 結果 % 作成条件: shmget() に用いるキー値(key)には自分のユーザーID を用いる。 方針: ・ 共有メモリは int サイズなので、共有メモリ ID は shmget( getuid(), sizeof(int), IPC_CREAT │ 0666 ); のようにして入手する。 2.3.2 中級 標準入力から入力された文字列を共有メモリに書き込むプログラム writestr と、共有メ モリから文字列を受け取り標準出力に表示するプログラム readstr を作成せよ。 仕様: ・ writestr 1. 標準入力から文字列を読み込む。 2. それを共有メモリに書き込む。ただし、まだ readstr が以前の文字列を表示し終え てないようなら待つ。 3. 1.から繰り返す。 ・ readstr 1. writestr によって文字列が更新されるまで待つ。 2. 更新されれば、それを標準出力に表示する。 3. 1.から繰り返す。 作成条件と注意: ・ shmget() に用いるキー値(key)には自分のユーザーID を用いること。 ・ readstr と writestr は、X-WINDOW 上に2つ以上のウィンドウを開き、それぞれ 別のウィンドウで実行すること。 ・ readstr よりも writestr の方を先に実行開始すること。(writer が最初に共有メ モリを作成し、初期化する。) 方針: readstr は writestr が文字列を更新したことを、また、writestr は readstr がその文 字列の表示が完了したことを確認するまで待たねばならない。 このような同期をとるため、共有メモリには次のような構造体を割り当てる必要がある。 struct { 34 int canread; 同期用フラグ char data[256]; 文字列格納バッファ } *sm; 同期用フラグ canread は、以下のように用いる。 ・ writestr は、共有メモリを作成した後(shmget() の後)、canread を 0 に初期化する。 (readstr では初期化しない。) ・ writestr は、canread が 0 の時のみ共有メモリに書き込みを行う( 0 でない場合は 0 になるまで待つ)。書き込んだ後には、 canread を 1 にする。 ・ readstr は、canread が 1 の時のみ共有メモリ内の文字列の表示を行なう( 1 でない 場合は 1 になるまで待つ)。表示した後には、canread を 0 にする。 2.3.3 上級 以下の仕様の多人数参加型ゲーム (特に名前はない) を作成せよ。 仕様: ・ 10 × 10 マスの空間内に、プレイヤーの数だけ戦車が存在する(一人一台)。 他プレイヤーの戦車を駆逐し、最後まで生き残ったプレイヤーの勝利とする。 ・ 各戦車の操作は、コマンド(文字)入力により行なう。 f - 前方に 1 マス移動(foward) b - 後方に 1 マス移動(backard) l - 90°左へ回転(left-turn) r - 90°右へ回転(right-turn) a - 攻撃(attack) 自機の前方5マス内に敵機がいれば、敵機を破壊したことになる。 s - 敵機検索(search) 自機を中心にして 5 × 5 マスの範囲に敵機がいれば、その相対座標を表示 する。 方針: C 言語の標準入出力関数は改行キーが入力されるまでバッファリングされるため、コマン ドを1つ打つ度に改行キーを押すことになる。これがうざったいと感じる人は、相当の余 力があれば、標準入力のバッファリングを行なわないようにする方法を探ってみるのもい い。 35 3. セマフォ IPC(Interprocess Comunication)とはプロセス間でデータの送受信を行う機能のことであ り、メッセージ、パイプ、セマフォ、共有メモリ等様々な形式のものが提供されている。 ここではそのうちの「セマフォ」について説明する。 3.1 セマフォ セマフォは主に、以下の目的で使用する機能である。 ・ 複数のプロセス間で同期をとる。 ・ 複数のプロセスで共有するリソース(共有メモリなど)の排他制御(簡単に言えば、同時 に変更しないこと。)を行う。 3.1.1 特殊変数セマフォ セマフォは、以下のような特殊な特徴を持った、整数型の変数と思っていい。 ・ 加算・減算しか出来ない。 ・ 識別子(セマフォ ID)を知っていれば、どのプロセスからもアクセスが可能。 ・ 減算して負の値にしようとしたプロセスはスリープ状態に落とされる。 最後の特徴は特に重要で、0 から 1 を引いたり 2 から 3 を引いたりしてセマフォ値を負に しようとしたプロセスは、それを行う前にシステムによってスリープ状態に陥 れら れる 。 そして、一度スリープ状態に入ったプロセスは、他のプロセスがそのセマフォに加算を行 なうまで目覚めない(セ マフォが削除されても目覚める。なお、スリープ中のシグナルは キャッチ可能。)。 目覚めたプロセスたちは再び減算を試み(プロセスが複数個目覚めた場合、減算を試みる 順序はシステムで決定される。)、まだ負になるようであればまたスリープ状態に陥れられ る。負にしようとしたプロセスはすぐさまスリープ状態に落とされるので、セマフォ値は 決して負になることはない。(最低でも0を保持する。) 3.1.1 セマフォによる同期と排他処理の方法 セマフォ関係のシステムコールの説明に入る前に、このセマフォという変数があればどの ように同期や排他処理を行われるかを説明しておく。 ● セマフォによる同期 セマフォを用いれば、メッセージ機能の msgsnd()や msgrcv()が提供していた 「データがない場合はデータが来るまで待つ(ブロックする)」といった機能が簡単に実現 できる。 ブロック付きアクセスの実現法: ・ セマフォにデータの残り個数を格納することにしておき、データを格納する際には「格 36 納後に」その個数だけセマフォ値を加算し、プロセスがデータを取り出す際には「取り出 す前に」その個数だけセマフォ値を減算するようにする。 ・ そうすれば、セマフォ値(残りデータ個数)が0の時に取り出そうとすればそのプロセス はスリープ状態に入り、他のプロセスによってセマフォ値が加算される(データが格納され る)まで目覚めない。 ・ 目覚めたプロセスは、自分が眠らされたことすら知らずに処理を再開する。(データが あるか否かを自分で判定して待つ、といった部分をプログラムする必要がなくなる。) セマフォの「他のプロセスを起こすことができる」という特徴に着目すれば、上記の例の 他にも様々な同期プログラムを作成することができるだろう。 「2. 共有メモリ」の中級 問題なども、ビジーウェイトにより CPU 時間を消費せず (待ちループ中は CPU を使用す る(実行を続ける)が、スリープ中は CPU を使用しない(実行を中断する)。) に、割り込み 駆動で書き直すことができる。 ● セマフォによる排他処理 プロセスが(たとえ見かけ上でも)並列に動作する以上、1つのデータを複数のプロセスが アクセスする場合が生じる。ここで、1つのデータを複数のプロセスが「同時」に変更し ようとした時、データの一貫性がとれなくなってしまう危険性がある。(あるプロセスが読 み込み中のファイルに、別のプロセスが書き出しを行ってしまう、など。) 排他処理とは、ひとつのプロセスがデータを使用中の間、他のプロセスを受け付けなく するようにする機能である。 セマフォを用いると、以下のように排他制御を行うことが出来る。 ・ セマフォには、初期値として 1 を入れておく。 ・ データをアクセスしようとするプロセスは、アクセス前にはセマフォを 1 減らし (ロックするという)、アクセス後にはセマフォを 1 増やす(アンロックするという) ことにする。 ・ こうすれば、誰もデータを使ってなければセマフォ値は 1 減らしても 0 になるだ けなのでスリープ状態にはならず、また既にアクセス中のプロセスがあればセマフ ォが負になってスリープ状態に落とされる。 3.2 システムコール セマフォを扱うシステムコールには、以下の3つがある。 semget() セマフォ ID の割り付け(セマフォの獲得) semop() セマフォの加減算 semctl() セマフォのコントロール(制御いろいろ) また、これらの関数の使用に先立って以下のヘッダファイルを include する必要がある。 37 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> 具体的には、セマフォを扱う手順は以下のようになる。 ・ セマフォを入手する。(semget())\\ (実際には、セマフォに対応したセマフォ ID というものを受け取る。) ・ セマフォに加減算を行う。(semop()) ・ もうそれ以上使わないのなら、セマフォを削除する。(semctl())) 3.2.1 semget:セマフォ ID の割り付け int semget( key_t key, int nsems, int semflg ) 機 能: セマフォ ID を割り付ける。 引 数: key_t key; セマフォ ID を割り当てるキー int nsems; セマフォの個数 int semflg; セマフォ ID の割り付け方の指定 semid; セマフォ ID 戻り値: int 割り当 て失敗 時には -1 が返り 、errno.h で定 義され る大域 変数 errno にエラー番号が格納される。 key にはキーを整数で指定する。メッセージ機能と同じくキー値を幾つにするかは自由だ が、重複を避けるために、誰も使わないような値(自分のユーザーID など)にしておくのが よい。(1つのプロセスだけでセマフォを使用したい場合には特別に、キーとして IPC_PRIVATE を指定できる。)nsems には、セマフォの個数を指定する。(これにより、1 つだけではなく複数個まとめて作成することができる。) semflg には、「割り付けの方 法」と「パーミッション」の2種類の値の論理和(OR)を設定する。 「割り付けの方法」としては、以下の3種類を選択出来る。 IPC_ALLOC 既にセマフォが存在する場合はその ID を、存在しなければエラー(-1)を返 す。 IPC_CREAT 既にセマフォが存在する場合はその ID を、存在しなければ新規にセマフォ を作成しその ID を返す。 IPC_CREAT │ IPC_EXCL 既にセマフォが存在する場合はエラー(-1)を返し、存在しなければ新規にセマ フォを作成しその ID を返す。 38 「 パ ー ミ ッ シ ョ ン 」 の 値 に つ い て は 、 semflg の 下 位 9 ビ ッ ト を 使 っ て 、 セ マ フ ォ の permission を指定する。 8 7 R W 6 5 4 − R W user 3 2 − R group 1 W 0 ビット − other R/W は、対応するビットを 1 にすると read/write が可能になる。実行ビットがない他は、 だいたい UNIX のファイルシステムと同じである。 ● 使用例 if ( (id = semget( getuid(), 2, IPC_CREAT │ 0666 )) < 0 ) { perror( "semget" ); exit( 1 ); } この例では、getuid()の戻り値(自分のユーザーID)をキー値としてセマフォを2つ作成し ている。エラーが発生した場合は、perror()関数によりエラーメッセージを表示してから 終了している。なお、perror() を使用するプログラムには errno.h も忘れず #include し ておくこと。 3.2.2 semop:セマフォの操作 int semop( int semid, struct sembuf *sops, int nops ) 機 能: セマフォの操作(加減算)を行う。 引 数: int semid; セマフォ ID struct sembuf *sops; int セマフォ操作指示配列 nops; 操作数 err; 正常時には 0 が返る。エラー時には -1 が返り、errno.h で定義さ 戻り値: int れた大域変数 errno にエラー番号が格納される。 semid には、セマフォ ID を指定する。 sops には、セマフォ操作指示配列名を指定する。 nops には、配列 sops の個数を指定する。 struct sembuf は sem.h で以下のように定義されている。 39 struct sembuf { ushort sem_num; 操作対象のセマフォ番号 short sem_op; オペレーション値 short sem_flg; セマフォフラグ }; ・ 操作対象のセマフォ番号(sem_num) 操作対象のセマフォ番号を指定する。セマフォ番号は、0∼(semget()で作成した セマフォ数)-1 までである。 ・ オペレーション値(sem_op) 1. sem_op <> 0 の場合: セマフォ値に sem_op を加える。加えた結果、セマフォ値が負ならロック、 正ならアンロックということになる。 2. sem_op = 0 の場合: セマフォ値が{\bf 0 以外}ならスリープ状態に陥る。ここで注意することは、 ロックによるスリープならセマフォが加算されることにより目覚めるが、この コマンド でスリー プし た場合はセ マ フ ォが 0 に なる まで目 覚め ないというこ とである。 ・ セマフォフラグ(sem_flg) 通常は 0 を指定する。IPC_NOWAIT を指定すると減算によりセマフォ値が負になった としても、スリープ状態に陥る事なく返って来る。この場合 semop() は -1 を返し、 errno.h で定義される大域変数 errno に EAGAIN が設定される。 ● 使用例 struct sembuf sops[2]; sops[0].sem_num = 0; /* セマフォ[0] */ sops[0].sem_op = -1; /* 減算する */ sops[0].sem_flg = 0; sops[1].sem_num = 1; /* セマフォ[1] */ sops[1].sem_op = 1; /* 加算する sops[1].sem_flg = 0; if ( semop( id, sops, 2 ) < 0 ) { perror( "semop" ); exit( 1 ); } 40 */ この例では、セマフォを2つ持つ id に対し、1番目のセマフォを減算し、2番目のセマ フォを加算している。 3.2.3 semctl:セマフォのコントロール int semctl( int semid, int semnum, int cmd, arg) 機 能: セマフォに関する制御を行う。 引 数: int semid; セマフォ ID int semnum; 操作対象のセマフォ番号 int cmd; コントロールコマンド arg; 引数(型はコントロールコマンドにより異なる) 戻り値: int rts; 正 常 時 に は 0 ま たは 戻 り 値 が 返 る 。 エ ラ ー 時 に は -1 が 返 り、 errno.h で定義された大域変数 errno にエラー番号が格納される。 semid には、セマフォ ID を指定する。 semnum には、操作対象のセマフォ番号を指定する。セマフォ番号は 0∼(semget()で作成 したセマフォ数)-1 までである。 cmd にはコントロールコマンドを指定する。また、semctl() の第4引数は、cmd の値によ って変わって来るので cmd と共に随時説明する。 以下、コントロールコマンドとその説明。 ・GETVAL 現在のセマフォ値を返す。 例) semafo_value = semctl( id, 0, GETVAL, 0 ); ・SETVAL セマフォ値をセットする。(arg はセットしたい値のアドレス) 例) セマフォ値を 100 にしたい場合: i = 100; semctl( id, 0, SETVAL, &i ); ・GETPID セマフォを最後に操作したプロセスの ID を返す。 例) lastpid = semctl( id, 0, GETPID, 0 ); ・GETNCNT 現在スリープに入っているプロセスの数を返す。(sem_op = 0 によるスリープは 数えない) 例) ncnt = semctl( id, 0, GETNCNT, 0 ); ・GETZCNT 41 sem_op = 0 によりスリープに入っているプロセスの数を返す。 例) zcnt = semctl( id, 0, GETZCNT, 0 ); ・GETALL arg を unsigned short int 配列(要素数はセマフォの個数と同じ)の名前と見な して, その中に現在のセマフォ値を順に格納する。 例) semget() によりセマフォを3つ作成していた場合、 semctl( id, 0, GETALL, arg); により arg[0], arg[1], arg[2]にそれぞれセマフォ値が格納される。 ・SETALL unsigned short int 配列 arg (要素数はセマフォの個数と同じ)に格納されてい る値を、セマフォ値に順次設定する。 例) semget() によりセマフォを3つ作成していた場合、 arg[0] = arg[1] = arg[2] = 1; semctl( id, 0, SETALL, arg); によりセマフォ値を3つとも 1 に初期化できる。 ・IPC_STAT arg を struct semid_ds 変数へのポインタと見なして、セマフォに関する情報 を入手する。(ここでは説明は省略するので、詳細を知りたければ man コマンド の intro(2) を参照のこと。) ・IPC_SET arg を struct semid_ds 変数へのポインタと見なして、セマフォに関する情報 を設定する。(同上) ・IPC_RMID セマフォ ID を解放する。 例) semctl( id, 0, IPC_RMID, 0 ); 3.3 練習問題 3.3.1 初級 実行するとセマフォによりスリープに入るプログラム lock と、それを目覚めさせるプロ グラム unlock を作成せよ。 作成条件と実行上の注意: ・ semget() に用いるキー値(key)には自分のユーザーID を用いること。 ・ lock と unlock は、X-WINDOW 上に2つ以上のウィンドウを開き、それぞれ別のウ ィンドウで実行すること。 方針: 1. lock ・ セマフォをひとつ確保し、セマフォ値を 0 に初期化する。 ・ セマフォ値から 1 引いてスリープに入る。 42 ・ 目が醒めたらセマフォを解放する。 ・ 終了。 2. unlock ・ セマフォを入手する。初期化はしない。 ・ セマフォ値に 1 加算する。(lock が目覚める。) ・ 終了。解放はしない。 3.3.2 中級 4つのプロセスの同期をとる(4つ同時に終了する)プログラム syncstart を作成せよ。 方針: 1. セマフォを入手した後、既にスリープ状態のプロセスの数を調べる。 (semctl() の GETZCNT を使う。) 2. スリープ数が 0 の場合: ・ 自分が一番のりであることになるので、セマフォ値を 1 に初期化する。 ・ semop() で sem_op = 0 の指定を行い、自らスリープ状態に入る。 ・ 起こされたらそのまま終了する。 3. スリープ数が 1∼2 の場合: ・ semop() で sem_op = 0 の指定を行い、自らスリープ状態に入る。 ・ 起こされたらそのまま終了する。 4. スリープ数が 3 の場合: ・ 自分が最後(4つ目)であることになるので、セマフォ値を 0 にする。 (眠っているプロセスが全部目覚める。) ・ セマフォを解放する。 ・ 終了する。 作成条件と実行上の注意: ・ semget() に用いるキー値(key)には自分のユーザーID を用いること。 ・ プログラムは、X-WINDOW 上に4つ以上のウィンドウを開き、それぞれ別のウィン ドウで実行すること。 3.3.3 上級 中級の syncstart を、同期プロセス数を動的に決められるように改造せよ。 仕様: ・ 引数なしで実行された場合は、スリープに入る。 ・ 何か引数付きで実行すると、スリープ状態にあるすべての syncstart が目覚める。 作成条件: ・ semget() に用いるキー値(key)には自分のユーザーID を用いること。 43 スレッドとマルチスレッドプログラミング 1 スレッドとは? スレッドとは、ひとまとまりの実行単位のことを言います。従来の UNIX のプロセスでは、それ自身1つのス レッドとして、実行されます。(図 1 の左)。マルチスレッド化されたプロセスは、複数のスレッドを持ち、複数の 命令を並行して実行することができます。(図 1 の右)。実際、ics 上でマルチスレッドのプロセスを実行する場合、 ics は 12 個の CPU の各々に実行コアが 2 個搭載されていますので、合計 24 個までののスレッドが同時並列に実 行することが可能です。 main !"#$% &' () main &' () &' () + &' ***** , () . / 図 1: 従来のプロセスとマルチスレッドのプロセス 2 スレッドをつくる システムコール fork() によって子プロセスを作り、複数のプロセスを同時に実行しました。スレッドをつくる のも同じように、thr create() というスレッドライブラリの関数を使います。新しいスレッドを作成したスレッド を親スレッドといいます。基本的なスレッドプログラムは、次のような手順で作成します。 1. ヘッダファイルに <thread.h> を指定します。 2. スレッド ID を thread_t 型で宣言します。 3. 新しいスレッドとして実行される関数を宣言します。 4. thr_create() でスレッドを作成します。 3番目の引数で指定された関数が新しいスレッドとして実行されます。 5. 親スレッドが子スレッドの終了を待ち合わせるためには、thr_join() を使います。 (親プロセスが子プロセスの終了の待ち合わせに使う wait() システムコールに相当します) 6. スレッドをして実行される関数を定義します。 7. スレッド間で同期をとる必要がある場合には、同期機構を使って同期を取ります。 8. スレッドの終了には、thr_exit() を使います。 44 1 1. の thread.h はスレッドライブラリを使うときには必ず書かなくてはいけません。 2. のスレッド ID とは、スレッドを識別するために、一意のに割り当てられる ID のことです。 7. の同期機構に関しては、後で詳しく述べるため、ここでは説明しません。 では、以下のプログラムを見てみましょう。 #include <stdio.h> #include <thread.h> main() { thread_t void talker_id,listner_id; *talker(),*listner(); thr_create(NULL,NULL,talker,NULL,NULL,&talker_id); thr_create(NULL,NULL,listner,NULL,NULL,&listner_id); thr_join(talker_id,NULL,NULL); thr_join(listner_id,NULL,NULL); } void { /* *talker() スレッド talker の処理 */ thr_exit(NULL); } void { /* *listner() スレッド listner の処理 */ thr_exit(NULL); } このプログラムでは、talker と listner というスレッドを作っています。thr create() の3番目の引数で、スレッ ドとして実行する関数をしています。6番目の引数で指定した変数に作成したスレッドのスレッド ID が格納され ます。thr join() では、1番目の引数で指定したスレッド ID のスレッドが終了するまで待ち状態となります。ス レッドの終了には thr exit() を使用します。 ここで注意したいことが1つあります。スレッドの生成というのはプロセスにおいて子プロセスを生成するのと は少し (かなり?) 違います。プロセスでは、子プロセスを生成したとき,親プロセスが持っていた情報 (プロセ スコンテキスト)を全て引き継ぎますが、スレッドでは関数として実行するため、親スレッドの情報を一切引き 継がないということです。したがって,コンテキストスイッチのような時間のかかる重い処理は必要なく,スレッ ドはしばしば軽量プロセス(LPW:Light Weight Process) と呼ばれます。 2.1 スレッドライブラリ ここで、主なスレッドライブラリ関数をリストしておきます。 45 関数名 機能 thr create() スレッドの作成と実行 thr exit() スレッドの終了 thr self() スレッド識別子の検索 thr join() スレッドの終了の待機 thr suspend() スレッドの実行中断 thr continue() スレッドの実行再開 thr setconcurrency() 多重レベル (LWP の数) の設定 thr getconcurrency() 多重レベル (LWP の数) の獲得 thr setprio() スレッドの優先度の設定 thr getprio() スレッドの優先度の獲得 thr yield() 実行権の譲渡 thr kill() スレッドへのシグナルの送信 以下、良く使用するスレッドライブラリ関数について説明します。 thr creat(): スレッドの作成と制御 int thr create()(void *stack base, size t stack size, void *(*start routine)(void *), void *arg, long flags, thread t *new thread) stack_base スレッドが使用するスタックの先頭アドレス NULLL が指定された場合、stack_size で指定された 大きさのスタックが割り当てられる。 stack_size スタックの大きさのバイト数 0が指定されるとデフォルトサイズが割り当てられる。 start_routine arg flags スレッドとして実行される関数名 start_routine に渡される引数のアドレス THR_SUSPENDED :停止状態のスレッドの作成 THR_DETACHED thr_continue() で起動されるまで実行されない :切り離されたスレッドの作成 スレッド ID などのリソースは、スレッド終了後 すぐに再使用可能となる。thr_join() でこの スレッドの終了を待つことはできない。 new_thread 戻り値 作成したスレッドのスレッド ID が格納される変数 正常終了:0 エラー :0 以外 thr exit():スレッドの終了と終了ステータスの設定 void thr exit(void *status) status スレッドの終了ステータス thr_join() で待ち状態のスレッドに渡される。 戻り値 なし thr join():スレッド終了の待ち合わせ int thr join(thread t wait for,thread t *departed,void **status) wait_for departed 待ち合わせ対象となるスレッド ID 終了したスレッド ID 46 status 終了したスレッドのステータス 戻り値 正常終了:0 エラー :0 以外 thr self():スレッド ID の取得 thread t thr self(void) 戻り値 呼び出したスレッドのスレッド ID thr suspend():スレッドの中断 int thr suspend(thread t target thread); target\_thread 中断するスレッドの ID 戻り値 正常終了:0 エラー :0 以外 thr continue():中断状態スレッドの再開 int thr continue(thread t target thread); target_thread 実行を再開するスレッドの ID 戻り値 正常終了:0 エラー :0 以外 これ以外のライブラリは、そんなに使われることはない (と、思う) ので、説明は省きます。スレッドのプログ ラムを作っていくうちに逐次勉強してください。これらの使用法に関しては、以降で説明して行きます。 スレッドプログラミング 3 さて、実際プログラミングをする上で、いくつか注意する点を説明します。 基本的なことで、プログラムを書いただけではだめで、実行してみなくてはいけません。そこで、以前にも書 きましたが、スレッドライブラリを使うのであれば、”thread.h”というヘッダファイルを include 宣言しなくては 行けません。また、これも重要なのですが、コンパイル時にオプションをつけなくてはいけません。 prog.c をコンパイルして実行形式 prog を作成するには次のようにします。 % gcc prog.c -o prog -D_REENTRANT -lthread 3.1 引数を渡す 関数に引数を渡せるように、スレッドにも引数を渡すことができます。以前に書いたように、thr create() の4 番目の引数に、作成するスレッドに渡したい引数のアドレスを記述します。ここで、疑問に思うことがあると思い ます。thr create() で渡す引数を記述する場所が一つしかないことです。複数の引数を渡したいときは、どうする のでしょうか?これには、構造体をつくり,渡したい引数の数だけ,構造体のメンバを定義し,その構造体のアド レスを引数として渡します。または、引数として渡さずにどのスレッドからでも参照できるではなくグローバル変 数を使ってスレッドにデータを渡すことも可能です。他にも対策はあると思いますが、プログラムをつくる上で、 もっとも適した方法を考えてください。 もう一つ重要なことはスレッドライブラリの thr creat() でスレッドに引数を渡す時には、引数の値ではなく、 引数のアドレスを渡す必要があると言うことです。 47 3.2 共有メモリ(グローバル変数)を使う ここで言う共有メモリとは、プロセス間の共有メモリではなく、スレッド間のの共有メモリということで一般に はグローバル変数と呼ばれているものです。スレッドプログラミングにおけるグローバル変数は、どのスレッドか らも参照・変更できる非常に便利な変数です。プロセス間で共有メモリを介して行なうのに比べて格段に扱いが 容易です。以下のプログラムを見てましょう。 #include <stdio.h> #include <thread.h> int global = 0; /* グローバル変数 */ void *func() { int i; for(i=0; i<1000000; ++i) ++global; thr_exit(NULL); } main() { thread_t thr_id; int i; thr_create(NULL,NULL,func,NULL,NULL,thr_id); for(i=0; i<1000000; ++i) ++global; thr_join(thr_id,NULL,NULL); printf("global = %d\n",global); } このプログラムは main と func のスレッドにより、グローバル変数 global を同時にインクリメントしています。 実行終了時、global の値は実行する毎に値が変わると思います。原因としては、2つ考えられます。1つはスレッ ドプログラムはシステムの状態によって振舞が異なってしまうということです。もう1つは、重要なことですが、 グローバル変数への同時アクセスを行なってしまったことです。マルチスレッドプログラミングにおいてグローバ ル変数にアクセスするコード部分は臨界領域になり得ることを注意しなければなりません。 3.3 同期制御を行なう 臨界領域を実行するにあたり、複数のスレッド間の同期を取らなくてはいけません。ここでは、スレッド間の同 期制御について解説します。スレッドの同期制御には、Mutex ロック、条件変数、セマフォ、Read/Write ロック があります。まず,簡単に Mutex ロックと条件変数について説明しましょう。 3.3.1 Mutex ロック Mutex ロックは単純なロック機構で、排他的なロックを行ないます。このロックは、メモリの使用量を押さえ、 高速な処理を行なえるため、頻繁な使用に適しています。動作としては、一方のスレッドがロックを取得している 最中にもう一方のスレッドがロックを取得しようとすると、後者のスレッドは前者のスレッドがロックが解除す るまで待ち状態となります。 先のプログラムを Mutex ロックを使って改良したものを見てみましょう。 #include <stdio.h> #include <thread.h> 48 #include <synch.h> int global = 0; /* グローバル変数 */ mutex_t lock; /* ロック変数 */ void *func() { int i; for(i=0; i<100000; ++i){ mutex_lock(&lock); /* ロックを取得 */ ++global; mutex_unlock(&lock); /* ロックを解除 */ } thr_exit(NULL); } main() { thread_t thr_id; int i; mutex_init(&lock,USYNC_THREAD,NULL); /* ロックの初期化。必ず必要 */ thr_create(NULL,NULL,func,NULL,NULL,thr_id); for(i=0; i<1000000; ++i){ mutex_lock(&lock); /* ロックを取得 */ ++global; mutex_unlock(&lock); /* ロックを解除 */ } thr_join(thr,NULL,NULL); printf("global = %d\n",global); } 3.3.2 条件変数 次に条件変数を説明しましょう。処理中に自らのスレッドをある状態になるまでスレッドの処理を止めたい場合 があったとしましょう。例えば、ある変数の値が 0 になるまでスレッドの処理を止めたいとき、次のように考え たとします。 int i; main(){ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ while(i != 0); /* i が 0 になるまでビジーウエイト */ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ } 上の例では、変数 i の値が他のスレッドによって 0 に変更されるまで while ループにより、ビジーウエイト状態に 入ります。これは、CPU 資源の無駄使いです。また変数 i はグローバル変数なので当然ロック処理も必要になり ます。この2つの問題を解決する方法として、条件変数があります。条件変数を使ったプログラム例を見てみま しょう。 #include <thread.h> #include <synch.h> int i = 100; 49 mutex_t lock; cond_t cond; /* Mutex ロック変数 */ /* 条件変数 */ void *func() { mutex_lock(&lock); i=0; cond_signal(&cond); mutex_unlock(&lock); thr_exit(NULL); } main() { thread_t thr_id; void *func(); mutex_init(&lock,USYNC_THREAD,NULL); cond_init(&cond,USYNC_THREAD,NULL); thr_create(NULL,NULL,func,NULL,NULL,thr_id); mutex_lock(&lock); while(i != 0) cond_wait(&cond,&lock); mutex_unlock(&lock); thr_join(thr_id,NULL,NULL); } 上のプログラムは main 関数が変数 i が 0 になるまで待ち状態になり、他のスレッド (func) によって i が 0 に変更 されたら待ち状態を解除するというプログラムです。待ち状態になるのは、main 関数の cond wait() という関数 の呼び出しによりますが、実はこの関数は Mutex ロックの入れ子の形で使用しなければならず,単独で使用する ことは出来ません。ここで、Mutex ロックによりロックしたまま待ち状態になったら問題があるかと思われます が、待ち状態になるときに自動的に一旦ロックが解除され,他のスレッドが変数 i をアクセスすることを可能にし ます。待ち状態になったスレッドは他のスレッドの cond signal() によって起こすことができます。 このように、条件変数はスレッドの同期を取ったりするのによく使われます。 50 4 演習 マルチスレッドプログラミングの練習として、3.2、3.3.1、3.3.2 のプログラムを実行してみましょう。実行結果 が分かりやすいよう printf() 文をたして実行してみてください。 うまく動作したならば、次は応用として、並列クイックソートをつくってみましょう。 #include<stdio.h> #define N 7 void qsort(int *a, int l, int r) { int i=l, j=r, p=a[l], w; while(1){ while(a[i] < p) i++; while(p < a[j]) j--; if(i >= j) break; if(i < j){ w = a[i]; a[i] = a[j]; a[j] = w; } i++; j--; } if(l < i-1) qsort(a, l, i-1); if(j+1 < r) qsort(a, j+1, r); } main() { int n = N; int a[N]={5,3,1,6,8,4,2}; int i; qsort(a, 0, n-1); for(i=0; i<n; i++) printf("%d ",a[i]); printf("\n"); } 上のプログラムはクイックソートのプログラムですが、このプログラムをマルチスレッド化してみましょう。 1) 再帰関数 qsort() をスレッド化する。 2) スレッド化した qsort() で全ての数列をソーティングするのはスレッドを生成するオーバヘッドが多くなる ので、qsort() でソートする数列の数がある数値以下になったら、スレッドを生成せずに、再帰的に関数によるク イックソート (または、バブルソート) を行なう。 3) スレッドは非同期にソートするので、対象の数列が全てソートし終わったことを判定しなくてはいけません。 そこで、まず対象の数列の全ての数を調べておいて、再帰関数でソートした数列の数をカウントして、ソートの終 了を判定してください。このとき、thr join() は使用しなくていいです。使いたい場合は、全ての引数を”NULL” に指定してください。また、共有メモリを使用すると思うので、排他制御を行なってください。ソートする対象 データは、ランダム関数 int rand() を使って 100 万個程度の整数の乱数列を作り使用して下さい。 51
© Copyright 2024 Paperzz