アプリからカーネルへ頼む

共有するものをカーネルが管理する

複数のアプリが同時に動くコンピューターでは、どのプロセスへCPU時間やRAMを割り当てるか、どのファイルを読めるか、どの通信を受け取るかをまとめて管理する必要があります。この管理をLinuxカーネルが担当します。

アプリは、ファイルを読む、通信を始める、プロセスを作る、仮想アドレス空間を変更するといった処理をカーネルへ頼みます。その依頼が通る、カーネルの決められた入口をシステムコールシステムコールユーザー空間のプロセスが、Linuxカーネルへ処理を頼むための入口。と呼びます。

最初に三つの言葉を分ける

空間の区分と、CPUの実行権限

アプリやライブラリは、プロセスごとに用意された仮想アドレス空間を使って動きます。このアプリ側の範囲をユーザー空間ユーザー空間アプリやライブラリが動き、プロセスごとの仮想アドレス空間を使う範囲。と呼びます。Linuxカーネルのコードとデータが置かれ、プロセス管理、メモリ管理、ファイルシステム、ネットワークスタック、デバイスドライバなどが動く範囲をカーネル空間カーネル空間Linuxカーネルのコードとデータが置かれ、OSの中核処理が動く範囲。と呼びます。

CPUは、実行できる命令とアクセスできる範囲を制限するため、実行中の権限を切り替えます。アプリを実行する権限をuser mode、カーネルを実行する権限をkernel modeと呼びます。これらをまとめてCPUの実行モードCPUの実行モード実行できる命令とアクセスできる範囲を、CPUが制限する仕組み。と呼びます。

ソフトウェアとアドレス空間

ユーザー空間

アプリ、ライブラリ、プロセスのメモリが属する範囲です。

ソフトウェアとアドレス空間

カーネル空間

Linuxカーネルのコードとデータが属する範囲です。

CPUの状態

user mode / kernel mode

実行できる命令とアクセス権限をCPUが切り替えます。

USER MODE

ユーザー空間ユーザー空間アプリやライブラリが動き、プロセスごとの仮想アドレス空間を使う範囲。

アプリプロセスとして実行
共有ライブラリlibcなど
プロセスのメモリ仮想アドレス空間

CPUが実行できる命令と、アクセスできるアドレスが制限されます。

システムコール
CPUの実行モードを切り替える
戻り値
KERNEL MODE

カーネル空間カーネル空間Linuxカーネルのコードとデータが置かれ、OSの中核処理が動く範囲。

プロセス管理スケジューラ
メモリ管理ページテーブル
ファイルと通信ファイルシステム・ネットワークスタック
デバイスドライバハードウェアを制御

カーネルは権限を確認し、共有するCPU、RAM、ファイル、デバイスを管理します。

アプリからカーネルへ処理を頼むとき、CPUは決められた入口を通って実行モードを切り替えます。
02.1
アプリからカーネルへ処理を移す

システムコールの入口と戻り道

アプリは、用途に合うシステムコールを使ってカーネルへ処理を頼みます。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へ切り替わり、カーネルが依頼内容を検査して処理します。

例:ファイルから最大4096 byteを読むread(fd, buffer, 4096)
  1. 01 / アプリ

    read()を呼ぶ

    fdは開いたファイル、bufferは読み込んだデータの保存先です。

  2. 02 / libc

    番号と引数を用意する

    システムコール番号と引数を、CPUアーキテクチャで決められたレジスタへ置きます。

  3. 03 / CPU

    カーネルへ処理を移す

    専用命令を実行し、CPUをkernel modeへ切り替えて、カーネルに登録された入口へ進みます。

  4. 04 / カーネル

    要求を検査する

    ファイルディスクリプタ、保存先のアドレス、アクセス権などを確認します。

  5. 05 / カーネル

    データを用意する

    page cacheやファイルシステムから読みます。ストレージへのI/Oが必要なら、block layerとデバイスドライバへ処理が進みます。

  6. 06 / アプリへ戻る

    結果を受け取る

    読み込んだbyte数、またはエラーを示す値が返り、user modeで実行を続けます。

02.2
関数とシステムコールの関係

ソースコードに書く関数名だけでは、境界を越えるか決まらない

ライブラリ内で完了

strlen()

文字列を調べるライブラリ関数です。通常はプロセスのメモリを読み、user modeのまま完了します。

必要な場合だけカーネルへ

malloc()

メモリアロケータの空き領域を再利用できれば、ライブラリ内で完了します。不足するとbrk()やmmap()を使うことがあります。

システムコールのラッパー

read() mmap()

libcの関数が、CPUアーキテクチャごとの手順に沿ってカーネルのシステムコールを呼び出します。

子プロセスを作る

fork()fork呼び出したプロセスをもとに、子プロセスを作るためのUnix系OSのAPI。

呼び出したプロセスをもとに子プロセスを作ります。親子の仮想アドレス空間は分かれますが、作成直後はcopy-on-writeにより、多くの物理pageを共有します。

共有する範囲を指定する

clone() / clone3()clone / clone3子タスクを作り、親と共有する実行環境を細かく指定できるLinuxのシステムコール。

仮想アドレス空間、ファイルディスクリプタ表、シグナル処理、namespaceなどを親と共有するか指定して子タスクを作ります。スレッドの作成にも使われます。

実行するプログラムを替える

execve()

現在のプロセスIDを保ったまま、仮想アドレス空間を新しいプログラムの内容へ置き換えます。forkの後に呼ぶ構成が広く使われます。

02.3
カーネルからプロセスへイベントを知らせる

シグナルによる非同期の通知

システムコールは、プロセスがカーネルへ処理を頼む入口です。逆向きに、カーネルや別のプロセスから実行中のプロセスへイベントを知らせる場合もあります。この通知をシグナルシグナルカーネルや別のプロセスが、プロセスへイベントの発生を非同期に知らせる仕組み。と呼びます。

端末で割り込みキーを押したときのSIGINT、終了を求めるSIGTERM、子プロセスの状態変化を知らせるSIGCHLDなどがあります。通知されたプロセスは、シグナルごとの既定動作に従うか、あらかじめ登録した処理を実行します。

プロセスからカーネルへシステムコール処理を依頼する
Linuxカーネル処理とイベントの管理要求を処理し、通知を保留する
カーネルからプロセスへシグナルイベントを知らせる

PID 1がSIGCHLDを受け、子プロセスの終了状態を回収する流れを見る

02.4
カーネル本体を書き換えず、観測や処理を追加する

用途に合わせたプログラムをカーネル内で実行する

通信パケットの到着、システムコールの実行、ファイルへのアクセスなど、Linuxが処理するイベントはカーネル内で起きます。これらを詳しく観測したり、その場で処理を選んだりする機能は、イベントが起きた位置で実行する必要があります。

Linuxには、用途に合わせたプログラムをユーザー空間で用意し、実行中のカーネルへ読み込む仕組みがあります。この仕組みをeBPFeBPFカーネル本体を書き換えず、用途に合わせたプログラムをカーネル内で実行する仕組み。と呼びます。カーネル本体のソースコードを変更せずに、システムの観測、ネットワーク処理、アクセス制御などの機能を追加できます。

カーネルは、読み込まれたプログラムの命令とメモリアクセスを実行前に検査します。検査を通ったプログラムを対応するイベントへ取り付けると、そのイベントが起きたときに実行されます。

USER SPACE
読み込みを行うツールプログラムとデータ保存用の領域を用意
収集結果を表示するツールBPF mapBPF mapBPFプログラムとユーザー空間のプログラムが、データを保持・共有するための構造。やring bufferから読む
bpf()システムコール↓↑データ
KERNEL SPACE
BPF verifierBPF verifierBPFプログラムをカーネルへ読み込む前に、命令やメモリアクセスを検査する仕組み。命令とメモリアクセスを検査
↓ 読み込みを許可
動作を観測するtracepoint / fentry
パケットを処理するXDP / tc
アクセスを調べるLSM / cgroup
↓ イベントが起きると実行
BPF mapBPF mapBPFプログラムとユーザー空間のプログラムが、データを保持・共有するための構造。カーネルとユーザー空間でデータを受け渡す
eBPFeBPFカーネル本体を書き換えず、用途に合わせたプログラムをカーネル内で実行する仕組み。プログラムは、読み込み時にカーネルの検査を受けます。許可される処理と使える機能は、プログラムの種類と取り付け先で決まります。
読み込み時

カーネルが実行前に検査する

カーネルは、到達する処理経路、レジスタの型、メモリアクセスなどを追跡します。この検査を行う機能をBPF verifierBPF verifierBPFプログラムをカーネルへ読み込む前に、命令やメモリアクセスを検査する仕組み。と呼び、検査を通らないプログラムは読み込みません。

実行時

イベントが起きた位置で実行する

プログラムは、対応しているイベントへあらかじめ取り付けます。この取り付け先をhookhook特定の処理が起きたとき、追加のプログラムや確認処理を呼び出すために用意された位置。と呼びます。パケットの受信や関数の呼び出しなどが起きると実行されます。

データの保持

結果をユーザー空間から読めるようにする

集めた値や設定を保持する構造をBPF mapBPF mapBPFプログラムとユーザー空間のプログラムが、データを保持・共有するための構造。と呼びます。連続するイベントデータはring bufferなどを通してユーザー空間のツールへ渡せます。

システムの観測

どの処理に時間がかかったか調べる

関数の実行、システムコール、処理時間、エラーなどを記録します。この用途ではtracepoint、fentry、uprobeなどへ取り付けます。

ネットワーク

受信したパケットの処理を選ぶ

パケットを調べ、転送、破棄、負荷分散などを行います。この用途ではXDPやtcなどへ取り付けます。

アクセス制御

プロセスの操作を調べ、許可を判断する

プロセス、ファイル、socketなどに関わる操作を記録し、一部の操作では許可するか判断します。この用途ではLSMLSMカーネル内の操作へ、アクセス制御を呼び出すhookを用意する共通の枠組み。やcgroupcgroupプロセス群ごとに、CPU時間、メモリ、I/Oの使用量や優先度を管理する仕組み。に用意されたhookなどを使います。