「プロセス管理」の版間の差分

提供:UnixClassWiki
ナビゲーションに移動 検索に移動
13行目: 13行目:
を小数点以下3300万桁まで求めるプログラムがここにあるとします。
を小数点以下3300万桁まで求めるプログラムがここにあるとします。
このπを求めるには、数分、数十分、あるいは数時間という具合に長いCPU実行時間が必要です。
このπを求めるには、数分、数十分、あるいは数時間という具合に長いCPU実行時間が必要です。
これを動かしたとしましょう。





2009年6月3日 (水) 10:21時点における版

プロセスの基本的概念

プログラムが動作する実行実体のことをプロセスと呼びます。 単純化して説明すると、プログラムの実行のことです。 それで説明が終わるのはあんまりなので、もうちょっと概念的にどのような位置づけなのか考えてみます。


ここではリソース(計算資源)という視点から説明してみます。 プログラムが実行する際のいろいろなリソースの1つとして、CPUリソースがありますが、 まずそのCPUリソースで考えてみましょう。円周率π [1] を小数点以下3300万桁まで求めるプログラムがここにあるとします。 このπを求めるには、数分、数十分、あるいは数時間という具合に長いCPU実行時間が必要です。 これを動かしたとしましょう。


ユーザ側から見る

ユーザ側の視点から見てみましょう。 CPUを1つしか搭載していないシステム上でユーザがπを求めるプログラムを3つ同時に動かした [2] とします。もちろん遅いでしょうが計算は3つ同時に動いているように見えます。


補足
話を単純化するために、ここではマルチコアとかハイパースレティングなどの能力も持っていない単純なCPUとします。


これをプログラム側の視点から見てみましょう。この時、πを求めている各々のプログラムは、他にどんなプログラムが同時に動作しているのか意識することはありません。プロセスは自分専用のCPUを持っているように見えています。


プログラムA <===> CPUリソース
プログラムB <===> CPUリソース
プログラムC <===> CPUリソース


もう一度ユーザ側視点に戻って、この状態を見てみると、 仮想的なリソースとしてのCPUリソースはプログラムに割り当てられ、 使っているユーザは同時に3つのプログラムが並行して動いているように見えます。

カーネル側から見る

今度はカーネル側から見てみましょう。物理的なCPUが1個しかありません。 ですから、まったく同時に複数のプログラムが処理できているわけではありません。 ある一瞬においては1つの物理的なCPUを専有できるのは1つのプログラムだけです。 また、その実行のためにCPUリソース以外にも必要なリソースも割り当てられています。


この時、この割り当てられたリソースを使って実行する主体をプロセスと呼ぶことができます。


プログラムの実行 (=プロセス) = [
  [CPUリソース]
  [必要なリソース]
  [必要なリソース]
  ....
]


実行中プロセスの持つリソースを分類すると次のようになります。


  • CPUリソース (タスク系リソース)
  • プロセス間通信系リソース
  • 記憶管理系リソース
  • ファイル管理系リソース
  • ユーザ管理系リソース


調べてみよう
個々のプロセスのデータ構造はsched.hに定義されているstruct task_structです。。個々のプロセスが保持する情報(とリソース)にはどのようなものがあるかチェックしてみよう。


実行するために必要なリソースを個々のプログラム実行に割り当てて、実行単位プロセスの説明を試みました。 さて、今度はプロセスを実際にCPUに割り当てていくことを考えてみましょう。 プロセスを割り当てるためにスケジュール(予定)を立て処理を進めていく必要があります。 それがスケジュリングです。 スケジュリングを説明するまえに、もう少しプロセスをみてみましょう。

プロセス・タスク・スレッド

ここまでの説明では実行実体の単位のことをプロセスと呼んでいますが、これは今時のUNIXから見て古典的な実行単位です。 今は、タスクとスレッドという単位で実行単位を考えるようになっています。タスク・スレッド・プロセスを定義をすると次のようになります。


  • スレッド実行に必要なすべてのリソースのすべてを指してタスク


  • リソース中でCPUリソースとして処理の流れを作るものをスレッド


  • 1つのタスクに1つのスレッドしかないものをプロセス


スレッドやタスクの考え方(と、実装)は80年代にUNIXに入って来た、UNIXの歴史から見ると、わりと新しい技術です。 タスクが1つのスレッドしか持たない時、これはUNIX の伝統的な実行実体のプロセスです。 ですから、単純にプロセスと呼んでも構わないですし、 また同時に(ここまでの説明の範囲では)「プロセス=タスクのこと」と考えてもかまいません。 タスクは複数のスレッドを持つことができます。このような状態の場合は、プロセスと呼ぶよりタスクと呼ぶ方が適切でしょう。 UNIXが生まれた当時の処理の粒度と今時のUNIXでの処理の粒度は違うので、 これから先はプロセスという言葉は廃れてタスクという言い方が主流になるかも知れません。


補足
複数のプロセスが動くことをマルチタスクと呼びます。なんでマルチプロセスではないのかというと60年代に最初にマルチタスクが出来たオペレーティングシステムでは実行実体の単位をタスクと呼んでいたからです。なのでマルチタスクと呼びます。タスクという用語は使われる場面で微妙に意味が違うので注意しましょう。

プロセスの状態

オペレーティングシステムは実行すべき、たくさんのプロセスを同時に抱え込んでいるわけですが、それよりも少ない数のCPUでどうにかこうにか、やりくりしなければいけません。


そこでプロセスの状態に着目して、その動きを見てみましょう。 その前にLinuxのタスクの状態遷移を細かく追っていくと、初心者にとって処理の流れの全体像がわかりづらくなると思います。 そこでここでは説明は必ずしもLinuxそのままというわけではない [3] ですが、 理解しやすいプロセス状態の基本モデルを考えて説明したいと思います。


ここでのプロセスを説明するために5つの状態を持つモデルを考えます。

  • (1) 実行中(RUNNING): プロセスがCPUを使用し実行している状態
  • (2) 待機中(READY): いつでも実行できるが、他のプロセスがCPUを使っているので待機している状態
  • (3) イベント待ち(SLEEP): 入力などのイベントを待ている状態
    • INTERRUPTIBLE: 割込みなどによって待ち状態が解除される
    • UNINTERRUPTIBLE: 解除されない限りSLEEP状態になっている
  • (4) 新しいプロセス生成(FORK): fork(2)で新しいプロセスが生成される状態
  • (5) 終了待ち (ZOMBIE) : プロセスの終了処理を待っている状態
  • (6) 停止 (STOPPED) : プログラムを一時的に停止させる


 終了待ち
   ^
   |    ---A-->
 実行中 <--B--- 待機中<-----+<--- 新しいプロセス
   |                     |
   +---C--->イベント待ち--->+
   |                     |
   +------>一時停止 ------>+


プログラムの状態遷移を単純化してみましょう。プログラムは生成後、消滅までのあいだ状態A、B、Cを繰り返します。


  • A : 割り当てられたCPUリソースを使ったので、CPUでの実行状態から待機状態になる。この時、CPUリソースが使える状態になれば、すぐに実行状態に戻れる。
  • B : すぐにでも実行を行える状態で待っている。
  • C : 入力などのイベントのために待ち状態になっている。


ここでのストップ / 一時停止(STOPPED)はたとえばシェル上でコントロールZを押してSIGSTOPを発生させプログラムを停止させるといった状態です。


さて先程例に出したπの計算をしている同時に3つ動かしているプログラムは、ほとんどの場合、遷移AとBをいったり来たりしているはずです。たまにIOのためにイベント待ちの状態になりますが、それよりもはるかにCPUを使いπの計算している時間の方が多いはずです。待機中(READY状態)のプロセス数を示すロードアベレージは1.0を越えて徐々に増えていき最後には3.0を越えるはずです。


プロセスの状態

皆さんが動かしているデスクトップレベルでは、いつもそんなに激しく色々なプロセスが同時にCPUを奪いあっている状態ではありません。動いているプロセスのほとんどは入力を待つSPLEEP状態です。コマンドtopを使ってプロセスの状況を見てみましょう。


% top
 18:49:03 up 49 days,  4:38,  8 users,  load average: 2.00, 0.82, 0.31
80 processes: 75 sleeping, 5 running, 0 zombie, 0 stopped
CPU states:  98.0% user,   2.0% system,   0.0% nice,   0.0% idle
Mem:    645016K total,   622120K used,    22896K free,    46656K buffers
Swap:  2441704K total,    16192K used,  2425512K free,   159200K cached
  PID USER     PRI  NI  SIZE  RSS SHARE STAT %CPU %MEM   TIME COMMAND
 6981 hironobu  20   0  6552 6552   140 R    33.4  1.0   0:17 pi
 6980 hironobu  17   0  6552 6552   140 R    32.8  1.0   0:17 pi
 6979 hironobu  14   0  6504 6504   140 R    32.6  1.0   0:18 pi
19707 hironobu   9   0  5128 5128  2336 S     0.3  0.7   2:38 sawfish
13399 hironobu  11   0  1060 1060   820 R     0.3  0.1   0:03 top
13398 hironobu   9   0  3024 3024  1932 R     0.1  0.4   0:01 xterm
    1 root       8   0   472  432   412 S     0.0  0.0   0:43 init
    2 root       9   0     0    0     0 SW    0.0  0.0   0:00 keventd
    3 root      19  19     0    0     0 SWN   0.0  0.0   0:00 ksoftirqd_CPU0


現在80プロセスが動いていて、待機中75、実行中5、終了待ち0、ストップ0です。


STATの部分はプロセスの状態を表しています。一文字目がプロセスの状態を示していています。

* R : RUNNING状態 (実行している、あるいは実行待ちの状態)
* S : SLEEP状態 (I/O待ちなどの状態)
* D : 割込みが利かないSLEEP状態 (sleep関数を呼んでSLEEPしている状態など)
* Z : ゾンビ状態 (終了したがまだ親プロセスが終了処理を終えていない状態)
* T : ストップ状態 (Control-Z/SIGSTOPによるプロセス停止の状態)


イベント待ちSLEEP状態で入力などの割込みで止まっている場合はSのSLEEP状態として表示されますが、bashやcsh のようなジョブコントロールが可能なシェル上でコマンドを実行し、コントロールZで止めたような場合はストップ状態となりTのストップ状態として表示されます。


ほとんどのSLEEP状態は場合は割込みがあればすぐに動き出すINTERRUPTIBLEなタイプです。カーネル中で解除されない限りSLEEP状態を続ける UNITERRUPTIBLE のような状態はデバイスをロックするなど特別な場合に使われるだけで、あまり多くはありません。


調べてみよう
プロセスの状態を表しているSTATの欄をみるとR/S/D/Z/Tの後ろにさらに文字がついていますがこの意味は何でしょうか?


πの計算のように数値計算を延々と行うプログラムは別として、ディスクトップで利用しているソフトウェアの多くはユーザからのインタラクション(入力)待ちの状態で止まっているようなものです。


たとえばコンパイラがソースコードをコンパイルするプロセスでも実行中、待機中、イベント待ち状態を遷移します。 なぜならばコンパイラは、ファイル入出力を発生させるので、その入出力が発生している最中はイベント待ち状態になっています。ファイル入出力に10msかかったとして、その間はCPUは遊んでいる状態になります。 10msというは今日のCPUにとっては、長い長い暇な時間で、かなり大量の計算ができます。 ですから前のプロセスがCPU専有時間が過ぎたので次のプロセスに順番を与える以外にも、次のプロセスがCPUを使う状態が多数発生します。

プロセスの生成

親プロセスが子プロセスを生むという形を取り、子プロセスは親プロセスのコピーとして動き始めます。例外はすべてのプロセスの始まりであるinitだけです。

調べてみよう
コマンドpstreeを使って、すべてのプロセスの親子関係がどうなっているかチェックしてみよう。

fork(2)

プロセスの生成はfork(2)を使い親プロセスが子プロセスを生み出します。下のコードを見て下さい。

1  #include <sys/types.h>
2  #include <unistd.h>
3  #include <stdio.h>
4  main() {
5    char *p;
6    fprintf(stderr,"we are here\n",p);
7    if ( fork() != 0 ) {
8      p="parent";
9      fprintf(stderr,"%s is here\n",p);
10   }
11   else {
12     p="child";
13     fprintf(stderr,"%s is here\n",p);
14   }
15   wait();
16   fprintf(stderr,"%s finish\n",p);
17 }

システムコールfork(2)はフォーク後、親プロセスでは子プロセスのプロセスIDを返却します。フォークができな時は-1を返却します。子プロセスの時は0を返却します。wait(2)は子プロセスの終了を待つシステムコールです。このプログラムを実行すると次のような出力になります

$ ./a.out 
here we are
parent is here
child is here
child finish
parent finish


最初は親も子もない1つのプロセスで"here we are"を出力しますが、fork(2)の時点で、2つのプロセスが出来ます。子プロセスは親プロセスのコピーで、そのまま子プロセス側の処理を行います。この時点で"child is here"が出力されます。wait(2)の部分では子プロセスは待つべきプロセスは存在しませんから、そのまま通過し"child finish"が出力されます。


親プロセスは、親プロセス側の処理を行い"parent is here"が出力されます。その下のwait(2)では子プロセスがあるので、その終了を待つ状態になります。子プロセスが終了した時点でwait(2)は処理され、次の"parent finish"の行に続きます。


ポインタpを見てみましょう。親プロセスと子プロセスに別れた後は、各々のプロセス中で別々に存在し違う値をもっていていることに注意してください。


 here we are
 |
 V 親プロセス  子プロセス
fork(2) ---------+  生成
 V               |      
 parent is here  V
 |      child is here
 |               V
 V       child finish
 wait()<-------- +  消滅
 V
 parent finish


子プロセスが新しい実行ファイルを動かすにはシステムコールexecve(2)使います。これは、子プロセスの資源を新しい実行ファイルの資源として与えるものです。


補足
プログラム中ではシステムコールexecve(2)よりもライブラリ関数execl(3)などを使う場合の方が多いでしょうが、ここでは説明としてexecve(2) を取り上げています。下のコードは/bin/dateを実行している例です。


1  #include <unistd.h>
2  main() {
3  if ( fork() == 0 ) 
4    execl("/bin/date",NULL);
5  wait();
6 }


調べてみよう
カーネルの中でforkをするのはkernel/fork.cの中の関数do_fork です。関数do_forkは関数copy_processを呼び出してプロセスをコピーするのがメインの役割です。関数copy_processは、必要なものを親プロセスからコピーし子プロセス独自に必要な情報を初期化しています。

プロセスを生成するシステムコール

プロセスを生成するシステムコールにはfork(2)の他にvfork(2)、さらにLinuxではclone(2)があります。vfork(2)の違いは親プロセスからページテーブルをコピーすることなく子プロセスを生成することです。子プロセスでexecve(2) が実行されれば、親プロセスからコピーされたページテーブルの内容は子プロセスの内容に変わります。つまり、fork(2)後すぐにexecve(2)が呼び出されるような場合、使われない内容をわざわざコピーする時間が短縮する分パフォーマンスが改善できます。

fork(2)とclone(2)ともに新しい子プロセスを生成しますが、fork(2)が内容をコピーするのに対しclone(2)は内容を共有しています。その意味では後に説明するスレッドと同じです。execve(2)で新しい内容を必要とした時に始めてコピーを行います。ですからvfork(2)はclone(2) の変形版といえます。ただしclone(2)はLinux独自のものなので、プログラムに使うと、他のシステムへの移植の際は問題が発生する可能性があります。

プロセスを殺すシステムコール

その反対にプロセスを殺すシステムコールはkill(2)です。ここでは内部でkill(2)を呼んでいるコマンドkillを紹介しましょう。

$ kill -9 8321

killにプロセスIDを与えた場合、そのプロセス番号にSIGKILLを送ります。SIGKILLはプロセスを殺すシグナルです。killに与えるプロセスIDが0の場合、自分自身を殺します。-1は、シグナルを受け取ることができる全てのプロセスに送られます。プロセスinitはプロセスIDが1で、むかしは1を指定するとシステム全体が死んでいたのですが、今はルート権限でkill -9 1としても死なないようになっています。


調べてみよう
killでマイナスの値を指定することができます。それはどのような役目をするのでしょうか?

スケジュリング

プロセスにCPU資源をどのように与えるかの順番を決めるスケジュール (schedule)ことを行うことがスケジュリング(scheduling)です。それを司るのがスケジュラ(scheduler)です。


スケジュリングの方式にはポリシーと呼ばれる、いくつかの方式があります。


  • FIFO : 最初に入ったプロセスが終了するまでそのプロセスが専有する方式。最初に入ったものが最初に出るなのでFirst-In-First-Out / FIFOと呼ばれる。


  • RR : ラウンドロビン( Round Robin: 一定の間隔で順繰りに回ってくる)でプロセスが割り当てられる方式。


  • 独自優先順位: 実行に使用した時間の違いなどにより優先順位をつけるなどし、その優先順位に従って割り当てる方式。(Linuxデフォルト)


  • バッチ: (たとえば大量の処理をしても)プライオリティの変化をつけない。尚、当然ながらタイムシェアリングシステムにおいてのバッチという意味である。


FIFOの場合、そのプロセスがCPUを専有するのでCPUが空かない限り他のプロセスはCPUは割り当てられません。プロセスがSLEEP状態から復帰した場合、次にCPUを割り当てられるのはFIFOプロセスです。


調べてみよう
FIFOでプロセスを動かすとリアルタイム処理性能は満足するのでしょうか?もし足らないものがあるとすると、それは何でしょうか?


ラウンドロビンは一定時間CPU時間を消化したプロセスを待ち状態に戻し、待っていた次のプロセスに切替えて順繰りにプロセスを実行していく方法です。この一定時間のことをタイムスライス(timeslice)といいます。公平にプロセスにCPUを割り当てることになります。タイムスライスを小さくすれば小さくするのど、細かくプロセスをCPUへ割り当てることができますが、しかし割り当て処理の回数が増えるにつれカーネル自体が消費するCPU時間も多くなっていきます。ですから、リーズナブルな値にしなければなりません。


補足
筆者の知る範囲では大体においてラウンドロビン方式と優先順位づけなどを併用しているので、完全に公正で総当たりするようなラウンドロビン方式というのを見たことがありません。


優先順位方式は、何らかの評価方式でCPUに割り当てる優先度や割り当てる時間を決めて順番を決める方式です。何らかという通り、色々な方法があって、オペレーティングシステムの違いによって異なりますし、あるいは同じカーネルであってもバージョンの違いによっても微妙に異なります。


Linuxの基本はUNIXの流れであるラウンドロビンと優先順位をミックスしたものです。優先順位によってタスク待ちの優先順位の高いキューにセットされます。またそれだけでもなくタイムスライスの値も変化させています。Linux 2.6.22の場合、タイムスライスは平均100msですが内部では最小5msから最大800msまで内部で動的に変化させています。

Linux 2.6系におけるスケジュラーは以前2.4系で使われていたスケジュラーからみると画期的に性能がいいO(1)スケジュラ(Order one scheduler)に変更されました。さらにLinux 2.6.23からO(1)スケジュラからCFS(Completely Fair Scheduler) [4]

に変更されました。 O(1)スケジュラがヒューリスティック(発見的)にスケジュリングするのですが、CFS のスケジュリングは(数学的な意味で)数値を計算してスケジュリングを決めます。 ヒューリスティックな方法では、ある種の特定の条件に嵌まってしまいスケジュリングが公平に処理できなくなったり、あるいは偶然に有利な条件になったり不利な条件になったりとする可能性があります。 それから比べるとCFSは公平にスケジュリングされます。 CFS スケジュラに関しての評価 [5] を見ていると安定したスケジュリングを実現しており良い結果を残しているようです。

今後もある意味オペレーティングシステムの心臓部ともいえるスケジュラーが色々な要件を満たすため、 あるいはさらなる効率化のために変更される可能性はあるでしょう。 あちらこちらの腕利きのカーネルハッカーがどんどん新しい提案をしてくるダイナミックさを持つLinuxらしいエピソードです。

プライオリティ

UNIX系のオペレーティングシステムではプロセスの持つ動的に変化するプライオリティ値によって、CPUの割当てが変化します。プライオリティ値が小さいものが実行優先順位が高く、大きいものが実行優先順位が低くなります。

プロセス待機キューの並べ変え、プロセスのタイムスライスの値を設定する時に、プライオリティが高い程、よりCPUが割り当てられるような計算がされます。

もう一度、コマンドtopの出力例を見てみましょう。

 PID USER     PRI  NI  SIZE  RSS SHARE STAT %CPU %MEM   TIME COMMAND
6981 hironobu  20   0  6552 6552   140 R    33.4  1.0   0:17 pi
6980 hironobu  17   0  6552 6552   140 R    32.8  1.0   0:17 pi
6979 hironobu  14   0  6504 6504   140 R    32.6  1.0   0:18 pi

RRIと書いてあるのがプライオリティ値です。NIと書いてあるのがnice値です。通常で使う範囲では、プロセスがCPUを専有すればするほどプライオリティ値が大きくなっていきます。カーネルの中では0から139の範囲で変化しています。優先順位が0が最低で139が最高です。ユーザ側に表示にされるのは20から-19 で、優先順はプラス側低く、マイナス側が高くなっています。

コマンド実行開始時にコマンドniceを使い底上げ値(優先順位を下げるのでハンディキャップ値といった方がいいかも知れませんが)を与えることができます。一般ユーザは優先順位を下げることしか出来ません。root権限では優先順位を上げることもできます。reniceは既に動いているプロセスのnice値を変更するものです。

% nice -19 ./setiathome 
補足| コマンドniceやreniceで与えるナイス値と、表示されるナイス値、カーネル内部で使うプライオリティ値、表示されるプライオリティ値ののプラス/マイナスは間違え安いので注意しましょう。上の例では-19の値を与えているが、topなどのNIの欄の値は19となります。

スレッド

スレッドは計算資源を共有しCPU資源だけは別の状態であるような処理の流れをいいます。スレッドにはclone(2)のようなカーネルレベルで実行するスレッドとglibcなどユーザライブラリに含まれているユーザレベルで実行するスレッドの2つがあります。ですからスレッドが必ずしもカーネルモードで動作しているわけではなく、それはプログラムの書かれ方によります。ここの説明ではカーネルスレッドとユーザスレッドとの両方取り上げてみます。

既に説明した通りfork(2)が親と子のプロセスで内容をコピーしているのですが、とはいえ fork(2) 後は各々の実体は独立しています。しかし、スレッドは内容が一緒ですから、つまり処理の流れを別に持つCPU資源だけは複数個持つのですから、新しいプロセスが生まれていないので親も子もありません。CPU資源以外の計算資源のコピーが必要ない場合、あるいは同じ計算資源でCPU 資源を複数持ちたい時、計算資源を共有したい時などにスレッドは便利です。

CPUリソースだけを同時に複数個持つことができることで、たとえば実行しているタスク中でネットワーク入出力からの処理を行いつつ同時に他の計算を繰り返すような場合に非常に有効に働きができます。ウインドウシステムを考えてみましょう。マウスのようなポインティングデバイスから入力を受け、さらにその位置を計算しポインタのアイコンを変化させつつ、さらに画面書き換えを行うようなプログラムでは効率良く動くプログラムができます。

           +--> スレッド 1 : 入力を受ける
           |
タスク ===>+--> スレッド 2 : 位置を計算
           |
           +--> スレッド 3 : 画面書き換えを行う

次はfork(2)と違い親プロセスの内容をコピーすることなく新しいプロセスを動かすことができるので速くプロセスが生成できることです。この利点は、資源コピーが発生しないまま処理スレッドだけを高速に立ち上げることができると説明しましたが実際にfork(2)とLinuxのpthreadライブラリ(カーネルスレッドではなくユーザスレッド)を使ってどれぐらい違うか試してみました。1000 回プロセスを生成するのとスレッドを生成するプログラムを書いてみます。結果はAthlon 1GHzマシンでスレッド生成側がトータル0.157秒、プロセス生成側が0.360秒と2倍以上の差をつけました。このように処理だけ大量に生成するような場合はスレッドが有利に働きます。

fork版
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
main() {
  int i;
  for(i=0;i<1000;i++) {
    if ( fork() == 0 ) {
      fprintf(stderr,".");
      _exit(0);
    }
    wait();
  }
  printf("%d\n",i);
}
% cc -O fork.c -o f
thread版
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
void *func() { fprintf(stderr,"."); }
int main(void) {
  pthread_t t;
  int i;
  for(i=0 ; i < 1000 ; i++) {
    pthread_create(&t,NULL,func,NULL);
    pthread_join ( t, NULL );
  }
  printf("%d\n",i);
}
% cc -O thread.c -lpthread -o t
出力 
% time ./t
...........(続く)....1000
real	0m0.xxxs
user	0m0.xxxs
sys	0m0.xxxs


補足
Linux上でThreadを使う方法に関しての良い資料を見つけることは難しいですが、IBMのLinux情報

[6] には良質の説明が載っていました。


マルチプロセッサとプロセス

複数のプロセッサの話題に移りますが、ここでの複数プロセッサといってもスーパーコンピュータのように超並列とかは、この話題の範囲ではないので除外した上で話を進めます。


さて、我々が現在利用の対象としているタイプのものは、対称マルチプロセッサ(SMP: Symmetric Multi-Processor) と呼ばれるタイプのものです。SMPの特徴で覚えておかなければならない点は各々のプロセッサが各々独自の入出力を持っていないのと、そして、どのプロセッサもシステム内のすべての構成部分に対して同等にアクセスできることの2点です。


似たようなシステムに共有メモリ型マルチプロセッサ ( Shared Memory Multiprocessor )というものがありますが、SMPはメモリを共有しているので共有メモリ型マルチプロセッサと同じ意味です。ただし、狭い定義を採用すれば共有メモリ型マルチプロセッサは、メモリ資源の共有だけに定義を持っているのに対して、SMPはシステムにある構成要素のすべてに対し同等にアクセスできるという点が違うことを注意しましょう。


まずはSMPとはプロセッサの数が増えただけで、あとは構成要素を共有していると理解するのが良いでしょう。ですからSMPの良い部分は単一プロセッサと同じプログラムのモデルが使用できる点です。考えた方として単純にプロセッサが複数あると考えて構いません。単純化したスケジュールのモデルで考えてみましょう。たとえばラウンドロビンのスケジュールを用いて、CPU(ここでの説明はプロセッサと同じということで捉えてください)が1つだったのを2つにしたときを考えてみると下のようになります。


P = プロセス
 \
  \                /-> CPU ->+
   P -> P -> P -- /          |
                  \          |
   ^               \-> CPU ->+
   |                         V
   +-------------------------+


カーネルがSMPの複数CPUに対し処理を割り当てる方法はいくつかあります。それはプロセス単位での割り当てとスレッド単位の割り当てです。


補足
ここではプロセス (1タスク/1スレッド) と タスク ( 1タスク / 複数スレッド ) とし、前者が「プロセス単位で」、後者が「スレッド単位」でという意味です。


ある一定のデータを処理するプログラムをまず仮定します。プロセス単位で割り当てるならプログラム側はSMPを何にも意識しなくても構いません。たくさんのプロセスがスケジュールされている場合でもCPU が多い分、自分の番が回ってくる回数が増えるため負荷に強いという意味で、処理能力が向上します。CPUが1個の時に同時に2つのプロセスを動かす時の処理時間とCPUが2個の時に同時に2つのプロセスを動かす時の処理時間を比較すると、後者の方が速くなります。しかし1つのプロセスに着目してみればCPUが1個で1つのプロセスしか動いていない時の処理時間よりは速くなることがありません。

マルチプロセッサとスレッド

スレッド単位で割り当てる場合、実行中プログラムの持つ複数のスレッドが複数のCPUに同時に割当たります。その場合はプログラムの処理のスピードはCPU 単体の最大処理速度を越えることが可能になります。ただ、それでも上手にスレッドを分割し、その処理タイミングをきちんとプログラミングできる場合という前提が満たされることが必要です。


例えば、配列 A = { a1, a2, a3, ... an }があって、表の値をすべて掛けたものを、表の値をすべて足したもので割るという計算をしたいとしましょう。


 a1 * a2 * a3 * ... * an
 ----------------------------  == ???
 a1 + a2 + a3 + ... + an


まず、a1 * a2 * a3 * ... * anを計算しAに代入するスレッドとa1 + a2 + a3+ ... + anを計算しBに代入するスレッドを作ります。両方のスレッドの計算の終了を待ってA/Bを計算します。この時、各々のスレッドが別々のCPUで並列に動くので1個のCPUで全部を計算させるよりも速く計算 [7] ができるはずです。


補足
この例でも単純に2倍に速度があがるわけではありません。たとえば1CPUでの計算に20秒かかったとしましょう。2CPUを使いきれいに分母、分子の計算が分割でき並列に計算されたとしても、もし、分子の計算が5秒かかり、分母の計算が14.5秒かかり、分子÷分母の計算が0.5秒かかったならば、その処理時間は15秒となります。


SMPでの処理で「速い」という言葉を使う時は要注意です。このように2つの違う意味が同時に存在しているので、どの意味で「速い」のかをきちんと把握する必要があります。


調べてみよう
ここでは単純なボトルネックの例ですが、並列処理一般において処理効率には上限があるということを示しているアムダールの法則というのがあります。アムダールの法則について調べてみましょう。

脚注

  1. GNU/Linux上で円周率の計算をおこなう http://h2np.net/pi/
  2. 同じ答えを3つも必要ないというツッコミはここではなし。
  3. LinuxカーネルではTASK_RUNNING、TASK_INTERRUPTIBLE、 TASK_UNINTERRUPTIBLE、 TASK_STOPPEDなどの状態をもちますが、詳しくはinclude/linux/sched.hを参照してください。
  4. Completely Fair Scheduler によるマルチプロセッシング http://www.ibm.com/developerworks/jp/linux/library/l-cfs/index.html
  5. Benchmarking CFS http://kerneltrap.org/Linux/Benchmarking_CFS
  6. Linuxのスレッド http://www-6.ibm.com/jp/developerworks/linux/001208/j_posix1.html
  7. マルチスレッドを使い円周率を計算したプログラム例: http://h2np.net/pi/mt-bbp.c

目次