hysryt / wiki

https://hysryt.github.io/wiki/
0 stars 0 forks source link

試して理解 Linuxのしくみ #112

Open hysryt opened 5 years ago

hysryt commented 5 years ago

第1章

プロセス

プログラムの単位。 多くのOSは複数のプロセスを実行できる。

CPU

CPUにはカーネルモードとユーザーモードがある。 通常のプロセスはユーザーモードで動作する。 カーネルモードで動作するものには以下のものがある。

デバイスドライバ

デバイスを操作するためのプログラム。 プロセスからはデバイスドライバを介してデバイスを操作する。 デバイスドライバはカーネルモードで動作する。

カーネル

カーネルモードで動作するプログラムをまとめたもの。 OSはカーネルにユーザーモードで動作するプログラムを付け加えたもの。 カーネルはCPUやメモリを管理し、リソースを各プロセスに割り振る。

システムコール

ユーザーモードからカーネルへはシステムコールを介して処理を依頼する。

hysryt commented 5 years ago

第2章

システムコール

システムコールには以下のようなものがある。 ・プロセスの生成、削除(fork, clone, kill) ・メモリの確保、開放(brk) ・プロセス間通信(pipe) ・ネットワーク(socket) ・ファイルシステム操作 ・ファイル操作

CPUのモード切り替え

システムコールが発行されると CPU に割り込みイベントが発生する。これによって CPU はユーザーモードからカーネルモードに切り替わり、カーネルの処理を開始する。カーネルでの処理が終了すると再びユーザーモードに切り替わり、プロセスの処理を継続する。

システムコールを介さずにユーザーモードからカーネルモードに切り替える方法はない。

strace

strace コマンドを使うことでプログラムが内部で行うシステムコールをトレースすることができる。

$ strace -o hello.log ./hello
$ cat hello.log 
execve("./hello", ["./hello"], 0x7fffb1c95b30 /* 21 vars */) = 0
brk(NULL)                               = 0x55bbcf5e4000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/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=25586, ...}) = 0
mmap(NULL, 25586, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa90876b000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-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>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa908769000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa90815a000
mprotect(0x7fa908341000, 2097152, PROT_NONE) = 0
mmap(0x7fa908541000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fa908541000
mmap(0x7fa908547000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa908547000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7fa90876a4c0) = 0
mprotect(0x7fa908541000, 16384, PROT_READ) = 0
mprotect(0x55bbcdf5a000, 4096, PROT_READ) = 0
mprotect(0x7fa908772000, 4096, PROT_READ) = 0
munmap(0x7fa90876b000, 25586)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
brk(NULL)                               = 0x55bbcf5e4000
brk(0x55bbcf605000)                     = 0x55bbcf605000
write(1, "hello world!\n", 13)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

プログラムの開始処理と終了処理だけでも多くのシステムコールが発行されることがわかる。

-T オプションでシステムコールにかかった時間、-tt オプションでシステムコールの開始時刻を表示できる。

-T オプション
$ strace -o hello.log -T ./hello
hello world!
$ cat hello.log 
execve("./hello", ["./hello"], 0x7ffc0903edb8 /* 21 vars */) = 0 <0.000310>
brk(NULL)                               = 0x5601e8c3e000 <0.000060>
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) <0.000066>
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory) <0.000063>
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 <0.000065>
fstat(3, {st_mode=S_IFREG|0644, st_size=25586, ...}) = 0 <0.000059>
mmap(NULL, 25586, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0f61895000 <0.000062>
close(3)                                = 0 <0.000058>
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) <0.000062>
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 <0.000065>
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832 <0.000060>
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0 <0.000065>
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f61893000 <0.000130>
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f0f61284000 <0.000065>
mprotect(0x7f0f6146b000, 2097152, PROT_NONE) = 0 <0.000066>
mmap(0x7f0f6166b000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f0f6166b000 <0.000067>
mmap(0x7f0f61671000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f0f61671000 <0.000068>
close(3)                                = 0 <0.000059>
arch_prctl(ARCH_SET_FS, 0x7f0f618944c0) = 0 <0.000058>
mprotect(0x7f0f6166b000, 16384, PROT_READ) = 0 <0.000064>
mprotect(0x5601e783f000, 4096, PROT_READ) = 0 <0.000063>
mprotect(0x7f0f6189c000, 4096, PROT_READ) = 0 <0.000066>
munmap(0x7f0f61895000, 25586)           = 0 <0.000068>
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 <0.000060>
brk(NULL)                               = 0x5601e8c3e000 <0.000058>
brk(0x5601e8c5f000)                     = 0x5601e8c5f000 <0.000060>
write(1, "hello world!\n", 13)          = 13 <0.000301>
exit_group(0)                           = ?
+++ exited with 0 +++
<>の中が処理にかかった時間(秒)


-tt オプション
$ strace -o hello.log -tt ./hello
hello world!
$ cat hello.log 
12:24:45.677458 execve("./hello", ["./hello"], 0x7ffe9e340e08 /* 21 vars */) = 0
12:24:45.677810 brk(NULL)               = 0x55e213ae9000
12:24:45.677881 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
12:24:45.677937 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
12:24:45.677982 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
12:24:45.678027 fstat(3, {st_mode=S_IFREG|0644, st_size=25586, ...}) = 0
12:24:45.678068 mmap(NULL, 25586, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f6aeaa0f000
12:24:45.678107 close(3)                = 0
12:24:45.678144 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
12:24:45.678188 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
12:24:45.678232 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
12:24:45.678273 fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
12:24:45.678311 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6aeaa0d000
12:24:45.678353 mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6aea3fe000
12:24:45.678393 mprotect(0x7f6aea5e5000, 2097152, PROT_NONE) = 0
12:24:45.678462 mmap(0x7f6aea7e5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f6aea7e5000
12:24:45.678583 mmap(0x7f6aea7eb000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6aea7eb000
12:24:45.678635 close(3)                = 0
12:24:45.678686 arch_prctl(ARCH_SET_FS, 0x7f6aeaa0e4c0) = 0
12:24:45.678775 mprotect(0x7f6aea7e5000, 16384, PROT_READ) = 0
12:24:45.678821 mprotect(0x55e213024000, 4096, PROT_READ) = 0
12:24:45.678863 mprotect(0x7f6aeaa16000, 4096, PROT_READ) = 0
12:24:45.678904 munmap(0x7f6aeaa0f000, 25586) = 0
12:24:45.679008 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
12:24:45.679083 brk(NULL)               = 0x55e213ae9000
12:24:45.679118 brk(0x55e213b0a000)     = 0x55e213b0a000
12:24:45.679160 write(1, "hello world!\n", 13) = 13
12:24:45.679996 exit_group(0)           = ?
12:24:45.680168 +++ exited with 0 +++

sar

sar コマンドを使うことで CPU のユーザーモードとカーネルモードの処理の割合を確認できる。

$ sar -P ALL 1
Linux 4.15.0-33-generic (ubuntu-bionic)     02/24/19    _x86_64_    (2 CPU)

12:28:19        CPU     %user     %nice   %system   %iowait    %steal     %idle
12:28:20        all      0.00      0.00      0.50      0.00      0.00     99.50
12:28:20          0      0.00      0.00      0.00      0.00      0.00    100.00
12:28:20          1      0.00      0.00      0.00      0.00      0.00    100.00

12:28:20        CPU     %user     %nice   %system   %iowait    %steal     %idle
12:28:21        all      0.00      0.00      0.50      0.00      0.00     99.50
12:28:21          0      0.00      0.00      0.99      0.00      0.00     99.01
12:28:21          1      0.00      0.00      0.00      0.00      0.00    100.00

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.50      0.00      0.00     99.50
Average:          1      0.00      0.00      0.00      0.00      0.00    100.00

%user と %nice がユーザーモード、%system がカーネルモードの処理の割合となっている。

%system が数十のような大きい値になっている場合はシステムの負荷が高すぎることが多い。

ラッパー関数

システムコールはアーキテクチャに依存したアセンブリコードからのみ発行することができ、C言語のような高級言語から直接発行することはできない。

しかしアプリケーション開発者が毎回アセンブリを書くわけには行かないため、各システムコールを呼び出すだけのラッパー関数が、アーキテクチャごとに用意されている。

Linux 場合、標準 C ライブラリおよびラッパー関数として多くは glibc が使用される。

hysryt commented 5 years ago

第3章

プロセスを生成する目的

プロセスの生成には fork() を使用する。

fork() はプロセスを複製する。 fork() をした瞬間からプロセスは枝分かれし、複製元が親プロセス、複製先が子プロセスとなる。 親プロセスでは fork() の戻り値として子プロセスのプロセスIDを取得し、 子プロセスでは fork() の戻り値として0を取得する。 fork() で複製したプロセスは同じプログラムが動作するため、処理を分けたい場合は fork() の戻り値で分岐させる。

execve() は別プログラムを起動する。 新規のプロセスを作らずに、現在のプロセスを使用するため、プロセス数は増えない。

別のプログラムを新しいプロセスで実行する場合は、fork() で子プロセスを作成し、 子プロセスで execve() を実行する。(Fork-Exec)

メモリマップ開始アドレスがよくわからない ↓ 実行ファイルはメモリの決まった位置に読み込まないといけないということらしい。 厳密には各仮想アドレス空間内の決まった位置ということなので、複数の実行ファイルを同時にメモリに読み込むことができる。

hysryt commented 5 years ago

第4章

プロセススケジューラ

複数プロセスを同時に動作させる(させているように見せる)機能。

1つのCPUで同時に(並行して)動作できるプロセスは1つ。短い時間で順番にプロセスを実行することで複数動作しているように見せる。

マルチコアの場合、1つのコアが1つのCPUとして認識される。コアの数だけプロセスを同時に(並列に)動作させられる。つまりデュアルコアCPUは2つ、クアッドコアCPUは4つのプロセスを同時に動作させることができる。

CPUの数(コアの数)は /proc/cpuinfo を見ればわかる

$ grep -c processor /proc/cpuinfo
2

マルチコアとマルチCPU

同じことを分散してやる場合はマルチコアが有利。 別のことをそれぞれ並行してやる場合はマルチCPUが有利。

tasksetコマンド

指定したプログラムを指定した論理CPU上でのみ動作させる。 内部的に sched_setaffinity というCPUの使用を制限するシステムコールを呼び出している。

$ taskset -c 0 echo a

-c でプロセッサを指定する。

コンテキストスイッチ

論理CPU上で動作するプロセスが切り替わること。 プログラムの進行状況に関係なく発生する。

wait システムコール

子プロセスのいずれかが終了するまで動作を停止する。 子プロセスがない場合はすぐに復帰する。 エラーの場合は -1 を返す。

プロセスの状態

システムのほとんどのプロセスはスリープ状態にあり、なんらかのイベントが発生するのを待っている。

プロセスの状態は ps コマンドの STAT フィールドの一文字目で確認できる。

$ ps u
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
vagrant  17480  0.0  0.5  23196  5268 pts/1    Ss   04:40   0:00 -bash
vagrant  17666  0.0  0.3  37792  3420 pts/1    R+   06:21   0:00 ps u

R が実行中または実行待ち状態、SまたはDがスリープ状態、Zがゾンビ状態。

D は主にストレージデバイスのアクセス待ちであり、数ミリ程度で別の状態に遷移する。長時間Dのままの場合は何らかの不具合が発生している。

アイドルプロセス

CPU 上で動作するプロセスがない場合に動作する特殊なプロセス。特殊な命令を使ってCPUを休止状態にし、他のプロセスが実行可能状態になるまで待機する。 アイドルプロセスが動作している割合は sar コマンドの %idle で確認できる。

$ sar 1
Linux 4.15.0-46-generic (ubuntu-bionic)         03/16/19        _x86_64_        (2 CPU)

06:26:02        CPU     %user     %nice   %system   %iowait    %steal     %idle
06:26:03        all      0.00      0.00      0.50      0.00      0.00     99.50

スループット

単位時間あたりのそう仕事量。高い程よい。

レイテンシ

処理の開始から終了までの経過時間。短いほど良い。

ロードバランサ(グローバルスケジューラ)

複数CPU間でプロセスを公平に分配する機能。 一つのCPUに処理が偏らないようにする。

time コマンド

time コマンドを使うことでプロセスの実行にかかった経過時間と実際のCPUの使用時間を得ることができる。

$ time curl -O www.google.com/index.html
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 12465    0 12465    0     0   129k      0 --:--:-- --:--:-- --:--:--  129k

real    0m0.104s
user    0m0.005s
sys     0m0.005s

real が経過時間。 user がCPUのユーザーモードでの使用時間。 sys がCPUのカーネルモードでの使用時間。

user と sys を足すことで CPU の使用時間がわかる。 使用時間はすべての CPU の使用時間の合計が出るため、マルチコアやマルチプロセッサの場合は経過時間より使用時間の方が大きくなることがある。

ps -eo

現在動作しているプロセスの現在までの経過時間と CPU の使用時間を表示できる。

$ ps -eo pid,etime,time
  PID     ELAPSED     TIME
    1    02:09:51 00:00:02
    2    02:09:51 00:00:00
    4    02:09:51 00:00:00
    6    02:09:51 00:00:00
(略)

プロセスの優先度

プロセスは -20 から 19 までの間で優先度を設定できる。 数が小さいほど優先度が高い(-20が最も優先度が高い)。 デフォルトは0。 優先度が高いほど多くのCPU時間を得られる。 root 権限を持ったユーザーのみ優先度を上げることができる。 一般ユーザでも下げることはできる。

nice システムコール

プロセスの優先度を変更するシステムコール。

nice コマンド

プロセスの優先度を変更するコマンド。 -n オプションで優先度を設定する。

$ nice -n 10 echo 'hello';

sar コマンド

%user は優先度が0のプロセスの実行時間の割合。 %nice は優先度が0以外のプロセスの実行時間の割合。

hysryt commented 5 years ago

第5章 前編

メモリ管理システム

Linux では PC に搭載されている全てのメモリをカーネルが管理している。 カーネル自身が使用するメモリも自分で管理している。

free コマンド

搭載されているメモリの量や、現在使用中のメモリの量などを確認できる。 デフォルトの場合、単位はキロバイト。

$ free
              total        used        free      shared  buff/cache   available
Mem:        1008928       88584      444984         608      475360      774484
Swap:             0           0           0

sar コマンド

-r オプションでメモリに関する情報を取得できる。

$ sar -r
Linux 4.15.0-46-generic (ubuntu-bionic)     03/19/19    _x86_64_    (2 CPU)

10:05:51     LINUX RESTART  (2 CPU)

10:15:01    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
10:25:01       451240    774160    557688     55.28     23668    410000    215460     21.36    332164    140744       136
10:35:01       444480    773896    564448     55.95     24204    415896    216640     21.47    334192    145208       136
10:45:01       444372    773824    564556     55.96     24236    415900    216640     21.47    334224    145212       136
10:55:01       444588    774116    564340     55.93     24272    415920    216056     21.41    334284    145216       136
Average:       446170    773999    562758     55.78     24095    414429    216199     21.43    333716    144095       136

OOM Killer

メモリの空きがなくなった時、適当なプロセスを強制終了してメモリ領域を確保するカーネルの機能。 業務サーバーの場合は中途半端にプロセスを終了させるよりシステム全体を終了した方が良いため、この機能をオフにすることもある。

プロセスへのメモリ割り当て

プロセスへのメモリ割り当てもカーネルが行う。 カーネルがメモリ割り当てを行うタイミングは以下の2つ。 ・プロセス生成時 ・プロセス生成後に追加で動的にメモリを割り当てる時

仮想記憶

CPU(とMMU?)の機能。 プロセスごとに仮想的に独自のアドレス空間を用意する。 プロセスからみたアドレスを仮想アドレス、メモリの実際のアドレスを物理アドレスという。 仮想アドレスと物理アドレスはページテーブルで紐づけられる。

readelf や cat proc/pic/maps で表示されるアドレスは全て仮想アドレス。

物理アドレスでメモリに直接アクセスする方法はない。 許可されていないアドレスにアクセスした場合、セグメンテーション違反が発生する。

仮想記憶のメリット

・物理メモリ上で断片化しているメモリ領域を一つの大きな領域として見せることができる。 ・プロセスごとにアドレス空間が独立しているため、他のプロセスのアドレス空間にアクセスさせないようにできる。(実装の都合上、カーネルのメモリ空間は全ての仮想アドレス空間にマッピングされるが、カーネル用のページテーブルエントリはカーネルモードからしかアクセスできないようになっている。) ・マルチプロセスの場合でも、各プロセスが独立したアドレス空間を持つため、干渉を気にしなくていい。

ページテーブル

仮想アドレスと物理アドレスの返還にはページテーブルを使用する。 ページテーブルはカーネルが使用するメモリ内に存在する。 仮想記憶ではメモリをページという単位で管理する。 ページテーブル内の1ページ分をページテーブルエントリという。

ページサイズはアーキテクチャによって変わる。 x86_64の場合は4キロバイト。

仮想アドレスから物理アドレスへの変換はカーネルを介さず、CPUが直接ページテーブルを見て行う。

ページフォールト

物理アドレスに対応しない仮想アドレスにアクセスすると、CPU上でページフォールトという割り込みが発生する。 ページフォールトが発生するとカーネル上でページフォールトハンドラという処理が動く。 ページフォールトハンドラからプロセスに対しSIGSEGVというシグナルを通知し、それを受け取る処理を書いていないプロセスは強制終了する。

仮想記憶を使用した、プロセスへのメモリ割り当て

  1. 実行ファイルを読み出す
  2. 実行ファイルの情報から必要なメモリサイズを計算する。
  3. 物理メモリ上にそのサイズを確保する。
  4. 実行ファイルをメモリにコピーする。
  5. ページテーブルを作成する。
  6. コードを実行する。

追加割り当て時は以下のようになる。

  1. プロセスがメモリを要求。
  2. カーネルが必要なサイズを確保。
  3. カーネルがページテーブルを作成。
  4. 確保したメモリ空間の仮想アドレスをプロセスに返す。

mmap と malloc

mmap はページ単位でメモリを確保する。 malloc はバイト単位でメモリを確保する。 glibc が事前に mmap で大きなメモリ領域を確保してプールしておき、malloc で要求された時に必要な分だけ返している。 プールに空きがなくなると再度 mmap でメモリーを要求する。

mmap はもともとファイルやデバイスをメモリにマップするシステムコールらしい https://www.ncaq.net/2018/01/19/10/27/30/

hysryt commented 5 years ago

第5章 後編

ファイルマップ

https://ja.wikipedia.org/wiki/メモリマップトファイル ファイルの内容を仮想アドレス空間にマップする機能。 いろんなOSで実装されている。 Linux では mmap システムコールによってマップを行う。 マップしたファイルにはメモリアクセスと同じ方法でアクセス(読み書き)できる。

ページ単位でマップするため、小さいファイルをマップするとフラグメンテーションによる無駄が生じやすくなってしまう。

プロセスローダーの実行ファイルの読み込みや、複数プロセス間でのメモリ共有にも使用されているらしい。

デマンドページング

必要になってから物理メモリ領域を確保する仕組み。 mmap などを呼び出した際、まずは仮想メモリ領域のみ確保し、物理メモリ領域は確保しない。 その後、仮想メモリ領域のアドレスにはじめてアクセスしたタイミングで物理メモリ領域をページ単位で確保する。 これによってメモリを無駄に確保するのを防ぐ。

流れとしては以下の通り。

  1. プロセスが仮想ページへアクセス
  2. CPU がページテーブルを参照
  3. 対応する物理ページがないことを検出し、ページフォールトを発生
  4. カーネルモードに入り、ページフォールトハンドラ内で物理ページを確保し、ページテーブルを書き換えて仮想ページに対応させる
  5. ユーザモードにもどり、プロセスを継続

プロセスはページフォールトの発生に気づくことなくプロセスを継続できる。

コピーオンライト

プロセスを fork する際、ユーザからはメモリ領域をすべてコピーしているように見えるが、実際にはページテーブルのみコピーし、メモリ領域は親子プロセスで共有している。 その後、どちらかのプロセスがメモリに書き込みを行ったタイミングでそのページのメモリ領域を複製する。

共有しているメモリへの書き込みを検出するために、ページテーブルのコピー時にメモリへの書き込み権限をなくしておく。 そうすることで書き込み時にページフォールトが発生し、ページフォールトハンドラにてコピーオンライトの処理をはさむことが出来る。

流れとしては以下の通り。

  1. プロセスを fork し、ページテーブルを複製 このとき各ページへの書き込み権限をなくす
  2. 子プロセスがメモリに書き込む
  3. CPU がページテーブルを確認するが、書き込み権限が無いためページフォールトを発生
  4. カーネルモードに入り、ページフォールトハンドラ内で以下の処理を行う ・書き込み対象のページを複製 ・親プロセスのページテーブルの対象ページへの書き込み権限を付与 ・子プロセスのページテーブルの対象ページを複製先のページに書き換え、書き込み権限を付与
  5. ユーザーモードに戻り、プロセスを継続

プロセスはページフォールトの発生に気づくことなくプロセスを継続できる。

スワップ

ストレージデバイス(HDDなど)の一部をメモリとして使用する仕組み。 物理メモリが枯渇した際、空きメモリを作り出すために物理メモリのうちの一部をストレージデバイスに退避させる。 退避させるためのストレージデバイスの領域を「スワップ領域」という。(Windowsでは仮想メモリと呼んでいる。)

メモリからスワップ領域へ退避させることを「スワップアウト」、 スワップ領域からメモリへ復帰させることを「スワップイン」という。 どの領域をスワップアウトするかはカーネルが判断する。 スワップアウトやスワップインをすることを「スワッピング」いう。 Linux ではページ単位でスワッピングするため、「ページング」ともいう。

物理メモリが少ないためにスワッピングが常に起こっている状態を「スラッシング」という。

swapon

swapon --show コマンドでスワップ領域を確認できる。

free

free コマンドでもスワップ領域の容量を確認できる。

sar

システム稼動中は sar -W コマンドでスワッピングしているページ数を確認できる。 sar -S ではスワッピングしている容量を確認できる。

ページフォールトの種類

メジャーフォールト

ストレージデバイスへのアクセスが発生するページフォールト

マイナーフォールト

ストレージデバイスへのアクセスが発生しないページフォールト

階層型ページテーブル

実際のページテーブルは階層的に実装されている。 これによってページテーブルサイズを節約できる。

ヒュージページ

Linux の機能。 通常のページよりサイズの大きいページ。 ページテーブルはページごとにエントリを作成するため、これによってページテーブルサイズを抑えることができる。

mmap 関数の引数に MAP_HUGETLB フラグを与えることでヒュージページを獲得する。

トランスペアレントヒュージページ

仮想アドレス空間内の連続する複数の4Kバイトページが所定の条件を満たしたときに自動的にヒュージページとする機能。 場合によっては性能が劣化することがあるため、無効化することもある。

hysryt commented 5 years ago

第6章

記憶階層

上のものほど速度が速いが、価格が高く、容量が小さい。

コンピュータの基本動作

  1. メモリからデータをレジスタに読み出す
  2. レジスタ上のデータをもとに計算する
  3. 計算結果をメモリに書き出す

つまりレジスタがいくら速くてもメモリの速度がボトルネックになる。 メモリの速度に律速するという。

キャッシュメモリ

メモリとレジスタの間を仲介し、所要時間の差を埋める記憶媒体。SRAM。 通常はCPUに内蔵されるが、外についているものもある。

キャッシュメモリは階層構造をとっており、複数のキャッシュメモリが存在する。 レジスタに近いキャッシュメモリほど速度が速いが、容量が少ない。 一番レジスタに近いキャッシュメモリを「L1キャッシュ(レベル1キャッシュ)」といい、レジスタから遠ざかるにつれて数字が大きくなっていく。 キャッシュメモリの数はCPUによって異なる。

キャッシュメモリの動作

メモリからレジスタにデータを読み出す際、キャッシュメモリにもデータを読み出す。

メモリ → キャッシュメモリ → レジスタ

読み出しは「キャッシュライン」という単位で行う。

一方、レジスタの値を書きかえた際はキャッシュメモリにのみかき戻す。

レジスタ → キャッシュメモリ

この時もキャッシュライン単位で書き込みを行う。 キャッシュメモリが書きかえられ、メモリと異なるデータを持っている状態を「ダーティ」という。

ダーティなキャッシュラインは所定のタイミングでメモリに書き戻す。 それに伴ってダーティなキャッシュラインはダーティではなくなる。

キャッシュラインの破棄

キャッシュメモリがいっぱいの状態でキャッシュに存在しないデータにアクセスした場合は、キャッシュラインのどれかを破棄する。 対象のキャッシュラインがダーティな場合、メモリに書き戻してから破棄する。

キャッシュメモリの情報

キャッシュメモリの情報は以下のディレクトリのファイルで確認できる。 以下はCPU0のL1キャッシュの例

$ ls /sys/devices/system/cpu/cpu0/cache/index0
coherency_line_size  physical_line_partition  size
id                   power                    type
level                shared_cpu_list          uevent
number_of_sets       shared_cpu_map           ways_of_associativity

たとえば size ファイルをみるとそのキャッシュメモリのサイズを取得できる。

$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K

Translation Lookaside Buffer (TLB)

ページテーブルにある仮想アドレスと物理アドレスの変換情報(ページテーブルエントリ)をキャッシュする。 CPUに内蔵されており、キャッシュメモリと同様に高速にアクセスできる。

ページキャッシュ

ストレージデバイス(HDDなど)のデータを主記憶(メモリ)にキャッシュする機能。 キャッシングはページ単位で行う。 ページキャッシュ用の領域はカーネル用のメモリ領域内に存在する。

ページキャッシュはすべてのプロセスで共有するため、別々のプロセスが同じファイルを読み込む際でも高速化が見込める。

キャッシュメモリと同様に、ページキャッシュに読み込んだあとはデータの読み書きはページキャッシュに対してのみ行い、所定のタイミングでストレージデバイスに書き戻す。 ページキャッシュに対して書き込みが行われ、ストレージデバイスのデータとの間に差異があるぺーじをダーティページと呼ぶ。

ページキャッシュのサイズ

メモリに空きがある限り、ページキャッシュのサイズは増え続ける。 メモリが足りなくなるとページキャッシュを解放する。 まずダーティでないページキャッシュから解放していき、ダーティなページキャッシュだけになったらライトバックしたのち解放する。

同期書き込み

ページキャッシュに書き込む際、ストレージデバイスにも同時に書き込む機能。 突然のシャットダウンなどでまだストレージデバイスに書き込まれてないデータが消えるのを防ぐ。 open システムコールを呼び出す際、O_SYNC フラグを設定することで使用できる。 都度ストレージデバイスに書き込むため、速度は低下する。

ハイパースレッド機能

CPUの空費状態を有効活用する仕組み。 CPUの処理速度に比べるとレジスタへのアクセス速度は遅いため、そのようなタイミングでCPUの空費状態が発生している。

https://wa3.i-3-i.info/word12754.html http://e-words.jp/w/ハイパースレッディング.html https://www.intel.co.jp/content/www/jp/ja/architecture-and-technology/hyper-threading/hyper-threading-technology.html コアを複数個あるように見せかけ、並列処理を行う。 複数個の論理コアでキャッシュメモリを共有したり、命令の種類によっては並列化できないため、最良でも20%~30%程度の速度向上とのこと。

hysryt commented 5 years ago

第7章

ファイルシステム

ストレージデバイス(HDD、SSDなど)にはファイルシステムを介してアクセスする。

ファイルシステムにはいくつか種類があるが、どのファイルシステムであってもアクセス時のシステムコールは一緒。

処理 システムコール
ファイルの作成、削除 creatunlink
ファイルを開く、閉じる openclose
開いたファイルからデータを読み出す read
開いたファイルにデータを書き込む write
開いたファイルの所定の位置に移動 lseek
ファイルシステム依存の特殊な処理 ioctl

システムコール発行時の処理は以下の通り

  1. ユーザー:システムコール発行
  2. カーネル:ファイルシステム共通処理
  3. カーネル:ファイルシステム別の処理
  4. カーネル:デバイスドライバに処理を依頼
  5. カーネル:データの読み書き

クォータ

ファイルシステムによっては容量制限を課すことができる。 この機能をクォータという。

クォータの種類 説明
ユーザクォータ ユーザごとに容量を制限する。
ディレクトリクォータ ディレクトリごとに容量を制限する。
サブボリュームクォータ サブボリュームという単位に分けて容量を制限する

ファイルシステムの不整合

ファイルシステムの更新中、システムの強制終了などでファイルシステムに不整合が生じることがある。この不整合を防ぐためにジャーナリングコピーライトという方式がある。

ジャーナリング

ファイルシステム内にジャーナル領域という特殊な領域を用意する。 まずジャーナル領域に更新処理の処理一覧を書き出す。 その後、実際に処理を行う。 ジャーナル領域に書き出し中にシステムが強制終了した場合、次回起動時にジャーナル領域の内容を捨てる。 実際の処理の途中でシステムが強制終了した場合、次回起動時にジャーナル領域の処理一覧を最初から全てやり直す。 こうすることで不整合が起こらないようにする。

コピーオンライト

コピーオンライト方式のファイルシステムはファイルを更新するとき既存のものを上書きするのではなく、新しい領域に作り直す。 更新が終了したら新しいものにリンクを貼り直す。 こうすることで不整合が起こらないようにする。

デバイスファイル

Linuxはほぼすべてのデバイスをファイルとして表現する。これをデバイスファイルという。(ネットワークアダプタはデバイスに対応するファイルはない。) デバイスファイルにも通常のファイルと同じように openread などのシステムコールでアクセスする。複雑な操作には ioctl システムコールを使用する。

デバイスファイルにはキャラクタデバイスブロックデバイスの2種類がある。ls -l コマンドを実行したとき、行の先頭文字が c のものはキャラクタデバイス、 b のものはブロックデバイスとなっている。 デバイスファイルには通常、 root のみアクセスできる。

キャラクタデバイス

読み書きはできるが、シークはできない。 端末、キーボード、マウスなど。

ブロックデバイス

読み書きだけでなく、シークによるランダムアクセスが可能。 ストレージデバイス(HDD、SDD)など。 ブロックデバイスはファイルシステムを作成し、それをマウントすることによってファイルシステム経由でアクセスする。

ブロックデバイスを直接操作するのは次のような場合。

tmpfs

メモリ上に作成するファイルシステム。 電源を切ると消えるが、高速にアクセスできる。 free コマンドの shared で表される。

ネットワークファイルシステム

リモートホスト上のファイルにアクセスするファイルシステム。 Windows ホスト上のファイルにアクセスするときには cifs 、 Linux などの UNIX 系 OS を使ったホスト上のファイルにアクセスするときは nfs というファイルシステムを使用する。

仮想ファイルシステム(VFS)

procfs

システムやプロセスについての情報を得るためのファイルシステム。 通常は /proc にマウントされる。

sysfs

procfs が複雑になったため作られたファイルシステム。デバイスやファイルシステムに関する情報を取得できる。

cgroupfs

cgroup を操作するためのファイルシステム。

Btrfs

マルチボリューム。 複数のストレージデバイスやパーティションから一つのストレージプールを作成し、その上にマウント可能なサブボリュームを作成する。

スナップショット

サブボリューム単位でスナップショットを採取できる。

RAID

ファイルシステムレベルでRAIDを組める。