標準入出力(2)

このエントリーをはてなブックマークに追加

標準入出力1ではC言語の標準関数を利用した標準入出力について説明してきました。今回はLinuxのシステム関数を使う方法を説明していきます。

システム関数とは

システム関数とはOSが提供する機能(関数)です。システムコールや低水準関数とも言います。WindowsユーザーにはAPIと言ったほうが伝わり易いかもしれません。
システム関数とは言っても通常の関数と使い方は同じです。ただ、利用するヘッダが異なる、関数の数が多い、使いこなすには知識が必要などの特徴があるくらいです。


標準出力と標準エラー

標準出力

では前回と同じく標準出力から説明していきます。前回はprintf(3)、fprintf(3)を使用しました。今回はwrite(2)を使用します。

//helloworld_stdout.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	char msg[] = "Hello World\n";

	write(1, msg, sizeof(msg));

	return 0;
}

コンパイルと実行をします。通常のHello Worldと同様の動作になっているはずです。

$ gcc -o helloworld_stdout helloworld_stdout.c
$ ./helloworld_stdout
Hello World

標準エラー

次は標準エラーです。とはいっても標準出力とほとんど同じです。0が2に変わっただけです。

//helloworld_stderr.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	char msg[] = "Hello World\n";

	write(2, msg, sizeof(msg));

	return 0;
}

コンパイルと実行をします。通常通りの動作です。

$ gcc -o helloworld_stderr helloworld_stderr.c
$ ./helloworld_stderr
Hello World

システム関数と標準入出力

システム関数プロトタイプ

write(2)の第一引数には"0"を指定していますが、まずはwrite(2)のプロトタイプを見てみます。

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

第一引数はint型になっています。標準関数ではFILE*型を使用しましたが、システム関数ではint型を使用するのです。この値をファイルディスクリプタと呼びます。


システム関数版標準入出力

では、これを元にシステム関数版標準入出力を見てみます。

名前標準入出力ファイルディスクリプタ
標準入力stdin0
標準出力stdout1
標準エラーstderr2

これらの値はプロセス毎に固定で決まっています。3以降は各プロセスがオープンした順でOS側が自動的に決めていきますのでプロセス毎に異なります。


標準関数とシステム関数の違い

ここまでならば、標準関数をシステム関数にただ単に置き換えれば良いように思えますが、実際の所は何か違いがあるのかを見ていきます。
そもそも、標準関数はC言語の規格(C89やC99等)で決められている関数ですので、どの環境でもコンパイル出来るように設計されています。ところが、システムコールはそのOSでしか動作しません。通常はwrite(2)、read(2)等のシステム関数は大体POSIX等の規格で決まっていてUnix系OSだと使えるようになっていますが。
当然ながら標準関数はシステムコールを利用して作られています。また標準関数はシステムコールとの間にバッファを持っていますので、大きなデータの読み書きには非常に効率が良い動作をします。その代わりプログラマがprintf(3)を呼んでも、バッファされてその瞬間に書き込みがされないなどの問題もあります。他にはバッファを持っている利点をいかしてungetc(3)のような関数も存在します。
逆にシステム関数を使うときは自前でバッファを用意して効率の良いプログラムを書くにはプログラマが意識する必要があります。その代わり非常に細かい操作も可能となります。


manとセクション番号

ついでに今まで説明してこなかった、関数の表記方法も説明しておきます。これまでprintf(3)やwrite(2)のように書いてきたと思います。この"3"や"2"の意味ですが、manのセクション番号を表しています。Unix系OSが初めての人に説明するとmanとはリファレンスを表示するコマンドで、セクションとは分類程度に思ってください。
セクションは一般には以下のようになります。ただし、Unixの種類やLinuxでもディストリビューションによって微妙に異なります。

  • 1:lsやtop等のコマンド
  • 2:システム関数
  • 3:標準関数 or ライブラリ関数

4以降もありますが、今回は関係ないので割愛します。詳しく知りたければ"man man"してください。


FILE*とintの相互変換

標準関数がシステム関数で作られているということはFILE*型は裏ではint型を知っているはずなので相互変換も可能なはずと考えるのが自然です。
結論はもちろん可能ですので、これからその方法を見ていきます。


FILE*からintへの変換

FILE*からintへの変換を行うには、fileno(3)を使用します。

//fileno.c
#include <stdio.h>

int main(void)
{
  printf("stdin:%d, stdout:%d, stderr:%d\n", fileno(stdin), fileno(stdout), fileno(stderr));

  return 0;
}

実行すると期待通りの動作になっています。

$ gcc -o fileno fileno.c
./fileno
stdin:0, stdout:1, stderr:2

intからFILE*への変換

次は逆にintからFILE*への変換を行います。これにはfdopen(3)を使用します。

//fdopen.c
#include <stdio.h>

int main(void)
{
	FILE *fp;

	fp = fdopen(0, "w");
	fprintf(fp, "stdin:%p, %p\n", stdout, fp);
	fclose(fp);

	return 0;
}

実行させると、stdoutとfpは同じアドレスを指しませんが、動作としては通常のstdoutと同じになっていることが分かります。つまり、fdopen(3)はファイルディスクリプタから動的にFILE*型を作るのです。

$ gcc -o fdopen fdopen.c
$ ./fdopen
stdin:0xf77234c0, 0x9210008

標準入力

最後に標準入力です。入力にはread(2)を使用します。1文字入力するサンプルです。

//stdin.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	char buf[2];

	printf("input:");
	read(0, buf, 2);
	printf("get:%c\n", buf[0]);

	return 0;
}

実行結果です。通常の動作になることを確認してください。

$ gcc -o stdin stdin.c
$ ./stdin
input:1
output:1

まとめ

これで、標準入出力を通してシステムコールの使い方が一通り理解できたはずです。ヘッダや関数名等が異なりますが、通常の関数と同じなのが理解できたと思います。
次はUnix系OSで標準入出力がどのような役割を果たしているかを見ていきます。