今日もLinuxと戯れています(第2章プロセス管理に突入)
こんにちは。Macを使う時間の方が圧倒的に増えたせいか、今日も私物Windows11のショートカットとマウスホイール操作で混乱しています。脳のスイッチが発生するので、これは良い脳トレ!(ほんとに?
今日は、Linuxのプロセス管理入門
Macに慣れてきたところですが、引き続き「試して理解Linuxのしくみ」でLinuxと仲良くなるため、実際に試しながら読み進めます。
1章の残りlibcについてもう少し
libcがシステムコールのラッパー関数を提供しているので、高級言語からアーキテクチャに依存しないでシステムコールできることを学習したあと、静的ライブラリと共有ライブラリについて確かめました。C言語で書かれたpause()システムコールを実行するだけの短いサンプルコードをコンパイルして比較します。まずは、静的リンクした場合について確認します。
$ cc -static -o pause pause.c
$ ls -l pause
-rwxr-xr-x 1 tetrapod tetrapod 871832 Jun 9 09:13 pause
$ ldd pause
not a dynamic executable
$
静的リンクでコンパイルしたので、プログラムサイズは、871,832バイト(900KiB弱)、共有ライブラリはリンクされていないことが確認できました。今度は、共有ライブラリを使用するようにコンパイルし直します。
$ cc -o pause pause.c
$ ls -l pause
-rwxr-xr-x 1 tetrapod tetrapod 16696 Jun 9 09:26 pause
$ ldd pause
linux-vdso.so.1 (0x00007fffe4f79000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1be46ed000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1be4908000)
$
プログラムサイズは、16,696バイト(16KiB強)、/lib/x86_64-linux-gnu/libc.so.6を動的リンクしていることが確認できました。前職でWindows用のデスクトップアプリケーションを開発していたとき、実行環境のライブラリのバージョン違いで動作が変わることがあって、あえて静的リンクしてリリースしたことがあったなぁとか、いろいろ思い出しました。ライブラリのリンクの仕方それぞれにメリット、デメリットがあるので目的に合った使い分けができれば良いというのは変わりないですね。Goプログラムは、必ず静的リンクしているというのは知りませんでした。実行形式単体で扱えたり、起動が高速、依存関係の解決に悩まされずに済むというメリットの方が大きいということですね。背景が変わるとトレンドも変わりそうなので、将来また共有リンクの方が好まれる時代が来るのかも?
fork()でプロセスを生成してみる
現在の実行環境(Windows 11 WSL2(Ubunts 20.04)で、実行中のプロセスの数を確認してみます。
$ ps aux --no-header | wc -l
74
74個のプロセスがあることがわかりました。こんな風にパイプでコマンドの出力を次の入力にしてシュッと目的の結果が取得できるようになりたいものです。(やはり、ワンライナーの修行が必要)
プロセスの生成について、まずはfork()から学習します。今度はpythonの@うログラムなので、実行権限を付与して実行しました。
$ ./fork.py
親プロセス:pid=9508,子プロセスのpid9509
子プロセス: pid=9509,親プロセスのpid=9509
あれ?思った結果と違う…
print("子プロセス: pid={},親プロセスのpid={}".format(os.getpid(), os.getpid()))
^^^^^^^
子プロセスと親プロセスのpidを取得したいのに、どちらも対象プロセスのpidを取得していました。getpid()を、getppid()に修正して再実行します。
print("子プロセス: pid={},親プロセスのpid={}".format(os.getpid(), os.getppid()))
^^^^^^^
今度は、思った通りの結果が出力できました。
$ ./fork.py
親プロセス: pid=10654,子プロセスのpid=10655
子プロセス: pid=10655,親プロセスのpid=10654
親プロセスのpidは、getppid()で。覚えた。fork.pyは、os.fork()を実行して、戻り値が0なら子プロセスのpidと親プロセスのpidを返し、戻り値が0以外なら親プロセスなので、親プロセスのpidと退避しておいた子プロセスのpidを出力しています。os.fork()の実行後、親プロセス、次に子プロセスが生成され、それぞれの復帰のタイミングでpidを出力していることが確認できまし
次にexecve()を試します。
execbve()でプロセスを生成してみる
今度は、fork()したプロセスが子プロセスだったら、echoコマンドをexecve()するプログラムを実行してみます。
$ ./fork-and-exec.py
親プロセス: pid=18600, 子プロセスの pid=18601
子プロセス: pid=18601, 親プロセスの pid=18600
pid=18601 からこんにちは
親プロセスと子プロセスの復帰の順番は同じですが、最後に子プロセスがexecve()を呼び出してechoに置き換えられるところが違います。fork()は知っていたのですが、execve()は知らなかったので、解説と挙動の図解で理解することができました。(図解ありがたい)
Linuxの実行ファイルが実行に必要な情報を保持する方法を確認するために、puaseを-no-pieオプションでコンパイルして、情報を見てみます。
$ readelf -h pause
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401050
Start of program headers: 64 (bytes into file)
Start of section headers: 14648 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
pauseの開始アドレスは、0x401050であることがわかりました。コードとデータのファイル内オフセットも、見てみます。
$ readelf -S pause
There are 31 section headers, starting at offset 0x3938:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400318 00000318
000000000000001c 0000000000000000 A 0 0 1
:
[29] .strtab STRTAB 0000000000000000 00003648
00000000000001ca 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00003812
000000000000011f 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
次に、pauseのメモリマップを確認します。
$ ./pause &
[1] 27910
$ cat /proc/27910/maps
00400000-00401000 r--p 00000000 08:20 255119 /home/tetrapod/StudyForLinux/list-01-05/pause
00401000-00402000 r-xp 00001000 08:20 255119 /home/tetrapod/StudyForLinux/list-01-05/pause
00402000-00403000 r--p 00002000 08:20 255119 /home/tetrapod/StudyForLinux/list-01-05/pause
00403000-00404000 r--p 00002000 08:20 255119 /home/tetrapod/StudyForLinux/list-01-05/pause
00404000-00405000 rw-p 00003000 08:20 255119 /home/tetrapod/StudyForLinux/list-01-05/pause
7f07162c7000-7f07162e9000 r--p 00000000 08:20 304280 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f07162e9000-7f0716461000 r-xp 00022000 08:20 304280 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f0716461000-7f07164af000 r--p 0019a000 08:20 304280 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f07164af000-7f07164b3000 r--p 001e7000 08:20 304280 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f07164b3000-7f07164b5000 rw-p 001eb000 08:20 304280 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f07164b5000-7f07164bb000 rw-p 00000000 00:00 0
7f07164dd000-7f07164de000 r--p 00000000 08:20 304273 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f07164de000-7f0716501000 r-xp 00001000 08:20 304273 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f0716501000-7f0716509000 r--p 00024000 08:20 304273 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f071650a000-7f071650b000 r--p 0002c000 08:20 304273 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f071650b000-7f071650c000 rw-p 0002d000 08:20 304273 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f071650c000-7f071650d000 rw-p 00000000 00:00 0
7ffe39661000-7ffe39683000 rw-p 00000000 00:00 0 [stack]
7ffe396a8000-7ffe396ac000 r--p 00000000 00:00 0 [vvar]
7ffe396ac000-7ffe396ae000 r-xp 00000000 00:00 0 [vdso]
$
メモリマップを確認したので、先程バックグラウンドで実行したpauseのプロセスをkillします。
$ kill 27910
[1]+ Terminated ./pause
今回メモリマップの確認用に使用したコンパイルオプション-no-pieは、セキュリティ強化のためのASLRを無効化して、毎回同じアドレスにマッピングするためのもの。こういう背景がわからないと、「えっ、思っていた結果と違う!?」と混乱してしまうので、大変ありがたいなと思いました。
まとめ
今週は、Linuxのしくみを理解するために、ちょっとだけコマンドを試してみました。まだまだ理解できていないことがたくさんあるので、引き続き試していきたいと思います。
この記事が気に入ったらサポートをしてみませんか?