Bytes 都去哪了?一次 OpenBSD 文件写入的旅程
Bytes 都去哪了?
或者更精确地说,它们是如何到达那里的?当你调用 _write_ 时会发生什么?
trap
libc 中的 _write_ 函数进行设置并发出系统调用,这可能会进入 _locore.s 中的某个地方,比如 _Xsyscall_meltdown_,但最终我们会在 arch/amd64/amd64/trap.c 中的 _syscall_ 中结束。或者我们可能在 arch/arm64/arm64/syscall.c 中的 _svc_handler_ 中。但此时,代码应该看起来非常相似。系统调用 ABI 的繁琐机制比有趣更有难度。
uvmexp.syscalls++;
code = frame->tf_rax;
args = (register_t *)&frame->tf_rdi;
if (code <= 0 || code >= SYS_MAXSYSCALL)
goto bad;
callp = sysent + code;
rval[0] = 0;
rval[1] = 0;
error = mi_syscall(p, code, callp, args, rval);
我们检查系统调用代码以确保它在范围内,进行一些小的统计,最后深入到与机器无关的系统调用处理程序,即 sys/syscall_mi.h 中的 _mi_syscall_。
这里有一整页的代码都在检查调试标志、跟踪点、堆栈有效性以及 pledge 和 pins。在这些过程中,bytes 实际上并没有去任何地方。
mi_syscall prolog
static inline int
mi_syscall(struct proc *p, register_t code, const struct sysent *callp,
register_t *argp, register_t retval[2])
{
uint64_t tval;
int lock = !(callp->sy_flags & SY_NOLOCK);
int error, pledged;
/* refresh the thread's cache of the process's creds */
refreshcreds(p);
#ifdef SYSCALL_DEBUG
KERNEL_LOCK();
scdebug_call(p, code, argp);
KERNEL_UNLOCK();
#endif
TRACEPOINT(raw_syscalls, sys_enter, code, NULL);
#if NDT > 0
DT_ENTER(syscall, code, callp->sy_argsize, argp);
#endif
#ifdef KTRACE
if (KTRPOINT(p, KTR_SYSCALL)) {
/* convert to mask, then include with code */
ktrsyscall(p, code, callp->sy_argsize, argp);
}
#endif
/* SP must be within MAP_STACK space */
if (!uvm_map_inentry(p, &p->p_spinentry, PROC_STACK(p),
"[%s]%d/%d sp=%lx inside %lx-%lx: not MAP_STACK\n",
uvm_map_inentry_sp, p->p_vmspace->vm_map.sserial))
return (EPERM);
if ((error = pin_check(p, code)))
return (error);
pledged = (p->p_p->ps_flags & PS_PLEDGE);
if (pledged && (error = pledge_syscall(p, code, &tval))) {
KERNEL_LOCK();
error = pledge_fail(p, error, tval);
KERNEL_UNLOCK();
return (error);
}
最后,我们准备开始工作了。
error = (*callp->sy_call)(p, argp, retval);
在 _sys_write_ 中见。
sys_write
一些“通用”的系统调用存在于 kern/sys_generic.c 中。_read_,_write_,_select_,_poll_,_ioctl_。仔细想想,其实并不通用,但仍然如此。
_sys_write_ 本身并没有做太多事情。只是重新打包参数以便与 _sys_writev_ 共享代码。每个系统调用都有一个 args struct,它格式化了寄存器数组。由于历史原因,它是通过 _SCARG_ 宏访问的。
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;
struct iovec iov;
struct uio auio;
iov.iov_base = (void *)SCARG(uap, buf);
iov.iov_len = SCARG(uap, nbyte);
if (iov.iov_len > SSIZE_MAX)
return (EINVAL);
auio.uio_iov = &iov;
auio.uio_iovcnt = 1;
auio.uio_resid = iov.iov_len;
return (dofilewritev(p, SCARG(uap, fd), &auio, 0, retval));
}
现在我们已经将 userland 参数重新打包到一个 io vector 中,并且在 user io request 内部,我们准备继续。与之前的函数非常相似,_dofilewritev_ 由比“实际”工作更多的跟踪和统计代码组成。有趣的部分可能是获取文件描述符的文件并确保它是可写的,然后跳转到实际的文件操作中。
if ((fp = fd_getfile_mode(fdp, fd, FWRITE)) == NULL)
return (EBADF);
error = (*fp->f_ops->fo_write)(fp, uio, flags);
从这里开始,我们可以去很多地方,但对于一个普通文件来说,它将是 _vn_write_。
vn_write
内核中有很多 _fileops_ 结构,但只有一个 _vnops_,位于 kern/vfs_vnops.c 中。这不仅用于普通文件,还用于可以通过文件系统访问的任何内容,但不用于 sockets 等。
_vn_write_ 将检查一堆标志并在 regime 之间进行转换。我们仍然没有对 bytes 做任何事情。它们在 uio 中的 iov 的指针中。从技术上讲,仍然在 userland 中。
/* note: pwrite/pwritev are unaffected by O_APPEND */
if (vp->v_type == VREG && (fp->f_flag & O_APPEND) &&
(fflags & FO_POSITION) == 0)
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;
并且,又是另一个间接调用。
error = VOP_WRITE(vp, uio, ioflag, cred);
_VOP_WRITE_ 是一个花哨的参数打包器和包装器,用于位于 kern/vfs_vops.c 中的另一行。古人低语,过去这里发生过更多的事情。
return ((vp->v_op->vop_write)(&a));
有很多 vops,但我们要前往 _ffs_write_。
ffs_write
与 _fileops_ struct 相比,有很多函数被打包到 _vops_ 中。ufs/ffs/ffs_vnops.c 中的 _ffs_vops_ 提供了一些想法。我们确认我们的下一站是 _ffs_write_。
ffs_vops
const struct vops ffs_vops = {
.vop_lookup = ufs_lookup,
.vop_create = ufs_create,
.vop_mknod = ufs_mknod,
.vop_open = ufs_open,
.vop_close = ufs_close,
.vop_access = ufs_access,
.vop_getattr = ufs_getattr,
.vop_setattr = ufs_setattr,
.vop_read = ffs_read,
.vop_write = ffs_write,
.vop_ioctl = ufs_ioctl,
.vop_kqfilter = ufs_kqfilter,
.vop_revoke = vop_generic_revoke,
.vop_fsync = ffs_fsync,
.vop_remove = ufs_remove,
.vop_link = ufs_link,
.vop_rename = ufs_rename,
.vop_mkdir = ufs_mkdir,
.vop_rmdir = ufs_rmdir,
.vop_symlink = ufs_symlink,
.vop_readdir = ufs_readdir,
.vop_readlink = ufs_readlink,
.vop_abortop = vop_generic_abortop,
.vop_inactive = ufs_inactive,
.vop_reclaim = ffs_reclaim,
.vop_lock = ufs_lock,
.vop_unlock = ufs_unlock,
.vop_bmap = ufs_bmap,
.vop_strategy = ufs_strategy,
.vop_print = ufs_print,
.vop_islocked = ufs_islocked,
.vop_pathconf = ufs_pathconf,
.vop_advlock = ufs_advlock,
.vop_bwrite = vop_generic_bwrite
};
花了一段时间才到达这里,除了错误检查和机械转换之外,没有太多可看的,但这即将改变。我们将做出一些决定,重要的决定,更令人兴奋的是,bytes 将会移动。仅仅是声明块就比我们经历过的某些函数还要长。这是认真的。
struct vop_write_args *ap = v;
struct vnode *vp;
struct uio *uio;
struct inode *ip;
struct fs *fs;
struct buf *bp;
daddr_t lbn;
off_t osize;
int blkoffset, error, extended, flags, ioflag, size, xfersize;
size_t resid;
ssize_t overrun;
还有一些关于标志和大小的检查(省略),现在我们进入了工作循环。
for (error = 0; uio->uio_resid > 0;) {
lbn = lblkno(fs, uio->uio_offset);
blkoffset = blkoff(fs, uio->uio_offset);
xfersize = fs->fs_bsize - blkoffset;
if (uio->uio_resid < xfersize)
xfersize = uio->uio_resid;
if (fs->fs_bsize > xfersize)
flags |= B_CLRBUF;
else
flags &= ~B_CLRBUF;
if ((error = UFS_BUF_ALLOC(ip, uio->uio_offset, xfersize,
ap->a_cred, flags, &bp)) != 0)
break;
if (uio->uio_offset + xfersize > DIP(ip, size)) {
DIP_ASSIGN(ip, size, uio->uio_offset + xfersize);
uvm_vnp_setsize(vp, DIP(ip, size));
extended = 1;
}
(void)uvm_vnp_uncache(vp);
size = blksize(fs, ip, lbn) - bp->b_resid;
if (size < xfersize)
xfersize = size;
error = uiomove(bp->b_data + blkoffset, xfersize, uio);
这里发生了很多事情,但简而言之,我们正在设置一个合理的传输大小,并从缓存中获取正确磁盘位置的 buffer。这是一个非常重要的 side quest。好奇的人可以查看 ufs/ffs/ffs_balloc.c 中的分配算法。现在,我们试图跟踪 bytes 的去向,而不是它们如何知道去哪里。
一旦设置完成,就该进行长期承诺的移动了。_uiomove_ 会将 bytes 从 userland 复制到 buffer 中。
uiomove
回到 kern/kern_subr.c 中,我们将通过 _uiomove_ 工作。这是一个在很多地方使用的花哨的企业级 memcpy 函数。我们从 _write_ 开始只有一个数据指针,但许多其他用途将包括一个更完全填充的 iovecs 数组。
while (n > 0) {
iov = uio->uio_iov;
cnt = iov->iov_len;
if (cnt == 0) {
KASSERT(uio->uio_iovcnt > 0);
uio->uio_iov++;
uio->uio_iovcnt--;
continue;
}
if (cnt > n)
cnt = n;
switch (uio->uio_segflg) {
case UIO_USERSPACE:
sched_pause(preempt);
if (uio->uio_rw == UIO_READ)
error = copyout(cp, iov->iov_base, cnt);
else
error = copyin(iov->iov_base, cp, cnt);
_uiomove_ 支持的技巧之一是从 userspace 或 kernel space 复制。这次我们是从 userspace 复制,所以我们也做一个小的 preemption check,这样大量的复制不会使系统长时间阻塞。我们正在执行一个写入操作,所以我们将调用 _copyin_ 来读取数据。读写,写读。
_copyin_ 位于 arch 目录下的某个地方,并且使用 fault handlers 执行神奇的事情。也许我们试图复制的页面已被交换出去,或者某个小丑给了我们一个无效的指针。我们可能还需要确保 userland 地址空间已映射,或者 SMAP 已禁用等。
但今天,一切都很顺利,我们已经移动了所有的 bytes。万岁。回到 _ffs_write_。
bdwrite
从我们离开的地方开始,bytes 在 buffer 中,现在是开始写入操作的下一阶段的时候了,写入。
if (ioflag & IO_SYNC)
(void)bwrite(bp);
else if (xfersize + blkoffset == fs->fs_bsize) {
bawrite(bp);
} else
bdwrite(bp);
我们假设它将是 _bdwrite_,一个延迟写入。将其排队,但不要等待完成。_bdwrite_ 的核心是来自 kern/vfs_bio.c 的第一个块。注释显然与这里的实际操作顺序不符,但非常接近。
/*
* If the block hasn't been seen before:
* (1) Mark it as having been seen,
* (2) Charge for the write.
* (3) Make sure it's on its vnode's correct block list,
* (4) If a buffer is rewritten, move it to end of dirty list
*/
if (!ISSET(bp->b_flags, B_DELWRI)) {
SET(bp->b_flags, B_DELWRI);
s = splbio();
buf_flip_dma(bp);
reassignbuf(bp);
splx(s);
curproc->p_ru.ru_oublock++; /* XXX */
}
_reassignbuf_ 将调用 _vn_syncer_add_to_worklist_ 以使事情与一些指针旋转和冒险保持一致。回到 _bdwrite_:
/* The "write" is done, so mark and release the buffer. */
CLR(bp->b_flags, B_NEEDCOMMIT);
CLR(bp->b_flags, B_NOCACHE); /* Must cache delayed writes */
SET(bp->b_flags, B_DONE);
brelse(bp);
就是这样。根据评论,我们完成了。嗯,不完全是。在这种上下文中释放 buffer 意味着放弃独占访问权,使其可供其他进程使用等。写入尚未以任何期望数据持久性的人会满意的方式进行。但在此之后,这是一个快速的返回调用堆栈的过程。您很快就会回到 userland 中。 Bytes 现在在内核中。
syncer
在其他地方,在其他时间,syncer 将运行。我们也可以通过调用 _fsync_ 到达相同的目的地,但我们将采取更轻松的道路。kern/vfs_sync.c 中的 _syncer_thread_ 是一个永无止境的循环,处理 dirty vnodes 列表。我们的文件的 vnode 及其包含 bytes 的相关 buf 已在上面排队。
这里的操作行:
(void) VOP_FSYNC(vp, p->p_ucred, MNT_LAZY, p);
所以它实际上是 fsync。回到 kern/vfs_vops.c 我们看到这只是另一个包装器,并且返回到 ufs/ffs/ffs_vnops.c 以查看 _ffs_fsync_。这里有一个循环,最终我们会遇到带有 bytes 的 buf,然后操作行变为:
if (passes > 0 || ap->a_waitfor != MNT_WAIT)
(void) bawrite(bp);
else if ((error = bwrite(bp)) != 0)
return (error);
嗯,这看起来很眼熟。剧透警告,_bawrite_ 和 _bwrite_ 是表兄弟,一个函数指针被删除,所以我们将直接进入 _bwrite_ 兔子洞。
bwrite
在完成上述所有操作之后,我们现在位于 kern/vfs_bio.c 中的 _bwrite_ 函数中。当然,我们非常接近看到 bytes 被写入到他们要去的地方。
嘿,看,还有一些 fixup 代码。我们从一个延迟写入开始,它变成了一个真正的写入,但是也许其他人希望他们真正的写入变成一个延迟写入。在 Kafka 的 bufcache 中,选择是无穷无尽的。
async = ISSET(bp->b_flags, B_ASYNC);
if (!async && mp && ISSET(mp->mnt_flag, MNT_ASYNC)) {
/*
* Don't convert writes from VND on async filesystems
* that already have delayed writes in the upper layer.
*/
if (!ISSET(bp->b_flags, B_NOCACHE)) {
bdwrite(bp);
return (0);
}
}
我们跳过相当多的 accounting++ 代码,现在进行严肃的业务。
VOP_STRATEGY(bp->b_vp, bp);
你可以猜到这是做什么的。
return ((vp->v_op->vop_strategy)(&a));
是时候访问一个新文件了,ufs/ufs/ufs_vnops.c,看看 _ufs_strategy_。
vp = ip->i_devvp;
bp->b_dev = vp->v_rdev;
VOP_STRATEGY(vp, bp);
没错,它一直是 strategy。顶层 strategy 将 buf 发送到文件系统,而底层 strategy 将把 buf 发送到磁盘。这里需要注意的重要一点是,我们已经从文件 vnode 切换到设备 vnode。因此,现在,在我们下次通过 _VOP_STRATEGY_ 时,我们将转到 _spec_strategy_。
spec_strategy
在 kern/spec_vnops.c 中,我们看到了一些以前没有见过的东西。
int
spec_strategy(void *v)
{
struct vop_strategy_args *ap = v;
struct buf *bp = ap->a_bp;
int maj = major(bp->b_dev);
(*bdevsw[maj].d_strategy)(bp);
return (0);
}
而不是让 vtable 指针存在于对象中,它是一个通过索引访问的全局 vtable。实际上并没有什么不同,只是一些变化。 有很多设备,但这是 OpenBSD,所以我们的下一站只能是一个地方:SCSI。
sdstrategy
准备好迎接一个新的抽象级别。
在 scsi/sd.c 中,我们进入 _sdstrategy_。在通常的参数检查之后,我们执行两个重要的操作。
/* Place it in the queue of disk activities for this disk. */
bufq_queue(&sc->sc_bufq, bp);
/*
* Tell the device to get going on the transfer if it's
* not doing anything, otherwise just wait for completion
*/
scsi_xsh_add(&sc->sc_xsh);
我们将把这个 buf 放在磁盘的队列中,但在我们告诉设备开始之前,不会发生太多事情。
让我们看看 scsi/scsi_base.c 中的一些函数。_scsi_xsh_add_ 将把磁盘放在链接队列上。
mtx_enter(&link->pool->mtx);
if (xsh->ioh.q_state == RUNQ_IDLE) {
TAILQ_INSERT_TAIL(&link->queue, &xsh->ioh, q_entry);
xsh->ioh.q_state = RUNQ_LINKQ;
rv = 1;
}
mtx_leave(&link->pool->mtx);
/* lets get some io up in the air */
scsi_xsh_runqueue(link);
_scsi_xsh_runqueue_ 是一个 do while 循环。
do {
runq = 0;
mtx_enter(&link->pool->mtx);
while (!ISSET(link->state, SDEV_S_DYING) &&
link->pending < link->openings &&
((ioh = TAILQ_FIRST(&link->queue)) != NULL)) {
link->pending++;
TAILQ_REMOVE(&link->queue, ioh, q_entry);
TAILQ_INSERT_TAIL(&link->pool->queue, ioh, q_entry);
ioh->q_state = RUNQ_POOLQ;
runq = 1;
}
mtx_leave(&link->pool->mtx);
if (runq)
scsi_iopool_run(link->pool);
} while (!scsi_pending_finish(&link->pool->mtx, &link->running));
我们只是将事物从一个链接队列移动到另一个链接队列,因此我们可以调用 _scsi_iopool_run_ 并进入另一个 do while 循环。
do {
while (scsi_ioh_pending(iopl)) {
io = scsi_iopool_get(iopl);
if (io == NULL)
break;
ioh = scsi_ioh_deq(iopl);
if (ioh == NULL) {
scsi_iopool_put(iopl, io);
break;
}
ioh->handler(ioh->cookie, io);
}
} while (!scsi_pending_finish(&iopl->mtx, &iopl->running));
这里的 _ioh->handler_ 调用很重要。这将是 _scsi_xsh_ioh_,它本身有一行重要的代码。
xsh->handler(xs);
这最终解析为 _sdstart_。跟踪所有这些指针有点困难。与 vnops tables 不同,您不会在一个地方找到所有分组在一起的 _sdstart_ 及其朋友。
还要重要的是要提到,此时,我们可能在 _syncer_ 线程中,也可能不在。某个其他线程可能正在处理队列,而 syncer 只会丢弃一些工作。这是一个团队的努力。
sdstart
为什么关注 SCSI?因为在 OpenBSD 上,这就是全部。我保证,我们正在努力进入 _nvme_ 驱动程序,但必须通过 SCSI 才能到达那里。如果您使用 _softraid_ 加密磁盘,那就是 SCSI。如果您使用的是 USB 驱动器,那就是 SCSI。您有点旧的笔记本电脑中的 SATA 驱动器?除非它是非常古老的,否则它是 _ahci_,并且是的,它将显示为 SCSI。壁橱里那个奇怪的 octeon gizmo 上的 MMC?SCSI。如果我们假设一个 ISA floppy,我们也许可以绕过这一层,但我宁愿不这样做。
现在我们回到了 scsi/sd.c,事情应该更容易理解了。我们将再次考虑 bytes,而不是无休止地传递它们。_sdstart_ 将从 bufq 中获取我们很久以前隐藏的 buf。
bp = bufq_dequeue(&sc->sc_bufq);
if (bp == NULL) {
scsi_xs_put(xs);
return;
}
read = ISSET(bp->b_flags, B_READ);
SET(xs->flags, (read ? SCSI_DATA_IN : SCSI_DATA_OUT));
xs->timeout = 60000;
xs->data = bp->b_data;
xs->datalen = bp->b_bcount;
xs->done = sd_buf_done;
xs->cookie = bp;
xs->bp = bp;
p = &sc->sc_dk.dk_label->d_partitions[DISKPART(bp->b_dev)];
secno = DL_GETPOFFSET(p) + DL_BLKTOSEC(sc->sc_dk.dk_label, bp->b_blkno);
nsecs = howmany(bp->b_bcount, sc->sc_dk.dk_label->d_secsize);
现在我们将通过复制大量值来设置传输,但也要执行一些数学运算。我们越来越接近硬件,需要知道哪些扇区以及多少个扇区。有一些代码可以选择正确的传输命令(大型扇区编号需要更大的命令)。然后我们开始。
scsi_xs_exec(xs);
别担心,_scsi_xs_exec_ 很容易理解。只需追逐指针。
xs->sc_link->bus->sb_adapter->scsi_cmd(xs);
我们现在正在离开抽象世界。下一站:_nvme_scsi_cmd_。
nvme
是时候在新子目录 dev/ic/nvme.c 中创建一个新文件了。PCI attachment 代码在 dev/pci/nvme_pci.c 中是分开的,但我们对与总线无关的设备操作感兴趣。今天不会讨论 Apple 设备。我们进入 _nvme_scsi_cmd_。
switch (xs->cmd.opcode) {
case READ_COMMAND:
case READ_10:
case READ_12:
case READ_16:
nvme_scsi_io(xs, SCSI_DATA_IN);
return;
case WRITE_COMMAND:
case WRITE_10:
case WRITE_12:
case WRITE_16:
nvme_scsi_io(xs, SCSI_DATA_OUT);
return;
再往下一步,进入 _nvme_scsi_io_。
struct scsi_link *link = xs->sc_link;
struct nvme_softc *sc = link->bus->sb_adapter_softc;
struct nvme_ccb *ccb = xs->io;
bus_dmamap_t dmap = ccb->ccb_dmamap;
int i;
if ((xs->flags & (SCSI_DATA_IN|SCSI_DATA_OUT)) != dir)
goto stuffup;
ccb->ccb_done = nvme_scsi_io_done;
ccb->ccb_cookie = xs;
if (bus_dmamap_load(sc->sc_dmat, dmap,
xs->data, xs->datalen, NULL, ISSET(xs->flags, SCSI_NOSLEEP) ?
BUS_DMA_NOWAIT : BUS_DMA_WAITOK) != 0)
goto stuffup;
bus_dmamap_sync(sc->sc_dmat, dmap, 0, dmap->dm_mapsize,
ISSET(xs->flags, SCSI_DATA_IN) ?
BUS_DMASYNC_PREREAD : BUS_DMASYNC_PREWRITE);
如果我们仔细研究 _bus_dmama_load_ 参数,我们将看到对 xs->data_ 的引用。bytes 再次出现。我们没有完全迷路。我们不需要复制 bytes,但我们需要确保有一个合适的 IOMMU mapping,以便 DMA 成功。
bytes 已准备好供设备使用,但仍需要一点推动。继续前进,我们看到了它。
nvme_q_submit(sc, sc->sc_q, ccb, nvme_scsi_io_fill);
这是 _nvme_q_submit_ 的主体。
tail = sc->sc_ops->op_sq_enter(sc, q, ccb);
sqe += tail;
bus_dmamap_sync(sc->sc_dmat, NVME_DMA_MAP(q->q_sq_dmamem),
sizeof(*sqe) * tail, sizeof(*sqe), BUS_DMASYNC_POSTWRITE);
memset(sqe, 0, sizeof(*sqe));
(*fill)(sc, ccb, sqe);
sqe->cid = ccb->ccb_id;
bus_dmamap_sync(sc->sc_dmat, NVME_DMA_MAP(q->q_sq_dmamem),
sizeof(*sqe) * tail, sizeof(*sqe), BUS_DMASYNC_PREWRITE);
sc->sc_ops->op_sq_leave(sc, q, ccb);
更多 DMA mapping,这次是 NVME command ring。我跳过了 _bus_dmamap_ 函数的实现,因为我们真的可能会迷路。我们几乎完成了,但我们已经走了这么远,所以让我们看看那个 fill 函数是什么。
_nvme_scsi_io_fill_ 将转换设备的块地址。
scsi_cmd_rw_decode(&xs->cmd, &lba, &blocks);
sqe->opcode = ISSET(xs->flags, SCSI_DATA_IN) ?
NVM_CMD_READ : NVM_CMD_WRITE;
htolem32(&sqe->nsid, link->target);
htolem64(&sqe->entry.prp[0], dmap->dm_segs[0].ds_addr);
htolem64(&sqe->slba, lba);
htolem16(&sqe->nlb, blocks - 1);
这样,一切都已就绪。上面的 _op_sq_leave_ 最终会调用 _nvme_write4_,它将更新设备上的寄存器以包括新的队列条目。
现在我们等待。驱动器将在准备就绪时并根据其固件的秘密愿望写入 bytes。我们再次到达了调用堆栈的底部,并且只有返回。
aftermath
bytes 已被写入,但我们如何知道这一点?我将简单地勾勒出指示完成的回调。
当设备发出中断信号时,会调用 _nvme_intr_,_nvme_q_complete_ 将调用 _nvme_scsi_io_done_ 调用 _scsi_done_,直到我们最终进入 _sd_buf_done_。这将调用 _biodone_,后者将调用 _wakeup_。如果有人在等待此写入完成,例如在调用 _fsync_ 的情况下,他们将在 biowait 中的 _tsleep_nsec_ 中,该函数返回到 _bwrite_ 并从那里返回。
recap
我们从 _write_ 系统调用开始。在通过特定于文件类型和文件系统的一些函数指针之后,我们将 bytes 复制到 buffer cache 中。稍后,syncer 会将 buf 推送到 SCSI 层,然后将 buf 转换为 SCSI cmd,然后才能到达 NVME 驱动程序,从而设置实际的 DMA 传输。
Posted 29 Mar 2025 10:38 by tedu Updated: 29 Mar 2025 10:38 Tagged: openbsd