Linux-Kernel-Notes

Thinking in linux kernel

View on GitHub

1.1 Boundary and Overall Architecture

中文 English Contents

Chapter 1: FUSE Subsystem Design · Article 1 of 4

FUSE is not simply “a filesystem implemented in user space.” It is a split filesystem architecture: the kernel keeps the VFS-facing half, while a user-space daemon implements filesystem policy. The protocol boundary between them is the central design fact from which performance, consistency, security, and failure behavior follow.

1. Learning goals

After this article, you should be able to:

2. The architectural boundary

A native filesystem usually resolves a VFS operation entirely inside the kernel. FUSE inserts a request/reply protocol between the VFS operation and the filesystem implementation:

application
    |
    | openat/read/write/stat/mmap/...
    v
Linux VFS and page cache
    |
    | inode_operations / file_operations / address_space_operations
    v
FUSE kernel client
    |
    | FUSE request/reply protocol
    v
transport: /dev/fuse, io_uring, or virtio-fs
    |
    v
user-space daemon or host-side backend
    |
    v
backing store, remote service, archive, object store, or synthetic data

The application still sees ordinary POSIX syscalls. It does not know whether the operation is serviced by ext4, a FUSE daemon, or a remote backend.

3. Responsibilities on each side

3.1 Kernel-side responsibilities

The kernel client must integrate with invariants that cannot be delegated:

The kernel is therefore a stateful protocol client, not a thin syscall forwarder.

3.2 Daemon-side responsibilities

The daemon supplies filesystem policy and backend integration:

A daemon may use a local directory, a database, an RPC service, or no persistent storage at all.

4. A syscall is not always one FUSE request

The VFS works on objects and caches, while the protocol exchanges messages. Their boundaries do not match one-to-one.

One openat() may require several LOOKUP requests and an OPEN; or zero requests if dentries and attributes remain valid. One large read() may be split into several READ requests. A page fault may issue I/O without a new read() syscall. Conversely, readahead and writeback can create requests that have no direct syscall counterpart.

This is why diagnosing FUSE solely from syscall traces is incomplete. You must also observe the protocol and cache state.

5. Metadata path example

For stat("/mnt/a/b"), the conceptual path is:

  1. VFS begins from the mount root.
  2. It checks the dentry cache for a and then b.
  3. For a missing or expired component, FUSE sends FUSE_LOOKUP(parent_nodeid, name).
  4. The daemon returns a node ID, generation, attributes, and validity intervals.
  5. The kernel instantiates or updates the dentry and inode.
  6. If cached attributes expire later, FUSE_GETATTR may refresh them.

A lookup reply is therefore both a namespace result and a time-bounded cache lease.

6. Data path example

For a buffered read(fd, buf, len):

  1. VFS enters the FUSE file operations.
  2. The page cache satisfies already-cached ranges.
  3. Missing folios cause one or more FUSE_READ requests.
  4. The daemon fetches data and writes a reply.
  5. The kernel fills the page cache and copies data to the application.

Direct I/O bypasses the page cache and divides the user range into protocol requests. Passthrough can redirect operations to a backing kernel file. DAX maps file ranges through a finite device window and services faults through filesystem DAX helpers. These modes have different coherency and fallback constraints; they are not interchangeable optimizations.

7. Transport variants

7.1 Classic /dev/fuse

A daemon reads requests from a FUSE device file and writes replies back. The kernel maintains pending and processing queues, while each request carries a unique identifier for reply matching.

7.2 FUSE over io_uring

Newer kernels can negotiate an io_uring-based transport. It changes how buffers and completion are delivered, but not the VFS-facing architecture or the meaning of protocol operations.

7.3 virtio-fs

virtio-fs reuses the FUSE protocol across a virtio transport, normally between a guest kernel and a host backend such as virtiofsd. It can also expose a DAX window for shared mappings. This removes some copies and VM exits on suitable workloads, but introduces finite-window allocation, mapping recall, and guest/host coherency questions.

8. Cost model

Potential costs include:

The largest optimization opportunities usually come from reducing request count, increasing useful batching, selecting the right data path, and assigning cache validity intervals that match the consistency model.

9. Design questions to ask first

Before tuning or extending FUSE, answer these questions:

  1. Which side is authoritative for names, attributes, and file contents?
  2. Can the backend change without going through this mount?
  3. Which objects may be cached, for how long, and who invalidates them?
  4. What happens when the daemon stalls, crashes, or replies late?
  5. Can the chosen data paths coexist without incoherent aliases?
  6. Which resources are bounded: requests, daemon workers, DAX mappings, or backing handles?
  7. Does a proposed fallback preserve the original I/O position and partial-completion semantics?

These questions are more useful than treating every latency spike as merely “user-space overhead.”

10. Summary

FUSE splits one filesystem into a kernel protocol client and a user-space policy engine. The kernel preserves VFS semantics and caches; the daemon defines the filesystem and accesses its backend. The protocol is the architectural seam, and all later topics—identities, leases, queueing, timeout, DAX, and teardown—must be understood relative to that seam.

Next: Object Model and Protocol