システムコール・ライブラリルーチン

UNIX の関数群の説明です。関数にはシステムコールとライブラリ関数があります。システムコールとは、 各プログラムが OS の提供する機能を利用するための関数で、これが呼ばれると UNIX のカーネルが処理を行い実行します。

一方、ライブラリ関数には2つのタイプがあり、

  • 最終的にはシステムコールを呼び出すもの。直接システムコールを使うより便利な造りになっている
  • 文字列操作や計算をおこないメモリ領域を操作するだけで、OS の提供する機能を使わないもの
があります。簡単に言うと、ライブラリ関数というのは「便利だから、よかったら使ってね」というもので、システムコールは「〜したいならこれを使うしかないよ」というものです。

例えば、open・read・write はシステムコールです。一方、fopen・fread・fgets などは、ライブラリ関数です。どちらの関数の機能も「ファイルの読み書きを行うこと」です。

fopen・fread・fgets などは、内部で open・read・write などを呼び出しています。つまり結局はシステムコールが呼ばれるわけです。

なぜなら、各プロセスにはファイルを操作する権限、つまりディスクにアクセスし、データのやりとりを行う権限はありません。必ず OS を通して操作をおこなわなわなければいけないのです。

では、fopen・fread・fgets の代わりに直接 open・read・write を使えばいいのかというと、そうではありません。ライブラリを使うことで

  • 移植性の向上
  • バッファリングによる速度向上
  • 利便性向上 (システムコールを使うよりライブラリを使った方が簡単かつ短く書ける)
というメリットが得られます。

UNIX でプログラムを作成する場合、ライブラリ関数を一切使わずに書くことも可能です。でも、めんどくさいので、基本的にはライブラリを使い、必要になったときだけシステムコールを使うことをお勧めします。

システムコールはマニュアルのセクション2に、ライブラリ関数はマニュアルのセクション3に説明があります。これを、open(2)、fopen(3) などと表記することがあります。

注意1: 〜という関数は、BSD 系ではシステムコールだけど、SystemV 系ではライブラリ関数である、ということもあります。また、その逆もあり得ます。

注意2: 使ってはいけない関数というのがあります。具体的にはgets や scanf です。必ずなぜ使ってはいけないのかを説明し、代替手段を説明していますので、よろしく。

alarm 

atof 

atoi 文字列を数値に変換する。

#include <stdlib.h>
char num_string[]="1234";
int i;
i = atoi(num_string);
この逆の、数値を文字列に変換するには、sprintf(3)・snprintf(3) を使えばよい。

chdir カレントディレクトリを変更する

/usr/local/bin に移動するには
#include <unistd.h>
chdir("/usr/local/bin");
とする。現在のカレントディレクトリを知るには、getcwd(3) を使う。

close ファイルディスクリプタをクローズする

open(2) でオープンしたファイルを閉じる時、close(2) を使う。fopen(3) でオープンしたファイルは、close(2) でなく fclose (3) することに注意。

ただし、ファイルだけでなく socket(2) で生成したソケットディスクリプタも close(2) で閉じる。

dup 

dup2 

ファイルディスクリプタを作成する。パイプのつなぎ変えを行う際などに使う。
dup2(old_fd,new_fd) は、close(new_fd); dup(old_fd) と同じ。

exec コマンドを実行

exec 系のライブラリ関数には
execl
execle
execlp
exect
execv
execvp
execvP
がある。これらは最終的にシステムコール execve(2) を呼び出す。

execve 

exit プロセスを終了する

exit(3) は、標準入出力のバッファを全てフラッシュし、全てのファイルをクローズしてから終了する。_exit(2) を呼ぶ。つまり、終了時にバッファをフラッシュしてほしくなければ _exit(2) を直接呼べばよい。

ただし、_exit(2) は、低水準入出力であるファイルディスクリプタの後始末だけはおこなってくれる。

exit の引数に渡した値は、プログラムの終了ステータスとなる。一般的には、正常終了時には 0 を返し、異常終了時には 1 以上を返すことが多い。

この終了ステータスは sh・bash からは $?、csh・tcsh からは $status で参照でき、コマンドの実行結果がどのようになったかを知ることができる。

fclose ファイルクローズ

fopen(3) でオープンしたファイルをクローズする。

open(2) でオープンしたファイルをクローズする場合は fclose(3) ではなく close(2) を使う。

fgetc 

fgets ファイルから1行読み込む

標準入力から1行取得したければ、
#include <stdio.h>
char buf[256];
fgets(buf,sizeof(buf),stdin);
とする。指定のファイルを読み込み、先頭に行番号を付けるには
#include <stdio.h>
char buf[256];
FILE *fp;
fp = fopen("sample.txt","r");
if ( fp == NULL ){
ファイルオープンエラー
}
while (1){
if ( fgets(buf,sizeof(buf),stdin) == 0 ){
break; // 全部読み込んだ
}
printf("%s",buf);
}
fclose(fp);
バッファの最後には改行コードが付いている。ただし、この例では、sample.txt に 255文字を越える行があればうまく動作しないことに注意。

gets を使ってはいけない。gets(buf) という呼び出し形式のため、領域の長さを指定できないため、長い行を読み込もうとするとバッファオーバーランが発生するからである。かわりに、必ず fgets(buf,sizeof(buf),stdin) としよう。

fopen 高水準のファイル入出力ライブラリ関数

#include <stdio.h>
FILE *fp;
fp = fopen("/foo/bar/file.txt", "r");
if ( fp == NULL ){
perror("fopen");
}

fork 新しいプロセスを作成するシステムコール

fork すると、新しいプロセスが作成される。元々存在したプロセスを親プロセス、新しく作成されたプロセスを子プロセスという。

#include <unistd.h>
int pid;
pid = fork();
if ( pid == -1 ){
エラー
} else {
if ( pid == 0 ){
子プロセス
} else {
親プロセス
}
}

fread 

free malloc・realloc で確保した領域を開放する

#include <stdlib.h>
char *p;
p = malloc(100);
if ( p != NULL ){
/* malloc 失敗 */
}
...
free(p);
/* free(3) がエラーになることはないので、エラーチェックは不要 */

メモリを free(3) した場合でも、確保したメモリが OS に即座に返されることはない。プロセスが終了して初めてメモリが OS に返却され、他のプロセスから利用可能になる。

既に free(3) した領域を、再度 free(3) してはいけない (これをダブルフリー (double free) と呼ぶ)。ダブルフリーはコアダンプの原因となるだけではなく、外部から悪意のあるコードを実行される可能性がある。ダブルフリーを行っても問題がない OS (FreeBSD) もあるが、多くの OS はダブルフリー対策はなされていないので、注意しよう。

fwrite 

getbsize 

getchar 標準入力から1文字取得する。

#include <stdio.h>
int c;
c = fgets(stdin);
と同じ。

getcwd カレントディレクトリを取得する

#include <unistd.h>
#include <sys/param.h>
char buf[MAXPATHLEN];
getcwd(buf,sizeof(buf));
printf("カレントディレクトリ=%s\n",buf);

カレントディレクトリを取得するのは実は結構難しい。

まずルートディレクトリ `/' の i-node を取得する (root-ino)。そして現在のカレントディレクトリ `.' の i-node を取得し (cur-ino)、`/' の i-node と比較する。一致しなければ、一つ上のディレクトリ `..' に移動し、opendir を使ってそのディレクトリの下のファイル・ディレクトリ一覧を取得する。そして一つずつ i-node を調べ、cur-ino と比較する。そこで一致するとやっと最下部のディレクトリ名がわかる。そして `..' と root-ino を比較し…と延々繰り返し、root-ino と cur-ino が一致するまで繰り返すのである。

カレントディレクトリを変更するには、システムコール chdir を使えばよい。

getenv 環境変数を取得する

#include <stdlib.h>
printf("%s\n",getenv("PATH"));

getopt オプションを得る

#include <stdlib.h>
int c;
while ( ( c=getopt(argc,argv,"abc") ) != -1 ){
case 'a': -a が指定された ; break;
case 'b': -b が指定された ; break;
case 'c': -c が指定された ; break;
default:
usage();
}
もちろん、getopt を使わずに
int i;
for ( i=1 ; i<argc ; i++ ){
if ( argv[i][0] == '-' ){
switch (argv[i][1]){
case 'a': -a が指定された ; break;
case 'b': -b が指定された ; break;
case 'c': -c が指定された ; break;
}
}
と自前で解析してもよい。

getpid 自分自身のプロセス ID を取得するシステムコール

全てのプロセスには番号が付いている。ps コマンドを見るとよくわかるだろう。
#include <sys/types.h>
#include <unistd.h>
pid_t pid = getpid();
pid_t ppid = getppid();
printf("プロセスID:%d 親プロセスのプロセスID:%d\n", pid, ppid);

getppid 親プロセスのプロセス ID を取得するシステムコール

全てのプロセスには必ず親プロセスがいる。もし親プロセスが先に終了したら、子プロセスは init に引き取られる。

gets 標準入力から1行取得

絶対に使ってはいけない。
#include <stdio.h>
char buf[256];
gets(buf);
これは、標準入力から1行分読み込むものだが、もじ長い文字列が渡された場合、buf の領域を越えて、関係ない領域にも文字列が書き込まれてしまうからである。

代わりに fgets を使って
#include <stdio.h>
char buf[256];
fgets(buf,sizeof(buf),stdin);
なら OK である。

getuid 自プロセスのユーザIDを取得するシステムコール

#include <unistd.h>
#include <sys/types.h>
uid = getuid();
if ( uid == 0 ){
スーパーユーザ
}
ユーザID = uid

gettimeofday 現在の時刻を求めるシステムコール

UNIX の内部では、時刻は全て UNIX 時間で管理されている。UNIX 時間というのは、1970年1月1日0時0分0秒からの経過時間のことである。gettimeofday を使うと、現在の UNIX 時間を求めることができる。

#include <sys/time.h>
struct timeval t;
struct timezone tz;
gettimeofday(&t,&tz);
1970年1月1日0時0分0秒からの秒数 t.tv_sec
マイクロセカンド秒 t.tv_usec

isatty 出力が端末かパイプか調べる

#include <unistd.h>
if ( isatty(STDOUT_FILENO) ){
コンソール
} else {
パイプ
}

ioctl デバイスの情報を取得

#include <sys/ioctl.h>
struct winsize win;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &win);
端末の横幅 = win.ws_col;

kill 

localtime UNIX 時間を年月日時分秒に変換

#include <time.h>
time_t t;
struct tm *tm_time;
time(&t);
tm_time = localtime(&t);
printf("%d-%02d-%02d %02d:%02d:%02d\n",
tm_time->tm_year+1900, /* 年 */
tm_time->tm_mon+1, /* 月 */
tm_time->tm_mday, /* 日 */
tm_time->tm_hour, /* 時 */
tm_time->tm_min, /* 分 */
tm_time->tm_sec /* 秒 */
);

malloc メモリを確保する

mkdir ディレクトリを作成するシステムコール

#include <sys/types.h>
#include <sys/stat.h>
mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
if ( mkdir("/foo/bar/newdir", mode) != 0 ){
/* エラー */
}

nanosleep 指定時間の間、動作を止める (ナノ秒単位)

nanosleep(2) は、ナノ秒単位で sleep を行うシステムコールである。FreeBSD・Linux いずれも、
  • システムコール: nanosleep(2)
  • ライブラリ関数: sleep(3), usleep(3)
であり、sleep(3) と usleep(3) は nanosleep(2) を呼び出すという関係性である。

下記は 1.5秒 sleep する擬似コード。

#include <time.h>
struct timespec req;
rec.tv_sec = 1;
rec.tv_nsec = 500000000; /* 500,000,000ナノ秒 = 500,000マイクロ秒 = 500ミリ秒 = 0.5秒 */
struct timespec rem;
int ret = nanosleep(&req,&rem);
if ( ret == -1 ) {
if ( errno == EINTR ) {
シグナルで中断してしまったので、rem に残り秒数が入っているのでリカバリ処理を行う
} else {
perror("nanosleep() failed");
}
}

user_from_uid 

group_from_gid uid/gidからユーザネーム、グループネームを求める

printf("%s",user_from_uid(1001,0));

open ファイルをオープンするシステムコール

opendir ディレクトリをオープンするライブラリ関数

#include <sys/types.h>
#include <dirent.h>
DIR *dp;
struct dirent *dir;
if ( (dp=opendir("/usr/bin")) == NULL ){
/* エラー */
}
/* カレントディレクトリのファイル一覧を表示 */
while ((dir = readdir(dp)) != NULL ){
printf("%s\n",dir->d_name);
}

pipe パイプを作成

pipe システムコールを実行すると、新しい2つのファイルディスクリプタが作成される。
int pipes[2];
pipe(pipes);
として、pipes[1] に対して出力すると、その出力内容が pipes[0] に送られる。pipes[0]、pipes[1] はそれぞれファイルディスクリプタなので、読み書きするには低水準入出力関数である read・write を使う。
int pipes[2];
char buf[256];

pipe(pipes);
write(pipes[1],"hoge\n",strlen("hoge\n"));
read(pipes[0],buf,sizeof(buf));

printf("buf=%s",buf);
pipes[1] に書き込んだ hoge\n という文字列が pipes[0] から読み込めていることがわかるだろう。

ただし、同じプロセス内でデータのやりとりをしたところで、何のメリットもない。標準入力・標準出力のつなぎ変えを行うときに pipe を使う。

#include <unistd.h>
int pipes[2];
if ( pipe(pipes) < 0 ){
エラー
}
if ( fork() == 0 ){ // 子プロセス
dup2(pipes[0],0);
close(pipes[1]);
execl("/bin/cat","cat","-n",NULL);
exit(0);
} else { // 親プロセス
FILE *fin,*fout;
char buf[256];
int status;

close(pipes[0]);
fout = fdopen(pipes[1],"w");
fin = fopen("sample.dat","r");

while (1){
if ( fgets(buf,sizeof(buf),fin) == 0 ){
break;
}
fprintf(fout,"%s",buf);
}
fclose(fin);
fclose(fout);

wait(&status);
if ( WIFEXITED(status) ){
printf(" status=%d", WEXITSTATUS(status));
} else if ( WIFSIGNALED(status) ){
printf(" signal=%d", WTERMSIG(status));
}
}

popen 

printf 標準出力に指定のフォーマットで文字列を出力するライブラリ関数

int i=10;
long int li=123456787890L;
long long int lli=123456787890L;
double d=123.456;
char str[]="Hello.";
printf("%d", i); /* int を出力するには %d */
printf("%ld", li); /* long int を出力するには %ld */
printf("%lld",lli); /* long long int を出力するには %lld */
printf("%f", f); /* double を出力するには %f */
printf("%s", str); /* 文字列を出力するには %s */
printf("%%"); /* % 自体を出力するに際は %% とする */
printf("%c", 'a'); /* 文字を出力するには %c */

printf("%10d", i); /* 10 桁で表示。10桁に満たない場合は左側を空白で埋める */
printf("%10s", str); /* こちらは文字列を 10桁で表示 */
printf("%-10s" ,str); /* 右詰めするにはマイナスを付ける */
printf("%05d", i); /* 5 桁で表示。10桁に満たない場合はゼロパディングして表示 */
printf("%.*s", 2, str); /* 桁数を動的に変更する場合は .* とする */

printf ファミリの関数には、
  • printf … 文字列を作成し、指定の変数にコピー
  • sprintf … 文字列を作成し、指定の変数にコピー
  • snprintf … 文字列を作成し、指定の変数にコピー。ただし変数の長さを指定する。
  • fprintf … 文字列を作成し、指定のファイル (ストリーム) に出力
などがある。

putenv 環境変数を設定する

#include <stdlib.h>
putenv("PATH=/bin:/usr/bin:/usr/local/bin:/usr/sbin");

read ファイルディスクリプタからデータを読み込むシステムコール

realpath 相対パスを絶対パスに変換する。

. や .. やシンボリックリンクを展開し、相対パスを絶対パスに置換する。

引数として渡すファイル・ディレクトリ名は、存在しないファイル・ディレクトリであっても構わない。
#include <sys/param.h>
#include <stdio.h>
#include <stdlib.h>
main(){
char resolved_path[MAXPATHLEN];
if ( realpath("../../foo/hoge", resolved_path) == NULL ){
perror("hoge");
exit(1);
}
printf("%s\n", resolved_path);
exit(0);
}

regcomp 

regexec 

regfree 正規表現ライブラリ

正規表現ライブラリには、いくつかのタイプがあるらしい。FreeBSD には BSD 系の regex ライブラリが付属する。Linux には GNU regex ライブラリが付いてくる (と思う。未確認)。

以下のサンプルは、簡易 grep である。
main( int argc, char *argv[] ){
char buf[256];
static regex_t preg;
regmatch_t pmatch[10];
int i;
int regerr;
char errbuf[256];

if ( argc < 3 ){
fprintf(stderr, "mygrep pattern file [file..]\n");
exit(1);
}

regerr = regcomp(&preg, argv[1], REG_EXTENDED);
if (regerr) {
regerror(regerr, &preg, errbuf, sizeof(errbuf));
fprintf(stderr, "%s\n", errbuf);
regfree(&preg);
exit(1);
}

for ( i=2; i<argc; i++ ){
int line = 1;
FILE *fp;
char *filename;

filename = argv[i];
printf("file=%s\n", filename);

fp = fopen(filename, "r");
if ( fp == NULL ){
fprintf(stderr, "Can't open %s\n", filename);
exit(1);
}
while (1){
if ( fgets(buf, sizeof(buf), fp) == 0 ){
break;
}
regerr = regexec(&preg, buf, 0, 0, 0);
if (!regerr) {
printf("%s:%d:%s", filename, line, buf);
}
line++;
}
}

regfree(&preg);
}

rmdir ディレクトリを削除するシステムコール

scanf 標準入力から指定のフォーマットを読み込む

用意してある領域以上の入力があると対応できないので、使っちゃダメ。fgets して sscanf すること。

setvbuf stdio のバッファリングを設定

signal 

sleep 指定秒数の間、動作を止める

#include <unistd.h>
sleep(秒数);

snprintf 最大文字数を指定できる sprintf

#include <stdio.h>
char buf[256];
snprintf(buf, 10, "abcdefghijklmnopqrstuvwxyz");
printf("出力=[%s]\n", buf);

出力=[abcdefghi]

sprintf 指定のフォーマットで文字列を作成

#include <stdio.h>
char buf[256];
sprintf(buf,"%s %d",str,num);

生成される文字列の最大長を制御できないときはけ sprintf は使わないようにしよう。代わりに snprintf を使うこと。

sscanf 文字列から指定のフォーマットに従って、文字列や数値を切り出す

フォーマットは printf とほぼ同じである。

strcat 文字列を連結する

strchr 

strcmp 文字列を比較する。

#include <string.h>
char str1[]="abc";
char str2[]="def";
if ( strcmp(str1, str2) == 0 ){
str1とstr2は同じ文字列
} else {
str1とstr2は異なる文字列
}
文字列が一致すると strcmp が 0 を返すことに注意。
if ( strcmp(str1,str2) ){
文字列が一致
}
ではない。

なぜこういう変な仕様かというと、
  • str1 が str2 より (文字コードで比較して) 大きかったら正数を返す
  • str1 が str2 より (文字コードで比較して) 小さかったら負数を返す
  • str1 と str2 が同じなら 0 を返す
と、どちらが大きいかという情報を受け取れるようになっているからである。

よく
if ( ! strcmp(str1, str2) )
if ( strcmp(str1, str2) == 0 )
どちらの書き方がよいかで議論になるが、当ページ管理人は後者を推奨する (前者の書き方も既に慣用句として認められているとは思うが)。「str1 と str2 の差がゼロである」と覚えよう。

strrchr 

strlen 

strsep 文字列を区切り文字で分割する

ANSI C には含まれていない。

strstr 

system コマンドを実行

コマンドを実行する。
system("ls -l /usr");
また、sh 風のパターンマッチも利用できる。
system("cat *.txt");
最後に & を付けることで、バックグラウンドで実行することもできる。
system("sleep 100 &");

system はライブラリ関数である。system の内部では fork して子プロセスを作成し、親プロセスは sh -c cat *.txt のように exec する。親プロセスは wait で子プロセスの終了を待つ。だから、sh でできることは system でも可能なわけ。

ただし、system ではコマンドの出力を取り込んだり、標準入力にデータを与えることはできない。そういう場合は popen を使うか、自前で pipe・fork・dup2・exec すればよい。

tcgetattr 

tcsetattr 

端末情報をセット。
#include <termios.h>
struct termios term;
tcgetattr(0,&term);
term.c_lflag &= ~ICANON;
term.c_lflag &= ~ECHO;
tcsetattr(0,TCSANOW,&term);
カノニカルモードの例。

umask ファイル新規作成時のパーミッションを指定するシステムコール

unlink ファイルを削除するシステムコール

unlink ではディレクトリの削除はできない。ディレクトリは rmdir を使わないと消せない。
ファイルの作成は、open で行う。

usleep マイクロセカンド単位の sleep

sleep では 1秒単位でしか秒数を指定できないが、usleep では 100万分の1 秒単位で sleep 時間を指定できる。
#include <unistd.h>
usleep(100000); // 0.1 秒 sleep
usleep(2000000); // 2.0 秒 sleep
ただし、普通の PC/AT 互換機のタイマは、精度が 100分の1 秒単位であるから、usleep(1) で本当にきっかり 100万分の1 秒のウェイトが入ることは期待しない方がよい。

他に、select(3) を使う方法もある。

関連コマンドは sleep(1) または usleep(1)。

wait 

waitpid 

wait3 

wait4 子プロセスの終了ステータスを取得する。

wait は、子プロセスの

write