まずはtruss(1)の実行結果から、システムコールの呼び出し回数を調べてみよう。標準ライブラリ関数であるfgetc(3)とfputc(3)を使ったサンプルプログラムは、システムコールread(2)とwrite(2)をそれぞれ3200回ほど呼び出している。前回の調査と同じ結果だ。fgetc(3)/fputc(3)はシステムコールread(2)/write(2)を呼び出すため、このような結果になる。
truss -c ./copy-fgetc-fputc syscall seconds calls errors readlink 0.000019556 1 0 lseek 0.000007264 1 0 mmap 0.000067188 8 0 open 0.012622972 5 1 close 0.000015715 2 0 fstat 0.000038063 3 0 write 0.055793547 3200 0 break 0.000008381 1 0 access 0.000009777 1 0 sigprocmask 0.000073194 8 0 munmap 0.000008660 1 0 read 0.092920187 3203 0 ------------- ------- ------- 0.161584504 6434 1
mmap(2)を使ったサンプルプログラムを調べた結果は次の通りだ。プログラムを起動するために必要なread(2)を呼び出しているだけで、write(2)の呼び出しはない。直前に挙げた、fgetc(3)とfputc(3)を使ったサンプルプログラムを調査した結果と比較するとシステムコール呼び出しの総数がとても少ないところが目を引く。
truss -c ./copy-mmap syscall seconds calls errors lseek 0.000010267 1 0 mmap 0.000068167 8 0 open 0.000046094 5 1 close 0.000014947 2 0 fstat 0.000013131 1 0 access 0.000020184 1 0 sigprocmask 0.000081015 8 0 read 0.000047423 2 0 ------------- ------- ------- 0.000301228 28 1
次は、mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムを調べてみる。システムコールの呼び出し回数は上記のmmap(2)のケースとまったく同じだ。memcpy(3)はシステムコールを呼び出していないので、truss(1)の結果には違いは現れない。
truss -c ./copy-mmap2 syscall seconds calls errors lseek 0.000007403 1 0 mmap 0.000068024 8 0 open 0.000046374 5 1 close 0.000014946 2 0 fstat 0.000013060 1 0 access 0.000010197 1 0 sigprocmask 0.000079481 8 0 read 0.000042394 2 0 ------------- ------- ------- 0.000281879 28 1
最後に、参考までにOSが備えるcp(1)コマンドでファイルをコピーした時の調査結果をご覧いただこう。先に示した3つのサンプルプログラムと比較すると、多くの種類のシステムコールを呼び出している。そして、read(2)とwrite(2)の呼び出し回数は100回ほどとなっている。呼び出しているシステムコールの種類が多いのは、オプションの判断など、先に挙げたサンプルプログラムより多くの処理をしているためだ。read(2)/write(2)の呼び出し回数が少ないのは、呼び出し1回当たりに読み書きするデータのサイズがfgetc(3)/fputc(3)を使う場合よりも大きいからである。
truss -c cp in out syscall seconds calls errors readlink 0.000014108 1 0 lseek 0.000006705 1 0 mmap 0.000074799 8 0 open 0.013091468 5 1 close 0.000036806 4 0 fstat 0.000012362 1 0 stat 0.000037924 3 0 write 1.028191286 100 0 break 0.000007194 1 0 access 0.000010197 1 0 sigaction 0.000009498 1 0 sigprocmask 0.000067046 8 0 munmap 0.000008241 1 0 read 0.015894474 103 0 ------------- ------- ------- 1.057462108 238 1
続いてtime(1)、つまり/usr/bin/timeを使って、プログラムが使用した物理メモリサイズやページフォールトの回数、コンテキストスイッチの回数などを見てみよう。最初に、fgetc(3)とfputc(3)を使ったサンプルプログラムを調べた結果をお見せしよう。
/usr/bin/time -lph ./copy-fgetc-fputc real 1.15 user 1.06 sys 0.08 1020 maximum resident set size 4 average shared memory size 2020 average unshared data size 128 average unshared stack size 115 page reclaims 0 page faults 0 swaps 0 block input operations 800 block output operations 0 messages sent 0 messages received 0 signals received 7 voluntary context switches 72 involuntary context switches
17種類もの項目を調査した結果が出てきたが、今回はすべての意味を理解する必要はない。「real」「maximum resident set size」「page reclaims」「block output operations」「voluntary context switches」「involuntary context switches」の6項目に注目する。
それぞれの項目の意味するところを説明しよう。realはプログラムの実行にかかった時間を表す。そして、maximum resident set sizeは、プログラムが利用した物理メモリの最大値を表す(単位はKbytes)。page reclaimsは入出力の発生を伴わないページフォールトが発生した回数を指す。block output operationsはファイルシステムがデータを出力した回数を表す。
voluntary context switchesは、割り当てられたプロセッサ時間を消費する前に、自発的にコンテキストスイッチを実行した回数を指し、involuntary context switchesは時間切れや、ほかの優先度の高いプロセスの実行のために強制的にコンテキストスイッチが発生した回数を表している。
ライブラリ関数fgetc(3)/fputc(3)を使ったサンプルプログラムの調査結果を見ると、read(2)/write(2)を頻繁に呼び出しているためか、block output operationsの値がほかのサンプルプログラムよりも多い。一方、maximum resident set sizeの値は小さく、メモリの使用量が少ないことが見て取れる。involuntary context switchesが多いというところにも注目だ。時間いっぱいまで処理を続けた結果、コンテキストスイッチが発生してしまったということが多いようだ。
次はmmap(2)を使ったプログラムの調査結果を見ていく。
/usr/bin/time -lph ./copy-mmap real 0.30 user 0.25 sys 0.04 201012 maximum resident set size 4 average shared memory size 4 average unshared data size 131 average unshared stack size 30822 page reclaims 0 page faults 0 swaps 0 block input operations 0 block output operations 0 messages sent 0 messages received 0 signals received 2 voluntary context switches 8 involuntary context switches
処理速度がfgetc(3)/fputc(3)よりも優れているのは前回調べた通りだが、maximum resident set sizeの値が201012Kbytes(193Mbytesほど)とかなり大きい点に注目したい。システムコールの呼び出し回数は少ないものの、物理メモリ使用量が大きいということだ。入出力を伴わないページフォールトの発生回数が30822回と多くなっているところも注目だ。入出力が発生していないのは、メモリの割り当て方が関係している。再割り当て待ちのページキャッシュから、ページフレームを再利用しているのだ。mmap(2)はページフォールトとデマンドページングを利用して動作するシステムコールなので、この観測結果はいわば当然の結果なのだが、mmap(2)の動作の特性がread(2)/write(2)とはまるで異なるものであることがよく分かるだろう。
mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムの調査結果は以下の通りだ。
/usr/bin/time -lph ./copy-mmap2 real 0.06 user 0.02 sys 0.03 197480 maximum resident set size 4 average shared memory size 4 average unshared data size 146 average unshared stack size 30822 page reclaims 0 page faults 0 swaps 0 block input operations 0 block output operations 0 messages sent 0 messages received 0 signals received 2 voluntary context switches 1 involuntary context switches
まず、ほかのサンプルプログラムと比較すると、処理速度が素晴らしく速い。通常のmmap(2)を利用したサンプルプログラムでは、コピーに0.3秒かかっているが、mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムでは0.06秒しかかかっていない。truss(1)による調査結果と同じように、基本的な動きはmmap(2)のみを使ったサンプルプログラムによく似ているが、実際の処理速度は5倍ほど高速になっている。先に説明したように、コンパイラがコンパイル時にmemcpy(3)の処理を最適なバイナリコードに変換するからだ。memcpy(3)はシステムコールではなく、標準ライブラリ関数だが、大きな効果を期待できる関数である。
最後に、ユーザーランドにインストールされているcp(1)コマンドを使ったファイルコピーについて調査した結果をお見せする。
/usr/bin/time -lph cp in out real 1.11 user 0.00 sys 0.07 2076 maximum resident set size 14 average shared memory size 1828 average unshared data size 115 average unshared stack size 368 page reclaims 0 page faults 0 swaps 0 block input operations 800 block output operations 0 messages sent 0 messages received 0 signals received 662 voluntary context switches 7 involuntary context switches
fgetc(3)/fputs(3)を使ったサンプルと似たような結果が得られるが、自発的なコンテキストスイッチの回数(voluntary context switches)が多い。自発的なコンテキストスイッチが発生する理由はいくつもあるので、ここでは説明しきれないが、fgetc(3)/fputs(3)で作成したシンプルなプログラムとは動き方に違いがあるということがよく分かる。
mmap(2)とmemcpy(3)を組み合わせたプログラムを作れば、コピー処理は高速になるが、実はライブラリの互換性などを考慮すると、どんな状況でも使える手段とは言えない。そして、mmap(2)も処理は高速だが、メモリを消費するという欠点がある。単純な考えで使うべきものではない。
一方、処理速度の面ではいいところがなかったcp(1)コマンドだが、truss(1)の調査結果が示すように、mmap(2)を呼び出しているなど、高速化のための工夫を凝らしているところが見える。
前回はmmap(2)システムコールを使うことによって、処理速度が上がるという点に焦点を当てて解説した。コピープログラムとカーネルの間でデータのコピーが発生しない分、mmap(2)は高速に動作する。今回は物理メモリの使用量やページフォールトの回数、コンテキストスイッチの切り替え回数などにも調査の対象を広げながら、システムコールそれぞれの特性を追ってみた。
処理速度の面ではmmap(2)はとても魅力的だが、物理メモリを多く消費するなど、考慮すべき点もあり、単純にすべてのシーンに適用できるわけではないことが見えてきたかと思う。大切なのはそれぞれのシステムコールが実際にどのように動き、どのような特徴を持っているかを把握するということである。
ちなみに、/usr/bin/timeの実行結果はgetrusage(2)システムコールで取得できるrusage構造体のデータを出力しているものなので、プログラムからデータを取得して標準出力へ出力させることもできる。ここではサンプルのソースコードをシンプルにしたいので、/usr/bin/timeを使って得たデータを掲載した。
なお、Solaris 11 11/11ではgetrusageはシステムコールではなく標準ライブラリ関数となっている。関数をシステムコールとして実装するか、標準ライブラリ関数として実装するかは、それぞれのOSの実装によるのだ。
本連載での各種検証作業には、PC-BSD 9.0/FreeBSD 9.0を利用している。今回の解説内容を自身でも検証してみたいという方は、利用する環境の違いに注意してほしい。環境が変わることによって発生する可能性がある事象や、OSごとのコマンドの使い方の違いなどは本連載第2回の末尾にまとめてあるので、参考にしていただきたい。
BSDコンサルティング株式会社取締役/オングス代表取締役
後藤 大地
@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。
Copyright © ITmedia, Inc. All Rights Reserved.