On this page

VFS Advanced

Coverage: page cache interaction with VFS → inode state machine → file locking (POSIX/OFD) → fsync/fdatasync guarantees → sendfile/splice/vmsplice zero-copy Kernel versions: 2.6 ~ 6.x

Page Cache and VFS Interaction

Relationship between inode and address_space

// inode->i_mapping points to the page cache for this file
// This is an address_space structure that manages all cached pages for this file in memory

// Most inode->i_mapping point to the embedded inode->i_data:
struct inode {
    struct address_space *i_mapping;  // usually = &inode->i_data
    struct address_space i_data;      // embedded (block device filesystems)
};

// For raw reads/writes on block devices:
struct block_device {
    struct inode bd_inode;            // block devices are also treated as inodes
    // bd_inode.i_mapping is the block device's own page cache
    // interacts with the filesystem via buffer_head
};

inode State Machine

// include/linux/fs.h
#define I_NEW           0   // inode just allocated, not yet read from disk
#define I_DIRTY_SYNC    1   // metadata dirty, needs sync (e.g., i_size)
#define I_DIRTY_DATASYNC 2  // data dirty (only fdatasync cares)
#define I_DIRTY_PAGES   3   // i_mapping contains dirty pages
#define I_WILL_FREE     4   // about to be freed
#define I_FREEING       5   // currently being freed

// inode lifecycle:
// alloc_inode()  → I_NEW
// insert_inode_locked() → added to inode hash
// unlock_new_inode() → clear I_NEW, wake up waiters
// ... active period ...
// iput() → i_count == 0 → evict() → I_FREEING → destroy_inode()

File Locking

POSIX Lock (fcntl)

// POSIX advisory/mandatory locks
struct flock {
    short l_type;       // F_RDLCK / F_WRLCK / F_UNLCK
    short l_whence;     // SEEK_SET/SEEK_CUR/SEEK_END
    off_t l_start;
    off_t l_len;
    pid_t l_pid;        // PID of the process holding the lock (filled for GETLK)
};

fcntl(fd, F_SETLK, &lock);   // non-blocking
fcntl(fd, F_SETLKW, &lock);  // blocking wait
fcntl(fd, F_GETLK, &lock);   // query

// Implementation: fs/locks.c
// Locks are attached to inode->i_flctx (file_lock_context)
// Conflict detection: posix_locks_conflict() → traverse conflict chain
// Process closes fd → automatically releases all POSIX locks (regardless of fork relationship)

OFD Lock (Open File Description, 3.15+)

// Problem with POSIX locks: different fds of the same process cannot hold locks independently
// OFD lock solves this: locks are attached to the "open file description" (file), not the process
fcntl(fd, F_OFD_SETLK, &lock);

// fork scenario:
//   POSIX: child process inherits parent's locks (because same PID)
//   OFD:   child process does not inherit (different file, not necessarily)

FLOCK (BSD Lock)

flock(fd, LOCK_SH);  // shared
flock(fd, LOCK_EX);  // exclusive
flock(fd, LOCK_UN);  // unlock

// Differences from POSIX locks:
//   FLOCK is attached to the file (not inode, not process)
//   Multiple flock calls on the same file are automatically associated
//   fork inherits flock (because file is shared)
//   Advisory only (no mandatory locks)

fsync / fdatasync: Durability Guarantees

// fs/sync.c
SYSCALL_DEFINE1(fsync, unsigned int, fd)do_fsync(fd, 0)vfs_fsync(file, 0)
    ├─ file_write_and_wait_range()  // 1. Write back dirty page cache
    │   └─ __filemap_fdatawrite_range()do_writepages()
    │       → for each dirty folio → a_ops->writepage()
    │   └─ __filemap_fdatawait_range()  // wait for writeback completion
    │
    └─ vfs_fsync_range()
        └─ f_op->fsync()  // 2. Filesystem: write journal / force flush
            → ext4: jbd2 commit transaction + blkdev_issue_flush
            → XFS: force log flush + flush
            → NFS: COMMIT operation

// fdatasync (FDATASYNC): only flushes data part (does not flush i_atime/i_mtime)
//   → vfs_fsync_range(file, start, end, 1)
//   → filesystem can skip timestamp synchronization

Zero-Copy: sendfile / splice / vmsplice

// sendfile(): file → socket, bypasses user space
sendfile(out_fd, in_fd, &offset, count)do_sendfile()
    └─ splice_file_to_pipe() → pipe buffer ← file content
        └─ splice_pipe_to_socket() → pipe buffer → socket

// splice(): pipe transfer between two fds (kernel space)
splice(in_fd, NULL, pipefd[1], NULL, count, 0);  // fd → pipe
splice(pipefd[0], NULL, out_fd, NULL, count, 0);  // pipe → fd

// vmsplice(): user space memory → pipe (used with splice)
vmsplice(pipefd[1], iov, nr_segs, SPLICE_F_GIFT);
  // user memory pages are "gifted" to the kernel (no longer user-space)
  // → pipe references these pages → splice to socket → zero-copy to NIC

DMA Zero-Copy Path

Zero-Copy Path Comparison: CPU Copy Counts Decrease from 2 to 0 Path A · Traditional Read/Write Disk page cache User Buffer socket buffer NIC 4 context switches · 2 CPU copies Path B · sendfile Disk page cache socket buffer NIC 2 context switches · 1 CPU copy Path C · sendfile + DMA scatter-gather (Recommended) Disk page cache NIC (DMA Direct Fetch) 2 context switches · 0 CPU copies! Copy counts drop steadily: Traditional 2 → sendfile 1 → sendfile+DMA 0; Context switches 4 → 2 → 2. Zero-copy relies on NIC support for TX checksum offload + SG — almost all modern NICs satisfy this.

Debugging

# inode cache statistics
cat /proc/sys/fs/inode-nr      # nr_inodes, nr_free_inodes
cat /proc/sys/fs/inode-state   # detailed inode usage status

# View process file locks
cat /proc/locks                 # all active POSIX/OFD/FLOCK locks
lslocks                         # better human-readable format

# Trace fsync behavior
strace -e trace=fsync,fdatasync -p <pid>

References and Extensions

  • Kernel Documentation: Documentation/filesystems/locks.rst, Documentation/filesystems/vfs.rst
  • LWN:
    • "Better file locking with OFD locks" (lwn.net/Articles/586904/)
    • "Splice and sendfile" (lwn.net/Articles/178199/)
  • Source Files:
    • fs/locks.c — POSIX/OFD/FLOCK implementation
    • fs/sync.c — fsync/fdatasync
    • fs/splice.c — splice/sendfile/vmsplice
    • fs/inode.c — inode lifecycle

Keywords: VFS, inode state, POSIX lock, OFD lock, FLOCK, fsync, fdatasync, sendfile, splice, zero-copy