コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB
システムコールの仕組み
自分のメモリしか触れないのに、どうやってファイルを開くのか
リング 3 で動くプロセスは、割り当てられたメモリの外に手が届きません。ということは、ディスクにも、ネットワークカードにも、画面にも、自分では一切触れないということです。それでも open("a.txt") は動きます。
種明かしは単純で、自分でやるのをあきらめて、できる人に頼んでいます。プロセスは「このファイルを開いてほしい」という依頼をカーネルへ渡し、カーネルがリング 0 の権限で実行して、結果だけを返します。この依頼の窓口がシステムコールです。窓口は Linux でおよそ 400 個あり、これ以外にカーネルへ入る入口はありません。
番号を書いた紙を渡して、呼ばれるのを待つ
依頼といっても関数を呼び出すわけにはいきません。呼び出した先はリング 0 で、リング 3 からは番地すら見えないからです。代わりに使うのが、CPU が持っている専用の命令です。
やることは決まっています。何番の仕事を頼みたいのかを 1 つのレジスタに置き、引数を決められた順番で別のレジスタに置き、syscall 命令を実行します。CPU はそこで特権をリング 0 に切り替え、カーネルが用意した表を引いて、番号に対応する処理へ飛びます。
asm
mov rax, 1 ; 1 番の仕事、つまり write
mov rdi, 1 ; 書き込み先は標準出力
mov rsi, msg ; 書きたい文字列の先頭
mov rdx, 3 ; 3 バイト
syscall ; ここでカーネルへ渡る引数を置く場所が決まっているのは、リング 3 とリング 0 で信頼していないもの同士が話すからです。好きな形式で渡させたら、カーネルが受け取った値を検証しきれません。Python の print も、何段か下ればこの形に行き着きます。
1 バイトずつ書くと、なぜ遅いのか
窓口を通るたびに、CPU はレジスタを退避し、特権を切り替え、渡された番地が本当にそのプロセスのものか確かめます。1 回あたり数百ナノ秒から数マイクロ秒です。単体では小さくても、1 バイト書くたびに窓口へ並べば、10 万バイトで 10 万回並ぶことになります。
ファイル出力を数キロバイト分ためてから 1 回で流す作り方が定番なのは、処理そのものが速くなるからではなく、並ぶ回数が減るからです。実際に何回並んでいるかは計測できます。
ターミナル
strace -e openat,read,write echo helloプレーンテキスト
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
read(3, "...", 832) = 832
write(1, "hello\n", 6) = 6たった 1 語を表示するだけでも、共有ライブラリを探して開いて読む一式が先に走っています。-c を付けると集計だけが出るので、自分のプログラムでどの依頼が何回出ているかを数えてみてください。futex や mmap が異様に多ければ、疑うべきは自分の計算部分よりその周辺です。
ついでに 1 つ、ライブラリ関数がすべてシステムコールというわけではありません。malloc や printf の多くは、ライブラリの中だけで完結して窓口まで来ません。