お題:open(2)でO_APPENDが指定されたファイル記述子に対してwrite(2)するとき、ファイルポジションが自動的に末尾に設定する箇所を特定せよ
※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。
open(2)でフラグにO_APPENDを組み合わせると、write(2)時に自動的にファイルオフセットが末尾に移動する。ファイル書き込みを追記モードで行いたいときに使う。
では、O_APPENDの有無に応じたファイルオフセットの移動はどこで行われているのか?今回はそれについて追ってみたい。
ソースの探索を始める。
まずwrite(2)の手がかりをつかむために、適当に"/usr/include/sys"をgrepしてみたところ syscall.h で以下の定義を見つけた。
/* syscall: "write" ret: "ssize_t" args: "int" "const void *" "size_t" */ #define SYS_write 4
ではSYS_writeがカーネル内部ではどうマッピングされているのか?
適当にgrepで漁ってみると、どうやらinit_sysent.cの中でのstruct sysent構造体の配列でマッピングされているようだ。
/usr/src/sys/kern/init_sysent.c:
struct sysent sysent[] = {
{ 0, 0, 0,
sys_nosys }, /* 0 = syscall (indir) */
{ 1, s(struct sys_exit_args), 0,
sys_exit }, /* 1 = exit */
{ 0, 0, 0,
sys_fork }, /* 2 = fork */
{ 3, s(struct sys_read_args), 0,
sys_read }, /* 3 = read */
{ 3, s(struct sys_write_args), 0,
sys_write }, /* 4 = write */
...
struct sysentの定義は sys/systm.h で見つかった。
typedef int sy_call_t(struct proc *, void *, register_t *);
extern struct sysent { /* system call table */
short sy_narg; /* number of args */
short sy_argsize; /* total size of arguments */
int sy_flags; /* flags. see below */
sy_call_t *sy_call; /* implementing function */
} sysent[];
各システムコールの引数は、sys/syscallargs.h内で構造体として定義されているようだ。
#define syscallarg(x) \
union { \
register_t pad; \
struct { x datum; } le; \
struct { /* LINTED zero array dimension */ \
int8_t pad[ /* CONSTCOND */ \
(sizeof (register_t) < sizeof (x)) \
? 0 \
: sizeof (register_t) - sizeof (x)]; \
x datum; \
} be; \
}
...
struct sys_write_args {
syscallarg(int) fd;
syscallarg(const void *) buf;
syscallarg(size_t) nbyte;
};
流れを戻すと、init_sysent.c内で sys_write というシンボルを参照している。
適当にgrepしてみると、sys_generic.cといういかにもなソースコード中に定義されていた。
sys_generic.c:
/*
* Write system call
*/
int
sys_write(struct proc *p, void *v, register_t *retval)
{
struct sys_write_args /* {
syscallarg(int) fd;
syscallarg(const void *) buf;
syscallarg(size_t) nbyte;
} */ *uap = v;
int fd;
struct file *fp;
struct filedesc *fdp;
fd = SCARG(uap, fd);
fdp = p->p_fd;
if ((fp = fd_getfile(fdp, fd)) == NULL)
return (EBADF);
if ((fp->f_flag & FWRITE) == 0)
return (EBADF);
FILE_USE(fp);
/* dofilewrite() will unuse the descriptor for us */
return (dofilewrite(p, fd, fp, SCARG(uap, buf), SCARG(uap, nbyte),
&fp->f_offset, FOF_UPDATE_OFFSET, retval));
}
struct proc, file, filedescと見慣れない構造体が出てきているので、確認してみる。
プロセス情報の構造体である。メンバ数が多いので、今回のお題に関連するメンバのみメモしておく。
sys/proc.h:
/*
* Description of a process.
*
* This structure contains the information needed to manage a thread of
* control, known in UN*X as a process; it has references to substructures
* containing descriptions of things that the process uses, but may share
* with related processes. The process structure and the substructures
* are always addressible except for those marked "(PROC ONLY)" below,
* which might be addressible only on a processor on which the process
* is running.
*/
struct proc {
...
struct filedesc *p_fd; /* Ptr to open files structure */
...
ファイル記述子を管理するための構造体。sys/filedesc.h で定義されている。
/*
* This structure is used for the management of descriptors. It may be
* shared by multiple processes.
*
* A process is initially started out with NDFILE descriptors stored within
* this structure, selected to be enough for typical applications based on
* the historical limit of 20 open files (and the usage of descriptors by
* shells). If these descriptors are exhausted, a larger descriptor table
* may be allocated, up to a process' resource limit; the internal arrays
* are then unused. The initial expansion is set to NDEXTENT; each time
* it runs out, it is doubled until the resource limit is reached. NDEXTENT
* should be selected to be the biggest multiple of OFILESIZE (see below)
* that will fit in a power-of-two sized piece of memory.
*/
#define NDFILE 20
#define NDEXTENT 50 /* 250 bytes in 256-byte alloc */
struct filedesc {
struct file **fd_ofiles; /* file structures for open files */
char *fd_ofileflags; /* per-process open file flags */
int fd_nfiles; /* number of open files allocated */
int fd_lastfile; /* high-water mark of fd_ofiles */
int fd_freefile; /* approx. next free file */
int fd_refcnt; /* reference count */
};
カーネルが使うファイル記述子で、vnodeあるいはsocketのエントリを表す。sys/file.hで定義されている。
/*
* Kernel descriptor table.
* One entry for each open kernel vnode and socket.
*/
struct file {
LIST_ENTRY(file) f_list; /* list of active files */
int f_flag; /* see fcntl.h */
int f_iflags; /* internal flags */
#define DTYPE_VNODE 1 /* file */
#define DTYPE_SOCKET 2 /* communications endpoint */
#define DTYPE_PIPE 3 /* pipe */
int f_type; /* descriptor type */
u_int f_count; /* reference count */
u_int f_msgcount; /* references from message queue */
int f_usecount; /* number active users */
struct ucred *f_cred; /* creds associated with descriptor */
struct fileops {
int (*fo_read) (struct file *fp, off_t *offset,
struct uio *uio,
struct ucred *cred, int flags);
int (*fo_write) (struct file *fp, off_t *offset,
struct uio *uio,
struct ucred *cred, int flags);
int (*fo_ioctl) (struct file *fp, u_long com,
caddr_t data, struct proc *p);
int (*fo_fcntl) (struct file *fp, u_int com,
caddr_t data, struct proc *p);
int (*fo_poll) (struct file *fp, int events,
struct proc *p);
int (*fo_stat) (struct file *fp, struct stat *sp,
struct proc *p);
int (*fo_close) (struct file *fp, struct proc *p);
} *f_ops;
off_t f_offset;
caddr_t f_data; /* descriptor data, e.g. vnode/socket */
};
...
#define FILE_USE(fp) \
do { \
(fp)->f_usecount++; \
FILE_USE_CHECK((fp), "f_usecount overflow"); \
} while (/* CONSTCOND */ 0)
#define FILE_UNUSE(fp, p) \
do { \
if ((fp)->f_iflags & FIF_WANTCLOSE) { \
/* Will drop usecount */ \
(void) closef((fp), (p)); \
} else { \
(fp)->f_usecount--; \
FILE_USE_CHECK((fp), "f_usecount underflow"); \
} \
} while (/* CONSTCOND */ 0)
/*
* Flags for fo_read and fo_write.
*/
#define FOF_UPDATE_OFFSET 0x01 /* update the file offset */
sys_write()に目を戻すと、fd_getfile()という関数が気になる。おそらく、プロセス情報(struct proc) → プロセスのファイル記述子テーブル(struct filedesc) → カーネルのファイル記述子(struct file) という順番で辿る関数だろうが、念のため目を通しておく。
src/sys/kern/kern_descrip.c というファイルに定義されている。
struct file *
fd_getfile(struct filedesc *fdp, int fd)
{
struct file *fp;
if ((u_int) fd >= fdp->fd_nfiles || (fp = fdp->fd_ofiles[fd]) == NULL)
return (NULL);
if (FILE_IS_USABLE(fp) == 0)
return (NULL);
return (fp);
}
ちなみに、この下にはsys_dup()やsys_dup2()などが定義されていて興味深い。
これでsys_write()を読み解くのに必要な情報は集まったので、コメントによる解説付きで再掲する。
sys_generic.c:
/*
* Write system call
*/
int
sys_write(struct proc *p, void *v, register_t *retval)
{
struct sys_write_args /* {
syscallarg(int) fd;
syscallarg(const void *) buf;
syscallarg(size_t) nbyte;
} */ *uap = v;
int fd;
struct file *fp;
struct filedesc *fdp;
/* システムコールの引数からファイル記述子の番号を取得 */
fd = SCARG(uap, fd);
/* プロセス情報からファイル記述子の管理テーブル(struct filedesc)を取得 */
fdp = p->p_fd;
/* カーネル内のファイル記述情報(struct file)を取得 */
if ((fp = fd_getfile(fdp, fd)) == NULL)
return (EBADF);
if ((fp->f_flag & FWRITE) == 0)
return (EBADF);
FILE_USE(fp);
/* dofilewrite() will unuse the descriptor for us */
return (dofilewrite(p, fd, fp, SCARG(uap, buf), SCARG(uap, nbyte),
&fp->f_offset, FOF_UPDATE_OFFSET, retval));
}
実際の書き込み処理は、dofilewrite()内で行われていることが分かった。
まずdofilewrite()の引数を確認する。
int
dofilewrite(
struct proc *p, /* プロセス情報 */
int fd, /* システムコールの引数:ファイル記述子番号 */
struct file *fp, /* fdに対応するカーネルのファイル記述情報 */
const void *buf, /* システムコールの引数:バッファ */
size_t nbyte, /* システムコールの引数:バッファサイズ */
off_t *offset, /* fp->f_offsetへのポインタ */
int flags, /* FOF_UPDATE_OFFSET */
register_t *retval)
{
off_t *offset
がおそらくファイルのoffsetに相当すると思われる。sys_write()からは
&fp->f_offset
が渡されている。
念のため、ファイルoffsetを変更するシステムコールの代表であるlseek()のソースを確認する。
grepすると、vfs_syscalls.c 内でsys_lseek() が定義されているのが見つかる。
ざっくり読んでみると、最終的に
*(off_t *)retval = fp->f_offset = newoff;
として、カーネルのファイル情報(struct file)のf_offsetメンバに新しいoffsetを設定している。
このことから、f_offsetメンバがファイルのoffsetであると考えてほぼ間違いないと思われる。
ではdofilewriteに戻り、KTRACEの"#ifdef"を除去したソースを載せる。
int
dofilewrite(struct proc *p, int fd, struct file *fp, const void *buf,
size_t nbyte, off_t *offset, int flags, register_t *retval)
{
struct uio auio;
struct iovec aiov;
long cnt, error;
error = 0;
aiov.iov_base = (caddr_t)buf; /* XXX kills const */
aiov.iov_len = nbyte;
auio.uio_iov = &aiov;
auio.uio_iovcnt = 1;
auio.uio_resid = nbyte;
auio.uio_rw = UIO_WRITE;
auio.uio_segflg = UIO_USERSPACE;
auio.uio_procp = p;
/*
* Writes return ssize_t because -1 is returned on error. Therefore
* we must restrict the length to SSIZE_MAX to avoid garbage return
* values.
*/
if (auio.uio_resid > SSIZE_MAX) {
error = EINVAL;
goto out;
}
cnt = auio.uio_resid;
error = (*fp->f_ops->fo_write)(fp, offset, &auio, fp->f_cred, flags);
if (error) {
if (auio.uio_resid != cnt && (error == ERESTART ||
error == EINTR || error == EWOULDBLOCK))
error = 0;
if (error == EPIPE)
psignal(p, SIGPIPE);
}
cnt -= auio.uio_resid;
*retval = cnt;
out:
FILE_UNUSE(fp, p);
return (error);
}
処理内容は斜め読みで、雰囲気で分かるが、struct uioというのが登場しているので定義を確認しておく。
struct iovec については readv()/writev() のmanpageを参照。
enum uio_rw { UIO_READ, UIO_WRITE };
/* Segment flag values. */
enum uio_seg {
UIO_USERSPACE, /* from user data space */
UIO_SYSSPACE /* from system space */
};
struct uio {
struct iovec *uio_iov; /* pointer to array of iovecs */
int uio_iovcnt; /* number of iovecs in array */
off_t uio_offset; /* offset into file this uio corresponds to */
size_t uio_resid; /* residual i/o count */
enum uio_seg uio_segflg; /* see above */
enum uio_rw uio_rw; /* see above */
struct proc *uio_procp;/* process if UIO_USERSPACE */
};
dofilewrite()に戻り、ポイントとなる箇所だけ見ていく。
/* バッファを struct iovec の形で渡す */
aiov.iov_base = (caddr_t)buf; /* XXX kills const */
aiov.iov_len = nbyte;
auio.uio_iov = &aiov;
auio.uio_iovcnt = 1;
/* residual(残余)カウンタをシステムコール引数:バッファサイズに初期化 */
auio.uio_resid = nbyte;
auio.uio_rw = UIO_WRITE;
auio.uio_segflg = UIO_USERSPACE;
auio.uio_procp = p;
/* cnt = fo_write前の未writeバッファサイズ */
cnt = auio.uio_resid;
error = (*fp->f_ops->fo_write)(fp, offset, &auio, fp->f_cred, flags);
if (error) {
/* 省略 */
}
/* fo_write後の未writeバッファサイズをマイナス
=> fo_writeで書き込まれたバッファサイズ */
cnt -= auio.uio_resid;
*retval = cnt;
実際の書き込み処理は、ファイルシステムごとのfo_write()に委譲される。
error = (*fp->f_ops->fo_write)(
fp, /* カーネルのファイル記述情報(struct file) */
offset, /* fp->f_offsetのポインタ */
&auio, /* struct uio */
fp->f_cred, /* struct ucred, sys/ucred.h 参照 */
flags /* FOF_UPDATE_OFFSET(sys_write()から引継ぎ) */
);
fo_writeに設定されるシンボルを探す必要があるが、ファイルシステムやマウントポイントが絡むため、お行儀よくそれらのアーキテクチャについて調査するといつまでも終わらない。
強引だが、お題目の趣旨からO_APPENDフラグを使っている箇所をgrepで検索してみる。
すると vfs_vnops.c 内でvn_write()という関数が見つかった。引数もfo_write()の呼び出しと一致するので、とりあえずこれが呼ばれるものと仮定して、ソースを読んでみる。
/*
* File table vnode write routine.
*/
int
vn_write(fp, offset, uio, cred, flags)
struct file *fp;
off_t *offset;
struct uio *uio;
struct ucred *cred;
int flags;
{
struct vnode *vp = (struct vnode *)fp->f_data;
int count, error, ioflag = IO_UNIT;
/* ようやく O_APPEND 登場 */
if (vp->v_type == VREG && (fp->f_flag & O_APPEND))
ioflag |= IO_APPEND;
if (fp->f_flag & FNONBLOCK)
ioflag |= IO_NDELAY;
if (fp->f_flag & FFSYNC ||
(vp->v_mount && (vp->v_mount->mnt_flag & MNT_SYNCHRONOUS)))
ioflag |= IO_SYNC;
else if (fp->f_flag & FDSYNC)
ioflag |= IO_DSYNC;
if (fp->f_flag & FALTIO)
ioflag |= IO_ALTSEMANTICS;
VOP_LEASE(vp, uio->uio_procp, cred, LEASE_WRITE);
vn_lock(vp, LK_EXCLUSIVE | LK_RETRY);
/* ロックする迄待ってから、最新のファイルoffsetをuio_offsetにコピー */
uio->uio_offset = *offset;
count = uio->uio_resid;
error = VOP_WRITE(vp, uio, ioflag, cred);
if (flags & FOF_UPDATE_OFFSET) {
if (ioflag & IO_APPEND)
/* !!!!!!!!!!!!!!!!!!!!!!!! */
*offset = uio->uio_offset;
else
*offset += count - uio->uio_resid;
}
VOP_UNLOCK(vp, 0);
return (error);
}
ようやくそれらしきコードが見つかった。O_APPENDが設定されていると、VOP_WRITE()の後に、カーネルのファイル記述情報(struct file)のoffsetを struct uio の uio_offset で更新している。
ここでまた struct vnode や VOP_WRITE マクロが登場しているので、念のため確認しておく。
/* * The vnode is the focus of all file activity in UNIX. There is a * unique vnode allocated for each active file, each current directory, * each mounted-on file, text file, and the root. */
/*
* Reading or writing any of these items requires holding the appropriate lock.
* v_freelist is locked by the global vnode_free_list simple lock.
* v_mntvnodes is locked by the global mntvnodes simple lock.
* v_flag, v_usecount, v_holdcount and v_writecount are
* locked by the v_interlock simple lock
*/
struct vnode {
struct uvm_object v_uobj; /* the VM object */
#define v_usecount v_uobj.uo_refs
#define v_interlock v_uobj.vmobjlock
voff_t v_size; /* size of file */
int v_flag; /* flags */
int v_numoutput; /* number of pending writes */
long v_writecount; /* reference count of writers */
long v_holdcnt; /* page & buffer references */
u_long v_id; /* capability identifier */
struct mount *v_mount; /* ptr to vfs we are in */
int (**v_op) __P((void *)); /* vnode operations vector */
TAILQ_ENTRY(vnode) v_freelist; /* vnode freelist */
LIST_ENTRY(vnode) v_mntvnodes; /* vnodes for mount point */
struct buflists v_cleanblkhd; /* clean blocklist head */
struct buflists v_dirtyblkhd; /* dirty blocklist head */
LIST_ENTRY(vnode) v_synclist; /* vnodes with dirty buffers */
union {
struct mount *vu_mountedhere;/* ptr to mounted vfs (VDIR) */
struct socket *vu_socket; /* unix ipc (VSOCK) */
struct specinfo *vu_specinfo; /* device (VCHR, VBLK) */
struct fifoinfo *vu_fifoinfo; /* fifo (VFIFO) */
} v_un;
struct nqlease *v_lease; /* Soft reference to lease */
enum vtype v_type; /* vnode type */
enum vtagtype v_tag; /* type of underlying data */
struct lock v_lock; /* lock for this vnode */
struct lock *v_vnlock; /* pointer to lock */
void *v_data; /* private data for fs */
};
struct vop_write_args {
const struct vnodeop_desc *a_desc;
struct vnode *a_vp;
struct uio *a_uio;
int a_ioflag;
struct ucred *a_cred;
};
extern const struct vnodeop_desc vop_write_desc;
int VOP_WRITE(struct vnode *, struct uio *, int, struct ucred *);
#ifndef VNODE_OP_NOINLINE
static __inline int VOP_WRITE(vp, uio, ioflag, cred)
struct vnode *vp;
struct uio *uio;
int ioflag;
struct ucred *cred;
{
struct vop_write_args a;
a.a_desc = VDESC(vop_write);
a.a_vp = vp;
a.a_uio = uio;
a.a_ioflag = ioflag;
a.a_cred = cred;
return (VCALL(vp, VOFFSET(vop_write), &a));
}
#endif
これ以上はファイルシステムやデバイスドライバの領域になるため、今回はここまでにしておく。
もしさらに調べるのであれば、たとえば
src/sys/XXYYfs/
などファイルシステムのソースツリー内でO_APPENDをgrepしてみると面白いだろう。
$ cd /usr/src/sys/ufs $ grep -r O_APPEND * ext2fs/ext2fs_readwrite.c: if (ioflag & IO_APPEND) ext2fs/ext2fs_vnops.c: (ap->a_mode & (FWRITE | O_APPEND)) == FWRITE) ufs/ufs_readwrite.c: if (ioflag & IO_APPEND) ufs/ufs_vnops.c: (ap->a_mode & (FWRITE | O_APPEND)) == FWRITE)
かなり肉薄しつつあるのがgrep結果だけからも見て取れる。
しかしvnodeがファイルシステム、ひいてはデバイスドライバを隠蔽するインターフェイスとして存在する以上は、vnodeまでで探索を止めておくのが無難だろう。
open(2)でO_APPENDが指定されたファイル記述子に対してwrite(2)するとき、ファイルポジションが自動的に末尾に設定する箇所を特定することが出来た。
writeシステムコールは src/sys/kern/init_sysent.c 内で sys_write シンボルに結び付けられ、writeシステムコールは以下の流れでvn_write()へたどり着く。
sys_write() : src/sys/kern/sys_generic.c
→ dofilewrite() : src/sys/kern/sys_generic.c
→ vn_write() : src/sys/kern/vfs_vnops.c
vn_write()内でvnodeインターフェイスを使って実際のファイルシステム・デバイスドライバに書き込み処理を委譲した後、O_APPENDフラグに応じてカーネルのファイル記述情報(struct file)のoffsetを更新している。これがすなわち、O_APPENDフラグが指定された時に、write()が自動的にoffsetをファイル末尾に移動する箇所に相当している。
今回のお題については、ここまで。