ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

ソースコードリーディング(head,tailコマンド編)

こんにちは、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

  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)

  1. kevent()は1秒でタイムアウト
  2. ファイルのstat(iノード番号、デバイスID)を取得して評価。必要があればファイルを再オープン。
  3. 再度kevent() -> 1秒後にタイムアウト
  4. 〜〜1-2-3を無限に繰り返し〜〜

-Fオプションの場合は1秒ごとにファイルをポーリングしているわけですね。


■まとめ

headとtailでは中の処理がかなり違うことを実感しました。
「カーネルイベントキューなんて初めて聞いた」という方はぜひ覚えて調べてみてください。
-fと-Fで、システムコールの呼ばれ方の違いを検証してみても面白いと思います。
(ファイルを消してみたりmvしてみたり再度touchしてみたり。。)

今回は-rオプションの動きについてはご紹介しませんでしたが、興味のある方はソースを読んでみてください。

今回のお話は以上です。またお会いしましょう。


lsコマンドをハックしてみようはこちら
ソースコードリーディング(killコマンド編)はこちら

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ