1. システムコールとは?

システムコールの役割
システムコールの役割
(イラストをクリックするとサムネイルが表示されます)

アプリケーションのインタフェース

システムコール(スーパーバイザコール)もシェルと同様に, カーネルの機能を利用する
ためのインタフェースであると述べました. 一方の繋ぎ先はOSのカーネルであることは
シェルと同じですが, もう一方の繋ぎ先はシェルとは異なります. シェルの場合は
ユーザコマンドでしたが, システムコールの場合はアプリケーションプログラムです.
すなわち, システムコールはカーネルとアプリケーションプログラムのインタフェースです.
これがシェルとの違いになります. またこのことから, システムコールはAPI
(Application Programming Interface)
とも呼ばれます(厳密には, APIの方が概念が
広く, APIの1つがシステムコールと言えます. 例えば, Web APIの場合, システムコールの
ことではなく, ベンダーが開発した検索や翻訳などのWebプログラムのことを指します).

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

C言語で作成されたアプリケーションがあるとします. そのアプリケーションプログラム
では, 入出力やファイル処理を行うために, 標準ライブラリ関数...printf( )やfopen( )
などが利用されているでしょう. では, printf( )やfopen( )はどのようにして入出力や
ファイル処理を行っているのでしょうか?直接OSのカーネルに処理命令を出している
のでしょうか?

システムコールは関数

これは少し違います. 先に述べたようにOSのカーネルを直接操作するのは困難です.
実はそれらの標準ライブラリ関数は, 内部でwrite( )やopen( )といったシステムコールを
利用してOSのカーネルとやりとりを行っています. これらは, プログラミング言語における
関数と同じく, 引数を与えたり戻り値を取得したりして利用します. つまり, システムコール
(API)は外見上, 関数としてプログラムのなかで扱うわけです. システムコールって何?
あるいは, システムコールを使ったことがない方におさえておいてもらいたいのは,
システムコールの実体は関数」ということです. システムコールがハードウェアの挙動を
隠蔽し, 抽象化する(関数としてブラックボックス化する)ことによって, プログラマは
ハードウェアの物理特性を意識することなく(すなわち, 通常の関数を利用する感覚で),
デバイスからの入出力処理を行うことが可能となるのです.

System VとBSD

ちなみに, 主にUNIX系OSが備えておくべきAPIの規格の1つがPOSIXです. また, それらの
規格を実装したUNIXの系統がSystem V系BSD系です.

2. プロセスとファイルディスクリプタ(ファイル記述子)

プロセスとファイルディスクリプタ
プロセスとファイルディスクリプタ
(イラストをクリックするとサムネイルが表示されます)

プロセスファイルディスクリプタに関してもシステムコールを利用するうえで,
理解しておいたほうが良いと思うので簡単に説明しておきます.

プロセス

プロセスとは, 簡単に言えばコンピュータにとってのプログラムの処理単位のことです.
もう少し具体的に述べますと, プログラムを実行する際にはメモリが割り当てられますが,
このメモリを割り当てる単位のことを言います. 反対に, ユーザにとってのプログラムの
処理単位はジョブと呼ばれ, 1つのジョブは1つ以上のプロセスで構成されています.
プロセスは, 実行可能ファイルを実行することによって生成されます. また, プロセスが
新たなプロセスを生成する唯一の手段はfork( )システムコールを呼び出すことです.
例えば, UNIXにおいてすべてのプロセスの始祖となる(厳密にはすべてではありません.
例えば, スワッパやページデーモンなどのカーネルプロセスがその例にあたります)
initプロセスは, 実行可能ファイル/sbin/initを実行することによって生成されます.
そして, initプロセスがfork( )を繰り返すことによって, 様々なプロセスが生成されて
いきます. 実際にどのようなプロセス, あるいはジョブが起動しているのかはUNIXで
psコマンドを試してみてください.

ファイルディスクリプタ

ファイルディスクリプタ(file descriptor)とは, プロセスがオープンされているファイルを
識別するための0以上の整数値のことです. 例えば, C言語でfopen( )標準ライブラリ関数を
利用した時, ファイルを識別するためにFILE構造体へのポインタ(ストリーム)が返されます.
これをシステムコールのレベルで見ると, ファイルがオープンされたときに, 0以上の整数値
が返され, それを利用することでプロセスがファイルを識別し, アクセスが可能になります.
ちなみに, ファイルディスクリプタの0, 1, 2はプロセス開始から使用されており, それらは
標準入力, 標準出力, 標準エラー出力に対応しています. また, ヘッダファイル<unistd.h>の
中で, それぞれSTDIN_FILENO, STDOUT_FILENO, STDERR_FILENOとマクロ定義されて
います. したがって, 通常新規にファイルをオープンすると3が返されることになり, 新たに
ファイルをオープンしていく度に, 4, 5, 6...と空いている非負整数値のなかで最小の値が
ファイルディスクリプタの値として返されていきます. 以下のプログラム例のExample. 2を
利用して, 実際にファイルディスクリプタの値を調べてみると少し理解が進むかもしれません.
また, ファイルディスクリプタはファイルに関連した値ではなく, プロセスに関連した値です.
そのため, 異なるプロセスが同じファイルをオープンしても同じファイルディスクリプタが
返される保証はありません.

ファイル

ファイルの種類

ファイルという用語が出てきたところで, UNIX(Linux)におけるファイルの種類を説明します.

通常ファイル
テキストファイルや, 実行可能ファイルや画像・音声などのバイナリファイル.
ディレクトリファイル
階層ファイルシステム(ルートディレクトリ(/)を起点とした, ただ1つのツリー状の階層)
を実現するためのファイル. ファイル名(ハードリンク)とiノード番号をセットにした情報を
もちます(ディレクトリエントリ). Windowsでは, フォルダと呼ばれます.
文字型特殊ファイル
ブロック型特殊ファイル
いわゆるデバイスファイルで, 接続されているデバイスを抽象化したファイル.
端末もこれに属します(/dev/tty).
名前つきパイプ(FIFO)
プロセス間通信(IPC : Inter Process Communication)において利用されるファイルです.
ソケット
ネットワーク上でのプロセス間通信において利用されるファイル.
シンボリックリンク
ハードリンクの制約を取り除いたリンク. Windowsでの, ショートカットと同じ役割.

また, これらの情報はstat()システムコールを利用することで取得できます.

3. システムコールを利用したプログラム例

Example1. Hello World

                    #include<unistd.h>

                    int main(void){

                        char *s = "Hello World";

                        /*標準出力へ"Hello World"を出力*/
                        write(STDOUT_FILENO, s, sizeof(s));

                        return 0;
                     }
                

Example2. ファイルからの読み込み / ファイルへの書き込み

                    #include<fcntl.h>
                    #include<unistd.h>

                    int main(void){

                        int fdr, fdw, n;
                        char buf[80];

                        /*ファイルを読み取り専用モードでオープン*/
                        fdr = open("read.txt", O_RDONLY);

                        /*ファイルを書き込み専用モードでオープン*/
                        fdw = open("write.txt", O_WRONLY);

                        /*ファイル内容をバッファへ格納*/
                        n = read(fdr, buf, sizeof(buf));

                        /*ファイル(ディスクリプタ)へバッファ内容を出力*/
                        write(fdw, buf, n);

                        /*ファイル(ディスクリプタ)のクローズ*/
                        close(fdr);
                        close(fdw);

                        return 0;
                     }
                

Example3. プロセス生成

                    #include<sys/types.h>
                    #include<sys/wait.h>
                    #include<unistd.h>

                    int main(int argc, char *argv[]){

                        pid_t pid;
                        int status;

                        if((pid_t = fork()) < 0){
                            exit(1);
                        }else if(pid_t == 0){
                            /*子プロセス*/

                            /*子プロセスのプロセスIDを表示*/
                            printf("Child PID : %d", (int)getpid());

                            /*子プロセスに何かをさせる*/
                            execlp(argv[0], argv[0], argv[1], NULL);

                            /*エラー処理*/
                            perror("Error");
                            exit(1);
                        }else {
                            /*親プロセス*/

                            /*子プロセスの終了状態を取得*/
                            wait(&status);

                            exit(0);
                        }
                 }
                

4. なぜシステムコールを使うのか ?

標準ライブラリ関数のほうが良い

上記のシステムコールを利用したプログラム例の, Example1, Example2を見て,
少し疑問をもたれた方もいると思います. その疑問はおそらく,
システムコールを使わなくても, 標準ライブラリ関数を使えばいいのでは ?
という疑問でしょう. つまり上記の例では, open(), read(), write(), close()といった
システムコールを使わずに, fopen(), fgets(), fputs(), fclose()などを使えば実現可能
ということですね. その疑問は, 正解です. 結論から言えば, システムコールを使っても,
標準ライブラリ関数を使っても, 結果的に同じことができるのであれば, (一般的に)
標準ライブラリ関数を使う方が, 使いやすさや実行効率の点で優位でしょう. 入出力
処理であれば, 標準ライブラリ関数を使うことで, バッファリングの問題を考える必要
はなくなり, また実行効率も良くなります. そして, メカニズムとして, そいうった
標準ライブラリ関数は, 内部でシステムコールを呼び出して処理を実現しているぐらい
に理解しておけば良いでしょう. デバイス(ハードウェア)に対して何らかの処理を行う
標準ライブラリ関数の多くは, システムコールをより使いやすくするため, また実行効率
をより向上させるためにシステムコールをラッパーしている, というのが結論の根拠です.
入出力処理以外では, プログラム実行時に動的にメモリ領域を確保する処理があげられます
(= メモリ管理をするための処理). これを実現するために, 標準ライブラリ関数である
malloc()やcalloc(), realloc()を利用するでしょう. これらの標準ライブラリ関数は,
sbrk()というシステムコールを内部で利用しています. しかし, 動的にメモリを確保する
ためにわざわざsbrk()システムコールを直接呼び出すことはしないでしょう. それは,
malloc()やcalloc(), realloc()を使う方が使い勝手が良く, また実行効率も優れている
からです.

システムコールでないと実現できない

しかし, システムコールを利用したプログラム例のExample3は事情が異なります.
プログラムの実行中に新しいプロセスを生成しています. これと同じことが実現できる
標準ライブラリ関数は存在しません. また, 親プロセスでは, 子プロセスの終了状態を
取得しています. これと同じことができる標準ライブラリ関数もありません.
システムコールを使う理由はここにあります. 標準ライブラリ関数で実現できることは,
システムコールを駆使すれば実現できますが, システムコールでしかできないこと
たくさんあります. 例えば, ファイルの属性を取得・変更したり, シグナルを送ったり,
端末属性を変更したりといったことです. UNIX(Linux)をツールとして利用している
方であれば, これらの処理をコマンドでおこなうことが多いかと思います. コマンドは,
C言語で作成されたプログラムですが, その多くはシステムコールを利用しています.
また, リダイレクトやパイプはシェルの仕事ですが, シェル自身もシステムコールを利用
して作成されているプログラムです.シェルの機能に関しては簡単に述べましたが,
その実体の概要は, fork()システムコールとexeclp()標準ライブラリ関数(内部的には,
execve()システムコール)です. 実は, Example3のプログラムは, シェルの骨格となる
プログラムです(もちろん実際のシェルはもっと多くのコードで実現されていますが...).
そのアルゴリズムを大まかに説明すると,

  1. 1. fork() : 新たなプロセスを生成
  2. 2. execve() : 子プロセスにコマンドラインで指定したコマンドを実行させる
  3. 3. wait() : 親プロセスは子プロセスの終了を待つ

これが, シェルの基本ルーティンです. システムコールを使いこなすことによって,
オリジナルのシェルも作成可能だと言えます.

デーモンプログラム

デーモンプログラムもシステムコールを利用して作成されているプログラムの1つです.
デーモンプログラムとは, 常にバックグラウンドで稼働し続け, 何らかのサービスを
提供するプログラムのことです. OSでは, 多くのデーモンプログラムが稼働しています.
身近なデーモンプログラムとしては, WebサーバにおいてApacheによって実行される,
httpdデーモンでしょう. また, 様々なインターネットサービスを提供するデーモン,
inetdxinetdなどのスーパーデーモンも身近な存在です.
デーモンプログラムを作成するためのアルゴリズムを大まかに説明すると,

  1. 1. fork() : 子プロセスを生成
  2. 2. setsid() : 子プロセスをセッションリーダに設定
  3. 3. fork() : 子プロセスが子プロセスを生成
  4. 4. close() : 不要なファイルディスクリプタを閉じる
  5. 5. 子プロセス(最初の親プロセスから見ると, 孫プロセス)に何かをさせる

システムコールを使いこなすことによって, システムにとって重要なプログラムや
システムの基幹部分に近いプログラムが作成可能になります.

できることがより多く

システムコールを使いこなしていく目的は, 結局のところできることを増やすためと言えます.
もちろん, 標準ライブラリ関数だけでも, 様々なことができますが, シェルやデーモンのように
システムにとって重要で, 基幹部分に近いプログラムを作成したい場合は, システムコールを
使うことが必須となります.
もし, そういった方向を目指す方であれば, ぜひリンクにある
参考書籍などを利用して, 知識を深めていってみてください.


  1. トップページ
  2. カーネルを理解する
  3. シェルを理解する
  4. システムコールを理解する
  5. システムコールを検索する