お題:getchar(3)がバッファを使うことでファイルアクセス回数を少なくしている、その仕組みを追跡せよ。
まずgetchar()のソースを特定する。
$ locate getchar ... /usr/src/lib/libc/stdio/getchar.c ...
→
int getchar() { FILE *fp = stdin; int r; FLOCKFILE(fp); r = __sgetc(fp); FUNLOCKFILE(fp); return r; }
FLOCKFILE/FUNLOCKFILEは本によるとマルチスレッド環境におけるファイルロックに関連しているので今はスルーする。
中心部分は "r = __sgetc(fp);"部分。まず "__sgetc()"がマクロかどうか判別する為、getchar.cのオブジェクトファイルgetchar.oのシンボル一覧に"__sgetc"があるか確認する。
$ nm /usr/lib/libc.a | less getchar.o: U __sF U __srget 00000000 T getchar 00000038 T getchar_unlocked
このように、"__sgetc"がシンボル一覧に無い。ということは、これは関数として定義されているのではなく、"#define"によるマクロ定義であることが予想される。そこで、"/usr/src/include/"の下を愚直にgrepしてみる。
$ cd /usr/src/include $ grep -r sgetc * stdio.h:#define __sgetc(p) (--(p)->_r < 0 ? __srget(p) : (int)(*(p)->_p++)) stdio.h:#define getc(fp) __sgetc(fp) stdio.h:#define getc_unlocked(fp) __sgetc(fp)
stdio.h中で定義されていることが分かった。
__sgetc(p) = --(p)->_r < 0 ? __srget(p) : (int)(*(p)->_p++))
ifブロックで分かりやすく整形する:
if ( --(p)->_r < 0 ) { __srget(p) } else { (int)(*(p)->_p++)) }
"--"や"++"演算子の優先順位を確認。
man operator
→次のように整形出来る:
--(p->_r); if ((p->_r) < 0) { return __srget(p); } else { (p->_p)++; return (int)(*(p->_p)); }
ここで"p"は__sgetc(p)のpで、すなわちFILE構造体へのポインタ。
/usr/src/lib/libc/stdio/getchar.cを見直してみる:
getchar() { FILE *fp = stdin; /* ... */ r = __sgetc(fp);
FILE構造体はstdio.h中で定義されている。
/* ... */ /* stdio buffers */ struct __sbuf { unsigned char *_base; int _size; }; /* ... */ typedef struct __sFILE { unsigned char *_p; /* current position in (some) buffer */ int _r; /* read space left for getc() */ int _w; /* write space left for putc() */ short _flags; /* flags, below; this FILE is free if 0 */ short _file; /* fileno, if Unix descriptor, else -1 */ struct __sbuf _bf; /* the buffer (at least 1 byte, if !NULL) */ int _lbfsize; /* 0 or -_bf._size, for inline putc */ /* operations */ void *_cookie; /* cookie passed to io functions */ int (*_close) __P((void *)); int (*_read) __P((void *, char *, int)); fpos_t (*_seek) __P((void *, fpos_t, int)); int (*_write) __P((void *, const char *, int)); /* ... */ } FILE; /* ... */
"__sgetc()"のマクロに戻れば、ここまでで処理内容が以下のように判明した。
--(p->_r); /* 1文字取得するので、読み込みバッファのサイズをデクリメント */ if ((p->_r) < 0) { /* マイナスになってしまったら、読み込み処理へ */ return __srget(p); } else { (p->_p)++; /* バッファの現在位置をインクリメント */ return (int)(*(p->_p)); /* 上でインクリメントした位置の値を返す */ }
"__srget(p)"か関数なのか、マクロなのかはlibc.aのシンボル一覧から判別可能。
$ nm /usr/lib/libc.a | less ... rget.o: U __srefill 00000000 T __srget
"rget.o"で定義、つまりrget.c中で関数定義されていることが予想される。
$ locate rget.c ... /usr/src/lib/libc/stdio/rget.c ...
ということで /usr/src/lib/libc/stdio/rget.c を覗いてみると、短いコードと分かりやすい説明コメント:
/* * Handle getc() when the buffer ran out: * Refill, then return the first character * in the newly-filled buffer. */ int __srget(fp) FILE *fp; { _DIAGASSERT(fp != NULL); _SET_ORIENTATION(fp, -1); if (__srefill(fp) == 0) { fp->_r--; return (*fp->_p++); } return (EOF); }
"_SET_ORIENTATION(fp, -1)" というのは "/usr/src/lib/libc/stdio/wcio.h" 中で定義されていて、wchar関連のフラグを操作しているらしい。今回は関連が薄そうなので、スルーする。
あとは見たままで、"__srefill(fp)"でバッファを埋め直し、戻り値が0であれば "_r"メンバを(1文字読むので)1デクリメントし、"_p"ポインタをインクリメントした上でその場所のデータを返している。
"__srefill()"は、libc.aのシンボル一覧を見ると refill.c にある。
refill.o: ... 00000028 T __srefill ...
→
$ locate refill.c /usr/src/lib/libc/stdio/refill.c
→ "/usr/src/lib/libc/stdio/refill.c"を見てみる。"__srefill(fp)"の主要部分だけ抜き出す。
/* * Refill a stdio buffer. * Return EOF on eof or error, 0 otherwise. */ int __srefill(fp) FILE *fp; { /* ... */ fp->_r = 0; /* largely a convenience for callers */ /* ... */ if (fp->_bf._base == NULL) __smakebuf(fp); /* ... */ fp->_p = fp->_bf._base; fp->_r = (*fp->_read)(fp->_cookie, (char *)fp->_p, fp->_bf._size); if (fp->_r <= 0) { /* ... */ return (EOF); } return (0); }
"__smakebuf(fp)"は " /usr/src/lib/libc/stdio/makebuf.c" 中で定義されており、適切なバッファサイズを計算・malloc()で確保する。FILE構造体の"_bf"メンバは"__sbuf"構造体であり、"_base"メンバはバッファへのポインタ、"_size"メンバはバッファのサイズとなる。
よって以下のコードで、"__smakebuf(fp)"で確保されたバッファのアドレスが "_p" メンバにコピーされたことになる。
fp->_p = fp->_bf._base;
次のコードだが、FILE構造体の"_read"メンバに入っている関数ポインタを経由し、バッファのアドレスとバッファサイズを引数に渡して実際の読み込み関数を呼び出している。
fp->_r = (*fp->_read)(fp->_cookie, (char *)fp->_p, fp->_bf._size);
FILE構造体の"_read"メンバに何が入るのかは、fopen(3)を調べてみるとよい。
$ locate fopen /usr/src/lib/libc/stdio/fopen.c
→
FILE * fopen(file, mode) const char *file; const char *mode; { FILE *fp; int f; /* ... */ if ((f = open(file, oflags, DEFFILEMODE)) < 0) goto release; /* ... */ fp->_file = f; /* ... */ fp->_cookie = fp; fp->_read = __sread; /* ... */ }
"_file"メンバにはopen(2)で取得したファイル記述子が、"_cookie"メンバにはFILE構造体へのポインタ自身が入るようだ。
"__sread"をlibc.aのシンボル一覧から調べてみる(関数ポインタに代入されている以上は、マクロではあり得ない筈)。
$ nm /usr/lib/libc.a | less ... stdio.o: ... 00000000 T __sread ...
→
$ locate stdio.c ... /usr/src/lib/libc/stdio/stdio.c
→
/* * Small standard I/O/seek/close functions. * These maintain the `known seek offset' for seek optimisation. */ int __sread(cookie, buf, n) void *cookie; char *buf; int n; { FILE *fp = cookie; int ret; /* ... */ ret = read(fp->_file, buf, (size_t)n); /* ... */ }
これでようやく、read(2)システムコールまで到達し、バッファが足りない場合にread(2)でデータを取得している事が判明した。
getchar()あるいはgetc()による読み込みでのバッファ周りをまとめ直してみる。
A. 読み込み済のバッファが充分ある場合 → /usr/src/include/stdio.h:"__sgetc()"マクロまでで完結。 B. 読み込み済のバッファが空になった場合 → /usr/src/include/stdio.h:"__sgetc()"マクロ → /usr/src/lib/libc/stdio/rget.c:"__srget()"関数 → /usr/src/lib/libc/stdio/refill.c:"__srefill()"関数 → FILE構造体の"_read"メンバ関数ポインタ経由 → /usr/src/lib/libc/stdio/fopen.c:"fopen()"関数でセットされた"__sread"関数 → /usr/src/lib/libc/stdio/stdio.c:"__sread()"関数 → read(2)システムコール
今回のお題に対しては、ここまで。