001 Notes
Linux I/O Path: From write() to NVMe Doorbell
0. Mental Model
userspace buffer
-> iovec / iov_iter
-> kiocb + file
-> filesystem path
-> page cache folio or direct-I/O user pages
-> bio
-> request
-> nvme_request + nvme_iod + nvme_command
-> NVMe submission queue entry
-> MMIO doorbell
-> device DMA and completion queue entry
-> request completion
-> bio completion
-> kiocb completion or syscall return
1. Four Path Comparison
| Path | Page cache | User pages pinned | bio payload | Device DMA target/source | Return condition |
|---|---|---|---|---|---|
| Buffered write | yes | no | later writeback folios | device reads cache pages | syscall may return after copy into cache |
O_DIRECT write | no | yes | user pages | device reads user pages | returns after direct I/O completion |
| Buffered read hit | yes | no | no device I/O | none | copies from cache to userspace |
| Buffered read miss | yes | no | cache folios | device writes cache pages | copies from uptodate cache folios |
O_DIRECT read | no | yes | user pages | device writes user pages | returns after direct I/O completion |
buffered I/O:
userspace <-> page cache <-> block layer <-> NVMe
direct I/O:
userspace pages <-> block layer <-> NVMe
2. Buffered Write
2.1 Userspace Entry
write(fd, const void *buf, size_t count);
2.2 Syscall And VFS Glue
__x64_sys_write()
-> ksys_write()
-> vfs_write()
-> new_sync_write()
-> call_write_iter()
/* uapi/linux/uio.h */
struct iovec {
void __user *iov_base;
size_t iov_len;
};
/* include/linux/uio.h */
struct iov_iter {
unsigned int type;
size_t iov_offset;
size_t count;
const struct iovec *iov;
};
/* include/linux/fs.h */
struct file {
const struct file_operations *f_op;
struct inode *f_inode;
struct address_space *f_mapping;
loff_t f_pos;
unsigned int f_flags;
};
struct kiocb {
struct file *ki_filp;
loff_t ki_pos;
int ki_flags;
};
| Field | Why the next layer cares |
|---|---|
iov_iter.count | bytes remaining in this operation |
iov_iter.iov | user buffer segments |
kiocb.ki_filp | file object that owns the operation |
kiocb.ki_pos | file offset |
file.f_op | filesystem entry points |
file.f_mapping | page-cache anchor |
2.3 Filesystem And Page Cache
generic_file_write_iter()
-> iomap_file_buffered_write()
-> copy_page_from_iter_atomic()
-> dirty folios
/* include/linux/fs.h */
struct address_space {
struct inode *host;
struct xarray i_pages;
const struct address_space_operations *a_ops;
};
/* include/linux/mm_types.h */
struct folio {
unsigned long flags;
/* wraps one or more physical pages */
};
user buffer
-> iov_iter
-> copy into folio memory
-> mark folio dirty
2.4 Writeback Builds bio
bioiomap_writepages()
-> bio_alloc_*()
-> bio_add_page()
-> submit_bio()
/* include/linux/bio.h */
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
struct bvec_iter {
sector_t bi_sector;
unsigned int bi_size;
};
struct bio {
struct block_device *bi_bdev;
struct bio_vec *bi_io_vec;
unsigned short bi_vcnt;
struct bvec_iter bi_iter;
unsigned int bi_opf;
bio_end_io_t *bi_end_io;
void *bi_private;
};
| Field | Meaning |
|---|---|
bio.bi_bdev | target block device |
bio.bi_iter.bi_sector | starting sector in 512-byte units |
bio.bi_iter.bi_size | bytes left in the bio |
bio.bi_io_vec | pages and offsets carrying payload |
bio.bi_opf | operation and flags, such as REQ_OP_WRITE, REQ_SYNC, REQ_FUA |
2.5 Block Layer Builds request
requestsubmit_bio()
-> __submit_bio()
-> blk_mq_submit_bio()
-> blk_mq_make_request()
-> blk_mq_alloc_request()
-> driver->queue_rq()
/* include/linux/blkdev.h */
struct request_queue {
const struct blk_mq_ops *mq_ops;
unsigned int queue_flags;
/* limits, scheduler state, hardware contexts */
};
struct request {
struct request_queue *q;
struct bio *bio;
struct bio *biotail;
blk_opf_t cmd_flags;
void *special;
};
mq_ops.queue_rq(hctx, &bd)
2.6 NVMe Host Driver
/* drivers/nvme/host/nvme.h */
struct nvme_ns {
struct nvme_ctrl *ctrl;
u32 ns_id;
u8 lba_shift;
struct gendisk *disk;
};
struct nvme_iod {
struct scatterlist *sg;
int nents;
dma_addr_t first_dma;
__le64 *dma_prps;
int npages;
size_t length;
};
struct nvme_request {
struct nvme_command cmd;
struct nvme_iod *iod;
int status;
u8 retries;
};
/* include/linux/nvme.h */
struct nvme_common_command {
__u8 opcode;
__u8 flags;
__u16 command_id;
__le32 nsid;
__le64 mptr;
__le64 prp1;
__le64 prp2;
__le32 cdw10;
__le32 cdw11;
__le32 cdw12;
__le32 cdw13;
__le32 cdw14;
__le32 cdw15;
};
struct nvme_rw_command {
__u8 opcode;
__u8 flags;
__u16 command_id;
__le32 nsid;
__le64 rsvd;
__le64 slba;
__le16 length;
__le16 control;
__le32 dsmgmt;
__le32 reftag;
__le16 apptag;
__le16 appmask;
};
struct nvme_command {
union {
struct nvme_common_command common;
struct nvme_rw_command rw;
/* other command formats */
};
};
| Field | Meaning |
|---|---|
rw.opcode | nvme_cmd_write for write, nvme_cmd_read for read |
rw.nsid | namespace id |
rw.slba | starting logical block address |
rw.length | number of LBAs minus one |
rw.control | command flags such as FUA |
common.prp1 / common.prp2 | DMA pointer or PRP list |
nvme_lba = bio_sector >> (ns->lba_shift - 9)
block layer sector = 512 bytes = 2^9
NVMe namespace LBA = 2^ns->lba_shift bytes
2.7 NVMe Queue And Doorbell
nvme_setup_cmd()
-> nvme_setup_rw()
-> dma_map_sg()
-> nvme_pci_setup_prps()
-> copy command into submission queue
-> update SQ tail
-> writel(new_tail, nvmeq->q_db)
struct nvme_queue {
volatile struct nvme_command *sq_cmds;
volatile struct nvme_completion *cqes;
u16 qid;
u16 sq_tail;
u16 cq_head;
void __iomem *q_db;
};
host memory SQ entry is ready
-> MMIO doorbell write
-> controller DMA-reads command and PRP/SGL descriptors
2.8 Device And Completion
struct nvme_completion {
__le32 result;
__u32 rsvd;
__le16 sq_head;
__le16 sq_id;
__u16 command_id;
__le16 status;
};
NVMe device writes CQE
-> interrupt or polling
-> nvme_irq() / nvme_poll()
-> nvme_process_cq()
-> blk_mq_complete_request(req)
-> bio_endio()
-> filesystem completion
-> kiocb completion or syscall return
3. Direct Write
write()
-> iovec / iov_iter
-> file
-> kiocb
-> file->f_op->write_iter()
3.1 iomap Direct I/O
iomap_dio_rw()
-> bio_iov_iter_get_pages()
-> submit_bio()
/* fs/iomap/direct-io.c */
struct iomap_dio {
struct kiocb *iocb;
struct iov_iter *iter;
struct bio *head;
struct bio *tail;
loff_t size;
loff_t submitted;
loff_t done;
bool wait_for_completion;
};
buffered write:
bio points at page-cache folios
direct write:
bio points at pinned user pages
bio
-> submit_bio()
-> blk-mq request
-> nvme_request
-> nvme_command
-> PRP/SGL from user pages
-> SQ doorbell
-> CQE
-> direct-I/O completion
3.2 Durability Flags
kiocb.ki_flags
-> request.cmd_flags
-> REQ_SYNC / REQ_FUA
-> NVMe RW command control field
O_DIRECT:
avoid page cache for file data
O_SYNC / RWF_SYNC / FUA:
control completion and durability semantics
4. Buffered Read
4.1 Syscall And VFS
read()
-> __x64_sys_read()
-> ksys_read()
-> vfs_read()
-> new_sync_read()
-> call_read_iter()
iovec
-> iov_iter
-> file
-> kiocb
4.2 Cache Hit
filemap_read()
-> find folios in mapping->i_pages
-> copy_to_iter()
-> return to userspace
4.3 Cache Miss
filemap_read()
-> page cache miss
-> readahead_control
-> iomap_readpages()
-> bio_alloc_*()
-> bio_add_page()
-> submit_bio()
/* include/linux/pagemap.h */
struct readahead_control {
struct address_space *mapping;
pgoff_t _index;
unsigned int _nr_pages;
};
bio with cache folios
-> request
-> nvme_cmd_read
-> PRPs for device-to-host DMA
-> controller writes into folios
-> CQE
-> bio_endio()
-> folios marked Uptodate
-> copy_to_iter()
-> return to userspace
buffered read miss:
device DMA writes page-cache folios
direct read:
device DMA writes pinned userspace pages
5. Direct Read
read()
-> iovec / iov_iter
-> file
-> kiocb
-> iomap_dio_rw()
-> pin destination user pages
-> build bio pointing at those pages
-> submit_bio()
-> blk-mq request
-> nvme_cmd_read
-> PRP/SGL to user pages
-> SQ doorbell
-> device DMA into user pages
-> CQE
-> ki_complete()
-> userspace return
6. Field Handoff Table
| Layer | Struct | Fields that matter |
|---|---|---|
| Userspace description | iovec | iov_base, iov_len |
| Iterator | iov_iter | type, count, iov_offset, iov |
| Per-I/O operation | kiocb | ki_filp, ki_pos, ki_flags, completion state |
| Open file | file | f_op, f_inode, f_mapping, f_flags, f_pos |
| Inode mapping | address_space | i_pages, a_ops, host |
| Page cache unit | folio | backing pages and flags such as dirty/uptodate |
| Block payload | bio | bi_bdev, bi_iter, bi_io_vec, bi_vcnt, bi_opf |
| Hardware request | request | q, bio, biotail, cmd_flags, special |
| NVMe namespace | nvme_ns | ctrl, ns_id, lba_shift |
| NVMe private request | nvme_request | cmd, iod, status, retries |
| DMA mapping | nvme_iod | sg, nents, first_dma, dma_prps, length |
| NVMe command | nvme_command | opcode, nsid, slba, length, prp1, prp2 |
| Queue | nvme_queue | sq_cmds, cqes, sq_tail, cq_head, q_db |
| Completion | nvme_completion | command_id, status, sq_head, sq_id, result |
7. Minimal Struct Shapes
7.1 Userspace To VFS
struct iovec {
void __user *iov_base;
size_t iov_len;
};
struct iov_iter {
unsigned int type;
size_t count;
size_t iov_offset;
const struct iovec *iov;
};
struct kiocb {
struct file *ki_filp;
loff_t ki_pos;
int ki_flags;
};
7.2 Core VFS Objects
struct file {
const struct file_operations *f_op;
struct path f_path;
struct inode *f_inode;
struct address_space *f_mapping;
loff_t f_pos;
unsigned int f_flags;
};
struct file_operations {
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
};
struct inode {
umode_t i_mode;
struct super_block *i_sb;
const struct inode_operations *i_op;
struct address_space *i_mapping;
loff_t i_size;
};
struct super_block {
const struct super_operations *s_op;
struct block_device *s_bdev;
struct dentry *s_root;
};
7.3 Page Cache And Mapping
struct address_space {
struct inode *host;
struct xarray i_pages;
const struct address_space_operations *a_ops;
};
struct address_space_operations {
int (*writepages)(struct address_space *, struct writeback_control *);
int (*readpage)(struct file *, struct page *);
/* modern filesystems often route reads/writeback through iomap helpers */
};
7.4 Block Layer
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
struct bvec_iter {
sector_t bi_sector;
unsigned int bi_size;
};
struct bio {
struct block_device *bi_bdev;
unsigned int bi_opf;
struct bio_vec *bi_io_vec;
unsigned short bi_vcnt;
struct bvec_iter bi_iter;
bio_end_io_t *bi_end_io;
void *bi_private;
};
struct request_queue {
const struct blk_mq_ops *mq_ops;
unsigned int queue_flags;
};
struct request {
struct request_queue *q;
struct bio *bio;
struct bio *biotail;
blk_opf_t cmd_flags;
void *special;
};
7.5 NVMe Host Driver
struct nvme_ns {
struct nvme_ctrl *ctrl;
u32 ns_id;
u8 lba_shift;
struct gendisk *disk;
};
struct nvme_iod {
struct scatterlist *sg;
int nents;
dma_addr_t first_dma;
__le64 *dma_prps;
int npages;
size_t length;
};
struct nvme_request {
struct nvme_command cmd;
struct nvme_iod *iod;
int status;
u8 retries;
};
struct nvme_queue {
volatile struct nvme_command *sq_cmds;
volatile struct nvme_completion *cqes;
u16 qid;
u16 sq_tail;
u16 cq_head;
void __iomem *q_db;
};
7.6 iomap Direct I/O
struct iomap_dio {
struct kiocb *iocb;
struct iov_iter *iter;
struct bio *head;
struct bio *tail;
loff_t size;
loff_t submitted;
loff_t done;
bool wait_for_completion;
};
8. How The Objects Point To Each Other
userspace buffer
|
v
iovec -> iov_iter -> kiocb
|
v
struct file
|
+-> f_op: file_operations
| +-> read_iter / write_iter
|
+-> f_inode: inode
| +-> i_sb: super_block
| | +-> s_type: file_system_type
| |
| +-> i_mapping: address_space
|
+-> f_mapping: address_space
address_space
|
+-> i_pages: folio cache
+-> a_ops: address_space_operations
bio
|
+-> bi_io_vec: pages
+-> bi_bdev: block device
+-> bi_iter: sector and size
request_queue
|
+-> mq_ops.queue_rq()
|
v
NVMe driver
kiocb does not directly know the filesystem.
kiocb points to file.
file points to f_op and inode.
f_op points to filesystem methods.
inode points to super_block and filesystem instance state.
9. Minimal Filesystem Wiring
static const struct file_operations myfs_file_ops = {
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.mmap = generic_file_mmap,
.fsync = generic_file_fsync,
};
static const struct address_space_operations myfs_aops = {
.writepages = iomap_writepages,
/* dirty_folio, write_end, readahead hooks depend on filesystem design */
};
static const struct inode_operations myfs_inode_ops = {
.getattr = myfs_getattr,
};
static const struct super_operations myfs_super_ops = {
.statfs = myfs_statfs,
};
/* during inode setup */
inode->i_fop = &myfs_file_ops;
inode->i_mapping->a_ops = &myfs_aops;
inode->i_op = &myfs_inode_ops;
buffered write()
-> generic_file_write_iter()
-> iomap_file_buffered_write()
-> dirty folios
-> writeback
-> bio
-> submit_bio()
-> blk-mq
-> NVMe
O_DIRECT write()
-> generic_file_write_iter()
-> filesystem direct-I/O decision
-> iomap_dio_rw()
-> bio with pinned user pages
-> submit_bio()
-> blk-mq
-> NVMe
10. How kiocb Maps To The Filesystem
kiocb Maps To The Filesystemsame struct file
-> many concurrent kiocbs
-> each with its own offset, flags, completion state
10.1 Where kiocb Comes From
kiocb Comes Fromssize_t vfs_write(struct file *file, const char __user *buf,
size_t count, loff_t *pos)
{
struct kiocb kiocb;
struct iov_iter iter;
init_sync_kiocb(&kiocb, file);
kiocb.ki_pos = *pos;
iov_iter_init(&iter, WRITE, &iov, 1, count);
return call_write_iter(file, &kiocb, &iter);
}
kiocb.ki_filp = file
10.2 Filesystem Indirection Chain
kiocb
|
v
ki_filp: struct file
|
+-> f_op: struct file_operations
|
+-> f_inode: struct inode
| |
| +-> i_sb: struct super_block
| | |
| | +-> s_type: struct file_system_type
| | |
| | +-> name = "ext4", "xfs", "btrfs", ...
| |
| +-> i_mapping: page cache and a_ops
|
+-> f_mapping: usually inode->i_mapping
10.3 call_write_iter()
call_write_iter()ssize_t call_write_iter(struct file *file,
struct kiocb *kiocb,
struct iov_iter *iter)
{
const struct file_operations *fop = file->f_op;
return fop->write_iter(kiocb, iter);
}
const struct file_operations ext4_file_operations = {
.read_iter = generic_file_read_iter,
.write_iter = ext4_file_write_iter,
/* other methods */
};
file->f_op->write_iter(&kiocb, &iter)
-> ext4_file_write_iter(&kiocb, &iter)
10.4 How Filesystems Use kiocb
kiocbssize_t ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
loff_t pos = iocb->ki_pos;
if (iocb->ki_flags & IOCB_DIRECT)
return iomap_dio_rw(iocb, from, &ext4_iomap_ops, ...);
return generic_file_write_iter(iocb, from);
}
| Field | Meaning |
|---|---|
ki_filp | which open file this operation targets |
ki_pos | offset for this operation |
ki_flags | direct I/O, nowait, sync, append, async properties |
| completion fields | how async completions are reported |
struct file:
persistent per-open state
struct kiocb:
transient per-operation state
11. Exact Handoff Cheat Sheet
11.1 Buffered Write
userspace buf
-> iovec + iov_iter
-> file + kiocb
-> file->f_op->write_iter()
-> generic_file_write_iter() / filesystem write_iter()
-> iomap_file_buffered_write()
-> folio in mapping->i_pages
-> copy_page_from_iter_atomic()
-> mark folio dirty
-> later writeback
-> bio with page-cache folios
-> submit_bio()
-> blk_mq_make_request()
-> request + request_queue
-> nvme_queue_rq()
-> nvme_request + nvme_iod + nvme_command
-> dma_map_sg()
-> PRP/SGL setup
-> submission queue entry
-> MMIO doorbell
-> device DMA reads host pages
-> completion queue entry
-> nvme_process_cq()
-> blk_mq_complete_request()
-> bio_endio()
11.2 Direct Write
userspace buf
-> iovec + iov_iter
-> file + kiocb
-> file->f_op->write_iter()
-> iomap_dio_rw()
-> pin user pages
-> bio with bvecs pointing at user pages
-> submit_bio()
-> blk-mq request
-> nvme_request + nvme_iod + nvme_command
-> PRP/SGL from user pages
-> SQ doorbell
-> device DMA reads user pages
-> CQE
-> direct-I/O completion
-> ki_complete()
11.3 Buffered Read
userspace buf
-> iovec + iov_iter
-> file + kiocb
-> file->f_op->read_iter()
-> filemap_read()
-> lookup folios in mapping->i_pages
cache hit:
-> copy_to_iter()
-> return
cache miss:
-> readahead_control
-> allocate / lock folios
-> bio with cache folios
-> submit_bio()
-> blk-mq request
-> nvme_cmd_read
-> PRP/SGL to cache folios
-> SQ doorbell
-> device DMA writes folios
-> CQE
-> bio_endio()
-> folios become Uptodate
-> copy_to_iter()
-> return
11.4 Direct Read
userspace buf
-> iovec + iov_iter
-> file + kiocb
-> file->f_op->read_iter()
-> iomap_dio_rw()
-> pin destination user pages
-> bio with bvecs pointing at user pages
-> submit_bio()
-> blk-mq request
-> nvme_cmd_read
-> PRP/SGL to user pages
-> SQ doorbell
-> device DMA writes user pages
-> CQE
-> ki_complete()
-> return
12. Observing The Path With Tracepoints
sudo bpftrace -e '
tracepoint:block:block_bio_queue {
printf("bio %p queue dev=%d:%d sector=%u bytes=%u rwbs=%s\n",
args->bio, args->dev >> 20, args->dev & 0xfffff,
args->sector, args->nr_sector * 512, args->rwbs);
}
tracepoint:block:block_rq_issue {
printf("rq %p issue dev=%d:%d bytes=%u rwbs=%s\n",
args->rq, args->dev >> 20, args->dev & 0xfffff,
args->nr_bytes, args->rwbs);
}
tracepoint:block:block_rq_complete {
printf("rq %p complete bytes=%u\n", args->rq, args->nr_bytes);
}'
sudo bpftrace -e '
tracepoint:nvme:nvme_sq {
printf("NVMe SQ qid=%d cid=%d opcode=%d nsid=%d\n",
args->qid, args->cid, args->opcode, args->nsid);
}
tracepoint:nvme:nvme_cq {
printf("NVMe CQ qid=%d cid=%d status=0x%x\n",
args->qid, args->cid, args->status);
}'
| Tracepoint | Object layer |
|---|---|
block:block_bio_queue | bio enters block layer |
block:block_rq_issue | request issued to driver |
nvme:nvme_sq | NVMe command submitted |
nvme:nvme_cq | NVMe completion observed |
block:block_rq_complete | block request completed |
sudo bpftrace -lv 'tracepoint:block:*'
sudo bpftrace -lv 'tracepoint:nvme:*'
13. NVMe Block Path Versus PMEM/DAX
13.1 NVMe Block Device Path
filesystem
-> page cache or direct user pages
-> bio
-> request
-> NVMe command
-> PRP/SGL DMA mapping
-> submission queue
-> MMIO doorbell
-> controller DMA
-> completion queue
13.2 PMEM Without DAX
filesystem
-> page cache or direct I/O
-> bio
-> request-like block path
-> pmem driver
13.3 DAX Path
filesystem
-> iomap / dax_iomap
-> persistent-memory PFNs
-> CPU load/store path
| Question | NVMe block path | PMEM/DAX path |
|---|---|---|
| Is there an NVMe submission queue? | yes | no |
| Is there a controller doorbell? | yes | no |
| Does data transfer require device DMA? | yes | no, CPU load/store can access mapped persistent memory |
| Is the page cache used for file data? | buffered path yes | DAX bypasses page cache for file data |
| Are persistence rules still needed? | yes | yes, but they involve CPU cache flush and ordering rules |
13.4 Why This Contrast Matters
host prepares DMA-visible command and buffers
-> rings MMIO doorbell
-> controller owns progress
filesystem maps persistent memory
-> CPU executes loads/stores
-> persistence depends on cache flush and ordering
14. Glossary
| Term | Meaning |
|---|---|
iovec | userspace buffer segment |
iov_iter | kernel iterator over one or more buffer segments |
kiocb | per-I/O operation context |
file | persistent per-open file object |
inode | filesystem object representing the file |
address_space | page-cache anchor for an inode |
folio | modern page-cache unit, possibly larger than one page |
bio | block I/O payload made of page vectors |
request | blk-mq hardware-dispatch object |
blk-mq | multiqueue block layer |
nvme_request | NVMe driver’s private state for a block request |
nvme_iod | NVMe I/O descriptor, including scatterlist and PRP state |
PRP | physical region page pointer used by NVMe DMA |
SGL | scatter-gather list |
SQ | NVMe submission queue |
CQ | NVMe completion queue |
doorbell | MMIO register write that tells the controller new queue entries exist |
FUA | force unit access, a durability-related write flag |
| DAX | direct access path for persistent memory mappings |
15. Final Shape
userspace buffer
-> iovec / iov_iter
-> file / kiocb
-> filesystem method through file_operations
-> buffered: folio in page cache
-> direct: pinned user pages
-> bio
-> request_queue / request
-> nvme_request / nvme_iod / nvme_command
-> PRP or SGL DMA mapping
-> NVMe submission queue entry
-> MMIO doorbell
-> controller DMA
-> NVMe completion queue entry
-> blk-mq completion
-> bio_endio()
-> kiocb completion or syscall return
kiocb tells the filesystem which file operation is happening.
bio tells the block layer which pages and sectors are involved.
request tells the driver what hardware operation to issue.
nvme_command tells the controller what DMA to perform.
Comments