[LinuxFocus-icon]
ホーム  |  マップ  |  一覧  |  検索

ニュース | アーカイブ | リンク | LFについて
[an error occurred while processing this directive]
convert to palmConvert to GutenPalm
or to PalmDoc

[Leonardo]
by Leonardo Giordani
<leo.giordani(at)libero.it>

著者紹介:

Politecnico of Milan の通信工学部の学生で、ネットワーク管理を行っています。 プログラミング (主にアセンブリ言語と C/C++) に興味を持っています。 1999 年以降は、ほとんど Linux/Unix だけを扱っています。

日本語訳:
須藤 賢一 <deep_blue(at)users.sourceforge.jp>

目次:


 

並列プログラミング - その原理とプロセス入門

[run in paralell]

要約:

この連載記事の目的は、マルチタスキングの考え方を紹介し、それが Linux オペレーティングシステムではどのように実装されているかを説明することです。 まずはマルチタスキングの基礎になる概念的な部分から始めて、最終的にはプロセス間通信を行うアプリケーションを完成させます。 そこではシンプルですが効率的な通信プロトコルを使います。

この記事を理解するには、以下の予備知識が必要です。

  • シェルについての最低限の知識
  • C言語の基礎知識 (構文、ループ、ライブラリ)
コマンド名の後の括弧の中には、マニュアルページへの参照が入っています。 glibc の関数は全て gnu の info ページに説明があります。 (info Libc と入力するか、konqueror で info:/libc/Top と入力します)。
_________________ _________________ _________________

 

はじめに

オペレーティングシステムで最も重要な節目となったのはマルチプログラミングの考え方を導入したことです。 これは複数のプログラムを組み合わせて実行し、システムのリソースを均一に使うようにする技術です。 一般的なワークステーションを見てみると、ワードプロセッサ、オーディオプレイヤ、プリントキュー、ウェブブラウザ等のプログラムを一人のユーザーが動かしています。 マルチプログラミングは現代のオペレーティングシステムではとても重要な考え方なのです。 後で分かるように、ここで挙げたのは代表的なものではりますが、我々のマシンで動作しているプログラムのほんの一部にすぎません。  

プロセスの考え方

プログラムを交互に実行しようとすると、オペレーティングシステムは極めて複雑になります。 実行中のプログラムが衝突するのを避けるには、プロセスとその実行に必要な情報をひとまとまりにしておくことが不可欠です。

Linux マシンの中で何が起きているのかを探る前に、まずは技術的な用語を定義しておきましょう。プログラムが動作しているとき、コードはそのプログラムを構成する命令の集まりで、メモリ空間はそのプログラムのデータが使っているマシンメモリです。 プロセッサ状態とはマイクロプロセッサのパラメータの値で、例えばフラグとかプログラムカウンタ (次に実行する命令のアドレス) などです。

実行中のプログラムとは、コード、メモリ空間、プロセッサ状態からなるいくつかのオブジェクトを指します。 マシンの実行中のある時点でこの情報を退避して、 別のプログラムから退避しておいた情報で置き換えれば、 それが停止したところから再開されます。 これをあるプログラムに対して行い、次に別のプログラムに対しても行えば、 先に述べたようにプログラムを交互に実行させることできます。 このように動作中のプログラムをプロセス (またはタスク) と呼びます。

概要で触れたワークステーションの中で何が起きているのかを説明しましょう。 ある瞬間には 1 つのタスクだけが実行されていて (マイクロプロセッサはひとつだけなので、2 つのことを同時に行うことはできません)、マシンはそのコードの一部を実行しています。 QUANTUM と呼ばれる時間が過ぎると、実行中のプロセスは停止させられ、プロセスの情報は退避されて待機中の他のプロセスの情報で置き換えられます。 そのプロセスはまた一定時間だけ実行されて、この手順が繰り返されます。 これがマルチタスキングと呼ばれるものです。

前にも述べたように、マルチタスキングを導入することでいくつかの問題が起きます。 プロセスを待たせておくキューの管理 (スケジューリング) などといった、それなりに難しい問題がほとんどです。 ですが、こういった問題はオペレーティングシステムの構造に関連しています。 将来の記事では主にこれを扱うことになります。 Linux カーネルのコードも一部紹介することになると思います。  

Linux と Unix におけるプロセス

私たちのマシンで動作しているプロセスについて見てみましょう。 そのための情報を提供してくれるのが ps(1) コマンドです。 このコマンドは 「process status (プロセス情報)」の頭文字をとったものです。 普通のコマンドシェルを開いて ps コマンドを入力すると、以下のように表示されます。

  PID TTY          TIME CMD
 2241 ttyp4    00:00:00 bash
 2346 ttyp4    00:00:00 ps

この一覧は完全なものではないのですが、まずはこれについて見ていきましょう。 ps コマンドで表示されたのは、現在のターミナルで実行中のプロセスの一覧です。 最後の列に表示されているのが、プロセスが起動されたときの名前 (例えばウェブブラウザ Mozill であれば「mozilla」、GNU Compiler Collection であれば「gcc」です) が分かります。 プロセスの一覧を表示する時点では ps 自身も動作していたため、それも一覧に出てきます。 もうひとつのプロセスは Bourne Again Shell で、ターミナルで動作しているシェルです。

TIME と TTY の部分は (当面) 置いておいて、Process IDentifier (プロセス識別子) PID を見てみましょう。 pid は一意の正の整数 (ゼロ以外) で、動作中のプロセス毎に割り当てられます。 プロセスが終了すれば pid は再利用されますが、プロセスの実行中は pid が変わることはありません。 ということは、皆さんが ps コマンドで得られる出力は上記の例とはたぶん違うということです。 試しに、今あるシェルをそのままにして、もうひとつシェルを開いて ps コマンドを入力してみてください。 今度も表示されるプロセスは同じになりますが、pid の番号が違っているはずです。 動いているプログラムは同じでもプロセスとしては違っていることの証拠です。

Linux マシンで動作中の全てののプロセスの一覧を表示することもできます。 ps コマンドのマニュアルページによれば、 -e スイッチは「全プロセスを選択する」ことを意味しています。 ではターミナルで「ps -e」と入力してみましょう。 上と同じ形式で、もっと長い一覧が表示されるでしょう。 この一覧をもっと見やすくするために、ps の結果を ps.log というファイルに書き出してみます。

ps -e > ps.log

これで好きなエディタで開いて読むことができるようになりました (単に less コマンドで表示させることもできます)。 この記事の最初で言ったように、動作中のプロセスの数は想像よりもかなり多いのです。 この一覧には、自分で (コマンドラインやグラフィカル環境から) 起動したプロセス以外にもたくさんのプロセスが載っています。 中には風変わりな名前のプロセスもあります。 一覧表示されるプロセスの数や種類はシステムの設定により異なりますが、共通していることもあります。 まず、システムにどんな設定をしようとも pid が 1 となるのは必ず「init」です。 これは全てのプロセスの親です。 pid が 1 なのは、オペレーティングシステムが起動する最初のプロセスだからです。 もうひとつ気づくことは、名前の最後が「d」で終わるプロセスがたくさんあることです。 これらのプロセスは「デーモン」と呼ばれていて、システムで最も重要なプロセスです。 init とデーモンについては後で詳しく説明します。  

libc におけるマルチタスキング

ここまででプロセスの概念と、それがオペレーティングシステムにとってどれほど重要なものなのかが分かったと思います。 ではさらに進めてマルチタスキングのコードを書いてみましょう。 プロセスを同時に実行する小さなコードから始めますが、さらには別の問題へと進んでいきます。 それは並列に動作するプロセス間での通信と同期という問題です。 この問題に対しては二つの簡潔な方法で解決します。メッセージとセマフォです。 後者についてはスレッドに関する別の記事で説明していきます。 まずはメッセージを説明し、それを使ってアプリケーションを書くことにしましょう。

標準 C ライブラリ (libc、Linux では glibc) は Unix System V のマルチタスキングの機能を使っています。 Unix System V (以降 SysV と呼ぶ) は商用の Unix で、Unix ファミリーの源流になっている 2 つのうちの 1 つです。ちなみに、もうひとつというのは BSD です。

libc では pid を格納しておくための整数値として pid_t が定義されています。 ここでは pid を保持するのにこの型を使いますが、これは分かりやすくするためです。 代わりに integer を使っても同じことです。

自分のプログラムのプロセスの pid を知るための関数は、

pid_t getpid (void)

です (この関数は pid_t と共に unistd.h と sys/types.h で定義されています)。 では pid を標準出力に表示するようなプログラムを書いてみましょう。 好きなエディタで以下のコードを入力してください。

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
  pid_t pid;

  pid = getpid();
  printf("The pid assigned to the process is %d\n", pid);

  return 0;
}
このプログラムを print_pid.c という名前のファイルに保存してコンパイルします。
gcc -Wall -o print_pid print_pid.c
これで print_pid という実行ファイルが生成されます。 カレントディレクトリがパスに入っていない場合には、実行するのに「./print_pid 」とする必要があります。 プログラムを実行しても大したことは起こりません。 たんに整数値が表示されるだけです。2 回以上実行すると、表示される番号が 1 だけ増えるのが分かるでしょう。 必ずしも 1 増えるとは限りません。print_pid を何度か実行する間に別のプロセスが起動されることもあるからです。 例えば、print_pid を 2 回実行する間に ps を実行してみてください。

では次にプロセスを作成する方法に移りますが、その際に実際に何が起きるのかを説明しておかないといけません。 プログラム (プロセス A のプログラム) が他のプロセス (B) を作ると、その 2 つは全く同一のものになります。 すなわち、同じコードを持ち、同じデータが入ったメモリ (メモリ自体は異なる) を持ち、プロセッサ状態も同じです。 2 つのプロセスはその時点から、ユーザの入力やランダムなデータに基づいて違った動作をし始めます。 プロセス A は「親プロセス」、B は 「子プロセス」と呼ばれます。 これで init が「全てのプロセスの親」と呼ばれている意味が良く分かったと思います。 プロセスを作成する関数は

pid_t fork(void)
です。この名前はプロセスの実行が分岐 (fork) することからきています。 返却値は pid ですが、注意が必要です。 先ほど、プロセスはそれ自身を親と子に複製して、それを他のプロセスと供に順番に実行し、別々のことをさせると説明しました。 では、複製の直後に実行されるのは親プロセスでしょうか、それとも子プロセスでしょうか。 答えは単純で、ふたつのうちのどちらかです。 どちらを実行するかはオペレーティングシステムの中のスケジューラーと呼ばれる部分が決定し、どっちが親でどっちが子かといったことは考慮せず、他のパラメーターに基づくアルゴリズムを使います。

そうは言っても、コードは同じですから、一体どちらを実行しているのかを知るのは重要です。 どちらのプロセスにも親のコードと子のコードが含まれていますが、互いに自分のコードだけを実行する必要があります。 この考え方を明確にするために、次のアルゴリズムを見てみましょう。

- FORK する
- もし 子プロセスなら これを実行する (...)
- もし 親プロセスなら これを実行する (...)
プログラムをメタ言語風に書いてみました。 ではそろそろタネを明かしましょう。 fork 関数は子プロセスに対しては 0 を返し、親プロセスに対しては子プロセスの pid を返すのです。 ですので、返された pid がゼロかどうかを判断すれば、どちらのプロセスがそのコードを実行しているのかが分かるのです。 これを C 言語で書くと以下のようになります。
int main()
{
  pid_t pid;

  pid = fork();
  if (pid == 0)
  {
    子プロセスのコード
  }
  親プロセスのコード
}
そろそろ完全はマルチタスキングのコードを書きましょう。 fork_demo.c というファイルに保存して以前のようにコンパイルしてください。 分かりやすくするために行番号を示しました。 このプログラムは自分自身を分岐させ、親と子のどちらも画面に出力を行います。 最終的には 2 つの出力が混じったものになるはずです (うまくいっていれば)。
(01) #include <unistd.h>
(02) #include <sys/types.h>
(03) #include <stdio.h>

(04) int main()
(05) {
(05)   pid_t pid;
(06)   int i;

(07)   pid = fork();

(08)   if (pid == 0){
(09)     for (i = 0; i  < 8; i++){
(10)       printf("-SON-\n");
(11)     }
(12)     return(0);
(13)   }

(14)   for (i = 0; i < 8; i++){
(15)     printf("+FATHER+\n");
(16)   }

(17)   return(0);
(18) }

(01)〜(03) 行目は必要なライブラリ (標準入出力ライブラリとマルチタスキングライブラリ) をインクルードしています。
main 関数は (GNU では普通ですが) integer を返します。 プログラムがエラーなしに終わりに到達すると通常はゼロで、どこかおかしいとそのエラーコードになります。 今回はエラーなく動くことにしておきましょう (エラー処理は基礎が理解できた時点で追加しましょう)。 次に pid 型の変数を定義し (05 行目)、 ループのカウンタとして使う integer 型の変数を定義しています (06 行目)。 前に言ったように、この 2 つの型は同じものですが、分かりやすいようにこのようにしています。
(07) 行目では、fork 関数を呼び出していて、子プロセスにはゼロを、親プロセスには子プロセスの pid が返ります。 値の判定は (08) 行目にあります。 (09)〜(13) 行目は子プロセスで実行され、残りの (14)〜(16) 行目は親プロセスで実行されます。
どちらのプロセスが実行するかによってそれぞれ「-SON-」または「+FATHER+」を 8 回出力し、最後に 0 でリターンします。 この「return」はとても重要です。これがないと、親プロセスのコードまで実行してしまいます (試してみてください。マシンが壊れるといったことはありません。単に意図したようにならないだけです)。 その手の間違いは見つけるのがとても難しいものです。 マルチタスキングのプログラム、特に複雑なものになると、実行するたびに結果が違ってきて、実行結果を基にしてデバッグを行うことが全くできなくなってしまうのです。

プログラムを実行してもがっかりするかも知れません。 ふたつの文字列が混ざった結果になるとは限らないからです。 これは、実行するループが短く、すぐに処理が終わってしまうためです。 きっと、「+FATHER+」という文字列が続いた後に「-SON-」がくるか、その逆かのどちらかでしょう。 プログラムを何度も実行すると結果が変わるかも知れません。

printf 呼び出しの前にランダムな遅延を入れれば、もっとマルチタスキングの様子が目に見えるかもしれません。 sleep と rand 関数を使ってやってみましょう。

sleep(rand()%4)
これでランダムに 0〜3 秒の間プログラムがスリープします (% は整数の割り算の余りを計算するためのものです)。 コードは以下のようになります。
(09)  for (i = 0; i < 8; i++){
(->)    sleep (rand()%4);
(10)    printf("-FIGLIO-\n");
(11)  }
親プロセスのコードも同様です。 これを fork_demo2.c という名前で保存し、コンパイルして実行してください。 今度はゆっくりと動きますが、出力の順序が変わっています。
[leo@mobile ipc2]$ ./fork_demo2
-SON-
+FATHER+
+FATHER+
-SON-
-SON-
+FATHER+
+FATHER+
-SON-
-FIGLIO-
+FATHER+
+FATHER+
-SON-
-SON-
-SON-
+FATHER+
+FATHER+
[leo@mobile ipc2]$

ではそろそろ次の問題に移ります。 並列プロセッシング環境では、親プロセスがたくさんの子プロセスを作って、親プロセスとは違う処理をさせられることは分かりました。 このとき、親プロセスは子プロセスと通信したり、最低でも同期を取る必要が出て来ます。 これは、タイミング見計らって処理を行うためです。 まずは wait 関数を使って同期を取ることにしましょう。

pid_t waitpid (pid_t PID, int *STATUS_PTR, int OPTIONS)
ここで PID は待ち合わせるプロセスの PID です。 STATUS_PTR は integer 型の整数へのポインタで、ここに子プロセスのステータスが格納されます (情報が不要な場合は NULL を指定します)。 OPTIONS はオプションですが、今は無視しておきます。 次のプログラム例では、親プロセスが子プロセスを作成してその終了を待ちます。
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
  pid_t pid;
  int i;

  pid = fork();

  if (pid == 0){
    for (i = 0; i < 14; i++){
      sleep (rand()%4);
      printf("-SON-\n");
    }
    return 0;
  }

  sleep (rand()%4);
  printf("+FATHER+ Waiting for son's termination...\n");
  waitpid (pid, NULL, 0);
  printf("+FATHER+ ...ended\n");

  return 0;
}
親プロセスで sleep 関数を呼んでいるのは、実行のたびに違う動作をさせるためです。 コードを fork_demo3.c というファイルに保存し、コンパイルして実行してみてください。 マルチタスクで同期を取るアプリケーションの第一弾が完成です。

次回の記事では、同期とプロセス間通信についてさらに勉強します。 ここで説明した関数を使ってプログラムを作ったら、私に送ってください。 出来の良いものや間違っているものは紹介していきます。 ファイルを送る際には、プログラムにコメントを入れた .c ファイルに加え、プログラムの説明、お名前、電子メールアドレスを書いたテキストファイルを同封してください。 では頑張ってください。  

お薦めの本


Webpages maintained by the LinuxFocus Editor team
© Leonardo Giordani, FDL
LinuxFocus.org
翻訳履歴:
it --> -- : Leonardo Giordani <leo.giordani(at)libero.it>
it --> en: Leonardo Giordani <leo.giordani(at)libero.it>
en --> jp: 須藤 賢一 <deep_blue(at)users.sourceforge.jp>

2003-04-04, generated by lfparser version 2.36