画面に文字を出すプログラムは、自分でディスプレイやSSDを直接操作しているのでしょうか。通常のアプリケーションは、OSが用意した窓口を通して処理を依頼します。この境界があるため、ひとつのプログラムの誤りが、ほかのプログラムのメモリや機器の設定を直ちに壊すことを防げます。
この単元でできるようになること
ユーザーモードとカーネルモードの違いを説明し、writeの引数が何を表すかを読めるようになります。また、システムコールで権限が切り替わることと、別のスレッドへ実行が切り替わることを区別します。
CPUの権限とOSの仕事を分けて考える
カーネルは、メモリの割当て、実行の調整、ファイルや機器へのアクセスなどを管理するOSの中核です。アプリケーションは通常、権限が制限されたユーザーモードで動きます。ページテーブルの変更など、システム全体に影響する操作は、決められた特権で動くコードが担当します。
特権モードは『何でも許可される利用者アカウント』と同じ意味ではありません。CPUの実行モードと、rootなどのOS上の資格は別の仕組みです。root権限のプログラムでも、通常の命令の実行はユーザーモードで行い、必要な操作をカーネルへ依頼します。
writeをひとつずつ読む
#include <unistd.h>
int main(void) {
const char message[] = "Hello\n";
ssize_t n = write(STDOUT_FILENO, message, sizeof(message) - 1);
return n == (ssize_t)(sizeof(message) - 1) ? 0 : 1;
}第1引数は出力先を表すファイルディスクリプタ、第2引数はデータのあるアドレス、第3引数は書きたいバイト数です。STDOUT_FILENOは通常1で、標準出力を表します。末尾のゼロ文字は出力対象にしないため、配列全体の大きさから1を引いています。
多くの環境では、Cライブラリのwriteが引数を定められた形に整え、CPUの専用命令を使ってカーネルの入口へ移ります。カーネルは出力先が有効か、書込みが許可されるか、指定された利用者側メモリにアクセスできるかを確認します。利用者の渡したアドレスを、信用できるカーネル内部のポインタとして扱うわけではありません。
成功でも要求した全部が終わるとは限らない
writeの戻り値は、実際に書き込めたバイト数です。要求より少ない値が返ることがあるので、汎用的な送信処理は残りの範囲を管理して繰り返します。上の短い例は6バイト全部が書けたかを判定するだけで、部分書込みを再試行する実装ではありません。失敗時には通常-1が返り、Cライブラリのerrnoで理由を調べます。
アプリケーションの関数呼出しとシステムコールの数も一致しません。printfは利用者側のバッファへためた後でwriteを呼ぶことがあります。反対に、ひとつの高水準なファイル操作が複数のシステムコールを使うこともあります。動作を調べるときは、ソースコードの行数とOSへの要求回数を分けて数えます。
モードの切替えと実行主体の切替え
システムコールに入っても、常に別のプロセスへ切り替わるわけではありません。同じスレッドの要求をカーネルが処理して、そのまま利用者側へ戻ることがあります。I/O待ちなどで実行を続けられない場合は、スケジューラが別の実行可能なスレッドを選びます。権限の切替えは入口と出口の話、コンテキストスイッチはCPUを誰に渡すかの話です。
理解を確かめる
問1:rootで起動したプログラムは、すべての命令をカーネルモードで実行するでしょうか。答えはいいえです。利用者の資格とCPUの実行モードは別で、特権が必要な処理はシステムコールなどを通して行います。
問2:write(fd, buf, 100)が40を返しました。『100バイト全部を送れた』と判断してよいでしょうか。40バイト分の書込みを終えたという結果なので、残り60バイトを扱う必要があります。再試行ではbufの位置と残りの長さを更新します。
プロセス・スレッド・コンテキストスイッチへ進むと、カーネルが実行中の状態をどう管理するかを確認できます。