MasaのITC Life

夢は起業家!全てにおいて凡人だけど頑張ることだけはいっちょ前!

プログラミング

【fork】Cでマルチプロセス化して、gdbで子プロセスをデバッグしてみた

投稿日:2020年2月12日 更新日:



マルチプログラミング(並列処理)を

実現するには様々な方法が存在します。

その一つにマルチプロセス化があります。



新たにプロセスを作成して、

それぞれのプロセスが並列に

処理を進めていく方法です。



新たにプロセスを作成するには、

fork()関数を使用します。

新しいプロセスは子プロセスと呼ばれ、

作成した側は親プロセスと呼ばれます。



ここでプロセスという単位は

どのような役割を持つか確認です。

  • 保護単位(プロセス間で干渉をしない)
  • 資源割り当て単位(使用中は割り当てられたハードウェア資源を独占する)



マルチプロセスにすると、

main文での処理とは別に

fork()で生成した子プロセスが

プログラムされた処理を行います。



ここで子プロセスについて確認します。

子プロセスは親プロセスを

そのまま継承します。



ファイルディスクリプタや変数などは

全く同じものをコピーしますが、

生成後は別々のものとして扱います。



別々のものになるのでこれらは、

メモリ上に新たにセットされます。



ややこしいのは、命令がセットされた

メモリ領域(コードセグメント)は

同一の領域を使用します。



親プロセスでも子プロセスでも

逆アセンブルすると同じ領域が

指し示されます。



※コード(テキスト)セグメントの話です。

main文や子プロセスの処理関数は、

親プロセスからでも子プロセスからでも

同じメモリ領域を指す、という意味です。




まずは実際にマルチプロセス化してみましょう。



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)とは別に、

下位アドレス側に向かって

スタックされていることが分かります。




以上です。

最後まで読んでいただきありがとうございました。


-プログラミング

Copyright© MasaのITC Life , 2023 All Rights Reserved Powered by STINGER.