こんにちは、Yahoo!ショッピング担当の吉野です。
今回はタイトルの通り、headコマンドとtailコマンドのソースコードを読んでいきたいと思います。
■はじめに
皆さんご存じの通り、headコマンドはファイルの先頭からn行(バイト)を出力し、
tailコマンドはファイルの末尾からn行(バイト)を出力するコマンドです。
ほかにもパイプの入力に使ったり、[tail -f]でログファイルの出力監視に使ったりと、
UNIXを使う上では欠かせないコマンドと言えるでしょう。
headとtailは見た目の動作は似ているようですが、ソースコードの中は結構違います。
さっそく見ていきましょう。
<headのソースコード>
http://www.freebsd.org/cgi/cvsweb.cgi/src/usr.bin/head/
<tailのソースコード>
http://www.freebsd.org/cgi/cvsweb.cgi/src/usr.bin/tail/
■headコマンド
headは非常にシンプルで、わずか百数十行しかありません。
●[コード1] head.c - main()
78 obsolete(argv);
79 while ((ch = getopt(argc, argv, "n:c:")) != -1)
80 switch(ch) {
81 case 'c':
82 bytecnt = strtoimax(optarg, &ep, 10);
83 if (*ep || bytecnt <= 0)
84 errx(1, "illegal byte count -- %s", optarg);
85 break;
86 case 'n':
87 linecnt = strtol(optarg, &ep, 10);
88 if (*ep || linecnt <= 0)
89 errx(1, "illegal line count -- %s", optarg);
90 break;
91 case '?':
92 default:
93 usage();
94 }
[コード1]はオプション取得部分です。-n と -c オプションをそれぞれstrtol(),strtoimax()を使って数値に変換しています。
ところで、headは [head -n 50] と同じ意味で [head -50] といった使い方ができますね。
しかし、ここのswitch文では [head -50] の場合のケースが書かれていません。
その秘密は78行目のobsolete()にあります。
●[コード2] head.c - obsolete()
167 while ((ap = *++argv)) {
168 /* Return if "--" or not "-[0-9]*". */
169 if (ap[0] != '-' || ap[1] == '-' || !isdigit(ap[1]))
170 return;
171 if ((ap = malloc(strlen(*argv) + 2)) == NULL)
172 err(1, NULL);
173 ap[0] = '-';
174 ap[1] = 'n';
175 (void)strcpy(ap + 2, *argv + 1);
176 *argv = ap;
177 }
[コード2]では結局何をしているかというと、[head -50 head.c] を [head -n50 head.c]にしています。
ちょっと強引に見えますが、これによって -50 が[コード1]で-nオプションとして処理されるのです。
●[コード3] head.c - main()
100 if (linecnt == -1 )
101 linecnt = 10;
102 if (*argv) {
103 for (first = 1; *argv; ++argv) {
104 if ((fp = fopen(*argv, "r")) == NULL) {
105 warn("%s", *argv);
106 eval = 1;
107 continue;
108 }
109 if (argc > 1) {
110 (void)printf("%s==> %s <==\n",
111 first ? "" : "\n", *argv);
112 first = 0;
113 }
114 if (bytecnt == -1)
115 head(fp, linecnt);
116 else
117 head_bytes(fp, bytecnt);
118 (void)fclose(fp);
119 }
120 } else if (bytecnt == -1)
121 head(stdin, linecnt);
122 else
123 head_bytes(stdin, bytecnt);
[コード3]はheadのメインとなる部分です。104行目でfopen()を使ってファイルを読み取り専用で開き、
バイト指定の場合はhead_bytes()に、それ以外はhead()にファイルポインタを渡します。
109〜113行目はファイルが複数指定されているときにファイル名を出力する処理です。
引数にファイルが指定されていない場合は、120〜123行目でファイルポインタの代わりに標準入力を使用します。
●[コード4] head.c - head()
134 while (cnt && (cp = fgetln(fp, &readlen)) != NULL) {
135 error = fwrite(cp, sizeof(char), readlen, stdout);
136 if (error != readlen)
137 err(1, "stdout");
138 cnt--;
139 }
[コード4]はhead()の中です。fgetln()を使って行単位に読み込んで、fwrite()で行単位に標準出力に出しています。
●[コード5] head.c - head_bytes()
145 char buf[4096];
146 size_t readlen;
147
148 while (cnt) {
149 if ((uintmax_t)cnt < sizeof(buf))
150 readlen = cnt;
151 else
152 readlen = sizeof(buf);
153 readlen = fread(buf, sizeof(char), readlen, fp);
154 if (readlen == 0)
155 break;
156 if (fwrite(buf, sizeof(char), readlen, stdout) != readlen)
157 err(1, "stdout");
158 cnt -= readlen;
159 }
[コード5]はhead_bytes()の中です。こちらではbuf[]の4096バイトずつ読み込んでは出力を繰り返します。
headについては以上です。次にtailを見ていきます。
■tailコマンド
こちらが本日のメインです。
headに比べてソースの量は多いですが、中ではけっこう面白いことをしているので見ていきましょう。
●[コード6] tail.c - main()
182 for (file = files; (fname = *argv++); file++) {
183 file->file_name = malloc(strlen(fname)+1);
184 if (! file->file_name)
185 errx(1, "Couldn't malloc space for file name.");
186 strncpy(file->file_name, fname, strlen(fname)+1);
187 if ((file->fp = fopen(file->file_name, "r")) == NULL ||
188 fstat(fileno(file->fp), &file->st)) {
189 file->fp = NULL;
190 ierr();
191 continue;
192 }
193 }
194 follow(files, style, off);
195 for (i = 0, file = files; i < no_files; i++, file++) {
196 free(file->file_name);
197 }
198 free(files);
●[コード7] tail.c - main()
213 if (rflag)
214 reverse(fp, style, off, &sb);
215 else
216 forward(fp, style, off, &sb);
[コード6]は引数にファイルが指定され、かつ -f or -F オプションが指定されたとき(fflagが1)の処理です。
少しごちゃごちゃしていますが、要点は187行目でファイルを読み取り専用でオープンし、
194行目でfile_info構造体(extern.hで定義されています)をfollow()に渡しているところくらいです。
[コード7]は -f -F オプションなしでファイル指定されたときの処理です。
rflagは -r オプションのフラグです。
ファイル指定なしの場合は、後続の236〜239行目でfpのところをstdin(標準入力)にして
reverse(),forward()が呼び出されます。
ちなみに、tailにもheadと同じようにobsolete()が定義されていて、
getoptの前に同じような変換の処理がされています。
●[コード8] forward.c - rlines()
214 curoff = size - 2;
215 while (curoff >= 0) {
216 if (curoff < map.mapoff && maparound(&map, curoff) != 0) {
217 ierr();
218 return;
219 }
220 for (i = curoff - map.mapoff; i >= 0; i--)
221 if (map.start[i] == '\n' && --off == 0)
222 break;
223 /* `i' is either the map offset of a '\n', or -1. */
224 curoff = map.mapoff + i;
225 if (i >= 0)
226 break;
227 }
228 curoff++;
229 if (mapprint(&map, curoff, size - curoff) != 0) {
230 ierr();
231 exit(1);
232 }
●[コード9] misc.c - mapprint()
87 if (n > len)
88 n = len;
89 WR(mip->start + (startoff - mip->mapoff), n);
90 startoff += n;
91 len -= n;
[コード8]は、[コード7]に書かれているforward()から呼び出されているrlines()の処理です。
通常のtail処理はここに入ります。
ここではまずファイルサイズを取得し(sizeから-2しているのは\0を除くため)、
220〜222行目のforループでファイルの一番下のバイトから逆順に1文字ずつ評価していきます。
offには表示する行数(-nで指定された数、もしくはデフォルト10)が格納されていますので、
「改行だったらoffを1マイナス」をoffの数分繰り返すことで、curoffには結果的に
【表示を開始するバイト】が格納されます。
curoffと、ファイルポインタが格納されている&mapを、実際に標準出力へ書き出すmapprint()に渡します。
[コード9]はmapprint()の一部です。WR()という関数が実際に標準出力に書き出しています。
(WRはextern.hに定義されています)
「[コード8]で割り出した【表示を開始するバイト】からnバイト出力する」といった事をやっています。
●[コード10] forward.c - follow()
338 kq = kqueue();
339 if (kq < 0)
340 err(1, "kqueue");
341 ev = malloc(n * sizeof(struct kevent));
342 if (! ev)
343 err(1, "Couldn't allocate memory for kevents.");
344 set_events(files);
345
346 for (;;) {
347 for (i = 0, file = files; i < no_files; i++, file++) {
348 if (! file->fp)
349 continue;
350 if (Fflag && file->fp && fileno(file->fp) != STDIN_FILENO) {
351 if (stat(file->file_name, &sb2) == 0 &&
352 (sb2.st_ino != file->st.st_ino ||
353 sb2.st_dev != file->st.st_dev ||
354 sb2.st_nlink == 0)) {
355 show(file);
356 file->fp = freopen(file->file_name, "r", file->fp);
357 if (file->fp == NULL) {
358 ierr();
359 continue;
360 } else {
361 memcpy(&file->st, &sb2, sizeof(struct stat));
362 set_events(files);
363 }
364 }
365 }
366 show(file);
367 }
368
369 switch (action) {
370 case USE_KQUEUE:
371 ts.tv_sec = 1;
372 ts.tv_nsec = 0;
373 /*
374 * In the -F case we set a timeout to ensure that
375 * we re-stat the file at least once every second.
376 */
377 n = kevent(kq, NULL, 0, ev, 1, Fflag ? &ts : NULL);
378 if (n < 0)
379 err(1, "kevent");
380 if (n == 0) {
381 /* timeout */
382 break;
383 } else if (ev->filter == EVFILT_READ && ev->data < 0) {
384 /* file shrank, reposition to end */
385 if (lseek(ev->ident, (off_t)0, SEEK_END) == -1) {
386 ierr();
387 continue;
388 }
389 }
390 break;
391
392 case USE_SLEEP:
393 (void) usleep(250000);
394 break;
395 }
396 }
[コード10]は -f -F オプション指定時のメインとなる処理です(ちょっと長いですが大事な所です)
ファイルを監視している間は、346行目から始まる無限ループをグルグル回っています。
350〜365行目は-Fオプション専用の処理で、iノード番号かデバイスIDが違った場合はファイルを再オープンしています。
366行目のshow()では、putchar()を使って書き込まれた差分を表示しています。
ここで使用されているのがカーネルイベントキュー(kqueueシステムコール)です。
[コード10]からカーネルイベントキュー関連のコードを抜粋すると、
338 kq = kqueue();
344 set_events(files);
362 set_events(files);
371 ts.tv_sec = 1;
372 ts.tv_nsec = 0;
377 n = kevent(kq, NULL, 0, ev, 1, Fflag ? &ts : NULL);
こんな感じです。
まず、kqueue()でカーネルイベントキューを生成します。
次にset_events()ですが、これはforward.cに定義されている関数で、
中ではEV_SET()が呼び出されています。
その際、EV_SET()にはtailの対象となるファイルのポインタが渡されています。
そして、377行目のkevent()でイベントの監視が開始されます。
kevent()の第6引数には、監視のタイムアウト値を指定することができ、
Fflag(-Fオプションのフラグ)が立っていた場合は、
371行目に設定してある1秒がタイムアウト値として設定されます。
-f オプションの場合はNULLがセットされますが、NULLの場合はイベントを無限に待ち続けます。
-f と -F のオプションの違いは、trussコマンドを使って見てみるとよくわかります。
$ touch hoge
$ truss tail -f hoge
-------中略-------
read(0x3,0x1005000,0x2000) = 0 (0x0)
kevent(0x4,0x9fbffa58,0x1,0x0,0x0,0x9fbff9f0) = 0 (0x0)
read(0x3,0x1005000,0x2000) = 0 (0x0) <--- 1
- kevent()が呼ばれた後はずっとイベントを待ち続ける。
$ truss tail -F hoge
-------中略-------
read(0x3,0x1005000,0x2000) = 0 (0x0)
kevent(0x4,0x0,0x0,0x9fbffa58,0x1,0x9fbff9f0) = 0 (0x0) <--- 1
stat("hoge",0x9fbff9f8) = 0 (0x0) <--- 2
read(0x3,0x1005000,0x2000) = 0 (0x0)
kevent(0x4,0x0,0x0,0x9fbffa58,0x1,0x9fbff9f0) = 0 (0x0) <--- 3
stat("hoge",0x9fbff9f8) = 0 (0x0)
read(0x3,0x1005000,0x2000) = 0 (0x0)
kevent(0x4,0x0,0x0,0x9fbffa58,0x1,0x9fbff9f0) = 0 (0x0)
stat("hoge",0x9fbff9f8) = 0 (0x0)
read(0x3,0x1005000,0x2000) = 0 (0x0)
- kevent()は1秒でタイムアウト
- ファイルのstat(iノード番号、デバイスID)を取得して評価。必要があればファイルを再オープン。
- 再度kevent() -> 1秒後にタイムアウト
- 〜〜1-2-3を無限に繰り返し〜〜
-Fオプションの場合は1秒ごとにファイルをポーリングしているわけですね。
■まとめ
headとtailでは中の処理がかなり違うことを実感しました。
「カーネルイベントキューなんて初めて聞いた」という方はぜひ覚えて調べてみてください。
-fと-Fで、システムコールの呼ばれ方の違いを検証してみても面白いと思います。
(ファイルを消してみたりmvしてみたり再度touchしてみたり。。)
今回は-rオプションの動きについてはご紹介しませんでしたが、興味のある方はソースを読んでみてください。
今回のお話は以上です。またお会いしましょう。
lsコマンドをハックしてみようはこちら
ソースコードリーディング(killコマンド編)はこちら
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました