dombri-dev.diary

ぺーぺークラウドインフラエンジニアの奮闘記。

Linux Day4:プロセス管理

f:id:dombri:20191117194128j:plain
こんにちは。

ぼちぼち進めている「Linuxのしくみ」を読み進める会(会員ひとり)、4日目です。

仕事の昼休みに読み進めているんですが、時々飲み込みづらい(言っていることはわかるんだが頭に入ってこない)内容があって、ひどいときは1ページも進めずにおわることもあります。大分のんびりやっています。

今日の内容は以下です。

  • プロセス生成の目的
    • 同じプログラムの処理を分けるfork()
    • 全く別のプログラムを生成するexecve()
  • 実行ファイルの正体

3章「プロセス管理」を読む

f:id:dombri:20191127224107j:plain

プロセス・・・各種プログラムを実行する単位。ソフトウェアは1つまたは複数のプロセスからなっている。

ここでの「プロセス管理」とは、カーネルによって行われるプロセスの生成・削除の機能のことを指す。まずは仮想記憶を考慮しない挙動について学ぶ。


プロセス生成の目的

生成する目的は大きく2つ。

1.同じプログラムの処理を複数プロセスに分けて処理する
 ex. Webサーバの複数リクエスト受付処理

2.全く別プログラムを処理する
 ex. bash から各プログラムの新規作成

それぞれを達成するための関数が用意されている。


fork()関数

  • 1. の場合に用いられる関数
  • fork()を発行したプロセスをもとにして、新しいプロセスを1つ生成する(↑の図左)
  • 復帰時に親プロセスには子プロセスのPIDを、子プロセスには0を返す

実際にプロセスの生成の様子を確認してみます。

①プロセスの新規作成

②親プロセスが自分のPID+子PIDを出力して終了
 子プロセスは自分のPIDを出力して終

# 上記の特性を利用して、fork.c というプログラムを作成しておく
$ cc -o fork fork.c

$ ./fork
I am parent! my pid is 448 and the pid of my child is 449.
I am child! my pid is 449.


execve()関数

  • ②の場合に用いられる関数
  • プロセス数が増えるのではなく、あるプロセスを別のプロセスで置き換える

①実行ファイルの読み出しが行われ、プロセスのメモリマップに必要情報を読み出す

②現在のプロセスのメモリを新しいプロセスのデータで上書き

③新しいプロセスの最初の命令から実行

・・・うーん。ここがいまいち飲み込めなかったんです。読み出すってなんや。

よくわからないから確かめてみる fork()

この部分、見開きで順を追って図解してくれる徹底ぶりなんですけど、本当によくわからなかった。というわけで、実際に手を動かして確かめてみます。

Linuxの実行ファイルはExecutable and Linkable Format(ELF)というフォーマットで、readelfコマンドで確認できます。

$ readelf -h /bin/sleep
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1a50
  Start of program headers:          64 (bytes into file)
  Start of section headers:          33672 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

「Entry point address」という項目の「0x1a50」が、このプログラムのエントリポイント(最初に実行する命令のメモリアドレス)です。ほお。

オプションを変えてもう一度実行します。

$ readelf -S /bin/sleep
There are 30 section headers, starting at offset 0x8388:
Section Headers:
:
  [14] .text             PROGBITS         0000000000001790  00001790
       0000000000003959  0000000000000000  AX       0     0     16
:
  [26] .data             PROGBITS         00000000002081c0  000081c0
       0000000000000080  0000000000000000  WA       0     0     32
:

ずらずらっと表示が出てきているが、ここで大切なのは

  • 2行で1組の出力になっている
  • 数値は16進数
  • .text はコード領域の情報、 .dataはデータ領域の情報
  • その他は以下
    • 1行目4フィールド:メモリマップの開始アドレス
    • 1行目5フィールド:ファイル内オフセット
    • 2行目1フィールド:サイズ

なので、/bin/sleep の場合は以下。

領域
コード領域・ファイルオフセット 0x1790
コード領域・サイズ 0x3959
コード領域・メモリマップ開始アドレス 0x1790
データ領域・サイズ 0x80
データ領域・メモリマップ開始アドレス 0x2081c0


さらに、プログラム実行時に作成されたプロセスのメモリマップは/proc/[pid]/mapsで得られる。

$ /bin/sleep 10000 &
[1] 364

$ cat /proc/364/maps
5568b9f65000-5568b9f6c000 r-xp 00000000 08:01 285207                     /bin/sleep
:
5568ba16d000-5568ba16e000 rw-p 00008000 08:01 285207                     /bin/sleep
  • 1つめがコード領域、2つめがデータ領域を表している
  • それぞれメモリマップ範囲内に収まっていることが分かる

・・・ん( 一一)?

どこをどう読んだらそう解釈できるんだ???

フィールドの意味は以下。

①メモリ・セグメントの開始番地と終了番地。
②アクセス許可。r(read), w(write), x(executable), p(private), s(shared)
③オフセット
④ブロック・デバイスのメジャー番号とマイナー番号。 8:2 なら、メジャー番号が、8、マイナー番号が2の意味。 デバイスに結びついていない場合には、00:00 になる。
⑤ファイルのinode番号。
⑥ファイル名。

http://www.coins.tsukuba.ac.jp/~yas/coins/os2-2010/2011-01-25/  より引用

これは・・・収まってないやつだ・・・なぜだ・・・?
ひとまず読み進めます。

sleepプロセスはころしちゃいましょう。

$ kill 364


execve() も確かめてみる

別のプロセスを新しく生成する場合は、親プロセスからfork()を発行 -> 復帰後に子プロセスがexec() を呼ぶ、という流れ(fork and exec)になることが多いらしい。

①プロセスを新規生成

②親プロセスが「echo hello」プログラムを生成後、PIDと子プロセスのPIDを出力して終了  子プロセスはPIDを出力して終了

というプログラムを実行してみる。

# 上記の特性を利用して、fork.c というプログラムを作成しておく
$ cc -o fork-and-exec fork.c

$ ./fork-and-exec
I am parent! my pid is 403 and the pid of my child is 404.
I am child! my pid is 404.
$ hello

終了処理には_exit()関数を用いる。

※直接呼び出すことは少ない。C言語ではexit()関数を呼び出し、自身の終了処理を呼び出したうえで_exit()を呼び出す。

今回のまとめ

一点腑に落ちない箇所(/proc/pid/mapsの挙動)が発生してしまったけど、これはVMで確認しているから起きる現象な気がしている。

こんな情報、よほど滅多なことなことない限り検証しないだろうなと思いつつ楽しかったです。