Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 読書メモ
1章 コンピュータシステムの概要
CPUにはカーネルモードとユーザモードがある
カーネルモードで動作している時のみ、デバイスにアクセスできるようになっている
デバイスドライバはカーネルモードで動作して、プロセスはユーザモードで動作する
これにより、デバイスを複数のプロセスが同時に操作することがなくなる
カーネルモードで動作するもの
プロセス管理システム
プロセススケジューラ
メモリ管理システム
これらOSの核となる処理をまとめたプログラムをカーネルと呼ぶ
プロセスは、これらの機能を使いたければ、システムコールを通じてカーネルに依頼する
2章 ユーザモードで実現する機能
システムコール
プロセス生成、削除
メモリ確保、解放
プロセス間通信
ネットワーク
ファイルシステム 操作
ファイル操作(デバイスアクセス)
CPUのモード遷移
プロセスは通常ユーザモード
カーネルに処理を依頼するためシステムコールを発行すると、CPUにおいて割り込みイベントが発生
CPUはこれを受けるとカーネルモードに遷移する
終了すると再びユーザモードに
システムコール呼び出しの様子
straceを使う。macOSならdtrussがいいらしい
code:hello.c
int main(void) {
puts("hello world");
return;
}
code:bash
$ strace -o hello.log ./hello
https://gyazo.com/e0451ca8410bdda67815311c1cbc757d
straceの出力は1つのシステムコール発行が1行に対応している
重要なのは①で、ここではwrite()システムコールによってhello world\nという文字列を画面出力している
code:hello.py
print("Hello World")
https://gyazo.com/cb6526fbb8d958681a22c916ec757432
大事なのは②で、こちらもwrite()システムコールが呼ばれる
実験
sarコマンドで、プロセスがどちらのモードで実行しているのかの割合を出せる
https://gyazo.com/62545dd1bcd4084307664754d9880e72
CPUがたくさんあるのは、マルチコアだから
%userと%niceがユーザモードの割合
親プロセスのプロセスIDを得るという単純なシステムコールgetppid()を無限ループするプログラムを作ってみる
code:ppidloop.c
int main(void) {
for (;;)
getppid();
}
https://gyazo.com/f3195d5e85734230e387f86d682132b1
②が当該のプログラム
72%が、このプログラムの親プロセスを取得するカーネルの処理
一概にはいえないが、%systemが大きい値の場合は、無闇にシステムコールを発行しすぎているかも
システムコールの所要時間
strace -Tを使うと、かかった時間を採取できる
%systemが高い時は、これで具体的にどのシステムコールに時間がかかっているのかに使える
システムコールのラッパー関数
システムコールは、通常の関数呼び出しと違って、C言語などの高級言語から直接呼び出せない。
アーキテクチャ依存のアセンブリコードを使って呼び出す
例えば、x86_64アーキテクチャのgetppid()は以下のように発行する
code:asm
mov $0x6e,%eax
syscall
普段アセンブリ書かない人は、読めなくてもOK
なんか普通のコードと違うなって雰囲気だけ感じ取れば
普通には使えないので、OSはシステムコールを呼び出すだけのラッパー関数を用意している
ラッパー関数はアーキテクチャ毎に存在している
標準Cライブラリ
C言語にはISOによって定められた標準ライブラリがある
通常はGNUが提供するglibcを標準ライブラリとして使用する
lddコマンドを使うと、プログラムがどのようなライブラリとリンクしてるかがわかる
macOSならotool -L
試しにechoコマンドをlddしてみる
https://gyazo.com/d70a41412699d171cc87462724284581
macOSならこう
code:zsh
❯ otool -L /bin/echo
/bin/echo:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
Linuxの場合はlibcという標準ライブラリがリンクされていることがわかる
OSが提供するプログラム
システムの初期化:init
OS の挙動を変える:sysctl、nice、sync
ファイル操作:touch、mkdir
テキストデータの加工:grep、sort、uniq
性能測定:sar、iostat
コンパイラ:gcc
スクリプト言語実行環境:perl、python、ruby
シェル:bash
ウィンドウシステム:X
3章 プロセス管理
2段階のプロセス生成
Linuxにおいて、プロセス生成は2つの目的がある
同じプログラムの処理を複数のプロセスに分けて処理する
全く別のプログラムを生成する
これらの目的のため、Linuxにはfork()とexecve()という2つの関数がある
内部的に、それぞれclone()、execve()というシステムコールに対応
fork()
同じプログラムの処理を複数のプロセスに分けて処理する目的として使う
発行したプロセスをもとに、新たに子プロセスを1つ生成する
発行の流れ
子プロセス用メモリ領域を確保して、親プロセスのメモリをコピー
親プロセスと子プロセスは違うコードを実行するように分岐する
これにはfork()の返り値が親プロセスと子プロセスで異なることを利用
code:fork.c
static void child() {
printf("I'm child! my pid is %d.\n", getpid());
exit(EXIT_SUCCESS);
}
static void parent(pid_t pid_c) {
printf("I'm parent! my pid is %d and the pid of my child is %d.\n", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void) {
pid_t ret;
ret = fork();
if (ret == -1)
err(EXIT_FAILURE, "fork() failed");
if (ret == 0) {
// 子プロセスの場合、fork()は0を返すのでここに到達する
child();
} else {
// 親プロセスの場合、fork()は1以上、具体的には子プロセスのpidを返すのでここに到達する
parent(ret);
}
err(EXIT_FAILURE, "shouldn't reach here");
}
code:zsh
❯ ./fork
I'm parent! my pid is 50311 and the pid of my child is 50312.
I'm child! my pid is 50312.
execve()
全く別のプログラムを生成する場合に使う
カーネルがそれぞれのプロセスを実行するまでの流れ
実行ファイルを読み出して、プロセスのメモリマップに必要な情報を読み出す
現在のプロセスのメモリを新しいプロセスのデータで上書きする
新しいプロセスの最初の命令から実行開始する
プロセスが増えるのではなく、あるプロセスを別のプロセスで置き換えている
https://gyazo.com/5e3528ea92535cacae8ed38a7bc1f958
もう少し具体的に
実行ファイルを読み出して、プロセスのメモリマップに必要な情報を読み出す
実行ファイルは、プロセスの実行中に用いるコードとデータ以外にも、以下のようなデータを保持
コードを含むデータ領域のファイル上オフセット、サイズ、及メモリマップ開始アドレス
コード以外の変数などデータ領域についての、上記と同じ情報
最初に実行する命令のメモリアドレス(エントリポイント)
例えば、これから実行するプログラムのファイルを以下のような構造になっているとする
https://gyazo.com/b9915ca589233b209df0ae7837b3f3fb
メモリマップ開始アドレスが必要な理由は、CPU上で実行される機械御命令は、特定のメモリアドレスを指定する必要があるから
https://gyazo.com/5b077512b9d9212a1d9d0eec5fe23284
https://gyazo.com/16def578635b87e01fae4d7d59d01f91
Linuxの実行ファイルは、Executable and Linkable Format(ELF)というフォーマットを利用する
ELFの各情報はreadelfコマンドで得られる
code:bash
root@28f9bd304eb7:/# 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: 0x2850
Start of program headers: 64 (bytes into file)
Start of section headers: 37336 (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: 30
Section header string table index: 29
-Sでファイル内のオフセット、サイズ、開始アドレスを得られる
code:bash
root@28f9bd304eb7:/# readelf -S /bin/sleep
There are 30 section headers, starting at offset 0x91d8:
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 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1
2 .note.gnu.propert NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8
3 .note.gnu.build-i NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4
4 .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4
5 .gnu.hash GNU_HASH 00000000000003a0 000003a0 00000000000000a8 0000000000000000 A 6 0 8
6 .dynsym DYNSYM 0000000000000448 00000448 0000000000000600 0000000000000018 A 7 1 8
7 .dynstr STRTAB 0000000000000a48 00000a48 000000000000031f 0000000000000000 A 0 0 1
8 .gnu.version VERSYM 0000000000000d68 00000d68 0000000000000080 0000000000000002 A 6 0 2
9 .gnu.version_r VERNEED 0000000000000de8 00000de8 0000000000000060 0000000000000000 A 7 1 8
10 .rela.dyn RELA 0000000000000e48 00000e48 00000000000002b8 0000000000000018 A 6 0 8
11 .rela.plt RELA 0000000000001100 00001100 00000000000003f0 0000000000000018 AI 6 25 8
12 .init PROGBITS 0000000000002000 00002000 000000000000001b 0000000000000000 AX 0 0 4
13 .plt PROGBITS 0000000000002020 00002020 00000000000002b0 0000000000000010 AX 0 0 16
14 .plt.got PROGBITS 00000000000022d0 000022d0 0000000000000010 0000000000000010 AX 0 0 16
15 .plt.sec PROGBITS 00000000000022e0 000022e0 00000000000002a0 0000000000000010 AX 0 0 16
16 .text PROGBITS 0000000000002580 00002580 0000000000003692 0000000000000000 AX 0 0 16
17 .fini PROGBITS 0000000000005c14 00005c14 000000000000000d 0000000000000000 AX 0 0 4
18 .rodata PROGBITS 0000000000006000 00006000 0000000000000f6c 0000000000000000 A 0 0 32
19 .eh_frame_hdr PROGBITS 0000000000006f6c 00006f6c 00000000000002b4 0000000000000000 A 0 0 4
20 .eh_frame PROGBITS 0000000000007220 00007220 0000000000000d18 0000000000000000 A 0 0 8
21 .init_array INIT_ARRAY 0000000000009bb0 00008bb0 0000000000000008 0000000000000008 WA 0 0 8
22 .fini_array FINI_ARRAY 0000000000009bb8 00008bb8 0000000000000008 0000000000000008 WA 0 0 8
23 .data.rel.ro PROGBITS 0000000000009bc0 00008bc0 00000000000000b8 0000000000000000 WA 0 0 32
24 .dynamic DYNAMIC 0000000000009c78 00008c78 00000000000001f0 0000000000000010 WA 7 0 8
25 .got PROGBITS 0000000000009e68 00008e68 0000000000000190 0000000000000008 WA 0 0 8
26 .data PROGBITS 000000000000a000 00009000 0000000000000080 0000000000000000 WA 0 0 32
27 .bss NOBITS 000000000000a080 00009080 00000000000001b8 0000000000000000 WA 0 0 32
28 .gnu_debuglink PROGBITS 0000000000000000 00009080 0000000000000034 0000000000000000 0 0 4
29 .shstrtab STRTAB 0000000000000000 000090b4 000000000000011d 0000000000000000 0 0 1
出力は全て2行で1組
数値は全て16進数
1行目の第二フィールドが.textなのがコード領域、.dataなのがデータ領域の情報
https://gyazo.com/4ea8aa2bb2fa4ac29f5b4693af7ce7cd
プログラム実行時に作成されたプロセスのメモリマップは/proc/:pid/mapsというファイルによって得られる
code:bash
root@28f9bd304eb7:/# /bin/sleep 10000 &
root@28f9bd304eb7:/# cat /proc/311/maps
55768273f000-557682741000 r--p 00000000 fe:01 4458692 /usr/bin/sleep
557682741000-557682745000 r-xp 00002000 fe:01 4458692 /usr/bin/sleep
557682745000-557682747000 r--p 00006000 fe:01 4458692 /usr/bin/sleep
557682748000-557682749000 r--p 00008000 fe:01 4458692 /usr/bin/sleep
557682749000-55768274a000 rw-p 00009000 fe:01 4458692 /usr/bin/sleep
全く別のプロセスを新規作成する場合は、親となるプロセスからfork()を発行して、復帰後に子プロセスがexec()を呼ぶという、いわゆるfork and execの流れになることが多い
https://gyazo.com/5db85df0f48b05e3bf6cd6f82bcb8c57
code:fork-and-exec.c
static void child() {
char *args[] = { "/bin/ech", "hello", NULL};
printf("I'm child! my pid is %d.\n", getpid());
fflush(stdout);
execve("/bin/echo", args, NULL);
err(EXIT_FAILURE, "exec() failed");
}
static void parent(pid_t pid_c) {
printf("I'm parent! my pid is %d and the pid of my child is %d.\n", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void) {
pid_t ret;
ret = fork();
if (ret == -1)
err(EXIT_FAILURE, "fork() failed");
if (ret == 0) {
child();
} else {
parent(ret);
}
err(EXIT_FAILURE, "shouldn't reach here");
}
code:bash
❯ ./fork-and-exec
I'm parent! my pid is 59622 and the pid of my child is 59623.
I'm child! my pid is 59623.
hello
終了処理
プログラム終了には_exit()関数を使用する
内部的にはexit_group()システムコール
通常は_exit()を呼び出すことは少なく、標準Cライブラリのexit()関数を呼び出すことが多い
この場合、標準Cライブラリは、自身の終了処理を呼び出した上で_exit()関数を呼び出す
main()関数から復帰した場合も、同様の挙動をする
https://gyazo.com/ed790215fe81b0f8c5ba5e932a8e99d7