UNIX V6のprintf()は内部で1文字ずつputchar()を呼んでいます。通常は1文字ずつwriteシステムコールが呼ばれますが、特殊な細工をするとバッファリングされるようになります。
今回はnmとputchar()のソースを取り上げます。
- http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/source/s2/nm.c
- http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/source/s5/putchr.s
ユーザーランドのソース全体は以下から入手できます。
バッファリング
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()のフローは以下のようになります。
- 初めて呼ばれた場合、バッファ初期化のためにサブルーチンflを呼ぶ。
- バッファへの書き込み。
- バッファの空き容量がなくなれば、サブルーチン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バイトのバッファリング |