ユーザー空間
アプリ、ライブラリ、プロセスのメモリが属する範囲です。
アプリがLinuxカーネルへ処理を頼む境界
Linuxカーネルは、CPU時間とRAMをプロセスへ割り当て、ファイル、通信、デバイスへのアクセスを管理します。アプリは、必要な処理をカーネルへ頼みながら動きます。
複数のアプリが同時に動くコンピューターでは、どのプロセスへCPU時間やRAMを割り当てるか、どのファイルを読めるか、どの通信を受け取るかをまとめて管理する必要があります。この管理をLinuxカーネルが担当します。
アプリは、ファイルを読む、通信を始める、プロセスを作る、仮想アドレス空間を変更するといった処理をカーネルへ頼みます。その依頼が通る、カーネルの決められた入口をシステムコールシステムコールユーザー空間のプロセスが、Linuxカーネルへ処理を頼むための入口。と呼びます。
アプリやライブラリは、プロセスごとに用意された仮想アドレス空間を使って動きます。このアプリ側の範囲をユーザー空間ユーザー空間アプリやライブラリが動き、プロセスごとの仮想アドレス空間を使う範囲。と呼びます。Linuxカーネルのコードとデータが置かれ、プロセス管理、メモリ管理、ファイルシステム、ネットワークスタック、デバイスドライバなどが動く範囲をカーネル空間カーネル空間Linuxカーネルのコードとデータが置かれ、OSの中核処理が動く範囲。と呼びます。
CPUは、実行できる命令とアクセスできる範囲を制限するため、実行中の権限を切り替えます。アプリを実行する権限をuser mode、カーネルを実行する権限をkernel modeと呼びます。これらをまとめてCPUの実行モードCPUの実行モード実行できる命令とアクセスできる範囲を、CPUが制限する仕組み。と呼びます。
アプリ、ライブラリ、プロセスのメモリが属する範囲です。
Linuxカーネルのコードとデータが属する範囲です。
実行できる命令とアクセス権限をCPUが切り替えます。
CPUが実行できる命令と、アクセスできるアドレスが制限されます。
カーネルは権限を確認し、共有するCPU、RAM、ファイル、デバイスを管理します。
アプリは、用途に合うシステムコールを使ってカーネルへ処理を頼みます。Linuxが提供するシステムコールのうち、後のページにも登場する代表例を役割ごとにまとめます。
openat() read() write() close()ファイルを開き、内容を読み書きし、使い終わった参照を閉じます。
fork() clone() clone3() execve() wait4()子プロセスやスレッドを作り、別のプログラムを実行し、終了を待ちます。
mmap() munmap() mprotect() brk()仮想アドレス空間へ範囲を追加または削除し、アクセス権や、動的なメモリ確保に使うheapheapプログラムが実行中に必要な大きさを確保し、不要になれば解放するメモリ領域。の終端を変更します。
socket() connect() accept() sendto() recvfrom()通信の端点を作り、接続を開始・受け付け、データを送受信します。
futex() epoll_wait() nanosleep()ほかのスレッドやI/Oの状態が変わるまで待ち、指定した時間だけ実行を止めます。
ioctl()端末、ネットワークインターフェース、GPUなどへ、種類ごとに定義された操作を頼みます。
read()を呼んでから、アプリへ結果が返るまでシステムコールへ入ると、CPUの実行モードがuser modeからkernel modeへ切り替わり、カーネルが依頼内容を検査して処理します。
read(fd, buffer, 4096)read()を呼ぶfdは開いたファイル、bufferは読み込んだデータの保存先です。
システムコール番号と引数を、CPUアーキテクチャで決められたレジスタへ置きます。
専用命令を実行し、CPUをkernel modeへ切り替えて、カーネルに登録された入口へ進みます。
ファイルディスクリプタ、保存先のアドレス、アクセス権などを確認します。
page cacheやファイルシステムから読みます。ストレージへのI/Oが必要なら、block layerとデバイスドライバへ処理が進みます。
読み込んだbyte数、またはエラーを示す値が返り、user modeで実行を続けます。
strlen()文字列を調べるライブラリ関数です。通常はプロセスのメモリを読み、user modeのまま完了します。
malloc()メモリアロケータの空き領域を再利用できれば、ライブラリ内で完了します。不足するとbrk()やmmap()を使うことがあります。
read() mmap()libcの関数が、CPUアーキテクチャごとの手順に沿ってカーネルのシステムコールを呼び出します。
呼び出したプロセスをもとに子プロセスを作ります。親子の仮想アドレス空間は分かれますが、作成直後はcopy-on-writeにより、多くの物理pageを共有します。
仮想アドレス空間、ファイルディスクリプタ表、シグナル処理、namespaceなどを親と共有するか指定して子タスクを作ります。スレッドの作成にも使われます。
execve()現在のプロセスIDを保ったまま、仮想アドレス空間を新しいプログラムの内容へ置き換えます。forkの後に呼ぶ構成が広く使われます。
システムコールは、プロセスがカーネルへ処理を頼む入口です。逆向きに、カーネルや別のプロセスから実行中のプロセスへイベントを知らせる場合もあります。この通知をシグナルシグナルカーネルや別のプロセスが、プロセスへイベントの発生を非同期に知らせる仕組み。と呼びます。
端末で割り込みキーを押したときのSIGINT、終了を求めるSIGTERM、子プロセスの状態変化を知らせるSIGCHLDなどがあります。通知されたプロセスは、シグナルごとの既定動作に従うか、あらかじめ登録した処理を実行します。
PID 1がSIGCHLDを受け、子プロセスの終了状態を回収する流れを見る
通信パケットの到着、システムコールの実行、ファイルへのアクセスなど、Linuxが処理するイベントはカーネル内で起きます。これらを詳しく観測したり、その場で処理を選んだりする機能は、イベントが起きた位置で実行する必要があります。
Linuxには、用途に合わせたプログラムをユーザー空間で用意し、実行中のカーネルへ読み込む仕組みがあります。この仕組みをeBPFeBPFカーネル本体を書き換えず、用途に合わせたプログラムをカーネル内で実行する仕組み。と呼びます。カーネル本体のソースコードを変更せずに、システムの観測、ネットワーク処理、アクセス制御などの機能を追加できます。
カーネルは、読み込まれたプログラムの命令とメモリアクセスを実行前に検査します。検査を通ったプログラムを対応するイベントへ取り付けると、そのイベントが起きたときに実行されます。
bpf()システムコール↓↑データカーネルは、到達する処理経路、レジスタの型、メモリアクセスなどを追跡します。この検査を行う機能をBPF verifierBPF verifierBPFプログラムをカーネルへ読み込む前に、命令やメモリアクセスを検査する仕組み。と呼び、検査を通らないプログラムは読み込みません。
プログラムは、対応しているイベントへあらかじめ取り付けます。この取り付け先をhookhook特定の処理が起きたとき、追加のプログラムや確認処理を呼び出すために用意された位置。と呼びます。パケットの受信や関数の呼び出しなどが起きると実行されます。
集めた値や設定を保持する構造をBPF mapBPF mapBPFプログラムとユーザー空間のプログラムが、データを保持・共有するための構造。と呼びます。連続するイベントデータはring bufferなどを通してユーザー空間のツールへ渡せます。
関数の実行、システムコール、処理時間、エラーなどを記録します。この用途ではtracepoint、fentry、uprobeなどへ取り付けます。
パケットを調べ、転送、破棄、負荷分散などを行います。この用途ではXDPやtcなどへ取り付けます。
プロセス、ファイル、socketなどに関わる操作を記録し、一部の操作では許可するか判断します。この用途ではLSMLSMカーネル内の操作へ、アクセス制御を呼び出すhookを用意する共通の枠組み。やcgroupcgroupプロセス群ごとに、CPU時間、メモリ、I/Oの使用量や優先度を管理する仕組み。に用意されたhookなどを使います。