2026/08/20

Cシステムコールとライブラリ関数

C 言語からコールする関数には大きく分けて システムコールライブラリ関数 の2種類があります。
どちらも関数呼び出しの見た目をしているため区別がつきにくいのですが、実行時の仕組み、コスト、権限モデルはまったく異なります。
そこで、両者の定義と違いを整理したうえで、システムコール実行時に発生するコンテキストスイッチが OS やアプリの性能にどのような負荷・影響を与えるかを簡単に解説します。

システムコール

システムコールとは、ユーザー空間で動作するプログラムがカーネルの機能を利用するための唯一の手段です。
man 2 open のようにマニュアルのセクション 2 に分類されています。

システムコールには open(2)read(2)write(2)socket(2) などがあります。

CPU には一般に実行できる命令や参照できるメモリ領域に制限のある「ユーザーモード」と、 すべての命令、メモリ空間、I/O ポートにアクセスできる「カーネルモード (特権モード)」の2つの動作モードがあります。 Unix や Linux を始めとするモダンな OS では、アプリケーションプログラムは基本的にユーザーモードで動作していて、 ディスクへの直接アクセスやプロセス管理などの危険な操作はハードウェア的に禁止されています。 そこで、こうした操作が必要な場合は CPU に用意された特別な命令を実行して、 CPU のモードをユーザーモードからカーネルモードへ切り替える必要があります。 Unix や Linux においてこの切り替えを実行するのがシステムコールです。

ライブラリ関数

ライブラリ関数は C 標準ライブラリ (libc) やその他のライブラリが提供するユーザー空間だけで完結する通常の関数です。
man 3 printf のようにマニュアルのセクション3に分類されています。

ライブラリ関数の分類

ライブラリ関数は大きく 2 種類に分類できます。
  1. システムコールを呼び出さない純粋なライブラリ関数
  2. strlen(3)sin(3) などで、これらは文字列操作や CPU 上での計算処理のみで完結しカーネルには影響しません。

  3. 内部でシステムコールをラップしているライブラリ
  4. fopen(3)fread(3)printf(3) などで、 これらは最終的に内部で open(2)read(2)write(2) といった システムコール発行していますが、 その前後でバッファリング、書式変換、エラー処理の付加などユーザー空間側の便利な処理を行っています。

たとえば fwrite(3) は、呼び出しのたびにシステムコール write(2) を発行するわけではなく、 FILE 構造体が持つユーザー空間のバッファにデータを蓄積して、バッファが一杯になった時、 あるいは fflush(3)fclose(3) が呼ばれた時にまとめて write(2) システムコールを発行する事で、システムコール呼び出し回数を減らし性能を大きく向上させています。

ライブラリ関数には fopen(3)fread(3)fwrite(3) などがあります。

コンテキストスイッチ

コンテキストスイッチとは、CPUがあるコンテキストから別のコンテキストへ切り替える処理全般を指します。

コンテキストスイッチの種類

コンテキストスイッチには大きく分けて 2 種類あります。
  1. モード切り替
  2. 前述したカーネルモードユーザーモードの切り替えです。
    このタイプのコンテキストスイッチはシステムコール発行や割り込み・例外発生時に起こります。 同一プロセス内での切り替えであり、後述する「プロセス切り替え」よりコストは小さいですがそれなりに負荷やコストがかかります。

  3. プロセス切り替え
  4. OS のスケジューラがプロセス/スレッドの実行を中断して別のプロセス/スレッドに実行を渡す処理です。 タイムスライスの満了、I/O 待ちによるブロック、割り込みによるプリエンプションなどで発生します。 ページテーブル (= アドレス空間) 自体が切り替わるためモード切り替えよりもさらにコストが大きくなります。

システムコールは、必ずモード切り替えを伴います。 更にそのシステムコールの処理が入出力など I/O 待ちなどでブロックが発生する場合には さらにプロセス切り替えを誘発することもあります。

コンテキストスイッチのシステム負荷・影響

モード切り替え(システムコール発行)のコスト要因

システムコールを 1 回発行するだけでも以下のような処理コストが発生します。
  • レジスタの退避・復元
  • 呼び出し元の CPU レジスタ一式をカーネルスタックへ保存し復帰時に復元します。

  • 特権レベルの遷移
  • CPU 内部の カーネルモードへの遷移、および復帰時のユーザーモードへの遷移で、CPU の内部的な検証・準備処理を伴います。

  • パイプラインフラッシュ・投機実行のリセット
  • モード遷移命令は CPU の命令パイプラインをある程度フラッシュさせ、 投機実行による先読みの効果を失わせます。

他にも TLB (Translation Lookaside Buffer) やキャッシュの汚染(cache pollution)、 セキュリティ機構によるオーバーヘッドなど、様々な影響があります。

これらの要因により、単純なシステムコール 1 回の呼び出しコストは、通常の関数呼び出しと比較して 100 から 1000 倍以上のコストがかかります。

プロセス/スレッド切り替えのコスト要因

システムコールがブロッキング I/O などで待ち状態に入ると、 OS のスケジューラは別のプロセス/スレッドを実行するためにプロセス切り替えを行いますが、 これには前述のモード切り替えのコストに加えて以下のコストが上乗せされます。
  • ページテーブルの切り替え
  • アドレス空間が完全に入れ替わるためTLB 全体が無効化されます。

  • スケジューラの実行コスト
  • 次に実行すべきプロセス / スレッドを選択するためのスケジューラの処理が実行されます。

  • キャッシュの入れ替え
  • キャッシュに載っていた前プロセスのデータ・命令が新しいプロセスのアクセスによって徐々に追い出される事により、 キャッシュミス率が一時的に急増して実行速度が低下します。

これらの処理が実行されるため、プロセス切り替えのコストはモード切り替え単体よりもさらに大きくなってしまいます。

頻繁なシステムコールがもたらすシステム全体への影響

例えばアプリケーションが 1 バイトずつ read(2)write(2) システムコールを呼び出すような設計をすると、 以下のような問題が顕在化します。
  • スループットの低下
  • システムコール自体のオーバーヘッドがボトルネックとなり本来の I/O 帯域を活かせなくなります。

  • CPU 使用率の増加
  • ユーザー時間ではなくシステム時間として CPU 時間が消費されてしまうので、システムの実行時間が高くなります。

  • キャッシュ効率の悪化
  • 頻繁なモード切り替えにより CPU キャッシュのヒット率が下がり、 システムコールを呼ばないコードパスの実行速度まで間接的に低下し、結果としてシステム全体のパフォーマンスが低下します。

  • スケーラビリティの低下
  • マルチコア環境では頻繁なシステムコールがカーネル内部の共有データ構造の競合を引き起こすので、 コア数を増やしても性能が線形にスケールしない原因となり得ます。

  • レイテンシのジッター増加
  • 割り込みやシステムコールによるコンテキストスイッチが頻発すると、リアルタイム性が求められる処理でジッターが増加します。

まとめ

システムコールはカーネルが提供するハードウェア資源にアクセスするための唯一の方法で、 呼び出しの都度、CPU のコンテキストスイッチが発生します。 一方ライブラリ関数は、多くの場合ユーザー空間だけで完結し、 内部でシステムコールをラップしている場合でもバッファリングなどによって呼び出し回数を抑える設計がされています。 プログラムの要件や仕様などでライブラリ関数では機能が不十分だったり、 特殊な制御が必要な場合以外はライブラリ関数を利用するのが好ましいです。

付録

具体例による比較を実施してみました。実行時間の計測はどちらも Intel CPU 上で稼働する Linux にて実施しました。

システムコールを利用する場合

1 バイト書き込むたびにシステムコール (=モード切り替え) が発生するため、コンテキストスイッチのコストが積み重なり著しく遅くなります。
  1#include <unistd.h>
  2#include <fcntl.h>
  3
  4int main(void)
  5{
  6
  7  int       fd,
  8                i;
  9
 10    if((fd = open("out.txt", O_WRONLY | O_CREAT, 0644)) >= 0){
 11        for(i=0; i<100000; i++)
 12            write(fd, "x", 1);
 13        close(fd);
 14    }
 15    
 16  return 0;
 17
 18}
        
$ time ./syscall

real    0m0.124s
user    0m0.018s
sys     0m0.106s
        

ライブラリ関数を利用する場合

ユーザー空間でバッファリングしてからまとめて write(2) を呼ぶため システムコール発行回数が数百分の一以下に抑えられ高速に動作します。
  1#include <stdio.h>
  2int main(void)
  3{
  4
  5    int     i;
  6  FILE  *fp;
  7
  8    if((fp = fopen("out.txt", "w"))){
  9        for(i=0; i<100000; i++)
 10            fputc('x', fp);
 11        fclose(fp);
 12    }
 13  
 14  return 0;
 15
 16}
        
$ time ./library

real    0m0.003s
user    0m0.001s
sys     0m0.001s
        

トラックバック
https://blog.bsdhack.org/index.cgi/Computer/20260820.trackback
コメント














メッセージ: Ready to post a comment.


Copyright © 2008-2020 Mitzyuki IMAIZUMI. All rights reserved.