ホーム > 読んだ > 国内

Linuxシステムコール

書誌

tagUNIX
text唯野
author塚越一雄
publisher技術評論社
year2000
price2,480+tax
isbn7741-1031-0

履歴

2000.11.6読了
2000.11.6公開
2002.6.3修正
2012.1.17タグ追加

感想

書名の通り Linux のシステムコールに関する入門書。初版本のせいか誤植も目立ったが、内容的には何よりもサンプルプログラムに適切なものが収められていて大変よかった。入門書でサンプルが長すぎると根気がいるし、後から参照しても目的のロジックを見つけるのに時間がかかってしまう。その点、この本は巻末の関数一覧や全体の構成の中で位置付けられた説明順序などを含め、非常にしっかりした作りとなっている。私は以前、システムコール関連の本では外れを引いているせいもあって、特にありがたかった。類書を探している方にはおすすめの一冊といえるし、これならば著者の別の本も読んでみたいと思わせるだけの内容になっている。

# 慣れというか手抜きによりコメントは /* */ ではなく // になっています :-)

抄録

第1章 システムコール

システムコール : OS がユーザに提供するサービスルーチンの集まり

#include <time.t>
#include <stdio.h>

int main()
{
    time_t t = time(NULL);
    struct tm* tp = localtime(&t);
    char* s = asctime(tp);

    printf("協定世界時   : %ld\n", t);
    printf("ローカル時間 : %s\n", s);
    return 0;
}

#include <time.h>
time_t time(time_t* t);
UTC(協定世界時、グリニッジ標準時を返す)
これは 1970 年 1 月 1 日 0 時からの経過秒数を返す
time_t は 32 ビット符合付き整数の typedef されたもの
引数に time_t ポインタを渡すことでアウトパラメータとなる

#include <time.h>
struct tm* localtime(const time_t* timep);
協定世界時をローカル時間に変換する

struct tm
{
    int tm_sec;    // 秒
    int tm_min;    // 分
    int tm_hour;   // 時
    int tm_mday;   // 日
    int tm_mon;    // 月
    int tm_year;   // 年
    int tm_mday;   // 曜日(日曜が 0)
    int tm_yday;   // 1 月 1 日からの経過日数(0-)
    int tm_isdst;  // 夏時間ならば 1
    /* Those are for future use. */
    long int __tm_gmtoff__;
    __const char* __tm_zone__;
};

#include <time.h>
char* asctime(const struct tm* timeptr);
tm 構造体のデータを更に文字列として整形する
e.g. Fri Dec 3 15:40:22 1999

システムコールの多くはエラーが発生すると -1 を返して内容を errno に保持する。errno の値は /usr/include/asm/errno.h で定義されている。但し、errno は次のエラーで上書きされ、POSIX では仮にエラーがなかったとしても上書きを認めるので、エラーの発生から次のエラーの発生までが値の有効範囲となる。

if(open("filenmae", O_RDONLY) == -1)  // ファイルオープンに失敗
{
    // errno はグローバル変数
    printf("%s\n", sys_errlist[errno]);
    perror("open");

}

#include <errno.h>
const char* sys_errlist[];
errno に対応した文字列を指す配列

#include <stdio.h>
void perror(const char* s);
STDERR にエラーメッセージを表示する(errno の指定が不要)
引数には通常、実行したシステムコールの名前を渡す

Linux のシステムコール一覧は /usr/include/asm/unistd.h で定義されている。

e.g. #define __NR_setup 0 // __NR_ を除いた部分がシステムコール

第2章 プロセスの置換

プロセス : 実行中のプログラム (メモリ上にロードされたプログラムコード)

プロセスの置換は exec ファミリが行う。exec ファミリのうち、システムコールなのは execve のみである。

#include <unistd.h>
[l 系 exec : execl, execlp, execle]
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(
    const char* file, const char* arg, char* const envp[]);
[v 系 exec : execv, execvp, execve]
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execve(
    const char* filename, char* const argv[], char* cosnt envp[]);
あるプログラムから別のプログラムを起動して
元のプログラム(プロセス)を新しいプログラム(プロセス)
で置き換える(プロセス ID は同じものを引き継ぐ)

execl("/usr/bin/lsss", "less", "/etc/inittab", NULL);
第 1 引数はコマンドのフルパス
第 2 引数以降はコマンドライン引数
0 番目はコマンド自身
最後に引数の終りを示す NULL を渡す

getchar( ) しているときには Ctrl+Z でサスペンドする。

<p class="">execv ではコマンド引数がそれぞれの文字列ではなく、メインの第 2 引数と同じポインタ配列となる。要素の最後には NULL をセットする。main( ) の引数である argv を渡してもよい。

コマンドは環境変数 PATH に従って検索されるので p の付いた exec は第 1 引数となるコマンドがフルパスでなくてもよい。また、l 系 exec に渡す argv[0] は起動するコマンドと必ずしも同じでなくてもよい。

e の付いた exec は main の第 3 引数となる環境変数のポインタ配列を受け取ることができ、e 形式でない exec には標準と同じ環境変数がそのまま渡される。(環境変数を引き継ぐ)

#include <unistd.h>
#include <stdio.h>

int main()
{
    char* const envp[] = {
          "FUJI = fuji",
          "NASU = nasu",
          NULL
    }

    // envp は環境変数の一覧を表示するプログラム
    if(execle("/home/foo/envp", "env", NULL, envp) == -1)
    {
        perror("execle");
        return 1;
    }

    return 0;
}

char* const ptr ポインタが固定のポインタ定数
const char* ptr ポインタの指す値が固定の定数へのポインタ
いずれも宣言時に初期化が必要

第3章 プロセスの複製

pstree コマンドで現在のプロセスを木構造に表示できる。これを見ると分かるように、すべてのプロセスの親は init である。カーネルは最初に init プロセスを起動して制御を委ね、init は /etc/inittab の内容に従って新しいプロセスを作っていく。このときに init 以外のプロセスの生成を行うのが fork となる。fork は自分のプロセスの複製を作るので、fork されたプロセスを exec して新しいプロセスに置き換えることによりプロセスの生成が実現されることになる。(Win32 API の CreateProcess のような単一のプロセスの生成ではない。)

#include <unistd.h>
pid_t getpid(void);
自分のプロセス ID の取得

#include <unistd.h>
pid_t setpid(void);
セッションを作成しプロセスグループ ID を設定する

プロセス ID は重複しないことが保証されているので、テンポラリファイルを作成するときなどにも利用されている。

#include <unistd.h>
pid_t fork(void);
プロセスの複製を作成する

fork が成功すると PID だけが異なるふたつのプロセスが(fork の次の文から)並行して動き始める。そして fork は戻り値として値を返し、これは親プロセスなら子プロセスの PID で、子プロセスでは 0 となる。これを利用して親子で別々の処理を実現する。(但し、子プロセスから親プロセスの PID を得ることはできない。)

if((pid = fork()) == -1)
{
    // エラー処理
}
else if(pid > 0)
{
    // 親プロセスの処理
}
else
{
    // 子プロセスの処理
}

fork したときに親プロセスが子プロセスの終了を待つこともでき、そのときに wait ファミリを使う。(wait4 だけがシステムコール。)

pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
pid_t wait3(int* status, int options, struct rusage* rusage);
pid_t wait4(pid_t pid, int* status, int options, struct rusage* rusage);
これらは引数の数だけが違い、wait4 以外は、これの簡易バージョンとなる

#include <sys/wait.h>
pid_t wait(int* status);
引数は子プロセスの終了状態を保存する領域へのポインタ
引数が NULL の場合、情報は保存されない
戻り値として終了した子プロセスの PID を返す

e.g. wait(NULL); // 子プロセスが終了するまで次の文を実行しない

int の引数には以下の値がセットされる
子プロセスが正常終了した場合
 上位 8 ビット : 終了ステータス
 下位 8 ビット : 0 (ゼロ)
子プロセスがシグナルで終了した場合
 上位 8 ビット : 0 (ゼロ)
 下位 8 ビット : 終了原因のシグナル番号

そのため処理結果を知るためにはビット演算が必要となるが、その煩雑さを避けるためのマクロが用意されている。

WIFEXITED(status)    子プロセスが正常に終了したかどうか
WEXITSTATUS(status)  子プロセスの終了ステータスを取得
WIFSIGNALED(status)  子プロセスがシグナルで終了したかどうか
WTERMSIG(status)     シグナルのシグナル番号を取得

#include <stdio.h>

int main()
{
    pid_t pid;

    printf("fork and exec sample start !");
    printf("PID = %d\n", getpid());

    if((pid = fork()) == -1)
    {
        perror("fork");
        return 1;
    }
    else if(pid > 0)  /* 親プロセス */
    {
        printf("parent : waiting...\n");
        wait(NULL);  /* 親プロセスの終了を待つ */
        printf("chidl process just finished\n");
    }
    else  /* 子プロセス */
    {
        printf("child process start...\n");
        printf("PID = %d\n", getpid());
        printf("Hit Enter Key...");
        getchar();

        printf("child : execlp start !\n");
        if(execlp("bc", "bc", NULL) == -1)
        {
            perror("execlp");
            return 1;
        }

        reurn 0;
    }

    return 0;
}

第4章 シグナル

シグナルはプロセスに送られるイベントで、シグナルを受け取ったプロセスは処理を中断してシグナルに対応した処理(シグナルハンドラ)を実行する。そのためシグナルはプロセスに対する一種のソフトウェア割り込みということができる。Linux でのシグナル一覧は /usr/include/asm/signal.h で確認できる。(kill -l でも同じ。)

シグナルは以下のようなときに発生する

  • ユーザが C-c や C-z した
  • ユーザが kill コマンドを実行した
  • プログラムが不正なメモリを参照した
  • 不正な浮動小数点演算が行われた (0 除算、オーバーフロー)
  • プロセスが他のプロセスにシグナルを送信した
  • シグナルを発生させる割り込みキーとして例えば以下のものがある。このとき stty コマンドを使えば割り込みキーの設定ができ、また端末属性を調べることができる。(stty -a で全ての割り込みキーの設定を表示する。)

    番号 名前    stty キー
    2    SIGINT  intr C-c
    3    SIGQUIT quit C-\
    18   SIGCONT star C-q
    19   SIGSTOP stop C-s
    20   SIGTSTP susp C-z
    

    「kill -シグナル番号かシグナル名 PID」コマンドで任意のタスクにシグナルを送信できる。(シグナルとして SIGTERM を送り、そのプロセスは終了する)

    bash では ulimit という組み込みコマンドでファイルにサイズ制限をかけることができるが、これの -c オプションを使うことで Core ファイルのサイズを設定できる。(0 だと Core ファイルが作成されない。)

    主なシグナルとして以下のものがある
    1  SIGHUP  hangup  端末回線が切れると発生しデーモンの再起動に使う
    2  SIGINT  interrupt  C-c  デフォルトではプロセスの終了
    3  SIGQUIT C-\  デフォルトではプロセスを終了し Core ファイルを作る
    4  SIGILL  illegal  プロセスが無効な CPU 命令を発すると発生
    6  SIGIOT  I/O trap  ハードウェアエラー  PDP-11 の名残
    9  SIGKILL プロセスを殺す  これを無視することはできない
    11 SIGSEGV segmentation violation  不正なメモリの参照で発生
    14 SIGALRM alarm  alarm システムコールによるタイマで発生
    15 SIGTERM terminate  kill でシグナルを指定しないと使われる
               デフォルトではプロセスの終了
    18 SIGCONT continue  SIGSTOP で停止した表示の再開  C-q
    19 SIGSTOP 表示の停止  C-s
    20 SIGTSTP Terminal Stop  C-z  サスペンドしてバックグラウンドにする
               バックグラウンドなプロセスの中断はできない
    

    #include <unistd.h>
    unsigned int sleep(unsigned int seconds);
    引数は停止させる秒数
    sleep( ) は内部的に alarm システムコールを使っている

    プログラムがシグナルに対する動作を変えるときに signal システムコールを使う
    #include <signal.h>
    void(*signal(int signum, void(*handler)(int)))(int);
    signum にシグナル番号、handler には
    SIG_DFL : デフォルトの動作 (動作を変えた後で元に戻すときに使う)
    SIG_IGN : シグナルの無視
    シグナルハンドラ : シグナルを受け取ったときに実行する関数
    を指定できる

    // シグナルハンドラの書式
    void handler(int sig)  // 引数はシグナル番号
    {
        // シグナルを受け取ったときの処理
    }
    
    signal(SIGINT, handler);  // シグナルハンドラの指定
    

    シグナルハンドラで制御が移り変わっても処理が終了すれば元の処理が継続される。そのため処理自体をそこで打ち切るには、シグナルハンドラの中で exit( ) する。

    また、signal で指定したシグナルハンドラは一度実行されると、デフォルトのシグナルハンドラに戻される。そのため常にシグナルハンドラの処理を行うには、シグナルハンドラの中で signal を実行し再設定する必要がある。

    ほかに signal はハンドラとして関数ポインタを受け取るが、ハンドラがエラーを起こした場合の戻り値も関数ポインタである必要があるので、その定義の煩雑さを避けるために定数(SIG_ERR)が定義されている。

    typedef void (*__sighandler_t)(int);
    #define SIG_IGN ((__sighndler_t)1)
    #define SIG_ERR ((__sighandler_t)-1)
    #define SIG_DFL ((__sighandler_t)0)

    if(signal(SIGINT, SIG_DFL) == SIG_ERR) // signal がエラーならば

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    指定秒後にシグナル SIGALRM を送信する
    (指定時間後に指定の処理を行えるようになる)

    #include <unistd.h>
    int pause(void);
    シグナルを受け取るまでスリープする alarm とセットで使い
    alarm してから pause して SIGALRM シグナルが送られるのを待つ

    #include <sys/types.h>
    int kill(pid_t pid, int sig);
    プロセスにシグナルを送信する

    例えば fork した戻り値の pid を kill に渡せば、子プロセスにシグナルを送ることができる。

    第5章 低水準ファイル入出力

    低水準ファイル入出力はファイルの入出力に限らず利用できる。(例えばパラレルポートのデバイスファイルに write すればプリンタに印刷できる。)その際、システムコールのファイル入出力では、ファイルをファイル名ではなくファイルディスクリプタ(0 以上の整数)から参照する。(ファイルに関する情報を集めた構造体を番号で参照するイメージということ。)ファイルディスクリプタとファイル名の関連付けを行うのが open システムコールとなる。

    #include <sys/types.h>
    int open(const char* path, int flags);
    int open(const char* path, int flags, mode_t mode);
    エラー時に -1 を返すので戻り値型が int となっている
    第 2 引数のアクセスフラグにはビット単位の論理和を指定する

    O_RDONLY  読み出し専用
    O_WRONLY  書き込み専用
    O_RDWR    読み書き両用
    

    #include <unistd.h>
    int close(int fd);
    ファイルディスクリプタのクローズ

    if(clase(fd) == -1)
    {
        // ファイル書き込みなどに失敗したならば
    }
    

    #include <unistd.h>
    ssize_t read(int fd, void* buf, size_t count);
    引数にはそれぞれファイルディスクリプタ、
    読み出したデータの格納先、バイト数を指定する

    read の読み出し結果が正でなければファイルの最後かエラーなので、ループに組み込んで次のような処理も行うことができる。

    while((len = read(fd, &buf, cnt)) > 0){} // 読み終わるまでループ

    #include <unistd.h>
    ssize_t write(int fd, const void* buf, size_t count);
    引数にはそれぞれファイルディスクリプタ、
    書き込むデータのポインタ、バイト数を指定する
    戻り値は書き込んだバイト数で失敗すると -1 を返す (何も書き込まないと 0)
    write には実際に read したバイト数を渡すようにする

    creat システムコールは最後に e が付かない

    その他のアクセスフラグ
    O_CREAT  ファイルが存在しなければ作成する
    O_EXCL   O_CREAT と併用しファイルが既に存在するとエラーになる
    O_TRUNC  ファイルが既に存在すると切り詰める  rtuncate
    O_APPEND 追加モードでオープンする
    

    open に O_CREAT を渡す場合は第 3 引数でファイルのパーミッションを指定する。(以下のシンボルの論理和を利用できる。)

    S_IRWXU  00700  所有者の読み書き実行
    S_IRUSR  00400  所有者の読み込み
    S_IREAD
    S_IWUSR  00200  所有者の書き込み
    S_IWRITE
    S_IXUSR  00100  所有者の実行
    S_IEXEC
    --
    S_IRWXG  00070  グループの読み書き実行
    S_IRGRP  00040  グループの読み込み
    S_IWGRP  00020  グループの書き込み
    S_IXGRP  00010  グループの実行
    --
    S_IRWXO  00007  他人の読み書き実行
    S_IROTH  00004  他人の読み込み
    S_IWOTH  00002  他人の書き込み
    S_IXOTH  00001  他人の実行
    

    しかし、open で指定するパーミッションはあくまでも希望であり、実際のパーミッションはユーザマスクによって異なる場合がある。ユーザマスクのビットが 1 となったパーミッションは open の引数で指定されても(論理和が取られて)受け入れられない。(ユーザマスクの変更は umask コマンドでも実行できる。)

    ユーザマスクは 3 桁の 8 進数で次のような意味を持つ
    3 桁目  所有者のパーミッション制限
    2 桁目  グループのパーミッション制限
    1 桁目  他人のパーミッション制限
    4  読み取り禁止
    2  書き込み禁止
    1  実行禁止
    

    #include <sys/types.h>
    mode_t umask(mode_t mask);
    ファイル作成マスクの設定 戻り値は以前の umask 値

    バッファサイズの指定には stdio.h の中で定義されている定数 BUFSIZ がよく使われる。また、標準で定義されたファイルディスクリプタとして以下のものがある。(定数は unistd.h の中での定義されている。)

    0  STDIN_FILENO   標準入力
    1  STDOUT_FILENO  標準出力
    2  STDERR_FILENO  標準エラー出力
    

    #include <unistd.h>
    int dup(int oldfd);
    ファイルディスクリプタの複製を作成する

    カーネルはファイルディスクリプタの割り当てで常に利用できる中でも最も小さな値から割り当てていくので、そのときに標準ファイルディスクリプタをクローズして dup すると、例えば標準入力(0)をクローズすれば dup したファイルディスクリプタが 0 を指すようになる。その結果、標準入力としてそのファイルディスクリプタを利用することができリダイレクトなどを実現することができる。

    close(0);   // 標準入力をクローズ
    dup(fd);    // fd を標準入力にリダイレクトする
    close(fd);  // 0 が使えるので fd は不要
    read(0, ・・・);  // 標準入力から入力
    

    #include <unistd.h>
    int dup2(int oldfd, int newfd);
    第 1 引数のファイルディスクリプタを第 2 引数のファイルディスクリプタに
    割り当てる (dup よりも手続きを簡略化できる)

    全文を読まれる場合はログインしてください


    Up