お題:time(1)コマンドがプログラムを起動し、時間をカウントする仕組みを調査せよ
※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。
場所とmanpageの確認:
$ which time /usr/bin/time $ man 1 time
簡単に動作確認してみる:
timetest.c:
#include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { char buf[200]; printf("Input 3 times.\n"); printf("1 > "); if (NULL != fgets(buf, sizeof(buf), stdin)) { printf("1 : %s", buf); } printf("2 > "); if (NULL != fgets(buf, sizeof(buf), stdin)) { printf("2 : %s", buf); } printf("3 > "); if (NULL != fgets(buf, sizeof(buf), stdin)) { printf("3 : %s", buf); } printf("sleeping 3 seconds...\n"); sleep(3); return 0; }
$ gcc -o timetest timetest.c $ time ./timetest Input 3 times. 1 > abc 1 : abc 2 > def 2 : def 3 > ghi 3 : ghi sleeping 3 seconds... real 0m6.912s user 0m0.000s sys 0m0.013s
ソースコードの場所は?
$ locate time ... /usr/src/usr.bin/time /usr/src/usr.bin/time/CVS /usr/src/usr.bin/time/CVS/Entries /usr/src/usr.bin/time/CVS/Repository /usr/src/usr.bin/time/CVS/Root /usr/src/usr.bin/time/CVS/Tag /usr/src/usr.bin/time/Makefile /usr/src/usr.bin/time/time.1 /usr/src/usr.bin/time/time.c ...
そんなに長くない。ざっくりと読んでみる。main冒頭の変数宣言:
int main(argc, argv) int argc; char **argv; { int pid; int ch, status; int lflag, portableflag; const char *decpt; const struct lconv *lconv; struct timeval before, after; struct rusage ru;
lflag, portableflagというのは"-l", "-p"オプションに対応する。
次、オプション解析。特に難しいところは無く、素直に読み進められる。
lflag = portableflag = 0; while ((ch = getopt(argc, argv, "lp")) != -1) { switch (ch) { case 'p': portableflag = 1; break; case 'l': lflag = 1; break; case '?': default: usage(); } } argc -= optind; argv += optind;
指定されたコマンドを実行し、経過時間を取得するメイン処理が続く。
gettimeofday(&before, (struct timezone *)NULL); switch(pid = vfork()) { case -1: /* error */ perror("vfork"); exit(EXIT_FAILURE); /* NOTREACHED */ case 0: /* child */ /* LINTED will return only on failure */ execvp(*argv, argv); perror(*argv); _exit((errno == ENOENT) ? 127 : 126); /* NOTREACHED */ } /* parent */ (void)signal(SIGINT, SIG_IGN); (void)signal(SIGQUIT, SIG_IGN); while (wait3(&status, 0, &ru) != pid); gettimeofday(&after, (struct timezone *)NULL); if (!WIFEXITED(status)) fprintf(stderr, "Command terminated abnormally.\n"); timersub(&after, &before, &after);
まずgettimeofday()で現在時刻を"before"に保存する。
続いてvfork()を呼び、子プロセスを生成する。fork()を使わず、メモリを共有するvfork()を呼んでいる理由は不明。
子プロセス側は、即座にexecvp(2)でコマンドラインで指定されたプロセスを起動している。
親プロセス側は、SIGINTとSIGQUITを無視に設定し、wait3(2)で子プロセスの終了を待機する。
wait3(2)で待機するのがポイントとなる。wait3(2)については後ほど、もう少し調べてみる。
ひとまず本体ソースを読み進めてみる。
子プロセスの終了が検出されたら、gettimeofday()をもう一度呼び、子プロセス終了時の時刻を"after"に保存する。
子プロセスの終了コードが0であれば、after - beforeの差分をtimersub()で計算している。
timersub()は sys/time.h で以下のように定義されている。
#define timersub(tvp, uvp, vvp) \ do { \ (vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec; \ (vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec; \ if ((vvp)->tv_usec < 0) { \ (vvp)->tv_sec--; \ (vvp)->tv_usec += 1000000; \ } \ } while (/* CONSTCOND */ 0)
ここまでくれば、あとは"-l"や"-p"オプションに応じて結果を出力するだけとなる。
if (portableflag) { fprintf (stderr, "real %9ld%s%02ld\n", (long)after.tv_sec, decpt, (long)after.tv_usec/10000); fprintf (stderr, "user %9ld%s%02ld\n", (long)ru.ru_utime.tv_sec, decpt, (long)ru.ru_utime.tv_usec/10000); fprintf (stderr, "sys %9ld%s%02ld\n", (long)ru.ru_stime.tv_sec, decpt, (long)ru.ru_stime.tv_usec/10000); } else { fprintf(stderr, "%9ld%s%02ld real ", (long)after.tv_sec, decpt, (long)after.tv_usec/10000); fprintf(stderr, "%9ld%s%02ld user ", (long)ru.ru_utime.tv_sec, decpt, (long)ru.ru_utime.tv_usec/10000); fprintf(stderr, "%9ld%s%02ld sys\n", (long)ru.ru_stime.tv_sec, decpt, (long)ru.ru_stime.tv_usec/10000); } if (lflag) { int hz = (int)sysconf(_SC_CLK_TCK); /* 省略 */ } exit(WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE); /* NOTREACHED */ } /* main()終了 */
あとはusage()関数が定義されて、time.cはおしまいとなる。
では、今回のポイントとなるwait3(2)について簡単に調べてみる。
$ man 2 wait3
WAIT(2) NetBSD Programmer's Manual WAIT(2) NAME wait, waitpid, wait4, wait3 - wait for process termination LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t wpid, int *status, int options); #include <sys/resource.h> pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid_t wpid, int *status, int options, struct rusage *rusage);
wait(2)系は子プロセスの終了を検出するシステムコールだが、wait3()とwait4()だけが "struct rusage *rusage" を引数に取得している。
"struct rusage"の詳細はgetrusage(2)のmanページに記載されている。
$ man -k rusage getrusage (2) - get information about resource utilization
GETRUSAGE(2) NetBSD Programmer's Manual GETRUSAGE(2) NAME getrusage - get information about resource utilization LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <sys/resource.h> #define RUSAGE_SELF 0 #define RUSAGE_CHILDREN -1 int getrusage(int who, struct rusage *rusage); DESCRIPTION getrusage() returns information describing the resources utilized by the current process, or all its terminated child processes. The who parame- ter is either RUSAGE_SELF or RUSAGE_CHILDREN. The buffer to which rusage points will be filled in with the following structure: struct rusage { struct timeval ru_utime; /* user time used */ struct timeval ru_stime; /* system time used */ long ru_maxrss; /* max resident set size */ ...
プロセスのリソース情報がstruct rusageに格納される。さらに、getrusage(2)の"int who"引数に応じてリソース情報の取得元を自分自身か、子プロセスか選択できるようになっている。
BSDの場合はwait3()/wait4()を使うことでgetrusage(2)を呼ばずとも、子プロセス終了のwaitと同時にリソース情報も取得できるようになっているようだ。
もう少しwait3()/wait4()を深追いしてみる。
$ locate wait3 /usr/share/man/cat2/wait3.0 /usr/share/man/man2/wait3.2 /usr/src/lib/libc/gen/wait3.c
/usr/src/lib/libc/gen/wait3.c:
/* ... 省略 ... */ pid_t wait3(istat, options, rup) int *istat; int options; struct rusage *rup; { return (wait4(WAIT_ANY, istat, options, rup)); }
NetBSD 1.6 においては、wait3(2)の実体はWAIT_ANYを指定したwait4(2)であることが分かる。
(以下の説明は C言語系/「デーモン君のソース探検」読書メモ/A09, write(2) + O_APPEND の経験を元に大幅に修正している)
wait4のシステムコールを探してみる。C言語系/「デーモン君のソース探検」読書メモ/A09, write(2) + O_APPENDの経験に基づくと、"src/sys/kern/init_sysent.c"を見ればシンボル名が分かるはず。
src/sys/kern/init_sysent.c:
struct sysent sysent[] = { { 4, s(struct sys_wait4_args), 0, sys_wait4 }, /* 7 = wait4 */
見つかった。"struct sys_wait4_args"は sys/syscallargs.hで定義されている。
struct sys_wait4_args { syscallarg(int) pid; syscallarg(int *) status; syscallarg(int) options; syscallarg(struct rusage *) rusage; };
src/sys/kern/ 内で"sys_wait4"をgrepしてみると、kern_exit.c 内で定義されていた。
int sys_wait4(struct proc *q, void *v, register_t *retval) { struct sys_wait4_args /* { syscallarg(int) pid; syscallarg(int *) status; syscallarg(int) options; syscallarg(struct rusage *) rusage; } */ *uap = v; struct proc *p, *t; int nfound, status, error, s; (省略) if (SCARG(uap, rusage) && (error = copyout((caddr_t)p->p_ru, (caddr_t)SCARG(uap, rusage), sizeof(struct rusage)))) return (error);
"p"は sys/proc.h で定義されている struct proc, プロセス情報へのポインタ。その"p_ru"メンバは"struct rusage"構造体へのポインタとして宣言されている。
以上でwait4()によりrusage情報を取得する箇所が特定できた。
ここで一旦まとめてみる。
time(1)コマンドは、コマンドラインで指定されたプログラムをvfork(2) + execvp(2)で起動後、wait3(2)で子プロセスの終了を待機。子プロセスが正常終了したら、gettimeofday()とwait3(2)がセットしたstruct rusageを使ってreal/user/sysのそれぞれの時間を表示する。
では、Linuxではどのように実装されているか?今回はstraceを使ってざっくりと追ってみる。
(CentOS 5.x) $ strace time /bin/ls ... gettimeofday({1290408739, 104680}, NULL) = 0 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb7f03708) = 22855 (lsの出力が混ざって表示される) --- SIGCHLD (Child exited) @ 0 (0) --- rt_sigaction(SIGINT, {SIG_IGN}, {SIG_DFL}, 8) = 0 rt_sigaction(SIGQUIT, {SIG_IGN}, {SIG_DFL}, 8) = 0 wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, {ru_utime={0, 999}, ru_stime={0, 999}, ...}) = 22855 gettimeofday({1290408739, 110785}, NULL) = 0 rt_sigaction(SIGINT, {SIG_DFL}, {SIG_IGN}, 8) = 0 rt_sigaction(SIGQUIT, {SIG_DFL}, {SIG_IGN}, 8) = 0 write(2, "0.00", 40.00) = 4 write(2, "u", 1u) = 1 ...
相違点として目立つのは次の2点。それ以外はおおむねNetBSD 1.6のtime(1)と同等に思える。
まずclone(2)はLinux特有の子プロセス作成のシステムコールであり、親プロセスとリソースの一部を共有することが出来る。その点ではvfork(2)と似ている。
次にwait4()については、今回使用した CentOS 5.x でも提供されており、wait3()がマクロになっているか、あるいは直接wait4()を呼んでいるものと推測される。CentOS 5.4 でのwait3()/wait4()のprototypeを以下に示すが、NetBSD側のプロトタイプと同じである。
pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
ではOpenGroupではtime(1)の実装に何か言及されているかと調べてみると、"times(2)"関数で取得される"struct tms"構造体の
tms_utime tms_cutime tms_stime tms_cstime
フィールドと同等云々と書かれている。times(2)は自分とその子プロセスの時間情報をstruct tms構造体につめて返すシステムコールである。
wait3()/wait4()はどこに行ったのか?と見ると、OpenGroupではwait3()/wait4()は規定されていない。あくまでもBSDローカルな関数であることが分かる。
以上でtime(1)が子プロセスの時間を取得する仕組みが判明した。子プロセスの終了をwait3()/wait4()で待機しstruct rusageを取得するのがtime(1)の仕組みの基本となっている。
ただしwait3()/wait4()はOpenGroupでは規定されておらず、どのUNIXでも通用するポータブルなものにはなっていない。ポータブルにするには、times(2)やgetrusage(2)を使うことになるだろう。
今回のお題については、ここまで。