🐧 Linux 総合学習プラットフォーム
システムコール ・ 上級

ライブラリ関数とシステムコールの違い

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では内部で複数のシステムコールに分かれて見えても、慌てずに済みます。

バッファリングが生む挙動の違い

二つの層の違いが最もはっきりと表に出るのが、バッファリングです。ライブラリ関数のfwriteやprintfは、書き込むデータをすぐにカーネルへ渡さず、いったんライブラリ内部のバッファ(一時的なため場所)に溜め込みます。そして、バッファがいっぱいになったときや、画面出力で改行が来たとき、あるいはプログラムが正常終了するときなどに、まとめて下層のwriteシステムコールを一度だけ呼んで吐き出します。これは前に触れたとおり、ユーザ空間(userspace)とカーネルの境界をまたぐ回数を減らして性能を稼ぐための工夫です。一方、システムコールのwriteを直接呼べば、データはその場で即座にカーネルへ渡ります。この違いがあるために、printfで出力したはずの文字が、プログラムが途中でクラッシュした瞬間にはまだバッファの中に残っていて、画面に出ていない、という一見不可解な現象が起こりえます。複数の出力の順序が直感と合わないときも、このバッファリングが原因であることが少なくありません。デバッグで確実に出力を見たいときは、バッファを強制的に吐き出すfflushを使う、という対処も覚えておくと役に立ちます。

strace と ltrace で層を見分ける

二つの層は、扱う「対象の表し方」も違います。高水準のライブラリ関数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で層をまたいだ実際の呼び出しを覗いてみる――この使い分けが、二つの層の区別を実践に活かすための確かな道筋になります。

この項目に出てくる用語

システムコールしすてむこーる
アプリがカーネルに処理を依頼する公式の窓口。
ファイルディスクリプタふぁいるでぃすくりぷた
開いたファイルを指す小さな整数(0以上)。
ユーザ空間ゆーざくうかん
アプリが動く、権限を制限された領域。

関連コマンド

straceltraceman 3 printf

▶ 学習アプリでこの続きを学ぶ・演習する