前回解説したように、システムコールは魔法の関数でもなければ、不思議なものでもなく、カーネルに正直に処理を依頼するものです。今回は、標準ライブラリ関数との違いについて解説します。(編集部)
今回から簡単なシステムコールプログラミングを通じて、いわゆる「システムコール」がどういったものであるかを紹介していこうと思う。システムコールに触れるための環境をどう構築するかという点については、連載:いまさら聞けないVim 第8回「Vimをプログラム開発環境にしてしまおう」に詳しくまとめてある。こちらを参考に環境を構築してほしい。
先に挙げた記事では、FreeBSDとUbuntuを例として環境構築法を解説している。ほかの環境(Mac OS X Lion、Ubuntu 11.10、Solaris 11)を使われている方のために、使用するコマンドをそれぞれの環境で置き換える方法も末尾で説明しているので、参考にしていただければと思う。
まずはシステムコールを使わない、よく見かけるスタイルのソースコードを作成し、コンパイル、実行してみよう。
次に挙げるソースコードは「in」というファイルを「out」というファイルへコピーする処理を、C言語で記述したものだ。大学などでC言語の演習を受けた経験がある人なら、記述したことがあると思う。fopen(3)とfclose(3)がファイルのオープン・クローズ処理で、fgetc(3)が1byte単位でのデータ読み出し、fputc(3)が1byte単位でのデータ書き出しに対応する。どの関数もstdio.hが提供する標準ライブラリだ。
#include int main(void) { FILE *fi, *fo; int b; fi = fopen("in", "r"); fo = fopen("out", "w"); b = fgetc(fi); while (EOF != b) { fputc(b, fo); b = fgetc(fi); } fclose(fi); fclose(fo); }
fgetc(3)はファイルの終端にたどり着いて、読み取るものがなくなるとEOFを返す。上記のサンプルコードでは、EOFに到達するまで1byteづつ読んでは書き出すという処理を繰り返している。このソースコードをコンパイルして実行してみる。以下のコマンドを実行して、コンパイラclangを動作させ、ソースコードから「copy-fgetc-fputc」という名前の実行ファイルを生成する。
clang -o copy-fgetc-fputc copy-fgetc-fputc.c
コピー元となるファイルは、次のようにdd(1)コマンドで作ればよい。ここでは10Mbytesのファイルを生成している。ddコマンドの引数はOSごとに変わるので、その点は末尾の説明を参考にして、ご自身の環境に合わせて引数を指定してほしい。
dd if=/dev/random of=in bs=1m count=10
プログラムを実行する前に、書き出す先の「out」ファイルを空の状態で作ることを忘れないでほしい。実行すると次のようになる。time(1)コマンドの引数としてプログラムを実行すると、そのプログラムの実行にかかった時間(総合時間、ユーザー時間、システム時間)を表示させることができる。この例では10Mbytesのファイルのコピーに0.13秒かかったことが分かる。
% rm -f out % touch out % /usr/bin/time ./copy-fgetc-fputc 0.13 real 0.08 user 0.03 sys %
「in」と「out」のハッシュ値を計算すれば、ファイルを正しくコピーできているかどうか確認できる。
% md5 in out MD5 (in) = 96f0fb6f9a5e9ca2585830bf5b43c06b MD5 (out) = 96f0fb6f9a5e9ca2585830bf5b43c06b %
ここまではよく見かけるソースコードと、その実行例だ。
今度は先ほどのソースコードを、システムコールを使って書き換えてみる。fopen(3)やfclose(3)は、関数内部でシステムコールであるopen(2)やclose(2)を呼んでいる。fgetc(3)やfputc(3)、FILE*はバッファを作るなどの処理をして、最終的にread(2)およびwrite(2)というシステムコールを呼ぶようになっている。以上に挙げた4つのシステムコール、open(2)、close(2)、read(2)、write(2)を使って、最初に挙げたソースコードを書き換えると次のようになる。
#include <unistd.h> #include <fcntl.h> int main(void) { int fdi, fdo; char b[1]; fdi = open("in", O_RDONLY); fdo = open("out", O_WRONLY); while (0 < read(fdi, b, 1)) { write(fdo, b, 1); } close(fdi); close(fdo); }
変数の名前や型、関数の引数などは変わっているものの、先ほどとほぼ同じ処理内容になっている。同じ要領でコンパイルして実行すると次のようになる。
% rm -f out % touch out % /usr/bin/time ./copy-read-write 15.48 real 0.33 user 14.91 sys %
最初に挙げたサンプルコードをコンパイルして実行したところ、ファイルのコピーを0.13秒で終えている。ところが、システムコールを使って書き換えたこちらのサンプルでは、処理完了まで15.48秒もかかっている。単純計算でおよそ200倍遅いということになる。
前回も説明したように、システムコールは簡単に実行速度を引き上げる秘密の関数というものではない(ただし、処理の高速化に役立つものもある)。基本的には、システムコールを呼び出さない方がプログラムの実行速度は速くなる。
次は、システムコールを使いながら、その呼び出し回数を減らしてみよう。read(2)とwrite(2)は読み書きするデータの大きさを指定してまとめて読み書きできる。ここでは10Mbytesの配列を用意して、データをまとめてコピーしてみる。サンプルコードは次のようになる。
#include <unistd.h> #include <fcntl.h> int main(void) { int fdi, fdo, len; char b[10485760]; fdi = open("in", O_RDONLY); fdo = open("out", O_WRONLY); len = read(fdi, b, 10485760); while (0 < len) { write(fdo, b, len); len = read(fdi, b, 10485760); } close(fdi); close(fdo); }
先ほどと同じ要領でコンパイルして実行すると次のようになる。
% rm -f out % rm -f out % touch out % /usr/bin/time ./copy-read-write2 0.02 real 0.00 user 0.01 sys %
今度は0.02秒で処理が済んだ。システムコールの呼び出し回数を減らすことで、実行速度が上がったというわけだ。
Copyright © ITmedia, Inc. All Rights Reserved.