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

【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

putchar()とバッファリング

UNIX V6

UNIX V6のprintf()は内部で1文字ずつputchar()を呼んでいます。通常は1文字ずつwriteシステムコールが呼ばれますが、特殊な細工をするとバッファリングされるようになります。

今回はnmとputchar()のソースを取り上げます。

ユーザーランドのソース全体は以下から入手できます。

バッファリング

nmには以下のような箇所があります。nm以外でも使われており、ある種の定石と思われます。

s2/nm.c: 81行目
	fout = dup(1);
	close(1);

foutはグローバル変数ですが、当時のC言語にはexternがなかったため、別の所で定義されていれば実体がそちらを指すことがあります。foutはlibcの定義が使われます。

s5/putchr.s: 54行目
.bss
_fout:	.=.+518.

foutはputchar()の出力先を指しているため、値を書き換えると出力先を変更できます。nmの場合はコピー(dup)した標準出力を指しています。新規にファイルディスクリプタを作成するときは昇順で空きが検索されます。何もファイルを開いていなければ、標準エラー出力(2)の次の3となります。

このようにfoutを書き換えるとputchar()内でバッファリングされるようになり、1文字ずつwriteシステムコールが呼ばれることはなくなります。大量に出力するコマンドではシステムコールのオーバーヘッドを避けるためにこのような細工をするようです。

fout

foutから518バイトはputchar()のバッファとして使われます。

fout 出力先のファイルディスクリプタ
fout+2 バッファの空き容量
fout+4 バッファ内の次の文字へのポインタ
fout+6 512バイトのバッファ

フロー

putchar()のフローは以下のようになります。

  1. 初めて呼ばれた場合、バッファ初期化のためにサブルーチンflを呼ぶ。
  2. バッファへの書き込み。
  3. バッファの空き容量がなくなれば、サブルーチンflを呼んで、バッファを出力する。

サブルーチンflはflushを意味しています。バッファにデータが存在すれば出力して、バッファを初期化します。初めて呼ばれた場合はバッファにデータが存在しないため、初期化のみが行われます。

バッファ初期化

具体的にputchar()の実装を追ってみます。

s5/putchr.s: 9行目
	mov	_fout+4,r0
	bne	1f
	jsr	pc,fl
	mov	_fout+4,r0
1:

次の文字へのポインタがチェックされます。bneではmovされた値がゼロ以外のときに分岐します。foutはbssに確保されているため、初期値はゼロです。そのため最初に呼ばれたときは分岐せずにjsrでflに飛びます。jsrはサブルーチン呼び出しで、x86のcallに相当します。

s5/putchr.s: 32行目
fl:
	mov	_fout+4,r0
	beq	1f

同じように次の文字へのポインタがチェックされます。beqはbneの逆で、値がゼロのときに分岐します。

s5/putchr.s: 45行目
1:
	mov	$_fout+6,_fout+4
	mov	$512.,_fout+2

分岐先はfoutの初期化処理です。次の文字へのポインタはバッファの先頭、空き容量は512がセットされます。

s5/putchr.s: 48行目
	cmp	_fout,$2
	bhi	1f
	mov	$1,_fout+2
1:
	rts	pc

出力先がチェックされます。ここで見ている値はC言語でfoutに代入したものです。2より大きい(3以上)ならbhiで分岐します。2以下なら分岐せず、空き容量に1がセットされます。空き容量が1というのは、バッファリングされずに1文字ずつ出力されることを意味します。rts pcはサブルーチンから戻る命令で、x86のretに相当します。

バッファへの書き込み

s5/putchr.s: 12行目
	mov	_fout+4,r0
1:
	movb	4(r5),(r0)+
	beq	1f
	inc	_fout+4
	dec	_fout+2
	bgt	1f
	jsr	pc,fl
1:

r0に次の文字へのポインタがセットされます。r5はスタックポインタで、PDP-11は引数をスタックで渡すため、4(r5)はputchar()の第一引数を指します。NULL文字は出力されないため、引数がゼロならbeqで分岐して終了処理に飛びます。そうでない場合はカウンタを操作して、空き容量が残っていればbgtで分岐して終了処理に飛びます。空き容量がなければサブルーチンflを呼びます。

バッファ出力

s5/putchr.s: 32行目
fl:
	mov	_fout+4,r0
	beq	1f
	sub	$_fout+6,r0
	mov	r0,0f
	mov	_fout,r0
	bne	2f
	inc	r0
2:
	sys	0; 9f
.data
9:	sys	write; _fout+6; 0:..
.text
1:

バッファに書き込んだ後はポインタがゼロではないため、beqで分岐しません。ポインタから先頭のアドレスを引き算して文字数を求め、システムコールの引数に代入します。foutの値をチェックして、ゼロでなければbneで分岐します。ゼロのときは分岐せずにincで増やします。foutを操作しなければ値はゼロのため、標準出力(1)を指すように補正するわけです。

システムコールを呼んだ後は、最初に呼ばれたときと同じバッファ初期化処理が行われます。foutが2以下のときは空き容量が1に初期化されるため、延々と1文字ずつwriteが呼ばれる結果となります。

まとめ

fout 処理
0 1に補正される
1 標準出力・バッファリングなし
2 標準エラー出力・バッファリングなし
3以上 512バイトのバッファリング