Bug #23136 » 23136-chatgpt.md
The Prompt
I am writing a Linux filesystem in userspace. This filesystem lets users read and write data stored on a remote network server. Multiple users may make changes to the data on the remote network server at the same time. My filesystem implementation periodically checks the data on the remote network server for changes. I want to ensure that I use the right FUSE functions to ensure that filesystem users always see data in the filesystem that's up-to-date with data on the remote network server.
When one of my periodic checks determines that data on the remote network server has changed, what FUSE function(s) should I call to ensure that the most up-to-date file content and directory listings are shown to the filesystem user? Please show the full signature of these function(s) and describe the circumstances when they should be called. Please cite your sources. Please link to any reference documentation about these function(s).
The Answer
If your userspace FUSE filesystem periodically detects that remote-server data has changed (for example, some file’s content or directory entries have been modified externally), you’ll need to notify the FUSE kernel side to discard or invalidate its cached state (attributes, dentries, pages) so that future user accesses will be forced to re‑fetch fresh data. The right approach depends on what changed (a file, a directory entry, or the contents of a directory) and which FUSE API level you are using (lowlevel libfuse, highlevel, or a binding such as llfuse/pyfuse3). Below is a sketch of the recommended functions and when to call them.
Which FUSE “notify / invalidate” functions exist
If you’re using the low-level interface (i.e. you manage inodes and issue your own replies), libfuse provides the following key functions in fuse_lowlevel.h:
int fuse_lowlevel_notify_inval_inode(struct fuse_session *se,
fuse_ino_t ino,
off_t off,
off_t len);
int fuse_lowlevel_notify_inval_entry(struct fuse_session *se,
fuse_ino_t parent,
const char *name,
size_t namelen);
int fuse_lowlevel_notify_expire_entry(struct fuse_session *se,
fuse_ino_t parent,
const char *name,
size_t namelen,
enum fuse_expire_flags flags);
int fuse_lowlevel_notify_store(struct fuse_session *se,
fuse_ino_t ino,
off_t offset,
struct fuse_bufvec *bufv,
enum fuse_buf_copy_flags flags);
(Additional variants such as fuse_lowlevel_notify_delete may also exist in recent versions, but are not always applicable.) (LibFuse)
Meaning and semantics
-
fuse_lowlevel_notify_inval_inode(se, ino, off, len):
This tells the kernel to invalidate (i.e. drop) cached pages and possibly attributes for the given inode number.- If
offis negative, it means “attributes only” (i.e. invalidate stat metadata). - If
len == 0, it means “invalidate from offsetoffto end of file” (i.e. entire file). - If the filesystem has writeback caching enabled, the call first triggers writeback of dirty pages, and will block until writeback completes (but not necessarily wait for all pending writebacks to complete). (LibFuse)
- If the kernel does not support this operation (i.e. FUSE protocol version too low), it returns
-ENOSYSand is a no-op. (LibFuse)
- If
-
fuse_lowlevel_notify_inval_entry(se, parent, name, namelen):
Invalidate the directory entry(parent, name), i.e. tell the kernel to forget the dentry fornamein directoryparentand thus next time a lookup or readdir must go to your filesystem again. It also invalidates the parent’s attributes. (LibFuse) -
fuse_lowlevel_notify_expire_entry(se, parent, name, namelen, flags):
A variant that “expires” rather than forcefully invalidates a dentry: i.e. the next access will revalidate. For many use cases,notify_expireis safer when there are mount overlays (submounts) such that you don’t inadvertently detach submounts. (LibFuse) -
fuse_lowlevel_notify_store(se, ino, offset, bufv, flags):
Instead of invalidating, this actively pushes updated file contents into the kernel’s page cache. In effect, you can update the kernel cache with fresh data you fetched, so that subsequent reads don’t have to go back to your userspace. The kernel then treats those pages as up-to-date. (If the pushed region extends beyond the file size, the file may be extended.) (LibFuse)In the libfuse example
notify_store_retrieve.c, they combinefuse_lowlevel_notify_store()with invalidation to make live updates visible. (Duke People)
If you use a higher-level binding (e.g. llfuse, pyfuse3), those libraries typically wrap or provide analogous calls:
-
In llfuse:
llfuse.invalidate_inode(fuse_ino_t inode, bool attr_only = false)llfuse.invalidate_entry(fuse_ino_t parent, const char *name)-
llfuse.notify_store(...)
(This is documented in llfuse’s API docs.) (Rath)
-
In pyfuse3:
pyfuse3.invalidate_inode(inode, attr_only=False)-
pyfuse3.invalidate_entry(inode_p, name, deleted=0)
These wrap the low-level notify operations. (Rath)
When to call which notification (in your periodic sync loop)
Given your scenario (you poll the remote server, detect that e.g. file X changed or a directory D changed), here is a recipe:
-
If a file’s content changed
- Call
fuse_lowlevel_notify_inval_inode(se, ino, 0, 0)to invalidate its entire cached contents (or supply a non-zero offset/length if only a region changed). - Optionally, after fetching the new contents, you could push them with
fuse_lowlevel_notify_store()so that the kernel's page cache is primed with fresh data (thus avoiding a read round-trip). - If you also changed the file’s size or metadata (mtime, permissions, etc.), you might pass a negative
offto invalidate just attributes, or reissue getattr from your side so that futuregetattrcalls yield the updated metadata.
- Call
-
If a directory’s entries changed (e.g. a file was added, removed, or renamed inside directory
D)- Use
fuse_lowlevel_notify_inval_entry(se, dir_ino, name, strlen(name))for the affected child name(s) to invalidate that dentry. Then on next lookup or readdir, the kernel must ask yourlookuporreaddirplusroutines. - (Alternatively,
fuse_lowlevel_notify_expire_entrymay be preferable if you don’t want to completely detach submounts.) - You might also consider invalidating the parent directory inode attributes (i.e. the link count) by calling
fuse_lowlevel_notify_inval_inodeon the parent withoff < 0.
- Use
-
If many files changed at once
- You might iterate and do multiple per-inode and per-entry invalidations.
- Some recent kernel patches are proposing an “invalidate all inodes” notify, but that is not yet widely available or stable. (Patchew)
- In practice, your filesystem might maintain a list of currently cached inodes (or active handles) and invalidate them selectively.
-
Do not call these from within FUSE request handlers or while holding internal locks
- Because
notify_inval_entryandnotify_inval_inodemight send asynchronous messages to the kernel, you must avoid deadlock: don’t call them inside your ownlookup,readdir,open, etc., or while holding locks that might block FUSE message processing. (LibFuse) - It’s safer to do cache invalidations from a separate thread or callback outside of direct request handling.
- Because
-
Fallback if kernel does not support notify
- If
fuse_lowlevel_notify_inval_inode()ornotify_inval_entry()returns-ENOSYS, the kernel is too old or protocol version is lacking; in that case you cannot force the kernel to drop its caches, and you have to rely on short caching timeouts (e.g. lowentry_timeout, smallattr_timeout, or disablingnodevcaching policies). This is less ideal but sometimes necessary.
- If
-
Syncing writeback state
- Be careful if your userspace filesystem allows writes: before invalidating, ensure all dirty pages have been flushed to your remote server. The FUSE kernel side will trigger writeback before doing invalidation, but you should make sure your writeback logic is robust to avoid races. (LibFuse)
Example: a dynamic file example
As an illustrative example, the notify_inval_inode.c example in the libfuse sources shows a file whose content is the current time. The userspace thread periodically invalidates the file’s inode (so cached content is dropped), then when user reads, the filesystem will recalc the content and return a fresh string. (LibFuse)
Similarly, the notify_store_retrieve.c example uses fuse_lowlevel_notify_store() to actively push fresh content so the kernel can satisfy reads without going back to userspace. (Duke People)
Caveats & practical considerations
- Invalidating too aggressively can degrade performance (lots of re-fetches). Use invalidation only when you know something changed.
- Be mindful of concurrency: if a user is concurrently doing
read()while your validation runs, the kernel might interleave operations. - For directories with large fan-out, invalidating many entries singly may be expensive.
- Always check return values of the notify calls (i.e.
-ENOSYS) and handle fallback. - In some environments (e.g. older kernels, older FUSE protocol) notify operations may not be supported.
In summary: when your sync loop detects a file’s content changed, call fuse_lowlevel_notify_inval_inode (or equivalent in your binding) so that the kernel drops its cache of that inode; when directory contents changed, call fuse_lowlevel_notify_inval_entry on those names to force re-lookup next time. Optionally, push fresh contents with notify_store. Do these calls from outside the FUSE request path, and check for -ENOSYS fallback.
If you like, I can help you sketch the appropriate integration in your userspace code (e.g. in a thread) or in your binding (llfuse / pyfuse3) — would you like me to?