読書メモ:[試して理解]Linuxのしくみ ― 実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識

読書メモ:[試して理解]Linuxのしくみ ― 実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識

読書メモ。

IT システムやソフトウェアの基盤 OS として幅広く使われている Linux。エンジニアとして Linux に関する知識はいまや必須とも言えますが、あなたはそのしくみや動作を具体的にイメージすることができるでしょうか。

本書では、Linux OS における、プロセス管理、プロセススケジューラ、メモリ管理、記憶階層、ファイルシステム、記憶階層、そして仮想化機能、コンテナなど、OS とハードウェアに関するしくみがどのように動くのか、実験とその結果を示す豊富なグラフや図解を用いてわかりやすく解説します。

改訂に際しては全面フルカラー化。グラフや図解がさらにわかりやすくなり、ソースコードは C 言語から、Go 言語と Python にアップデートしています。さらに仮想化、コンテナなどの章が加わりました。今どきの Linux のしくみを本書でしっかり理解しておきましょう。

目次 #

  • 序章:はじめに
  • 第 1 章:Linux の概要
  • 第 2 章:プロセス管理(基礎編)
  • 第 3 章:プロセススケジューラ
  • 第 4 章:メモリ管理システム
  • 第 5 章:プロセス管理(応用編)
  • 第 6 章:デバイスアクセス
  • 第 7 章:ファイルシステム
  • 第 8 章:記憶階層
  • 第 9 章:ブロック層
  • 第 10 章:仮想化機能
  • 第 11 章:コンテナ
  • 第 12 章:cgroup
  • 終章:本書で学んだことと今後への生かし方

序章:はじめに #

本書内での実験プログラムのソースコードはすべて実際に使う場面で紙面に掲載しているほか、下記で同じものを公開しています。

https://github.com/satoru-takeuchi/linux-in-practice-2nd

第 1 章:Linux の概要 #

カーネル #

マシンの電源を入れると最初にカーネルが起動します。それ以外のすべてのプログラムは、カーネルの後に起動します。起動後に動作中のプログラムのことをプロセスと呼びます。

CPU に備わっているモードという機能があります。パソコンやサーバで使われているような一般的な CPU にはカーネルモードとユーザーモードという2つのモードがあります(実際にはそれ以上のモードを持つ CPU がありますが、Linux カーネルは2つのモードしか使いません)。

CPU がカーネルモードであれば何の制限もないのに対して、ユーザモードで実行中ならば特定の命令を実行できないようにする、のような制約をかけられます。Linux の場合はカーネルのみがこのカーネルモードで動作して、デバイスにアクセスできます。それに対してプロセスはユーザモードで動作するため、デバイスにアクセスできません。プロセスがユーザーモードで実行しているとき、「プロセスはユーザランド(あるいはユーザ空間)で実行している」ということがあります。

このため、プロセスはカーネルを介して間接的にデバイスにアクセスします。システムコールとは、プロセスがカーネルに処理を依頼するための方法です。システムコールには次のようなものがあります。

  • プロセス生成、削除
  • メモリ確保、解法
  • 通信処理
  • ファイルシステム操作
  • デバイス操作

システムコールは、CPU の特殊な命令を実行することによって実現しています。プロセスは通常ユーザモードで実行していますが、カーネルに処理を依頼するためにシステムコールを発行すると、CPU において例外というイベントが発生します。これをきっかけとして、CPU のモードがユーザモードからカーネルモードに遷移し、依頼内容に応じたカーネルの処理が動き始めます。カーネル内のシステムコールが終了すれば、再びユーザモードに戻ってプロセスの動作を継続します。

システムコール発行の可視化 #

プロセスがどんなシステムコールを発行するかは、strace コマンドによって確認できます。

(Docker でコンテナを起動して、そのなかで実験する)

docker run --rm -it ubuntu:latest bash

(必要なパッケージをインストールする)

apt update
apt install -y vim python3 strace

(vim で次のファイルを作成する)

hello.py

#!/usr/bin/python3
print("hello world")

(実行権限を付ける)

chmod +x ./hello.py

(まずは普通に実行してみる)

./hello.py
> hello world

strace を介して実行する)

strace -o hello.py.log ./hello.py

トレース情報を見てみましょう。

cat hello.py.log
---

execve("./hello.py", ["./hello.py"], 0xffffe7bd78b0 /* 8 vars */) = 0
brk(NULL)                               = 0x7c36000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf83c51696000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=5397, ...}) = 0
mmap(NULL, 5397, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf83c51694000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/aarch64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=723144, ...}) = 0
mmap(NULL, 851992, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xf83c5158c000
mmap(0xf83c51590000, 786456, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51590000
munmap(0xf83c5158c000, 16384)           = 0
munmap(0xf83c51651000, 45080)           = 0
mprotect(0xf83c5163f000, 65536, PROT_NONE) = 0
mmap(0xf83c5164f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xaf000) = 0xf83c5164f000
mmap(0xf83c51590000, 716800, PROT_READ|PROT_EXEC|0x10, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51590000                                  close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/aarch64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832                                                          fstat(3, {st_mode=S_IFREG|0644, st_size=133432, ...}) = 0
mmap(NULL, 262312, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xf83c5154f000                                                      mmap(0xf83c51550000, 196776, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51550000
munmap(0xf83c5154f000, 4096)            = 0                                                                                                         munmap(0xf83c51581000, 57512)           = 0
mprotect(0xf83c5156b000, 81920, PROT_NONE) = 0                                                                                                      mmap(0xf83c5157f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f000) = 0xf83c5157f000
mmap(0xf83c51550000, 110592, PROT_READ|PROT_EXEC|0x10, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51550000                                  close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/aarch64-linux-gnu/libexpat.so.1", O_RDONLY|O_CLOEXEC) = 3                                                                read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=198912, ...}) = 0                                                                                           mmap(NULL, 327832, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xf83c514ff000
mmap(0xf83c51500000, 262296, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51500000
munmap(0xf83c514ff000, 4096)            = 0
munmap(0xf83c51541000, 57496)           = 0
mprotect(0xf83c51528000, 90112, PROT_NONE) = 0
mmap(0xf83c5153e000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2e000) = 0xf83c5153e000
mmap(0xf83c51500000, 163840, PROT_READ|PROT_EXEC|0x10, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51500000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\3101\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1788240, ...}) = 0
mmap(NULL, 1957440, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xf83c51322000
mmap(0xf83c51330000, 1891904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51330000
munmap(0xf83c51322000, 57344)           = 0
munmap(0xf83c514fe000, 7744)            = 0
mprotect(0xf83c514d9000, 81920, PROT_NONE) = 0
mmap(0xf83c514ed000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ad000) = 0xf83c514ed000
mmap(0xf83c514f2000, 48704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf83c514f2000
mmap(0xf83c51330000, 1740800, PROT_READ|PROT_EXEC|0x10, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xf83c51330000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf83c51692000
set_tid_address(0xf83c51692ba8)         = 2590
set_robust_list(0xf83c51692860, 24)     = 0
rseq(0xf83c51692f60, 0x20, 0, 0xd428bc00) = 0
mprotect(0xf83c514ed000, 12288, PROT_READ) = 0
mprotect(0xf83c5153e000, 8192, PROT_READ) = 0
mprotect(0xf83c5157f000, 4096, PROT_READ) = 0
mprotect(0xf83c5164f000, 4096, PROT_READ) = 0
mprotect(0xa9f000, 4096, PROT_READ)     = 0
mprotect(0xf83c5169b000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
getrandom("\x84\x78\xf0\x8c\x04\x32\x0d\xfb", 8, GRND_NONBLOCK) = 8
munmap(0xf83c51694000, 5397)            = 0
getrandom("\xbd\x54\x28\x20\xba\x3b\x22\xbc\xbd\xd8\x34\x3a\x3c\x37\x84\x38\xcc\xfb\xad\xff\x94\xf9\x1e\x35\x79\xbd\x30\x56\xb5\x7b\xff\x30", 32, GRND_NONBLOCK) = 32
openat(AT_FDCWD, "/proc/sys/vm/overcommit_memory", O_RDONLY) = 3
read(3, "0\n", 32)                      = 2
close(3)                                = 0
brk(NULL)                               = 0x7c36000
brk(0x7e00000)                          = 0x7e00000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/locale/C.UTF-8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/locale/C.utf8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=369120, ...}) = 0
mmap(NULL, 369120, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf83c512d5000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/aarch64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=27028, ...}) = 0
mmap(NULL, 27028, PROT_READ, MAP_SHARED, 3, 0) = 0xf83c5168b000
close(3)                                = 0
futex(0xf83c514f151c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
getcwd("/", 4096)                       = 2
getrandom("\x73\x30\x19\x49\xf2\xd9\x03\xb1\x89\x29\x1c\x20\x8b\xf6\x26\x18\xf4\xd5\x35\x01\x5d\x71\xc5\x73", 24, GRND_NONBLOCK) = 24
gettid()                                = 2590
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "00400000-00a8b000 r-xp 00000000 "..., 1024) = 1024
read(3, " 00000000 00:32 6359104         "..., 1024) = 1024
read(3, "-f83c5164f000 ---p 000af000 00:3"..., 1024) = 1024
read(3, "lib/aarch64-linux-gnu/ld-linux-a"..., 1024) = 174
close(3)                                = 0
sched_getaffinity(2590, 32, [0 1 2 3])  = 8
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf83c511d5000
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf83c51687000
openat(AT_FDCWD, "/usr/pyvenv.cfg", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/pyvenv.cfg", O_RDONLY) = -1 ENOENT (No such file or directory)
readlinkat(AT_FDCWD, "/usr/bin/python3", "python3.14", 4096) = 10
readlinkat(AT_FDCWD, "/usr/bin/python3.14", 0xffffe599d0c0, 4096) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, "/usr/bin/python3._pth", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/python3.14._pth", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/pybuilddir.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/bin/Modules/Setup.local", 0xffffe59a2090, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/bin/lib/python314.zip", 0xffffe59a1d70, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python314.zip", 0xffffe59a2090, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/bin/lib/python3.14/os.py", 0xffffe59a2090, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/bin/lib/python3.14/os.pyc", 0xffffe59a2090, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14/os.py", {st_mode=S_IFREG|0644, st_size=41959, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf83c510d5000
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=309, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=309, ...}) = 0
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\0\0\0\4\0\0\0\0"..., 4096) = 309
lseek(3, -176, SEEK_CUR)                = 133
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\0\0\0\4\0\0\0\0"..., 4096) = 176
lseek(3, 308, SEEK_SET)                 = 308
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python314.zip", 0xffffe59a1840, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python314.zip", 0xffffe59a1d10, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=12288, ...}) = 0
getdents64(3, 0x7d010e0 /* 199 entries */, 32768) = 6776
getdents64(3, 0x7d010e0 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.cpython-314-aarch64-linux-gnu.so", 0xffffe59a1d10, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.abi3.so", 0xffffe59a1d10, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.abi3-aarch64-linux-gnu.so", 0xffffe59a1d10, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.so", 0xffffe59a1d10, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.py", {st_mode=S_IFREG|0644, st_size=5945, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/__init__.py", {st_mode=S_IFREG|0644, st_size=5945, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/encodings/__pycache__/__init__.cpython-314.pyc", O_RDONLY|O_CLOEXEC) = 3
fcntl(3, F_GETFD)                       = 0x1 (flags FD_CLOEXEC)
fstat(3, {st_mode=S_IFREG|0644, st_size=6738, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "+\16\r\n\0\0\0\0\276\3773j9\27\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\6\0\0"..., 6739) = 6738
read(3, "", 1)                          = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/encodings", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7d0dcf0 /* 126 entries */, 32768) = 4264
getdents64(3, 0x7d0dcf0 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/aliases.py", {st_mode=S_IFREG|0644, st_size=15957, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/aliases.py", {st_mode=S_IFREG|0644, st_size=15957, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/encodings/__pycache__/aliases.cpython-314.pyc", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=12556, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "+\16\r\n\0\0\0\0\276\3773jU>\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\25\0\0"..., 12557) = 12556
read(3, "", 1)                          = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/utf_8.py", {st_mode=S_IFREG|0644, st_size=1005, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/encodings/utf_8.py", {st_mode=S_IFREG|0644, st_size=1005, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/encodings/__pycache__/utf_8.cpython-314.pyc", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2330, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "+\16\r\n\0\0\0\0\276\3773j\355\3\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\5\0\0"..., 2331) = 2330
read(3, "", 1)                          = 0
close(3)                                = 0
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_ONSTACK}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGXFSZ, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_ONSTACK}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGHUP, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGINT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGQUIT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGILL, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTRAP, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigprocmask(SIG_BLOCK, ~[], [], 8)   = 0
rt_sigaction(SIGABRT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigaction(SIGBUS, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGFPE, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGKILL, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGUSR1, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGSEGV, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGUSR2, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGPIPE, NULL, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_ONSTACK}, 8) = 0
rt_sigaction(SIGALRM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTERM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGSTKFLT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGCHLD, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGCONT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGSTOP, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTSTP, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTTIN, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTTOU, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGURG, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGXCPU, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGXFSZ, NULL, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_ONSTACK}, 8) = 0
rt_sigaction(SIGVTALRM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGPROF, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGWINCH, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGIO, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGPWR, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGSYS, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_2, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_3, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_4, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_5, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_6, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_7, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_8, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_9, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_10, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_11, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_12, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_13, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_14, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_15, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_16, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_17, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_18, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_19, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_20, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_21, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_22, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_23, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_24, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_25, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_26, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_27, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_28, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_29, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_30, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_31, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGRT_32, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x6ebd80, sa_mask=[], sa_flags=SA_ONSTACK}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
fcntl(0, F_GETFD)                       = 0
fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
ioctl(0, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
lseek(0, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
ioctl(0, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
fcntl(1, F_GETFD)                       = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
ioctl(1, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
lseek(1, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
ioctl(1, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
fcntl(2, F_GETFD)                       = 0
fstat(2, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
ioctl(2, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
lseek(2, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
ioctl(2, TCGETS2, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
newfstatat(AT_FDCWD, "/usr/bin/pyvenv.cfg", 0xffffe59a1540, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/pyvenv.cfg", 0xffffe59a1570, 0) = -1 ENOENT (No such file or directory)
geteuid()                               = 0
getuid()                                = 0
getegid()                               = 0
getgid()                                = 0
newfstatat(AT_FDCWD, "/root/.local/lib/python3.14/site-packages", 0xffffe59a1890, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7da5b10 /* 2 entries */, 32768) = 48
getdents64(3, 0x7da5b10 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7da5b10 /* 2 entries */, 32768) = 48
getdents64(3, 0x7da5b10 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/dist-packages", 0xffffe59a18c0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/sitecustomize.py", {st_mode=S_IFREG|0644, st_size=155, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/sitecustomize.py", {st_mode=S_IFREG|0644, st_size=155, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/__pycache__/sitecustomize.cpython-314.pyc", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=305, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "+\16\r\n\0\0\0\0\276\3773j\233\0\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\4\0\0"..., 306) = 305
read(3, "", 1)                          = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7da5b10 /* 50 entries */, 32768) = 3248
getdents64(3, 0x7da5b10 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7da6320 /* 2 entries */, 32768) = 48
getdents64(3, 0x7da6320 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/python3/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0x7da6320 /* 2 entries */, 32768) = 48
getdents64(3, 0x7da6320 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14", {st_mode=S_IFDIR|0755, st_size=12288, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3.14/lib-dynload", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/local/lib/python3.14/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/usr/lib/python3/dist-packages", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "//./hello.py", {st_mode=S_IFREG|0755, st_size=40, ...}, 0) = 0
openat(AT_FDCWD, "//./hello.py", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0755, st_size=40, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = 0
lseek(3, 0, SEEK_CUR)                   = 0
lseek(3, 0, SEEK_END)                   = 40
lseek(3, 0, SEEK_CUR)                   = 40
lseek(3, 0, SEEK_SET)                   = 0
read(3, "#!/usr/bin/python3\nprint(\"hello "..., 131072) = 40
read(3, "", 131032)                     = 0
lseek(3, 0, SEEK_SET)                   = 0
close(3)                                = 0
newfstatat(AT_FDCWD, "//./hello.py", {st_mode=S_IFREG|0755, st_size=40, ...}, 0) = 0
readlinkat(AT_FDCWD, "./hello.py", 0xffffe5990930, 4096) = -1 EINVAL (Invalid argument)
getcwd("/", 1024)                       = 2
readlinkat(AT_FDCWD, "/hello.py", 0xffffe59914c0, 1023) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, "//./hello.py", O_RDONLY) = 3
ioctl(3, FIOCLEX)                       = 0
fstat(3, {st_mode=S_IFREG|0755, st_size=40, ...}) = 0
ioctl(3, TCGETS2, 0xffffe59a2908)       = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
fstat(3, {st_mode=S_IFREG|0755, st_size=40, ...}) = 0
read(3, "#!/usr/bin/python3\nprint(\"hello "..., 4096) = 40
lseek(3, 0, SEEK_SET)                   = 0
read(3, "#!/usr/bin/python3\nprint(\"hello "..., 4096) = 40
read(3, "", 4096)                       = 0
close(3)                                = 0
write(1, "hello world\n", 12)           = 12
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_ONSTACK}, {sa_handler=0x6ebd80, sa_mask=[], sa_flags=SA_ONSTACK}, 8) = 0
munmap(0xf83c511d5000, 1048576)         = 0
munmap(0xf83c51687000, 16384)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

以下の行を見ると、write() システムコールが発行されていることが分かりました。

write(1, "hello world\n", 12)           = 12

なお、strace-T オプションを付けると、各種システムコールの処理にかかった時間をマイクロ秒の制度で採取できます。

write(1, "hello world\n", 12)           = 12 <0.000017>

ここでは hello world という文字列を出力するために 17 マイクロ秒かかったことが分かります。

(終わったら Docker イメージを削除しておく)

docker rmi ubuntu:latest

ライブラリ #

標準 C ライブラリ #

C 言語には、国際標準化機構(International Organization for Standardization, ISO)によって定められた標準ライブラリがあります。Linux でも、この標準 C ライブラリが提供されています。通常は GNU プロジェクトが提供する glibc を標準 C ライブラリとして使用します。glibc を指して libc と呼ぶこともあります。C 言語で書かれたほとんどすべてのプログラムは、libc をリンクしています。

プログラムがどのようなライブラリをリンクしているかは、ldd コマンドで確認できます。

試しに echo コマンドについて ldd の実行結果を見てみましょう。

ldd /bin/echo
> linux-vdso.so.1 (0x0000ec0fe197a000)
> libselinux.so.1 => /usr/lib/aarch64-linux-gnu/libselinux.so.1 (0x0000ec0fe18f0000)
> libgcc_s.so.1 => /usr/lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ec0fe18b0000)
> libm.so.6 => /usr/lib/aarch64-linux-gnu/libm.so.6 (0x0000ec0fe0d30000)
> libc.so.6 => /usr/lib/aarch64-linux-gnu/libc.so.6 (0x0000ec0fe0b60000)
> /lib/ld-linux-aarch64.so.1 (0x0000ec0fe193d000)
> libpcre2-8.so.0 => /usr/lib/aarch64-linux-gnu/libpcre2-8.so.0 (0x0000ec0fe0aa0000)

上記のうち、libc.so.6 が標準 C ライブラリを指します。

Python3 の処理系である python3 コマンドについても見てみましょう。

ldd /usr/bin/python3
> linux-vdso.so.1 (0x0000e6c6274d8000)
> libm.so.6 => /usr/lib/aarch64-linux-gnu/libm.so.6 (0x0000e6c6273d0000)
> libz.so.1 => /usr/lib/aarch64-linux-gnu/libz.so.1 (0x0000e6c627390000)
> libexpat.so.1 => /usr/lib/aarch64-linux-gnu/libexpat.so.1 (0x0000e6c627340000)
> libc.so.6 => /usr/lib/aarch64-linux-gnu/libc.so.6 (0x0000e6c627170000)
> /lib/ld-linux-aarch64.so.1 (0x0000e6c62749b000)

同じく libc がリンクされています。つまり Python プログラムを実行する時、内部的には標準 C ライブラリを使っているのです。

libc は標準 C ライブラリだけではなく、システムコールのラッパー関数というものを提供しています。システムコールは通常の関数呼び出しとは違って、C 言語などの高級言語から直接呼び出せません。アーキテクチャ依存(x84_64, arm64 など)のアセンブリコードを使って呼び出す必要があります。システムコールを発行するたびにアーキテクチャ依存のアセンブリコードを書いて、高級言語から呼び出さなくてはなりませんが、libc はシステムコールのラッパー関数と呼ばれる一連の関数を提供しています。高級言語で書かれたプログラムからそれらのラッパー関数を呼び出すだけで、内部的にシステムコールを呼び出してくれます。

静的ライブラリと共有(あるいは動的)ライブラリ #

ライブラリは静的ライブラリと共有(あるいは動的)ライブラリの2種類に分類できます。どちらも同じ機能を提供しますが、プログラムへの組み込み方が違います。

プログラムの生成時には、まずソースコードをコンパイルしてオブジェクトファイルというファイルを作ります。そのうえでオブジェクトファイルが使うライブラリをリンクして、実行ファイルを作ります。

静的ライブラリはリンク時に、ライブラリ内の関数をプログラムに組み込みます。

これに対して共有ライブラリは、リンク時には「このライブラリのこの関数を呼び出す」といった情報だけを実行ファイルに埋め込みます。そのうえでプログラムの起動時、あるいは実行中に、ライブラリをメモリ上にロードして、プログラムはその中の関数を呼び出します。

プログラムに libc をリンクさせた結果を見てみましょう。

pause.c

#include <unistd.h>
int main(void) {
    pause();
    return 0;
}

まずは静的ライブラリである libc.a を使う場合についてです。

cc -static -o pause pause.c

ls -l pause
> -rwxr-xr-x 1 root root 711816 Aug 14 13:42 pause

ldd pause
> not a dynamic executable

プログラムのサイズは 700KiB 強であり、共有ライブラリはリンクされていない(not a dynamic executable)ということが分かります。このプログラムは libc を内部に組み込んでいる状態となっているため、libc.a を削除しても動作します。

続いて共有ライブラリ libc.so を使う場合です。

cc -o pause pause.c

ls -l pause
> -rwxr-xr-x 1 root root 70424 Aug 10 21:48 pause

ldd pause
> linux-vdso.so.1 (0x0000f4d63cedb000)
> libc.so.6 => /usr/lib/aarch64-linux-gnu/libc.so.6 (0x0000f4d63cca0000)
> /lib/ld-linux-aarch64.so.1 (0x0000f4d63ce9e000)

サイズは 70KiB 強であり、libc を静的リンクした場合の十分の一の大きさとなっています。libc を動的にリンクしていることがわかります。libc を動的リンクしているため、libc.so を削除すると実行できなくなります。

静的ライブラリと共有ライブラリはどちらも一長一短ですが、次の理由によって過去ながらく共有ライブラリが主に使われてきました。

  • システム全体としてサイズを小さく抑えられる。
  • ライブラリに問題があった場合は共有ライブラリを修正版に置き換えるだけで、ライブラリを使用するすべてのプログラムを修正できる。

最近ではやや事情が変わってきました。これには次のような理由があります。例えば Go 言語は基本的にライブラリをすべてを静的リンクしています。

  • メモリやストレージの大容量化によってサイズの問題は相対的に小さくなった。
  • プログラムが1つの実行ファイルだけで動けば、当該ファイルをコピーするだけで別環境で動作するので扱いが楽。
  • 実行時に共有ライブラリをリンクしなくて済むので起動が高速。

第 2 章:プロセス管理(基礎編) #

システムには複数のプロセスが存在しているのが普通です。ps aux コマンドを実行すればシステムに存在する全プロセスを列挙できます。

プロセスの生成 #

新しくプロセスを生成する目的は次の2つに分けられます。

  • 同じプログラムの処理を複数のプロセスに分けて処理する(例:Web サーバによる複数リクエストの受け付け)。
  • 別のプログラムを生成する(例:bash から各種プログラムの新規生成)。

これらを実現するために、Linux では fork() 関数と execve() 関数を使います。これらは内部的にそれぞれ clone()execve() というシステムコールを呼び出します。上記の前者の場合は fork() のみ、後者の場合は fork()execve() の両方を使います。

fork() #

fork() 関数を発行すると、発行したプロセスのコピーを作ったうえで、どちらも fork() 関数から復帰させます。生成元のプロセスを「親プロセス」、生成されたプロセスを「子プロセス」と呼びます。このときの流れは次のとおりです。

  1. 親プロセスが fork() 関数を呼ぶ。
  2. 子プロセス用メモリ領域を確保して、そこに親プロセスのメモリをコピーする。
  3. 親プロセスと子プロセスは両方とも fork() 関数から復帰する。親プロセスと子プロセスは fork() 関数の戻り値が異なるため、処理を分岐させることができる。

fork() 関数がプロセスを生成する様子を見てみましょう。

fork.py

#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
    print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
    exit()
elif ret > 0:
    print("親プロセス: pid={}, 子プロセスのpid={}".format(os.getpid(), ret))
    exit()
sys.exit(1)

fork() 関数復帰時に、親プロセスの場合は子プロセスのプロセス ID が、子プロセスの場合は 0 が帰ります。プロセス ID は必ず 1 以上なので、これを利用して呼び出し後の処理を分岐させることができます。

./fork.py
> 親プロセス: pid=78897, 子プロセスのpid=78898
> 子プロセス: pid=78898, 親プロセスのpid=78897

execve() #

execve() 関数を発行すると、発行したプロセスは別のプログラムに置き換えられます。つまり fork() 後に、子プロセス上で execve() することで親プロセスとは異なる処理に置き換えることができます。処理の流れは次のとおりです。

  1. execve() 関数を呼び出す。
  2. execve() 関数の引数で指定した実行ファイルからプログラムを読み出して、現在のプロセスのメモリを新しいプロセスのデータで上書きする。
  3. プロセスを新しいプロセスの最初に実行すべき命令(エントリポイント)から実行開始する。

fork-and-exec.py

#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
    print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
    os.execve("/bin/echo", ["echo", "pid={} からこんにちは".format(os.getpid())], {})
    exit()
elif ret > 0:
    print("親プロセス: pid={}, 子プロセスのpid={}".format(os.getpid(), ret))
    exit()
sys.exit(1)
./fork-and-exec.py
> 親プロセス: pid=89941, 子プロセスのpid=89947
> 子プロセス: pid=89947, 親プロセスのpid=89941
> pid=89947 からこんにちは

posix_spawn() #

あるプロセスから別の処理を実行するために fork()execve() を順番に呼ぶのは冗長に見えます。このようなときに、UNIX 系 OS の C 言語インタフェース規格である「POSIX」に定義されている posix_spawn() を使えば処理を簡略化できます。

spawn.py

#!/usr/bin/python3
import os
os.posix_spawn("/bin/echo", ["echo", "echo", "posix_spawn()によって生成されました"], {})
print("echo コマンドを生成しました")
./spawn.py
> echo コマンドを生成しました
> echo posix_spawn()によって生成されました

プロセスの親子関係 #

コンピュータの電源を入れると、次のような順序でシステムが初期化されます。

  1. コンピュータの電源を入れる。
  2. BIOS や UEFI などのファームウェアが起動してハードウェアを初期化する。
  3. ファームウェアが GRUB などのブートローダを起動する。
  4. ブートローダが OS カーネルを起動する。ここでは Linux カーネルとする。
  5. Linux カーネルが init プロセスを起動する。
  6. init プロセスが子プロセスを起動して、さらにその子プロセスを …… と続き、プロセスの木構造を作る。

すべてのプロセスの先祖は pid=1 の init プロセスになります。

プロセスの状態 #

┌────────────┐
│ プロセス誕生 │
└──────┬─────┘
┌──────────────────────────────────────────────┐
│ 動作中                                        │
│   ┌────────────┐  CPU実行権を得る ┌─────────┐  │
│   │ 実行可能状態 │ ─────────────→ │ 実行状態 │  │
│   │            │ ←───────────── │         │  │
│   └────────▲───┘  CPU実行権を失う └───┬───┬─┘  │
│ イベント発生 │                        │   │    │
│   ┌────────┴───┐                    │   │    │
│   │ スリープ状態 │ ←──────────────────┘   │    │
│   └────────────┘     イベント待ち         │    │
└─────────────────────────────────────────│────┘
                             終了処理が完了 │
                                  ┌──────────┐
                                  │ ゾンビ状態 │
                                  └──────────┘
                   親プロセスが終了状態を得る │
                                  ┌────────────┐
                                  │ プロセス終了 │
                                  └────────────┘

プロセスはその生存中にさまざまな状態を行き来します。

システムの全プロセスがスリープ状態の場合、CPU はアイドルプロセスという「何もしない」特殊なプロセスが動作しています。なおアイドルプロセスは ps からは見えません。

CPU の特殊な命令を用いて CPU を休止状態にし、1つ以上のプロセスが実行可能状態になるまで消費電力を抑えた状態で待機します。

ノート PC やスマートフォンなどにおいて、何もプログラムを動かしていない状態のほうがバッテリの持ちが良いのは、CPU がアイドル状態になっている時間が長く、消費電力が少なくなることが大きな要因です。

シグナル #

プロセスは、基本的には一本の実行の流れに沿ってひたすら実行し続けます。条件分岐命令があるじゃないかという話もありますが、これについても、あらかじめ定義された条件分岐などで決められた流れに移動するだけです。これに対してシグナルとは、あるプロセスが他のプロセスに何かを通知して、外部から実行の流れを強制的に変えるための仕組みです。

シグナルには複数の種類がありますが、一番多用されるのは SIGINT です。このシグナルは bash などのシェルにおいて Ctrl + C と打つと送られるものです。SIGINT を受け取ったプロセスはデフォルトではそのまま終了します。

シグナルには以下のようなものがあります。

  • SIGSTOP:プロセスの実行を一時停止する。bash 上で Ctrl + Z を押すとこのシグナルが送られる。
  • SIGCONT:SIGSTOP により停止したプロセスを実行を再開する。
  • SIGINT:Interrupt の意。ユーザによる意図的な割り込みを意味するシグナル。
  • SIGTERM:Terminate の意。プロセスを終了したいときに送られる。SIGINT もデフォルトでは終了動作となるが、正常終了することの要求という意味では SIGTERM のほうがより適切。
  • SIGKILL:強制終了。ハング状態など、SIGTERM でも終了できない状態になっているものを終了させる最終手段。

シグナルは、kill コマンドによっても送ることができます。たとえば kill -INT <pid>SIGINT を送ることができます。

SIGINT を受け取ったプロセスはデフォルトではそのまま終了」と書きましたが、プロセスは各シグナルについて、シグナルハンドラという処理をあらかじめ登録しておけます。プロセス実行中に当該シグナルを受信すると、実行中の処理をいったん中断してシグナルハンドラを動作させ、それが終わったら元の場所に戻ってきて動作を再開させます。

シグナルハンドラを使えば、Ctrl + C を押しても終了しないという迷惑なプログラムを作ることもできます。

intignore.py

#!/usr/bin/python3
import signal
signal.signal(signal.SIGINT, signal.SIG_IGN)
while True:
  pass
./intignore.py
> ^C^C^C^

^CCtrl + C を押したことを示しています。もしこのプログラムを実行した場合は、ps auxpid を調べ、kill コマンドで終了させてください。kill コマンドはオプション指定無しのデフォルトでは SIGTERM を送ります。

セッション #

セッションは、ユーザが gterm のような端末エミュレータ、あるいは ssh などを通してシステムにログインしたときのログインセッションに対応するものです。セッション内のプロセスを操作したい時は、端末を介してシェルをはじめとしたプロセスに対して指示をし、また、それらプロセスの出力を受け取ります。通常は、pty/<n> という名前の仮想端末がそれぞれのセッションに対して割り当てられます。

例えば、以下のような3つのセッションが存在する場合を考えます。

  • A さんのセッション:ログインシェルは bash。この上で vim によって Go プログラムを開発しており、現在は go build でプログラムをビルド中。
  • B さんのセッション:ログインシェルは zsh。この上で ps aux を使ってシステムの全プロセスをリストし、結果を less で受けている。
  • C さんのセッション:ログインシェルは zsh。この上で calc という自作計算プログラムを実行している。

このとき以下の図のようになっています。

Session                     │  Terminal
┌ A ──────────────────────┐ │  ┌───────┐
│ [bash] [vim] [go build] │───▶  pty/0 │
└─────────────────────────┘ │  └───────┘
┌ B ──────────────────────┐ │  ┌───────┐
│ [zsh] [ps aux] [less]   │───▶  pty/1 │
└─────────────────────────┘ │  └───────┘
┌ C ──────────────────────┐ │  ┌───────┐
│ [zsh] [calc]            │───▶  pty/2 │
└─────────────────────────┘ │  └───────┘

セッションにはセッション ID、あるいは SID と呼ばれる一意な値が割り振られています。

セッションには、セッションリーダーというプロセスが1つ存在していて、通常は bash などのシェルになります。セッションリーダーの PID はセッションの ID に等しいです。セッションについての情報は、例えば ps ajx によって得られます。

セッションに紐づいている端末がハングアップすると、セッションリーダーに SIGHUP が送られます。端末エミュレータのウィンドウを閉じたときにもこの状況になります。bash はこのとき、自分が管理するジョブを終了させてから自分も終了します。

実行に時間がかかるプロセスの実行中に bash が終了しては困る、というような場合には nohup コマンドが使えます。これは SIGHUP を無視する設定にしたうえでプロセスを起動するコマンドです。セッションが終了して SIGHUP が送られてもプロセスは終了せず動き続けることができます。

デーモン #

簡単に言うと、デーモンとは常駐プロセスのことです。普通のプロセスはユーザが立ち上げてから何らかの一連の処理をして終了することが前提とされます。デーモンはそうではありません。

  • あらゆるログインセッションが終了しても影響を受けないように、独自のセッションを持つ。
  • デーモンを生成したプロセスがデーモンの終了を気にしなくていいように、init が親になっている。

上記に当てはまらなくとも、常駐プロセスであれば便宜的にデーモンと呼ばれることもあります。

第 3 章:プロセススケジューラ #

  • CPU リソースを使い切っている(100% に張り付いているような)状態の場合、プロセス数を増やしてもスループットは向上しない。
  • CPU をたくさん積んでいるマシンがあったとしても、そこに十分な数のプロセスを実行させてはじめてスループットが向上する。

プログラムの並列実行の重要性 #

プログラムの並列実行の重要性は年々高まっています。なぜかというと CPU の性能向上のアプローチが変わってきたからです。

かつては CPU が新しくなるたびに論理 CPU ごとの性能(これをシングルスレッド性能と呼びます)の劇的な向上が期待できました。この場合は、プログラムを一切変更しなくても処理速度がどんどん上がっていきました。しかし、ここ最近はシングルスレッドの向上が難しくなってきており、その代わりに CPU コア数の増加によって CPU のトータル性能を上げる方向に向かっています。

カーネルもこのような時代の流れに沿って、コア数が増えた場合のスケーラビリティを向上させてきました。時代が変われば前提も変わり、それに合わせてソフトウェアも変わるのです。

第 4 章:メモリ管理システム #

(省略)

第 5 章:プロセス管理(応用編) #

プロセス間通信 #

複数のプログラムを協調動作させるときには、各プロセスがデータを共有したり、あるいはお互いの処理のタイミングを合わせる(同期させる)必要があります。このために OS が提供する機能のことを「プロセス間通信」と呼びます。

Linux は、目的別にたくさんのプロセス間通信の手段を提供しますが、ここでは分かりやすいものをいくつか紹介します。

共有メモリ #

異なるプロセスでメモリを共有することでデータを受け渡す事が可能です。

shared-memory.py

#!/usr/bin/python3

import os
import sys
import mmap
from sys import byteorder

PAGE_SIZE = 4096

data = 1000
print("子プロセス生成前のデータの値: {}".format(data))
shared_memory = mmap.mmap(-1, PAGE_SIZE, flags=mmap.MAP_SHARED)

shared_memory[0:8] = data.to_bytes(8, byteorder)

pid = os.fork()
if pid < 0:
  print("fork()に失敗しました", file=os.stderr)
elif pid == 0:
  data = int.from_bytes(shared_memory[0:8], byteorder)
  data *= 2
  shared_memory[0:8] = data.to_bytes(8, byteorder)
  sys.exit(0)

os.wait()
data = int.from_bytes(shared_memory[0:8], byteorder)
print("子プロセス終了後のデータの値: {}".format(data))
  1. 1000 という整数データを作ってデータの値を出力する
  2. 共有メモリ領域を mmap() システムコールにより作って、データの値を領域先頭に格納する。
  3. 子プロセスを作る。
  4. 親プロセスは、子プロセスの終了を待つ。子プロセスは共有メモリからデータを読み出して2倍し書き戻す。その後、子プロセスは終了する。
  5. 親プロセスはデータの値を出力する。
./shared-memory.py
> 子プロセス生成前のデータの値: 1000
> 子プロセス終了後のデータの値: 2000

シグナル #

シグナルも、プロセス間通信の1つです。

SIGINTSIGTERMSIGKILL のような用途が決まっているものの他に、POSIX では、SIGUSR1SIGUSR2 という、プログラマは自由に用途を決めて良いシグナルがあります。これらを使ってプロセスが互いにシグナルを送り合って処理を進める、といったことができるようになります。

パイプ #

複数のプロセスは、パイプと呼ばれるものを介して通信することができます。パイプのもっとも身近な使用例は、bash などのシェルにおいて | という文字で複数プログラムをつなぐことでしょう。

free | awk '(NR=2){print &2}'

パイプは他にも双方向通信ができたり、ファイルを介してプロセスをつないだりといったさまざまなことができます。

ソケット #

Linux では、複数のプロセスを「ソケット」と呼ばれるもので繋いで通信することができます。ソケットは非常に広く使われています。

ソケットには大きく分けて2つあります。1つ目は UNIX ドメインソケットです。このソケットは1つのマシン上のプロセスのみを通信させる方法です。

2つ目は、TCP ソケット、UDP ソケットです。これらはインターネットプロトコルスイートあるいは TCP/ID と呼ばれるプロトコルに従って、複数のプロセスを通信させます。UNIX ドメインソケットと比べると一般に低速ですが、別マシン上のプロセスとも通信できるのが大きな利点です。これらのソケットはインターネットにおいて広く使われています。

マルチプロセスとマルチスレッド #

マルチプロセスはすでに説明した fork()execve() 関数を使ってプロセスを生成して、その後にそれぞれがプロセス間通信機能によってやり取りしながら処理します。

その一方でマルチスレッドは、プロセス内に複数の流れを作ります。1つのスレッドしか持たないプログラムをシングルスレッドプログラム、2つ以上のスレッドを持つプログラムをマルチスレッドプログラムと呼びます。

スレッド機能を提供する方法は多々あります。例えば POSIX は「POSIX スレッド」とうスレッド操作用の API を提供しています。Linux でも libc などを介して POSIX スレッドを扱えます。

あるプログラムを複数の流れで実現する場合、マルチプロセスと比較してマルチスレッドは次のような長所があります。

  • さまざまなリソースを同じプロセス内の全スレッド間で共有するため、メモリをはじめとしたリソース消費量が少ない。
  • 全スレッド間でメモリを共有するため、見かけ上の協調動作がしやすい。

その一方で次のような短所もあります。

  • 1つのスレッドの障害が全スレッドに影響する。例えば1つのスレッドが異常終了すると、プロセス全体が異常終了する。
  • 各スレッドが呼び出す処理が、マルチスレッドプログラムから呼び出して良い処理(スレッドセーフ)である必要がある。例えば内部的にグローバル変数を排他制御なしにアクセスしている処理は、スレッドセーフではない。この場合、同時に1つのスレッドからしか当該処理を呼ばないようにプログラマが制御しなければならない。

マルチスレッドプログラムを期待通りに作るのは大変なので、マルチスレッド化の恩恵を受けながら、プログラミングを簡単にするさまざまな方法が存在します。例えば Go 言語においては goroutine という言語組み込み機能によってスレッドの扱いを簡単にしています。

第 6 章:デバイスアクセス #

プロセスがデバイスにアクセスする際、カーネルにデバイスへのアクセスを代行してもらいます。具体的には以下のようなインタフェースを使います。

  • デバイスファイルという特殊なファイルを操作する。
  • ブロックデバイスのデバイス上に構築したファイルシステムを操作する。

デバイスファイル #

デバイスファイルは、デバイスごとに存在します。例えばストレージデバイスであれば、/dev/sda/dev/sdb などがデバイスファイルです。より正確に言うと、ストレージデバイスをパーティションに区切っている場合は /dev/sda1/dev/sda2 のように、パーティションごとにデバイスファイルが存在します。

Linux では、プロセスがデバイスファイルを操作すると、カーネルの中のデバイスドライバというソフトウェアが、ユーザの代わりにデバイスにアクセスします。

                         ┌───────────────┐
                         │    プロセス    │
                         └───────────────┘
                           /           \
                    読み書き             読み書き
                       ↓                   ↓
                  ┌──────────┐        ┌──────────┐
                  │ /dev/AAA │        │ /dev/BBB │
                  └────┬─────┘        └────┬─────┘
┌──────────────────────┼───────────────────┼──────────────────────┐
│ カーネル              │                   │                      │
│              ┌────── ▼ ───────┐  ┌────── ▼ ───────┐             │
│              │ デバイス0の      │  │ デバイス1の      │             │
│              │ デバイスドライバ  │  │ デバイスドライバ  │             │
│              └───────┬────────┘  └───────┬────────┘             │
└──────────────────────┼───────────────────┼──────────────────────┘
                       │ 操作               │ 操作
                       ↓                   ↓
                ┌───────────────┐  ┌───────────────┐
                │  デバイス0     │   │  デバイス1     │
                └───────────────┘  └───────────────┘

プロセスは、通常のファイルと同じようにデバイスファイルを操作できます。つまり、open()read()write() などのシステムコールの発行によってそれぞれのデバイスにアクセスできます。

デバイスファイルは通常 /dev/ ディレクトリ以下に存在します。ls -l /dev/ でデバイスファイルがどれくらいあるか確認してみましょう。

ls -l /dev/
> crw-rw-rw- 1 root tty    5, 0 3月  6 19:02 tty
> brw-rw---- 1 root disk 259, 0 2月 27 09:39 nvme0n1
> ......
> ...

行頭の文字が「c」であればキャラクタデバイス、「b」であればブロックデバイスです。

キャラクタデバイスは読み出しと書き出しができます。代表的なキャラクタデバイスは、端末、キーボード、マウスなどです。

ブロックデバイスはそれに加えてシークができます。代表的なブロックデバイスは HDD や SSD などのストレージデバイスです。

では端末デバイス用のデバイスファイルへのアクセスによって、端末デバイスを操作してみましょう。まずは現在のプロセスに対応する端末と、その端末に対応するデバイスファイルを探します。各プロセスに結びつけられている端末は ps ax の第2フィールドによって得られます。

ps ax | grep bash
> 6417 pts/9  Ss  0:00 -bash
> 6432 pts/9  S+  0:00 grep bash

この結果、手元の bash は pts/9 という端末を使っていることが分かりました。/dev/ 以下の /pts/9 というファイルがこの端末に対応するデバイスです。このファイルに適当な文字列を書き込んでみます。

sudo su
echo hello >/dev/pts/9
> hello

端末デバイスに「hello」という文字列を書き込むと(正確には、デバイスファイルに write() システムコールを発行している)、端末上にこの文字列が出力されました。これは echo hello コマンドを実行した場合と同じ結果です。なぜかというと、echo コマンドは標準出力に「hello」を書き込んでいて、かつ Linux によって標準出力が端末に結びつけられているからです。

続いてシステムに存在する、現在操作中 " 以外 " の端末を操作してみましょう。まずは、先ほどの状態からもうひとつ端末を起動した後に ps ax コマンドを実行します。2つ目の端末に対応するデバイスファイルの名前は /dev/pts/10 と分かりました。このファイルに対して、1つ目の端末化から文字列を書き込んでみましょう。

sudo su
echo hello >/dev/pts/10

すると、echo を実行した1つ目の端末においては何も出力がなく、2つ目の端末上で文字列が出力されていることが分かると思います。

第 7 章:ファイルシステム #

第 6 章において、各種デバイスはデバイスファイルを介してアクセスできると述べました。このうちストレージデバイスは、ほとんどの場合、ファイルシステムを介してアクセスします。

データを保存するとき、それをディスク上のどの位置に保存するのか自分で決めなければいけません。他のデータを壊さないために空き領域の管理も必要です。さらに書き込んだ後にデータを読み出すためには、どの位置にどのサイズでどのデータがあるか覚えておく必要があります。

ファイルシステムは、このような管理を代行してくれます。ファイルシステムはユーザにとって意味のあるひとかたまりのデータをファイルという単位で管理します。それぞれのデータがどこにあるかは、ユーザが管理しなくても、ストレージデバイス上の管理領域に保存しています。

Linux では「ext4」「XFS」「Btrfs」といったファイルシステムがよく使われます。

  • ext4:過去 Linux でよく使われてきた ext2、ext3 からの移行が楽。
  • XFS:スケーラビリティに優れる。
  • Btrfs:機能が豊富。

それぞれ、ストレージデバイス上に作るデータ構造、およびそれを扱うための処理が異なることで性能や機能に違いがあります。どのファイルシステムが良いかはユーザの要件によって異なり、一長一短です。

ほかにも、ストレージデバイスの代わりにメモリ上に作成する「tmpfs」というファイルシステムがあります。このファイルシステムに保存したデータは電源を切るとなくなってしまいますが、ストレージデバイスへのアクセスが一切発生しないため高速にアクセスできます。tmpfs は、再起動後に残っている必要のない /tmp/var/run などに使われることが多いです。

tmpfs はユーザが任意で作成することも可能です。以下は 1GiB の tmpfs を作って /mnt 以下にマウントする例です。

sudo mount -t tmpfs tmpfs /mnt -osize=1G

tmpfs が使うメモリは、ファイルシステムを作ったときにすべて獲得するわけではなく、データに最初にアクセスしたときに、ページ単位でメモリを獲得するという仕組みになっています。

第 8 章:記憶階層 #

(省略)

第 9 章:ブロック層 #

(省略)

第 10 章:仮想化機能 #

仮想化機能は、PC やサーバなどの物理的なマシン上で、仮想マシンを動かすためのソフトウェア機能、および、それを助けるためのハードウェア機能の組み合わせです。

これにより、1台の物理マシン上で複数のシステムを動かしたり、ある OS 上で別の OS を動かすことができます。

仮想マシンは、物理マシン上に存在する仮想化ソフトウェアが生成、管理および廃棄をします。仮想マシンは、物理マシンのリソースが許す限り何台でも作れます。

仮想化ソフトウェアと仮想マシンとの関係や、カーネルのプロセス管理システムとプロセスの関係に非常によく似ています。

物理マシン上の OS をインストールした場合

┌───────────────────────────────────────┐
│  ┌─────────┐  ┌────────┐  ┌─────────┐ │
│  │ プロセス │  │ プロセス │  │ プロセス │ │
│  └─────────┘  └────────┘  └─────────┘ │
│  ┌─────────────────────────────────┐  │
│  │             カーネル             │  │
│  └─────────────────────────────────┘  │
│  ┌─────────────────────────────────┐  │
│  │             物理マシン            │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘

仮想マシン上に OS をインストールした場合

┌───────────────────────────────────────────────────────────┐
│  ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│  │ Linuxのシステム  │ │ Linuxのシステム  │ │ Linuxのシステム  │ │
│  │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│  │ │Linuxプロセス│ │ │ │Linuxプロセス │ │ │ │Linuxプロセス│ │ │
│  │ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ │ │
│  │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│  │ │Linuxカーネル│ │ │ │Linuxカーネル │ │ │ │Linuxカーネル│ │ │
│  │ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ │ │
│  │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│  │ │  仮想マシン  │ │ │ │  仮想マシン │ │ │ │  仮想マシン  │ │ │
│  │ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ │ │
│  └────────────────┘ └────────────────┘ └────────────────┘ │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                    仮想化ソフトウェア                  │  │
│  └─────────────────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                       物理マシン                      │  │
│  └─────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────┘

仮想化ソフトウェアの実現にはさまざまな方法があります。たとえば、物理ハードウェアの上に直接ハイバーバイザと呼ばれる仮想化ソフトウェアをインストールするもの、既存 OS の上のアプリケーションとして動かすもの、などです。具体的なソフトウェアとしては次のようなものが有名です。

  • VMware 社の各種製品
  • Oracle 社の VirtualBox
  • Microsoft 社の Hyper-V
  • Citrix Systems 社の Xen

第 11 章:コンテナ #

仮想マシンとコンテナは、それぞれの中に独立したプロセス実行環境を提供するという点では同じです。しかし、プロセスのしたにあるカーネル以下のレイヤに大きな違いがあります。仮想マシンの場合は、個々の仮想マシンは専用の仮想的なハードウェアとカーネルを使うのに対して、コンテナの場合は、コンテナを動かすホスト OS、および全コンテナが1つのカーネルを共有します。このため、仮想マシンの場合は、Windows のような Linux とは完全に異なるホスト OS を動かせるのに対して、コンテナの場合は、Linux カーネル上で動作するシステム(Ubuntu や Red Hat Enterprise Linux など)だけを動かせます。

たとえばコンテナ上に Ubuntu 環境を作る場合は、コンテナランタイムと呼ばれるプロセスが、コンテナを作ったうえで最初のプロセスを起動するだけです。これらの違いによって、コンテナは、仮想マシンに比べて、起動速度やハードウェアへのアクセス速度の点で軽量です。

コンテナを実現するソフトウェアはさまざまありますが、今日では Docker が広く使われています。以降は Docker を例にコンテナについて説明します。

namespace #

コンテナは、カーネルの「namespace」という機能をうまく活用して実現されています。

namespace はシステム上に存在するさまざまな種類のリソースについて存在します。namespace は、所属するプロセスに、見かけ上は独立したリソースを見せる機能です。namespace にはたとえば以下のようなものがあります。

  • pid namespace (pid ns):独立した pid 名前空間を見せる
  • user namespace (user ns):独立した uid, gid を見せる
  • mount namespace (mount ns):独立したファイルシステムマウントを見せる

pid namespace #

システム起動時には、すべてのプロセスが所属する「root pid ns」というものが存在します。

プロセス A、B、C が root pid ns に所属しているとします。プロセス A から見ると、プロセス B、C は pid 2、3 として認識できます。

ここで root pid ns とは異なる pid ns foo を作成してプロセス B、C をそこで実行させるとします。

このとき、pid ns foo は root pid ns の中に存在します。このとき以下となります。

  • root pid ns からは子である pid ns foo のプロセスが見える。つまりプロセス A はプロセス B、C が見える。
  • 子である pid ns foo からは親である root pid ns のプロセスは見えない。つまりプロセス B、C からはプロセス A は見えない。

コンテナの正体とは namespace です。コンテナごとに独立した namespace を持ち、さらに user ns と mount ns を組み合わせて、ほかのプロセスと実行環境が分かれている1つないし複数のプロセス、これがコンテナです。

コンテナが明らかになったところで分かることがあります。それは、ホスト OS や他のコンテナに起因する問題がある場合には、コンテナの中からは原因が分からないということです。

第 12 章:cgroup #

cgroup は、Linux カーネルが持っている機能です。システムのメモリや CPU などのリソースをどのプロセスにどれだけ与えるかという細かい制御をするための機能です。「プロセスをグループ(group)に分けて各種リソースを制御する(control)」から cgroup とう名前が付いています。

特にシステムを複数のユーザで共有するレンタルサーバや IaaS のようなサービスプロバイダにとっては、非常に重要な機能となります。

メインフレームやエンタープライズ UNIX サーバなどのいわゆるミッションクリティカルなサーバの OS では昔から当たり前となっていた機能ですが、Linux カーネルでは実現されてきませんでした。昨今のクラウド化の流れを受けて、クラウドサービスプロバイダの強い後押しを受けてついに cgroup という機能として実現されました。

終章:本書で学んだことと今後への生かし方 #

  • カーネル開発をしてみたい方へのお勧めの書籍
    • オペレーティングシステム設計と実装(第3版):いわゆる「タネンバウム本」。Linux カーネルに限らず OS カーネルについての一般的な知識が得られます。
    • Linux Kernel Development 3rd edition:Linux カーネルの基礎知識が得られます。
    • 詳解 Linux カーネル(第3版):Linux カーネルの過去のバージョンについて細かい解説をしています。実装の細かい記述が中心です。
  • ハードウェアに近い知識を得たい方へお勧めの書籍
    • コンピュータの構成と設計:コンピュータシステムを構成するハードウェアのアーキテクチャに関する古典的名著です。
    • What Every Programmer Should Know About Memory:ハードウェアとしてのメモリについて包括的に説明した論文です。店舗で販売されてはおらず、作者ホームページから無償でダウンロードできる pdf として提供されています。
    • Write Great Code vol. 1:ハードウェアとソフトウェアの境界部分についての広く浅い知識が得られます。