#navi_header|C言語系| お題:tailコマンドの"-f"オプションを調査せよ。 今回も、tailコマンドのmanページで使い方をおさらいしておく。 tail [-f | -F | -r] [-b number | -c number | -n number] [file ...] "-f"はファイル監視モードを指示し、普段良く使うオプション。"-F"は"-f"に似ているが、5秒ごとにファイル名が変更されたりしていないかチェックし、もしそうなら再オープンする。syslogなど、ローテーションされるファイルを監視する時に使うと便利。 "-b", "-c", "-n" は初期表示するサイズ。順に、512バイトのブロック単位、バイト単位、行単位で指定する。 それではソースの探索に進む。 $ locate tail.c /usr/src/usr.bin/tail/tail.c $ cd /usr/src/usr.bin/tail $ ls CVS/ Makefile extern.h forward.c misc.c read.c reverse.c tail.1 tail.c 幾つかのファイルに分かれている。とりあえずtail.cから探索を始める。 #more|| #outline|| ---- * tail.cの探検 tail.cについては "-b", "-c", "-n" オプションの処理や過去のオプション指定との互換性維持で特殊な部分がある。それ以外は素直に読めると思うので、駆け足で進める。 まず"-r", "-f"オプションなどを保持しておく為のグローバル変数が宣言される。 int fflag, rflag, rval; char *fname; 関数のプロトタイプ宣言が続き、main()関数の冒頭でローカル変数が宣言される。 #pre||> int main(argc, argv) int argc; char *argv[]; { struct stat sb; FILE *fp; long off; enum STYLE style; int ch, first; char *p; off = 0; ||< ここで重要なのはsb, fp, off, styleである。sbとfpはファイル情報、offは"-b", "-c", "-n"で指定されたオフセット、styleは"extern.h"で定義されたenum値で、オフセット単位はバイトか行か・ファイル先頭からのオフセットか末尾からのオフセットか、などの組み合わせを指定出来るようになっている。 extern.h: enum STYLE { NOTSET = 0, FBYTES, FLINES, RBYTES, RLINES, REVERSE }; main()関数ではこれに続いて、いきなりARGV()というマクロが定義される。 #define ARG(units, forward, backward) { ... 詳細は割愛する。ソースも特に読みづらいものではないと思う。 概要だけ書いておくと、"-b", "-c", "-n" で指定された数値を元に"off"(オフセット)の計算と"style"の設定を行っている。 指定された数値が -b +10 のように"+"始まりの場合、ファイルの先頭からのオフセットとして扱う。 -b -10 -b 10 のように"-"始まりor符号未指定の場合、ファイルの末尾からのオフセットとして扱う。 マクロ定義に続いてobsolete()という関数が呼び出されているが、これは過去のオプション指定方法との互換性維持なので、ここでは取りあげない。 obsolete(argv); その下にgetopt(3)によるオプション処理の見慣れたwhileループがある。 #pre||> while ((ch = getopt(argc, argv, "Fb:c:fn:r")) != -1) switch(ch) { case 'F': fflag = 2; break; case 'b': ARG(512, FBYTES, RBYTES); break; case 'c': ARG(1, FBYTES, RBYTES); break; case 'f': fflag = 1; break; case 'n': ARG(1, FLINES, RLINES); break; case 'r': rflag = 1; break; case '?': default: usage(); } ||< "-b", "-c", "-n" の処理で先ほど取りあげたARGVマクロが使われている。 {F|R}BYTES, {F|R}LINES についてはextern.hで定義されたenum値でARGVマクロの中でstyleにセットされる。 ARGVマクロの定義を振り返って整理すれば、オフセット値の符号指定で以下のようにstyleは設定される。 "+"始まりの数値 → style = FBYTES or FLINES (ファイル先頭からのオフセット) "-"始まりor符号無し → style = RBYTES or RLINES (ファイル末尾からのオフセット) この後、"-f"が指定されていてかつ、fileが2つ以上指定された場合はエラーとしている。 if (fflag && argc > 1) err(1, "-f and -F options only appropriate for a single file"); 続けて以下のifブロック。見たままなので特にソースは載せないが、"-r"が指定された場合はstyleを RBYTES or RLINES に強制的に再設定している。また"-r"と"-f"が同時指定されていたらエラーとしている。 if (rflag) { /* ... */ } さらに次のifブロックでは、"-b","-c","-n"が指定されなかった場合のoffとstyleのデフォルト値を設定している。 if (style == NOTSET) { if (rflag) { off = 0; style = REVERSE; } else { off = 10; style = RLINES; } } これに続くifブロックのソースは割愛する。大雑把な概要だけ示すと、以下のようになる。 + fileが指定された場合は、指定されたfile毎に fopen()の戻り値をfpに、fstat()の戻り値をsbに入れ、"-r"指定時はreverse()を、それ以外ならforward()を呼ぶ。 + file未指定時は標準入力に対して reverse()/forward() を呼び分ける。 "-r"フラグに応じてreverse()/forward()を呼び分けるコードは以下のようになっている。 if (rflag) reverse(fp, style, off, &sb); else forward(fp, style, off, &sb); reverse()/forward()は、それぞれ reverse.c / forward.c で定義されている。 今回のお題は"-f"オプション指定時の動作なのでforward.cの探索へ進む。 reverse.c にも軽く目を通してみたが、forward.cと比べてかなり読みやすい。というのもforward.cだと"-f","-F"オプションによる監視(ポーリング)処理があるのに対し、"-r"オプションのreverse()の場合は逆順に出力してお仕舞いだからだ。 reverse()本体は非常にスッキリしているし、ファイルが通常ファイル(S_ISREG==true)の場合に呼ばれるr_reg()関数もmmap()を使った短くて分かりやすいコードになっている。「デーモン君のソース探検」では、forward()の解析中にmmap()の解説も出てくるが、mmap()に注目するのであればreverse.cのr_reg()の方が分かりやすいかも知れない。 一応参考までにr_reg()を載せておく。 #code|c|> static void r_reg(fp, style, off, sbp) FILE *fp; enum STYLE style; long off; struct stat *sbp; { off_t size; int llen; char *p; char *start; if (!(size = sbp->st_size)) return; if (size > SIZE_T_MAX) { err(0, "%s: %s", fname, strerror(EFBIG)); return; } if ((start = mmap(NULL, (size_t)size, PROT_READ, MAP_FILE|MAP_SHARED, fileno(fp), (off_t)0)) == (caddr_t)-1) { err(0, "%s: %s", fname, strerror(EFBIG)); return; } p = start + size - 1; if (style == RBYTES && off < size) size = off; /* Last char is special, ignore whether newline or not. */ for (llen = 1; --size; ++llen) if (*--p == '\n') { WR(p + 1, llen); llen = 0; if (style == RLINES && !--off) { ++p; break; } } if (llen) WR(p, llen); if (munmap(start, (size_t)sbp->st_size)) err(0, "%s: %s", fname, strerror(errno)); } ||< ソースが短いせいもあり、ぱっと見で大凡の動作は分かると思う。 * forward.cのforward()関数の探検 forward.c の forward() を見ていく。 ローカル変数宣言の後にswitchブロックが鎮座している。 #pre||> switch(style) { case FBYTES: if (off == 0) break; if (S_ISREG(sbp->st_mode)) { if (sbp->st_size < off) off = sbp->st_size; if (fseek(fp, off, SEEK_SET) == -1) { ierr(); return; } } else while (off--) if ((ch = getc(fp)) == EOF) { if (ferror(fp)) { ierr(); return; } break; } break; /* ... */ ||< offで指定されたオフセット処理を行うが、caseブロックにより、styleの値に応じてパターンを分けている。 例えば上のFBYTESの場合は、ファイル先頭からのオフセットかつバイト単位になるので、 if (off == 0) break; →まずoffが0の場合はそもそもオフセット処理は無用。 if (S_ISREG(sbp->st_mode)) { /* ... */ if (fseek(fp, off, SEEK_SET) /* ... */ 通常ファイル(S_ISREG()マクロについては"sys/stat.h"参照)であればfseek(3)にてオフセット位置まで移動。 } else while (off--) if ((ch = getc(fp)) == EOF) { 通常ファイル以外なら、getc(3)を使ってオフセット位置まで読み飛ばす。 FLINES, RBYTES の場合も大同小異で、通常ファイルとそれ以外に分けた上で、fseek(3)やgetc(3)でオフセット位置までの移動を実現している。 RLINESの場合だけ特殊なので、詳しく見てみる。 #pre||> case RLINES: if (S_ISREG(sbp->st_mode)) { if (!off) { if (fseek(fp, 0L, SEEK_END) == -1) { ierr(); return; } } else { if (rlines(fp, off, sbp)) return; } } else if (off == 0) { while (getc(fp) != EOF); if (ferror(fp)) { ierr(); return; } } else { if (lines(fp, off)) return; } break; ||< fseek(3), getc(3)だけで済むケースだけ見てみる。 + 通常ファイルでoffが0の場合はfseek(3)でオフセット位置決め。 + 通常ファイル以外でoffが0の場合はgetc()でEOFが出るまで読み飛ばし、つまり末尾へ移動。 それ以外では・・・ + 通常ファイルでoffが0以外の場合は rlines() でオフセット処理 + 通常ファイル以外でoffが0以外の場合は lines() でオフセット処理 lines()は read.c で定義されているが、「デーモン君のソース探検」では特に取りあげられていないので割愛する。rlines()ではmmap(2)を使っているのに対し、lines()ではmalloc()/realloc()/memset()/memmove()を使って行単位で構造体を確保し、処理している点だけメモしておく。 rlines()のソースだが、「デーモン君のソース探検」の方ではcvswebを使って過去の簡単なソースを使って解析している。 しかし落ち着いて読んでみたところNetBSD1.6の"forward.c,v 1.18"の状態でも理解できたので、メモしておく。 * forward.cのrlines()関数の探検 先に概要を書くと、ファイル末尾から10MB単位で切り出し、改行の数を数える。offで指定された数だけ改行が見つかれば、その位置までfseek(3)でファイルポインタを進める。もし切り出した10MBの中に改行が足りない時は、さらにその前の10MBを切り出して調べていく。 #pre||> static int rlines(fp, off, sbp) FILE *fp; long off; struct stat *sbp; { off_t file_size; off_t file_remaining; char *p; char *start; off_t mmap_size; off_t mmap_offset; off_t mmap_remaining; #define MMAP_MAXSIZE (10 * 1024 * 1024) if (!(file_size = sbp->st_size)) return (0); file_remaining = file_size; ||< MMAP_MAXSIZEというのが、mmap(2)で切り出す最大サイズを定義していて、ここでは10MBになっている。その後、"file_remaining"を"file_size"、つまりファイルのサイズと同じにしている。 続いて、MMAP_MAXSIZEを境目として、最初にmmap(2)で切り出す時のサイズとオフセットを調整している。 if (file_remaining > MMAP_MAXSIZE) { /* MMAP_MAXSIZEより大きいファイルの場合は、 ファイルの末尾からMMAP_MAXSIZE分切り出す */ mmap_size = MMAP_MAXSIZE; mmap_offset = file_remaining - MMAP_MAXSIZE; } else { /* MMAP_MAXSIZEより小さい場合は、mmap_offset = 0にすることで ファイル全体を切り出してしまう。 */ mmap_size = file_remaining; mmap_offset = 0; } whileブロックが続く。ループ条件が"off"、つまりオフセット変数になっていることから、これが改行の数をカウントする部分。先にループ脱出条件に関連するコードを見てみる。 #pre||> while (off) { /* ... mmap, 改行検出処理, offのデクリメント ... */ file_remaining -= mmap_size - mmap_remaining; if (off == 0) break; if (file_remaining == 0) break; /* ... */ } ||< 改行が検出されるたびにoffがデクリメントされていく。0になればループを抜ける。また、file_remainingはファイルの残りサイズを表すことになるが、それが0 = 逆から調べていき先頭まで到達した場合もループを抜ける。 ではwhileの中身を見ていく。まずmmap(2)で、while()の前に調整していたmmap_offsetからmmap_size分だけメモリにマッピングし、先頭アドレスをstartに格納する。 start = mmap(NULL, (size_t)mmap_size, PROT_READ, MAP_FILE|MAP_SHARED, fileno(fp), mmap_offset); if (start == MAP_FAILED) { err(0, "%s: %s", fname, strerror(EFBIG)); return (1); } 続いて、確保した領域の末尾から逆方向へ1文字ずつ読み進め、改行コードの数をカウントしていく。末尾一文字は改行でもそれ以外でも無視することにしているらしく、for分の初期値で "-1" されている。 mmap_remaining = mmap_size; /* Last char is special, ignore whether newline or not. */ for (p = start + mmap_remaining - 1 ; --mmap_remaining ; ) if (*--p == '\n' && !--off) { ++p; break; } mmap_remainingというのが、逆方向から読み進めた時の、メモリ上での残りサイズになっている。ということは メモリ上で逆方向から読み終えたバイト数 = mmap_size - mmap_remaining となり、 file_remaining -= mmap_size - mmap_remaining; これにより file_remaining = mmap(2)で切り出した分を計算に入れた上で、 まだ読み終えていないファイルの残りサイズ となる。 この後ループを抜けるifブロックが続き(前掲, 省略)、munmap(2)でメモリのマッピングを解除する。 if (munmap(start, mmap_size)) { err(0, "%s: %s", fname, strerror(errno)); return (1); } ログファイル監視で良く使う tail -f foobar.log であれば、 off = 10 style = RLINES となるので、通常のログファイルであればwhileループの1回目で改行を10個検出し、off = 0でループを抜けてしまう。offが非常に大きかったり一行が非常に巨大なログファイルの場合は、以下のif文で次にmmap(2)する領域とオフセットを調整する。 if (mmap_offset >= MMAP_MAXSIZE) { /* mmap_offsetがまだMMAP_MAXSIZEを超えている場合は、 さらにMMAP_MAXSIZE分引く = mmap(2)する先頭をファイルの前方にずらす */ mmap_offset -= MMAP_MAXSIZE; } else { /* mmap_offsetの値がMMAP_MAXSIZEより小さい場合は、 次にmmap(2)する領域のサイズがMMAP_MAXSIZE以下になるので、 mmap_offset = 0 とし、マップするサイズもfile_remaining、 つまり未読のファイル領域全てにする。 */ mmap_offset = 0; mmap_size = file_remaining; } ループを抜けた段階での各種変数の関係は以下のようになる。なお、以下はファイルサイズが数十MBで、なおかつ末尾の10MBブロックだけでは改行を必要な数だけ検出出来ず、さらに前方の10MBブロックでようやく検出してwhileループを抜けた状態をイメージしている。 [file] - file offset = 0 ----------------------- | ^ (...) | | [mmaped space] | +-> mmap_offset -+-> start | | ^ | ^ [file_remaining] | | | | | | | | [mmap_remaining] | | [10MB = | | | | mmap_size] '\n' v v | | +------> p ------------- | | | | v | +----------------+ | (...) | - file_offse = file_size この段階で、「メモリ上は」オフセット位置までずらせた。後は mmap_size - mmap_remaining で、メモリ上のpから、mmap(2)したブロックの末尾までのサイズになり、これをWRマクロで出力する。 WR(p, mmap_size - mmap_remaining); WRマクロはextern.hにて定義されている。 #define WR(p, size) \ if (write(STDOUT_FILENO, p, size) != size) \ oerr(); そしてファイルポインタ側のオフセットを計算する。既に"mmap_size - mmap_remaining"分のデータはWRで出力したので、その分だけfile_remainingに加算すれば良い。 file_remaining += mmap_size - mmap_remaining; whileループを抜けるbreakは、munmap()を処理していない段階で抜けてしまう。そのためmunmap()をこちらで呼び、メモリマッピングを解放する。 if (munmap(start, mmap_size)) { err(0, "%s: %s", fname, strerror(errno)); return (1); } 最後にfseeko(3)で、ファイルポインタ側のオフセット位置を設定する。 if (fseeko(fp, file_remaining, SEEK_SET) == -1) { ierr(); return (1); } 以上でrlines()のソースは読み解けた。なお、上で示した例のように、数十MBを超えるファイルで改行数や一行のサイズが巨大だった場合、rlines()を抜けた段階では最後にmmap(2)したエリアまでしか出力していないことになる: [file] - file offset = 0 ----------------------- | ^ (...) | | [mmaped space] | +-> mmap_offset -+-> start | | ^ | ^ [file_remaining] | | | | | | | | [mmap_remaining] | | [10MB = | | | | mmap_size] '\n' v | | | +------> p ------------|----+ | | | | |<- この領域がWR()されている。 | v | v | +----------------+--------+-------------+<- ファイルポインタはココにfseeko(3)される | | (...) |<- 【領域A】この領域はWR()されていない! | | - file_offse = file_size -+ 【領域A】は、forward()関数のソースに戻り読み進めていくとちゃんと出力されることが分かる。 * forward.cのforward()関数の探検(再開) forward()関数のソースへ戻る。オフセット位置を調整するswitchブロックに続くのは、forループになる。 #pre||> for (;;) { while ((ch = getc(fp)) != EOF) { if (putchar(ch) == EOF) oerr(); } if (ferror(fp)) { ierr(); return; } (void)fflush(stdout); if (!fflag) break; ||< 最初のwhile()で、【領域A】の分が出力される事になる。また"-f"or"-F"が指定されていない(= fflagが0)の場合はこれでforループを抜け、処理を終了している。 "-f"又は"-F"が指定されている場合は、引き続きループ内の処理を続行する。 まず"-f"が指定された場合は fflag = 1 となるため、以下のifブロックに進む。 #pre||> second.tv_sec = 1; second.tv_usec = 0; if (select(0, NULL, NULL, NULL, &second) == -1) err(1, "select: %s", strerror(errno)); clearerr(fp); if (fflag == 1) continue; ||< コメントにも書かれているが、select(2)で1秒間waitを入れた後、continueする。 "-F"が指定された場合は fflag = 2 となっているので、continueせずに下の処理に進む。 #pre||> if (dostat > 0) { dostat -= 1; } else { dostat = 5; if (stat(fname, &statbuf) == 0) { if (statbuf.st_dev != lastdev || statbuf.st_ino != lastino || statbuf.st_size < lastsize) { /* ファイル名変更やinodeが変わった場合、 fclose()した後もう一度fopen()しなおす処理 */ } else { lastsize = statbuf.st_size; } } } ||< これにより、"-F"オプションによる「5秒間隔でチェック→ファイル名変更/inode変更に応じたファイルの再読込」が実現されている。 以上でtail(1)での"-f"オプションの挙動を解析出来た。 今回のお題については、ここまで。 #navi_footer|C言語系|