マルチプログラミング(並列処理)を
実現するには様々な方法が存在します。
その一つにマルチプロセス化があります。
新たにプロセスを作成して、
それぞれのプロセスが並列に
処理を進めていく方法です。
新たにプロセスを作成するには、
fork()関数を使用します。
新しいプロセスは子プロセスと呼ばれ、
作成した側は親プロセスと呼ばれます。
ここでプロセスという単位は
どのような役割を持つか確認です。
- 保護単位(プロセス間で干渉をしない)
- 資源割り当て単位(使用中は割り当てられたハードウェア資源を独占する)
マルチプロセスにすると、
main文での処理とは別に
fork()で生成した子プロセスが
プログラムされた処理を行います。
ここで子プロセスについて確認します。
子プロセスは親プロセスを
そのまま継承します。
ファイルディスクリプタや変数などは
全く同じものをコピーしますが、
生成後は別々のものとして扱います。
別々のものになるのでこれらは、
メモリ上に新たにセットされます。
ややこしいのは、命令がセットされた
メモリ領域(コードセグメント)は
同一の領域を使用します。
親プロセスでも子プロセスでも
逆アセンブルすると同じ領域が
指し示されます。
※コード(テキスト)セグメントの話です。
main文や子プロセスの処理関数は、
親プロセスからでも子プロセスからでも
同じメモリ領域を指す、という意味です。
まずは実際にマルチプロセス化してみましょう。
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 |
#include<unistd.h> #include<stdlib.h> #include<stdio.h> #include<sys/wait.h> int varA; void processFunc(int n){ int varB; varA = 50; varB = 100*n; printf("Child: varA=%d, varB=%d\n",varA, varB); exit(0); } int main(void){ pid_t pid; int varB; varA = 1; varB = 2; printf("Parent: varA=%d, varB=%d\n", varA, varB); if((pid = fork())==0){ processFunc(1); } sleep(2); printf("Parent: varA=%d, varB=%d\n", varA, varB); waitpid(pid, NULL, 0); return 0; } |
fork()で子プロセスを作成します。
forkが呼び出された時点で、
親プロセスが子プロセスに複製され、
並列処理が始まります。
forkは子プロセスの複製に成功したら、
親プロセスには子プロセスのPIDを
子プロセスにはPID=0を返します。
ややこしいですが、fork()は
通常の関数とは異なり、1コールで
2回の返り値が戻ってきます。
なので実用的なプログラムでは、
fork後の処理を親プロセスと
子プロセスの両方に分けて書きます。
また子プロセスは、_exit (exit) を読んだら、
その時点で終了となります。
その際に親プロセスには、子プロセスが
終了した連絡が届きます。
これにはプロセス間通信で用いるシグナルの
SIGCHLDが親プロセスに送信されます。
その時に親はwait()、waitpid()をして、
子プロセスが終了したことを確信してから
親プロセスもreturnで終了します。
上のコードを実行すると以下になります。

本来はグローバル変数として宣言した
varAが共有されるイメージですが、
マルチプロセスではメモリ空間が
異なるので、別物として扱います。
そのためvarAに値を代入しても
他プロセスのvarAには影響を与えません。
それではgdbでデバッグしてみましょう。
子プロセスをgdbでデバッグしてみる
gccコンパイルするときに
-qオプションを付けます。
まずは親プロセスを追ってみます。
親プロセスはmain文です。
GDBが起動したら
disassemble mainで
逆アセンブルしてみます。

スタック領域を覗くために、
子プロセスを作成した次の段階で、
breakポイントを置きます。

listでコードを表示します。
break 数字でブレイクポイントを
置く場所を設定します。
今回は30行目に置きました。

run で実行します。
30行目でプログラムが止まります。
この段階でスタック領域を見てみます。
info register rip rsp rbpをタイプします。
rip : プログラムカウンタ
rsp : スタックポインタ(スタックの先頭)
rbp : ベースポインタ(スタックの末端)
すると各レジスタに格納してある、
物理アドレスが表示されます。
main(親プロセス)のスタック領域は
0x7fffffffe190から0x7fffffffe1a0
の領域に確保しています。
x/s &varA でvarAグローバル変数が
セットされたアドレスと中身の値を見ます。
これは、mainの命令がセットされた
コードセグメントに近い値を
示していることが分かります。
※varAは実際には、
bssセグメントに格納されています。
次に x/s &varB とタイプして、
varBローカル変数を表示します。
アドレスが 0x7fffffffe19c で
スタック領域にしっかりと
格納されいることが分かりますね。
それでは子プロセスの
スタック領域、varAとvarBについて
同様に表示してみましょう。
このままでは親プロセスしか
見えないので、子プロセスに変更します。
set follow-fork-mode child
とタイプします。

まずは子プロセスを逆アセンブルします。
disassemble processFuncで
子プロセスの命令がセットされた領域を
覗けます。(コードセグメント)

すると親プロセス(main)の命令に
ほぼ連続で下位アドレス側に
格納されていますね。
続いて肝心のvar変数たちを
見てみましょう。
ブレイクポイントは、
子プロセスが終了する直前の
14行目に指定して実行しています。

子プロセスのvarAグローバル変数は、
あれ・・・・・?
親プロセスと同じアドレスですね(汗
値は確かに0x32=50なので
良いのですが、なぜでしょうか。
コピーオンライト機能が
まだ継続しているのでしょうか。うーん。
ちなみにコピーオンライトとは、
子プロセスを作成しても実際に、
メモリアドレスに書き込みが
行われるまでは親プロセスと
アドレス空間を共有する機能です。
書き込みが行われて初めて、
子プロセスのみが保有する
物理メモリ領域が新たに作成されます。
もう少しアドレス空間の勉強を
深める必要があるようですね。
一方でvarBを見てみましょう。
子プロセス(processFunc)の
スタック領域の中にちゃんとありました。
中の値も0x64=100で正しいです。
ちなみにrip rsp rbpレジスタが
保持しているアドレスも出力しました。
親プロセス(main)とは別に、
下位アドレス側に向かって
スタックされていることが分かります。
以上です。
最後まで読んでいただきありがとうございました。