ライブラリ関数とシステムコールの違い
printf や fopen はC標準ライブラリ(libc)の関数で、システムコールそのものではありません。これらは内部でバッファリングなどの工夫をしつつ、最終的に write や open といったシステムコールを呼び出します。つまりライブラリ関数は「使いやすい上の層」、システムコールは「カーネルへの直接の入口」という関係です。両者の境目は strace(システムコールを表示)と ltrace(ライブラリ関数呼び出しを表示)を比べると体感できます。fwrite が即座に write を呼ばずバッファに溜めるなど、層が違うことで挙動も変わるため、性能やデバッグを考えるうえで区別が重要になります。
C言語でファイルを扱おうとすると、fopenとopen、fwriteとwrite、fprintfとwriteのように、似た目的の関数が二系統あることに気づく。
なぜ、わざわざ似たものが二つ用意されているのか。これは、システムコール(syscall)とライブラリ関数が、まったく別の層に属しているからだ。
この二つの層の違いをきちんと理解すると、一段深い話がすっきり腑に落ちる。なぜprintfで出力したはずの文字がすぐに画面へ出ないことがあるのか、なぜ性能やデバッグの場面で両者を区別する必要があるのか、といった話だ。
低レイヤを扱ううえで避けて通れない、とても重要な区別なので、ここで丁寧に押さえておこう。
🪜 二つの層の上下関係
システムコールは、これまで見てきたとおり、カーネルへ直接仕事を頼むための窓口だ。
一方、printfやfopenといった関数は、C標準ライブラリ(libc、Linuxでは多くの場合glibc)が提供するライブラリ関数であって、システムコールそのものではない。
両者はきれいな階層になっている。一番上にアプリのコードがあり、その下にlibcのライブラリ関数があり、さらにその下にシステムコールがあって、最下層にカーネルがいる。
つまりライブラリ関数は「アプリから使いやすいように整えられた上の層」、システムコールは「カーネルへの直接の入口」という役割分担だ。
fopenやprintfは、内部でさまざまな下準備をしたうえで、最終的にopenやwriteといったシステムコールを呼び出す。
私たちが普段書くコードの多くは、この使いやすい上の層を経由して、カーネルの機能を間接的に利用している。ライブラリ関数は、エラー処理やデータの整形といった面倒な部分を肩代わりしてくれるぶん、手軽に使える。
🎁 ラッパー関数が肩代わりしているもの
ここで一段細かい話をすると、C言語でwriteやopenを呼ぶときでさえ、実は本物のシステムコールを直接叩いているわけではない。
多くの場合、glibcが用意したラッパー関数を経由している。ラッパーとは、システムコールを呼びやすいように薄く包んだ関数のことだ。
具体的には、引数をカーネルが期待する形に整えてシステムコールを発行し、カーネルから戻ってきた結果を見て、失敗していればerrno(errno)に理由を入れて-1を返す、という後始末まで肩代わりしてくれる。
私たちが、戻り値とerrnoを見るだけで簡潔にエラー処理を書けるのは、このラッパーが裏で働いてくれているおかげだ。
整理すると、ソースコードに書くのが「glibcのラッパー(あるいはより上位のライブラリ関数)」、straceで実際に見えるのが「カーネルへ発行された生のシステムコール」という二段構えになっている。
この対応関係を頭に入れておくと、ソースでは一回しか呼んでいないライブラリ関数が、straceでは内部で複数のシステムコールに分かれて見えても、慌てずに済む。
📤 printf した文字がすぐ画面に出ないわけ
二つの層の違いが最もはっきりと表に出るのが、バッファリングだ。
ライブラリ関数のfwriteやprintfは、書き込むデータをすぐにカーネルへ渡さず、いったんライブラリ内部のバッファ(一時的なため場所)に溜め込む。
そして、バッファがいっぱいになったときや、画面出力で改行が来たとき、あるいはプログラムが正常終了するときなどに、まとめて下層のwriteシステムコールを一度だけ呼んで吐き出す。
これは前に触れたとおり、ユーザ空間(userspace)とカーネルの境界をまたぐ回数を減らして性能を稼ぐための工夫だ。
一方、システムコールのwriteを直接呼べば、データはその場で即座にカーネルへ渡る。
この違いがあるために、printfで出力したはずの文字が、プログラムが途中でクラッシュした瞬間にはまだバッファの中に残っていて、画面に出ていない、という一見不可解な現象が起こりえる。
複数の出力の順序が直感と合わないときも、このバッファリングが原因であることが少なくない。
デバッグで確実に出力を見たいときは、バッファを強制的に吐き出すfflushを使う、という対処も覚えておくと役に立つ。
📦 FILE 構造体と fd の関係
二つの層は、扱う「対象の表し方」も違う。
高水準のライブラリ関数fopenが返すのは、FILE構造体へのポインタという、バッファなどの情報を内部に抱えた箱だ。
一方、低水準のopenが返すのは、これまで見てきたファイルディスクリプタ(fd)という、ただの小さな整数だった。
両者はまったく別物のようでいて、実は前者は後者を包んだものになっている。FILE構造体の内側にはfdが収まっているのだ。
必要なときはfileno関数でFILEからfdを取り出せるし、逆にfdをfdopen関数でFILEに包み直すこともできる。この対応を知っておくと、二系統の関数を行き来する場面でも混乱しない。
🔬 strace と ltrace を並べると層の違いが見える
二つの層の違いは、観察ツールを使い分けると体感として理解できる。
straceはカーネルへのシステムコールを表示し、ltraceはライブラリ関数の呼び出しを表示する。同じプログラムをそれぞれで追ってみると、層の違いが目に見える形で現れる。
たとえばprintfを一度だけ呼ぶプログラムをltraceで見るとputsやprintfの呼び出しが現れるが、同じものをstraceで見ると、その出力は内部でまとめられて一回のwriteとして発行されている、といった具合だ。
fwriteを何度も繰り返し呼んでいるのに、straceにはwriteが数回しか出てこない、という観察も、バッファリングが確かに働いている証拠になる。
では実務では、どちらを選べばいいのか。勘所はこうだ。手軽さと性能を取りたいなら、バッファリングが効く高水準のライブラリ関数(fopen系)を使う。出力の即時性が必要だったり、デバイスファイルを細かく制御したかったりするなら、低水準のシステムコール(open系)を直接使う。
そして「なぜこういう動きになるのか」が分からなくなったら、straceとltraceで層をまたいだ実際の呼び出しを覗いてみる。この使い分けが、二つの層の区別を実践に活かすための確かな道筋になる。