読者です 読者をやめる 読者になる 読者になる

tailコマンドの実装を見る。

unix

tailって真面目に作るとどうなるんだろうということが気になったので、
ソースコードを読んでみます。ソースは NetBSD 5.0.2です。
場所は usr/src/usr.bin/tailになります。tail -fとかは考えずに
普通にファイルの終わり n行を表示する場合についてのみ考えます。


実装を見ていきます。NetBSDユーザランドがビルドできる環境で
見るといいかと思います。Macだとビルドできました。bsdmakeを使って
ください。FreeBSDでは警告でエラーとなるので(Werrorのせい)
makeを使わず gccでファイルを指定してコンパイルすればいいかと思います。
Ubuntuではそのままではビルドできません。


まずは main関数からということで、tail.cです。
はじめはオプション解析なので全部無視。ついでオプションにより
立ったフラグに関する処理ですが、そんなのも無視。はじめに
関連しそうなのは 155行目からの styleと offの設定です。フラグは
立っていないので、off=10, style=RLINESとなります。Reverse Linesで
10行ということが推測されます。

	if (style == NOTSET) {
		if (rflag) {
			off = 0;
			style = REVERSE;
		} else {
			off = 10;
			style = RLINES;
		}
	}


次に関連しそうなところを見ていきましょう。
ファイル名を引数に指定した場合の処理が 164行目からあります。
引数のファイルを一つずつ openして、fstatでファイル情報を取得して
reverseか forward関数を呼んでいます。

フラグは立っていないので今回は forword関数です。
引数は fp=FILEポインタ、style=RLINES、offset=10、ファイル情報を
格納した変数 sbのアドレスとなります。

	if (*argv)
		for (first = 1; (fname = *argv++) != NULL;) {
			if ((fp = fopen(fname, "r")) == NULL ||
			    fstat(fileno(fp), &sb)) {
				ierr();
				continue;
			}
			if (argc > 1) {
				(void)printf("%s==> %s <==\n",
				    first ? "" : "\n", fname);
				first = 0;
				(void)fflush(stdout);
			}

			if (rflag)
				reverse(fp, style, off, &sb);
			else
				forward(fp, style, off, &sb);
			(void)fclose(fp);
		}


次に forward関数を見ます。forward関数はforward.cに定義されています。
引数 styleに対する switch文がはじめにあります。今回は RLINESなので
そこだけ見ます。154行目からです。S_ISREGはレギュラーファイルなら
真を返すマクロです。ソケットなり、デバイスファイルじゃなくて普通の
ファイルを指定することを想定するので、この if文に入ります。
続いて offは 10なので elseの方に入ります。rlinesが呼ばれることが
わかります。引数は FILEポインタ、オフセット、ファイル情報のポインタです。

	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;


つづいて rlinesを見ます。同ファイルに定義があります。
279行目からです。ファイルサイズが 0でないことを確認し、
その値を file_remainingに格納しています。


続いて、mmapに関する設定を行っています。mmapを使うんだろうなって
ことがわかります。ファイルサイズが mmap可能な最大サイズ MMAP_MAXSIZEより
大きければ (ファイルサイズ - MMAP_MAXSIZE)を mmap, ファイルサイズが
MMAP_SIZEより小さければファイル全体を mmapすることになります。

static int
rlines(FILE *fp, off_t off, struct stat *sbp)
{
	off_t file_size;
	off_t file_remaining;
	char *p = NULL;
	char *start = NULL;
	off_t mmap_size;
	off_t mmap_offset;
	off_t mmap_remaining = 0;

#define MMAP_MAXSIZE  (10 * 1024 * 1024)

	if (!(file_size = sbp->st_size))
		return (0);
	file_remaining = file_size;

	if (file_remaining > MMAP_MAXSIZE) {
		mmap_size = MMAP_MAXSIZE;
		mmap_offset = file_remaining - MMAP_MAXSIZE;
	} else {
		mmap_size = file_remaining;
		mmap_offset = 0;
	}


続いてみていきましょう。メイン処理になります。offsetが真である間の
ループです。はじめに mmapしています。mmapのはじめの引数が NULLの
場合はシステムがマップアドレスを決め、それが戻り値と返ります。

	while (off) {
		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);
		}


次。mmapされている領域の残りサイズ mmap_remainingに mmap_sizeを入れて
初期化しています。次のループが本当のメイン部分です。一文字ずつ逆に見て
いってそれが改行であれば offを 1減らしています。offがゼロになったら
breakしてループを抜けています。

offが 0のとき ++pしてますが、これは改行文字の位置にあるので、次の文字から
表示させるためのものです。

終了条件は mmap_remainningが 0になることです。つまりファイル全体を
mmapしていれば、このループでファイルすべてを調べることになります。

		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;
			}


次。ファイルの残りサイズを更新しています。off == 0つまり表示させる
位置にたどり着いたか file_remaining == 0。ファイルをすべて調べたの
いずれかの場合、メインのループ処理が終了します。

		file_remaining -= mmap_size - mmap_remaining;

		if (off == 0)
			break;

		if (file_remaining == 0)
			break;


次。これ以降は繰り返し調べていくための処理となります。mmapした
領域を munmapし、続きから mmapするための設定を行っています。
ファイルサイズが MMAP_MAXSIZEより大きい場合ときのみここを通ります。

		if (munmap(start, mmap_size)) {
			err(0, "%s: %s", fname, strerror(errno));
			return (1);
		}

		if (mmap_offset >= MMAP_MAXSIZE) {
			mmap_offset -= MMAP_MAXSIZE;
		} else {
			mmap_offset = 0;
			mmap_size = file_remaining;
		}
	}


ループを抜けて結果を表示させる部分を見てみましょう。
WRマクロを呼んでいます。WRマクロは extern.hで定義されています。

	/*
	 * Output the (perhaps partial) data in this mmap'd block.
	 */
	WR(p, mmap_size - mmap_remaining);


WRの定義は以下のとおりで、単に標準出力 writeしているだけです。
pからファイルの残り部分を表示していることになります。

#define	WR(p, size) \
	if (write(STDOUT_FILENO, p, size) != size) \
		oerr();

あとは mmapした領域を unmapしたり、ストリームのオフセットを
元に戻すような処理だけなので割愛します。

これで tailの実装がわかりました。

まとめ

tailがファイルを終わりから n行表示させるのにどのようなことを
やっているかがわかりました。mmapって意味はわかっていたけど、
どういうときに使うのかがいまいちわかっていないんですが、こういう
ときにも使えるんだなということがわかりました。


Linuxというか GNUユーザランドってやたら機能拡張されていて
私のような素人が読むことは困難ですが、BSD系統はわりとシンプルなので
なんとか読むことができます。ユーザランドの仕組みが知りたいって
人は Linuxより *BSDがおすすめです。


NetBSDユーザランドは下記のページの src.tgzを展開すると得られます。
ftp://ftp2.jp.netbsd.org/pub/NetBSD/NetBSD-5.0.2/source/sets/