flak rss random

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