試して理解 Linuxのしくみ 読みました 1章~3章
上記を読んだので学んだことをアウトプットしたいと思います。大学の授業で低レイヤのことを学んできませんでした。そのため、本書でLinuxに関して体系的に学べたの非常に良かったです。
本書の中で手を動かすパートに関しては、C言語やPythonの代わりにGo言語を使って試してみてます。また、PCはMacを使っているのですが、Linuxの環境で試したいという気持ちが有ったので、Dockerで仮想的にDebian系のOS上で動かすようにしています。リポジトリは以下になります。
第1章概要
ハードウェアの動作の流れ
1.外部デバイスから処理を依頼される
- ディスプレイ・マウス・キーボードなどの入出力デバイス
- ネットワークアダプタ
2.メモリに存在する命令を読み出してCPUにおいて実行
3.出力する
4.1に戻る
コンピュータの起動
- BIOSやUEFIと呼ばれるハードウェア組込ソフトウェアによるハードウェア初期化処理
- 起動させるOSを選択するブートローダが動作
- ストレージデバイスからOSを読み出す コンピュータにとってストレージデバイスは欠かせない存在
2章ユーザモードで実現する機能
ユーザモードとカーネルモード
デバイスの操作やプロセス管理、メモリ管理、プロセススケジューラなど、通常のプロセスから実行できると困る、OSの核となる処理をまとめたプログラムをカーネルという。
これらは特権モード、即ちカーネルモードで動作する。
OS=カーネルではなく、カーネル以外にもユーザモードで動作する様々なプログラムから構成される。
ユーザモードのプロセス処理から、システムコールを通じてカーネルの処理を呼び出す。それを呼び出すのは、プロセス固有のコードやそれが使用するライブラリ(OS提供のもの・そうでないもの)
CPUのモード遷移
システムコールを発行すると、CPU割り込みが発生。これによって、CPUでは、ユーザモードからカーネルモードに遷移して、カーネルの処理を行う。終わったらユーザモードに戻る。
システムコールの呼び出し
sh main.sh -d 02-syscall-and-non-kernel-os/hello
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。
-------------------------PROGRAM START------------------------- hello world --------------------------PROGRAM END-------------------------- -------------------------SYSCALL START------------------------- execve("/go/bin/app", ["/go/bin/app"], [/* 7 vars */]) = 0 <0.000401> arch_prctl(ARCH_SET_FS, 0x54c6b0) = 0 <0.000087> ***(省略)******************************************************* fcntl(2, F_GETFL) = 0x1 (flags O_WRONLY) <0.000139> write(1, "hello world\n", 12) = 12 <0.000149> exit_group(0) = ? +++ exited with 0 +++ --------------------------SYSCALL END--------------------------
下から3行目のwriteがデータを画面やファイルに出力する「write()」システムコール。省略している部分もシステムコールだが、それはプログラムの開始処理が発行するもの。
ユーザモードとカーネルモードの割合
自前のアプリケーションを走らせない場合
以下のようにUbuntuの環境でsar
コマンドを発行してみた
docker run --rm -it ubuntu:xenial /bin/bash # apt-get update # apt-get install -y sysstat # sar -P ALL 1 1
結果は以下のように得られた
12:36:45 CPU %user %nice %system %iowait %steal %idle 12:36:46 all 0.00 0.00 0.50 0.00 0.00 99.50 12:36:46 0 0.00 0.00 0.00 0.00 0.00 100.00 12:36:46 1 0.00 0.00 0.99 0.00 0.00 99.01 Average: CPU %user %nice %system %iowait %steal %idle Average: all 0.00 0.00 0.50 0.00 0.00 99.50 Average: 0 0.00 0.00 0.00 0.00 0.00 100.00 Average: 1 0.00 0.00 0.99 0.00 0.00 99.01
各行が1つのコアに対応する。今回は殆どがidleモードとなっている。
- %user -> ユーザモード
- %nice -> ユーザモード
- %system -> カーネルモード
- %idle -> 何もしていない時間
自前の無限ループするだけのアプリケーションを走らせた場合
sh main.sh -d 02-syscall-and-non-kernel-os/loop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。
Linux 4.9.93-linuxkit-aufs (aad086a81679) 01/14/19 _x86_64_ (2 CPU) 13:18:18 CPU %user %nice %system %iowait %steal %idle 13:18:19 all 50.00 0.00 0.50 0.00 0.00 49.50 13:18:19 0 0.00 0.00 1.00 0.00 0.00 99.00 13:18:19 1 100.00 0.00 0.00 0.00 0.00 0.00 Average: CPU %user %nice %system %iowait %steal %idle Average: all 50.00 0.00 0.50 0.00 0.00 49.50 Average: 0 0.00 0.00 1.00 0.00 0.00 99.00 Average: 1 100.00 0.00 0.00 0.00 0.00 0.00
CPUコア1上でユーザプロセス、すなわちloopプログラムが常に動作していることがわかる。
自前の親プロセス取得を無限ループするアプリケーションを走らせた場合
sh main.sh -d 02-syscall-and-non-kernel-os/ppidloop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。
Linux 4.9.93-linuxkit-aufs (4bbb02f02797) 01/14/19 _x86_64_ (2 CPU) 13:26:00 CPU %user %nice %system %iowait %steal %idle 13:26:01 all 20.71 0.00 30.30 0.00 0.00 48.99 13:26:01 0 41.00 0.00 59.00 0.00 0.00 0.00 13:26:01 1 0.00 0.00 1.02 0.00 0.00 98.98 Average: CPU %user %nice %system %iowait %steal %idle Average: all 20.71 0.00 30.30 0.00 0.00 48.99 Average: 0 41.00 0.00 59.00 0.00 0.00 0.00 Average: 1 0.00 0.00 1.02 0.00 0.00 98.98
CPUコア0上でppidloopプログラムを41%の割合で実行し、親プロセス取得のシステムコールを59%の割合で実行していた。
システムコールのラッパー関数
システムコールはC言語などの高級言語から直接呼び出せない。アーキテクチャ依存(amd64とかarmとか?)のアセンブリコードを使って呼び出す必要がある。もしシステムコールをラップする関数を内包するライブラリ群がなければ、アーキテクチャ依存のアセンブリソースを書かなくてはいけない。
標準Cライブラリ
GNUプロジェクトが提供するglibcをLinuxの標準Cライブラリとして使用する。glibcがシステムコールをラップする関数を内包している。またPOSIXという規格に定義されている関数も提供。
プログラムがどのようなライブラリをリンクしているかは以下でldd
コマンドでわかる。
% docker run --rm -it golang:1.11.4 /bin/bash # which go /usr/local/go/bin/go # ldd /usr/local/go/bin/go linux-vdso.so.1 (0x00007ffef2965000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fe2c2ba2000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe2c2803000) /lib64/ld-linux-x86-64.so.2 (0x00007fe2c2dbf000)
第3章プロセス管理
fork関数
同じプロセスの処理を複数のプロセスに分ける際に呼び出される(例:Apacheで子プロセスを生成する)。 子プロセス用メモリ領域を作成し、親プロセスのメモリをコピーする。fork関数はカーネルモードからユーザモードに処理が再び移るときに、親プロセスには子プロセスのプロセスIDを、子プロセスには0を返す。これを利用して親子で処理を分岐させる。
sh main.sh -d 03-process-management/fork
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。
fork()されたときに同じプログラムが複製されて2個同時に動いているような感覚。
I'm parent! my pid is 7 and the pid of my child is 11. I'm child! my pid is 11.
execve関数
全く別のプログラムを生成するときに呼び出される(例:bashからsleepコマンド実行)。 ます実行ファイルを読み出し、その後現在のプロセスのメモリを上書く。 実行ファイルの中身 * プロセスの実行に必要なコード、データ * コードを含むデータ領域のオフセット(ベースアドレスに加えられるアドレスの数)-> ファイルの情報 * コードを含むデータ領域のサイズ * コードを含むデータ領域のメモリマップ開始アドレス -> コンピュータのメモリのどこにマッピングするか * 最初に実行する命令のメモリアドレス(エントリポイント)
Linuxの実行ファイルはELFというフォーマットを使用する。
エントリポイントのアドレスを得るには「-h」オプション、ファイル内オフセット、サイズ、メモリマップ開始アドレスを得るには「-S」オプション
% docker run --rm -it ubuntu:xenial /bin/bash # apt-get update # apt-get install binutils # readelf -h /bin/sleep ELF Header: ------(省略)--------------------------------------------- Entry point address: 0x401760 ------(省略)--------------------------------------------- # readelf -S /bin/sleep There are 29 section headers, starting at offset 0x7370: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ------(省略)-------------------------------------------------------- [14] .text PROGBITS 00000000004014e0 000014e0 0000000000003319 0000000000000000 AX 0 0 16 ------(省略)-------------------------------------------------------- [25] .data PROGBITS 00000000006071c0 000071c0 0000000000000074 0000000000000000 WA 0 0 32 ------(省略)-------------------------------------------------------- Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
全く新規のプロセスを新規生成する場合には、親となるプロセスからfork()を発行して、復帰後に子プロセスがexec()を発行する。
sh main.sh -d 03-process-management/execve
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。fork()の直後にexec()が動いています。
sh main.sh -d 03-process-management/execve I'm parent! my pid is 6 and the pid of my child is 10. I'm child! my pid is 10. hello