VFS Dentry Object

The dentry (directory entry; struct dentry, defined in include/linux/dcache.h) is the kernel object that binds one name to one inode within one parent directory. It is the building block of the directory tree as the kernel sees it in memory: a dentry stores a path component (d_name), a pointer to its parent dentry (d_parent), and a pointer to the inode it names (d_inode). Unlike the inode and superblock, the dentry has no on-disk equivalent — it exists purely as an in-memory cache, the dcache, so that resolving a path like /usr/bin/env becomes a sequence of in-memory hash lookups instead of repeated disk reads of directory blocks. Because many dentries can point at the same inode, the dentry is also where hard links become legible: each name is a separate dentry, all sharing one inode. This note describes the 6.12 LTS (2024-11-17) object; the cache’s eviction/RCU machinery is owned by The Dentry Cache and RCU-Walk and Ref-Walk.

Why Dentries Exist

A filesystem on disk stores directories as some structure mapping names to inode numbers — for ext4, blocks of (name, inode#) records; for XFS, a B+tree. To resolve /usr/bin/env from those structures alone, the kernel would have to read the / directory’s blocks, find usr, read its inode, read its directory blocks, find bin, and so on — a disk access per component, every single time, even for paths walked thousands of times a second. The dentry is the kernel’s answer: an in-memory object caching the result “in directory D, the name n resolves to inode I.” Once /usr, /usr/bin, and /usr/bin/env each have a cached dentry, the next lookup of that path touches no disk at all — it is a walk through a hash table.

The path-lookup documentation states it precisely: “Each entry (known as a ‘dentry’) contains three significant fields: a component name, a pointer to a parent dentry, and a pointer to the ‘inode’ which contains further information about the object in that parent with the given name.” That three-field core — name, parent, inode — is the entire conceptual payload; everything else in struct dentry is bookkeeping for caching, locking, and the parent/child tree.

A second, equally important reason: the dentry is more than a cache. The mount table is expressed in terms of dentries — “What the mount table actually stores is which dentry is mounted on top of which other dentry” (path-lookup.rst). And the dentry is where the kernel records the absence of a name: a lookup that fails leaves a negative dentry (d_inode == NULL) so that repeated lookups of a non-existent path are also cache hits. (The negative-dentry mechanism is detailed in Dentry States and Negative Dentries.)

Mental Model

flowchart TB
  ROOT["dentry '/'<br/>d_parent = itself"]
  USR["dentry 'usr'<br/>d_parent -> /"]
  BIN["dentry 'bin'<br/>d_parent -> usr"]
  ENV["dentry 'env'<br/>d_parent -> bin<br/>d_inode -> inode 8231"]
  ROOT --> USR --> BIN --> ENV
  ENV -->|d_inode| IN["struct inode 8231<br/>i_nlink = 2"]
  ENV2["dentry 'python3'<br/>(another name, e.g. hardlink)<br/>d_inode -> inode 8231"]
  ENV2 -->|d_inode| IN
  IN -->|i_dentry hlist| ENV
  IN -->|i_dentry hlist| ENV2

Dentries form the path tree; inodes sit underneath. What it shows: the components of /usr/bin/env are four dentries chained by d_parent; the leaf’s d_inode points at inode 8231. A second dentry (python3) is a hard link — a different name whose d_inode points at the same inode. The inode’s i_dentry list links back to both names. Insight: the directory hierarchy lives in the dentry layer (names + parent pointers), while file identity lives in the inode layer; a hard link is two dentries, one inode — exactly why i_nlink == 2.

The Object — Field by Field

The 6.12 struct dentry (dcache.h, line 82) is laid out with deliberate cache-line discipline — the fields touched during a lockless RCU path walk are packed first:

struct dentry {
    /* RCU lookup touched fields */
    unsigned int d_flags;          /* DCACHE_* flags, protected by d_lock */
    seqcount_spinlock_t d_seq;     /* per-dentry seqlock for lockless walk */
    struct hlist_bl_node d_hash;   /* link in the global lookup hash */
    struct dentry *d_parent;       /* parent directory's dentry */
    struct qstr d_name;            /* this component's name (+ precomputed hash) */
    struct inode *d_inode;         /* the inode, or NULL == negative dentry */
    unsigned char d_iname[DNAME_INLINE_LEN];  /* inline storage for short names */
    /* --- cacheline 1 boundary --- */
 
    /* Ref lookup also touches following */
    const struct dentry_operations *d_op;  /* per-fs dentry ops (may be NULL) */
    struct super_block *d_sb;      /* the filesystem this dentry belongs to */
    unsigned long d_time;          /* used by d_revalidate */
    void *d_fsdata;                /* filesystem-private data */
    /* --- cacheline 2 boundary --- */
    struct lockref d_lockref;      /* combined spinlock + reference count */
 
    union {
        struct list_head d_lru;    /* LRU list when unused */
        wait_queue_head_t *d_wait; /* for in-lookup dentries */
    };
    struct hlist_node d_sib;       /* this dentry as a child of its parent */
    struct hlist_head d_children;  /* our own children */
    union {
        struct hlist_node d_alias;        /* link in inode's i_dentry list */
        struct hlist_bl_node d_in_lookup_hash;
        struct rcu_head d_rcu;
    } d_u;
};

The fields, grouped, with the lock that protects each

struct dentry is unusual among kernel objects in that its field order is load-bearing. The header comments mark the split explicitly — /* RCU lookup touched fields */ for the first group, /* Ref lookup also touches following */ for the second, and literal cache-line boundary markers at 64 and 128 bytes. The first seven members are exactly what a lockless RCU walk must read to decide “is this the child I want?”, so they are packed into one cache line and the walk touches nothing else on a hit.

The locking column is not guessable from the struct; it is transcribed from the “Usage:” comment at the top of fs/dcache.c (lines 40–74), which is the kernel’s authoritative statement and reads, in part: “d_lock protects: d_flags, d_name, d_lru, d_count, d_unhashed(), d_parent and d_children, childrens’ d_sib and d_parent, d_u.d_alias, d_inode” — while “dcache->d_inode->i_lock protects i_dentry, d_u.d_alias, d_inode of aliases.”

JobFieldsWhat it meansProtected by
The binding itselfd_name, d_parent, d_inodethe three fields path-lookup.rst calls “significant”: a component name, a pointer to the parent, and a pointer to the inode — or NULL, which is a negative dentryd_lock; d_inode and the alias linkage additionally under the inode’s i_lock
Inline name storaged_iname[DNAME_INLINE_LEN]the characters of short names, stored in the dentry itselfd_lock (via d_name); published to RCU readers with smp_store_release
Lockless-walk machineryd_seq, d_hash, d_flagsper-dentry seqcount validating an RCU read; the hlist_bl_node linking into the global hash; the DCACHE_* bit setd_seq is its own seqlock; d_hash under the per-bucket bit-spinlock; d_flags under d_lock
Filesystem identityd_sb, d_op, d_fsdata, d_timewhich mounted filesystem this name lives in, the optional per-fs vtable, fs-private scratch, and a timestamp for d_revalidated_sb/d_op set at allocation and read-only afterwards; d_fsdata/d_time are the filesystem’s business
Reference + lock, fusedd_lockrefa lockref: a spinlock and a reference count in one 64-bit word. d_lock is literally #define d_lock d_lockref.lockitself — the point is that lockref_get() can bump the count with one CAS without taking the lock
Tree linkaged_children, d_sibour children (an hlist_head) and our own link in the parent’s child listd_lock — and, for the sibling links, the parent’s d_lock
Reclaim positiond_lru (union with d_wait)the per-superblock LRU list when the dentry is unusedsb->s_dentry_lru_lock for the list, d_lock for the member
Alias / teardownd_u.d_alias | d_u.d_in_lookup_hash | d_u.d_rculink in the inode’s i_dentry list, or in the parent’s in-lookup hash, or the RCU callback headd_alias under the inode’s i_lock; the others by lifecycle phase

The ordering constraint the comment states is also worth memorising, because it is the one that catches people writing dcache code: i_lock outside d_lock outside s_dentry_lru_lock/the hash-bucket lock, and among dentries, ancestor before descendant — or, if there is no ancestor relationship, any order, “since it’s serialized on rename_lock.”

Two unions, both keyed on lifecycle phase. d_lru/d_wait is the smaller one: a dentry that is in lookup is not on any LRU (it is in use by definition), so the list head can hold a wait-queue pointer instead, which is how other threads block on an in-progress lookup. The three-way d_u union is the same idea taken further: a dentry has an alias link only while positive, an in-lookup-hash link only while a lookup is running, and an rcu_head only while dying — never two at once. That is roughly 40 bytes saved per dentry across a cache with tens of millions of entries.

Why the inline name buffer is 40 bytes. DNAME_INLINE_LEN is defined in dcache.h (lines 65–77) with the sizing rationale written directly into the code:

/*
 * Try to keep struct dentry aligned on 64 byte cachelines (this will
 * give reasonable cacheline footprint with larger lines without the
 * large memory footprint increase).
 */
#ifdef CONFIG_64BIT
# define DNAME_INLINE_LEN 40 /* 192 bytes */
#else
# ifdef CONFIG_SMP
#  define DNAME_INLINE_LEN 36 /* 128 bytes */
# else
#  define DNAME_INLINE_LEN 44 /* 128 bytes */
# endif
#endif

The number 40 is not a guess about filename lengths — it is whatever makes sizeof(struct dentry) come out at exactly 192 bytes, three 64-byte cache lines, on 64-bit. The comment says so on the same line. On 32-bit the target is 128 bytes, and the constant is 36 or 44 depending on CONFIG_SMP (because seqcount_spinlock_t and lockref change size). Treat 40 as a 64-bit default, not a universal.

Notes on the individual fields

d_name (struct qstr) holds the path component. A qstr (“quick string”) is { name, len, hash } packed so that len and the precomputed name hash share a single 64-bit hash_len word — meaning the hash a path walk needs is already computed and stored, not recomputed on each lookup. For short names the characters live inline in d_iname[DNAME_INLINE_LEN] (the embedded buffer), avoiding a second allocation; only names longer than the inline buffer get a separate external_name allocation. This is verified in __d_alloc() (fs/dcache.c, line ~1630): if (name->len > DNAME_INLINE_LEN-1) it kmallocs external storage, else it uses d_iname. (DNAME_INLINE_LEN and why it is 40 on 64-bit are covered above.)

d_inode is the inode this name resolves to — or NULL, which is the definition of a negative dentry (the comment in dcache.h reads literally /* Where the name belongs to - NULL is negative */). path-lookup.rst notes that “The association between a dentry and its inode is fairly permanent”: when a file is renamed, the dentry and inode move together; the binding changes mainly at creation (negative → positive) and deletion.

d_parent points at the dentry of the containing directory; the root dentry’s d_parent points at itself (set in __d_alloc: dentry->d_parent = dentry;). Following d_parent repeatedly walks back up toward the filesystem root — this is how d_path() reconstructs an absolute pathname and how .. is resolved. The reverse direction (a directory’s children) is the d_children hlist, with each child linked via its d_sib node — but, as path-lookup.rst notes, that linkage is not used for forward path lookup; lookup goes through the global hash, not by scanning a parent’s child list.

d_sb (struct super_block *) records which mounted filesystem the dentry belongs to. It is needed even before d_inode is set (negative dentries have no inode but still belong to a filesystem), and d_op->d_compare/d_hash may consult d_sb to honor case-insensitivity or encoding.

d_op (const struct dentry_operations *) is the per-filesystem hook table — almost always NULL for ordinary local filesystems, because the VFS defaults are correct. Its members let a filesystem override caching policy: d_revalidate (re-check a cached dentry against the server — vital for FUSE and network filesystems, NULL for ext4/XFS because their dcache is always authoritative), d_hash/d_compare (custom name hashing/comparison for case-insensitive or encoded names), d_delete (whether to cache or immediately drop on final reference), d_release/d_iput (cleanup hooks), d_automount/d_manage (autofs), and d_real (overlayfs, to reach the underlying real dentry). The full table, with each method’s contract and the locks the VFS holds when it calls it, is enumerated below under dentry_operations — the Optional Vtable; the cross-table survey is in VFS Operation Tables.

d_lockref is a single lockref structure that fuses the per-dentry spinlock (d_lock, accessed as d_lockref.lock) and the reference count (d_lockref.count, surfaced via the d_count() helper) into one cache-line-friendly word. This is a deliberate scalability design: on the fast path, taking a reference can be a single atomic compare-and-swap on the combined word without ever acquiring the spinlock, so heavily-shared hot dentries (like / or /usr) don’t serialize every reference on a contended lock. dget() calls lockref_get(&dentry->d_lockref); dput() releases. The detailed dput kill-path and LRU mechanics belong to The Dentry Cache.

dentry_operations — the Optional Vtable

VFS Operation Tables covers why the VFS uses hand-written vtables and how it dispatches through them; this is the dentry’s own table, enumerated from dcache.h line 138, with two columns that live nowhere else in one place: what locks the VFS holds when it calls the method, and whether the method may block. Those come from the locking matrix in Documentation/filesystems/locking.rst; the contracts come from vfs.rst. The header itself points at both files and adds “Keep it updated!” twice — a hint that they do drift.

Every method may be NULL. vfs.rst is explicit: “These methods may be set to NULL, as they are either optional or the VFS uses a default.” Most local filesystems set d_op to NULL entirely.

MethodContractrename_lockd_lockMay blockCallable in rcu-walk
d_revalidatere-check a dentry found in the dcache. Return positive if still valid, zero or negative if not. “Most local filesystems leave this as NULL, because all their dentries in the dcache are valid”nonoyes (ref-walk only)maybe — must not block or store to the dentry, and d_parent/d_inode may change or become NULL underneath; return -ECHILD to be retried in ref-walk
d_weak_revalidaterevalidate a “jumped” dentry — one not reached by looking it up in its parent: /, ., .., procfs-style symlinks, mountpoint traversal. Cares that the inode is still valid, not that the name isnonoyesno — “only called after leaving rcu-walk mode”
d_hashoverride name hashing when adding to the hash table. First argument is the parentnononomaybe
d_compareoverride name comparison. “Must be constant and idempotent”, must not take locks, must not store into the dentry, and must not dereference d_parent/d_inode/d_name — only d_sb is guaranteedyesnonomaybe
d_deletecalled when the last reference drops and the dcache is deciding whether to cache. Return 1 to delete immediately, 0 to cache. Default NULL means “always cache a reachable dentry”noyesnono
d_initcalled when a dentry is allocatednonoyesno
d_releasecalled when a dentry is really deallocatednonoyesno
d_prunecalled before a dentry is pruned from the cache. Documented in locking.rst but absent from vfs.rst’s listnoyesnono
d_iputcalled when a dentry loses its inode, just before deallocation. If you define it, you must call iput() yourself — the default is that the VFS does itnonoyesno
d_dnamegenerate a pathname lazily. Used by pseudo-filesystems (sockfs, pipefs) so /proc/<pid>/fd/N can render pipe:[12345]. Must write the string at the end of the buffer and return a pointer to its first character — dynamic_dname() handles thisnononono
d_automountcreate and return a new mount record when an automount point is traversed. Returns the vfsmount with 2 references to prevent immediate expiry. Only called when DCACHE_NEED_AUTOMOUNT is setnonoyesno
d_managecontrol transit through a managed dentry (autofs blocking, -EISDIR to fall through)nonoyes (ref-walk)maybe
d_realresolve to the underlying real dentry on a stacking filesystem. Takes enum d_real_type (D_REAL_DATA or D_REAL_METADATA) — used by overlayfs to reach the lower or upper objectnonoyesno

Two design points are visible only in the table. First, d_compare is the only method that runs under rename_lock, and it is simultaneously the one with the harshest restrictions — no locks, no blocking, almost nothing safe to dereference. That is the price of being called from inside a lockless walk. Second, d_delete returning 1 is the opt-out from caching, which is the single knob a filesystem has to say “this name should not linger”, and its default (NULL ⇒ cache everything reachable) is exactly what makes the dcache a cache rather than a lookup table.

Uncertain

Verify: the d_delete prototype’s constness. Reason: dcache.h at v6.12 declares int (*d_delete)(const struct dentry *) while locking.rst at the same tag prints int (*d_delete)(struct dentry *) without the const; vfs.rst agrees with the header. Likewise locking.rst lists d_prune in its locking table but omits it from its prototype block. Both are documentation drift, not ambiguity in the interface. To resolve: read signatures from include/linux/dcache.h at the pinned tag and treat the .rst files as the source for semantics only. uncertain

How the VFS avoids even looking at d_op

Because d_op is NULL for most filesystems, dereferencing it on every lookup would be a wasted branch and a wasted cache line. The dcache instead caches the presence of each method as a flag in d_flags: d_set_d_op() sets DCACHE_OP_HASH, DCACHE_OP_COMPARE, DCACHE_OP_REVALIDATE, DCACHE_OP_WEAK_REVALIDATE, DCACHE_OP_DELETE, DCACHE_OP_PRUNE and DCACHE_OP_REAL according to which members are non-NULL. The hot path then tests a bit in a word it has already loaded — d_flags is the very first member of the struct — instead of chasing a pointer. d_real() in dcache.h shows the pattern literally:

static inline struct dentry *d_real(struct dentry *dentry, enum d_real_type type)
{
	if (unlikely(dentry->d_flags & DCACHE_OP_REAL))
		return dentry->d_op->d_real(dentry, type);
	else
		return dentry;
}

(dcache.h line 569.) This is why “assume d_op is present” is a bug and “assume d_op->d_revalidate is present” is a worse one: for ext4 or XFS both are NULL, and the flag bits are what the VFS actually consults.

The DCACHE_* Flags

d_flags is a 32-bit word, protected by d_lock, that encodes three different kinds of information at once: which operations exist (the DCACHE_OP_* group, derived from d_op), what kind of object the name resolves to (a 3-bit type field), and what state the dentry is in (everything else). The full set at v6.12 is enumerated from dcache.h lines 164–221.

FlagBitGroupMeaning
DCACHE_OP_HASH0ops cached_op->d_hash exists
DCACHE_OP_COMPARE1ops cached_op->d_compare exists
DCACHE_OP_REVALIDATE2ops cached_op->d_revalidate exists — the bit that makes NFS pay a per-lookup callback and ext4 not
DCACHE_OP_DELETE3ops cached_op->d_delete exists
DCACHE_OP_PRUNE4ops cached_op->d_prune exists
DCACHE_OP_WEAK_REVALIDATE11ops cached_op->d_weak_revalidate exists
DCACHE_OP_REAL26ops cached_op->d_real exists (stacking filesystems)
DCACHE_ENTRY_TYPE20–22 (mask 7 << 20)typethree bits holding the type of the thing named
DCACHE_MISS_TYPE0 << 20typenegative dentry — the name resolves to nothing
DCACHE_WHITEOUT_TYPE1 << 20typea whiteout: “stop pathwalk”. This is how overlayfs renders a file deleted in the upper layer but still present below
DCACHE_DIRECTORY_TYPE2 << 20typea normal directory (has ->lookup)
DCACHE_AUTODIR_TYPE3 << 20typea “lookupless directory (presumed automount)”
DCACHE_REGULAR_TYPE4 << 20typea regular file
DCACHE_SPECIAL_TYPE5 << 20typedevice node, FIFO, socket
DCACHE_SYMLINK_TYPE6 << 20typea symlink
DCACHE_DISCONNECTED5statethe dentry is not connected to the tree root — an NFS filehandle was resolved into an object with no known path
DCACHE_REFERENCED6state“recently used, don’t discard” — the second-chance bit for reclaim
DCACHE_DONTCACHE7state“purge from memory on final dput()”
DCACHE_CANT_MOUNT8statenothing may ever be mounted on this dentry. Set by dont_mount(), which vfs_rmdir(), vfs_unlink() and vfs_rename() call on the victim immediately after the filesystem removes it (fs/namei.c lines 4346, 4471, 5023)
DCACHE_GENOCIDE9stateset once per dentry by d_genocide() while tearing down a pseudo-filesystem tree, so that its one-time d_lockref.count-- cannot be applied twice (fs/dcache.c lines 3070–3083)
DCACHE_SHRINK_LIST10stateisolated onto the shrinker’s private list — no longer on the normal LRU
DCACHE_NFSFS_RENAMED12stateNFS silly-rename in progress
DCACHE_FSNOTIFY_PARENT_WATCHED14statethe parent directory is being watched, so events must be sent
DCACHE_DENTRY_KILLED15statedentry_kill() has committed to destroying this dentry
DCACHE_MOUNTED16statesomething is mounted on this dentry
DCACHE_NEED_AUTOMOUNT17statetraversing this directory must call d_automount
DCACHE_MANAGE_TRANSIT18statetraversal must call d_manage
DCACHE_LRU_LIST19statethe dentry is on its superblock’s LRU
DCACHE_NOKEY_NAME25statean fscrypt name encoded without the key available
DCACHE_PAR_LOOKUP28statein-lookup: a lookup for this (parent, name) is in progress, with the parent locked shared
DCACHE_DENTRY_CURSOR29statethis is a readdir position cursor, not a real name. Set by d_alloc_cursor() (fs/dcache.c line 1742) and skipped explicitly by tree walks and by libfs.c “to avoid livelocks”
DCACHE_NORCU30statefree without an RCU grace period

Three of these deserve to be read as a group, because together they are the “does the walk have to stop here?” test. DCACHE_MOUNTED, DCACHE_NEED_AUTOMOUNT and DCACHE_MANAGE_TRANSIT are OR’d into a single composite mask in the header:

#define DCACHE_MANAGED_DENTRY \
	(DCACHE_MOUNTED|DCACHE_NEED_AUTOMOUNT|DCACHE_MANAGE_TRANSIT)

so d_managed(dentry) is one and against one already-loaded word. An ordinary dentry fails that test in a single instruction and the walk continues; only a managed dentry takes the slow path into mount traversal. This is the mechanism behind Mount Point Traversal During Lookup — a mount is, from the dentry’s point of view, nothing but a bit in d_flags.

The type field is the other one worth internalising. It is not derived from the inode on each query: __d_entry_type() masks d_flags, so d_is_dir(), d_is_reg(), d_is_symlink() and d_is_negative() are all pure bit tests on a cache-hot word, with no inode dereference at all. DCACHE_MISS_TYPE being zero means a freshly allocated, never-instantiated dentry is a negative dentry by default — negativity is the absence of a type, not a flag that has to be set.

The relationship between dentries and inodes is many-to-one, and this is the structural fact that makes hard links comprehensible. A hard link (link(2)) creates a second directory entry — a second name — for an existing inode. In memory, that is a second struct dentry whose d_inode points at the same struct inode. The inode’s link count i_nlink is incremented to reflect the new name. Both names are equal first-class references to the file; there is no “original” and “copy.”

The kernel tracks the set of names pointing at an inode through the alias list: the inode’s i_dentry field (an hlist_head in fs.h) is the list head, and each dentry links into it via d_u.d_alias. So from a positive dentry you can reach its inode (d_inode), and from the inode you can enumerate every cached name pointing at it (walk i_dentry via d_alias). When you d_instantiate() a dentry with an inode, the dentry is added to that inode’s alias list and i_count is bumped; when a dentry loses its inode (the d_iput path), it is removed from the list. Most files have exactly one alias; hard-linked files have several; directories are constrained by POSIX to a single dentry alias (you cannot hard-link directories), which the VFS enforces. This note states the relationship from the name side; the inode side is in VFS Inode Object — together they describe one mechanism from two ends.

A symbolic link, by contrast, is not a second dentry on the same inode — it is its own inode (of type symlink) whose data is the target path string; resolving it is a separate step handled by Symlink Resolution and Loop Detection, not by the alias list.

Allocation and Instantiation

A dentry is created by d_alloc(parent, name) (fs/dcache.c, line 1714), which calls the internal __d_alloc. Read directly from the source, allocation:

  1. Allocates the struct dentry from the dedicated dentry_cache slab.
  2. Copies the name into d_iname (inline) or a fresh external_name allocation, NUL-terminating it; publishes the name pointer with smp_store_release so RCU readers always see a valid string.
  3. Initializes d_lockref.count = 1, d_flags = 0, the seqlock, and sets d_inode = NULL (it starts negative), d_parent = dentry (self, until parented).
  4. Inherits d_op from the superblock’s default (d_set_d_op(dentry, sb->s_d_op)), and calls d_op->d_init if present.

d_alloc then parents the new dentry: it takes dget_dlock(parent) (a reference on the parent, since a child pins its parent) and links itself into the parent’s d_children via d_sib. At this point the dentry exists and is negative.

To give it an inode, the filesystem’s lookup (or create/mkdir/etc.) calls d_instantiate() (or the combined d_add(), which also hashes the dentry into the global lookup table). The VFS docs describe d_instantiate as: “add a dentry to the alias hash list for the inode and updates the ‘d_inode’ member” — and it increments the inode’s i_count. After instantiation the dentry is positive and discoverable by d_lookup(). Releasing a reference is dput(); the VFS docs summarize: “decrements the usage count. If the usage count drops to 0, and the dentry is still in its parent’s hash, the ‘d_delete’ method is called to check whether it should be cached” — i.e. an unused dentry is normally retained in the cache (placed on an LRU), not freed, which is the whole point of the dcache. The eviction policy and shrinker are owned by The Dentry Cache.

The Life of One Dentry

The object has a lifecycle distinct from both the cache policy that governs it (The Dentry Cache) and the positive/negative taxonomy (Dentry States and Negative Dentries). Read from fs/dcache.c and fs/namei.c, one dentry passes through five conditions.

1. Allocated, unparented. __d_alloc() carves a struct dentry out of dentry_cache, copies the name into d_iname or an external allocation, sets d_lockref.count = 1, d_inode = NULL, and d_parent = dentry (itself). It is negative by construction, because DCACHE_MISS_TYPE is 0 << 20 and d_flags starts at zero.

2. In lookup (DCACHE_PAR_LOOKUP). The slow path does not call d_alloc() directly. __lookup_slow() calls d_alloc_parallel(dir, name, &wq), which either returns an existing dentry or publishes a new one into the parent’s in-lookup hash with DCACHE_PAR_LOOKUP set and d_wait pointing at the caller’s on-stack wait queue. A second thread looking up the same name in the same directory finds that entry and waits rather than allocating a rival dentry. This is the machinery Al Viro built to replace the directory i_mutex with a shared-mode i_rwsem: the requirement was that “there are never two hashed dentries with the same parent and name at the same time”, and the object-level trick was that a dentry mid-lookup has fields that “are unused at that point, so they can be repurposed” (LWN, VFS parallel lookups, 2016). lookup_slow() therefore wraps __lookup_slow() in inode_lock_shared(inode) / inode_unlock_shared(inode) — a shared lock on the directory inode, which is why many threads can resolve different names in one hot directory at once.

3. Negative or positive. The filesystem’s ->lookup() runs. vfs.rst states the contract exactly: it “must call d_add() to insert the found inode into the dentry… If the named inode does not exist a NULL inode should be inserted into the dentry (this is called a negative dentry). Returning an error code from this routine must only be done on a real error.” That last sentence is the load-bearing one — a missing file is not an error at this layer, it is a successful lookup with a NULL result, and returning -ENOENT here would break create(2), mknod(2) and mkdir(2), all of which need the negative dentry the failed lookup produced. d_lookup_done() then clears DCACHE_PAR_LOOKUP and wakes the waiters.

4. In use, then unused. While d_lockref.count > 0 the dentry is pinned — by an open file, by being someone’s cwd or root, or by being an ancestor of any of those, since a child holds a reference on its parent. When the count reaches zero, dput() consults d_op->d_delete (if DCACHE_OP_DELETE is set) and, by default, retains the dentry: it goes on the superblock LRU with DCACHE_LRU_LIST set. vfs.rst puts it plainly: dput “decrements the usage count. If the usage count drops to 0, and the dentry is still in its parent’s hash, the d_delete method is called to check whether it should be cached.” Retention is the default and it is the whole point of a cache.

5. Killed. Either the shrinker isolates it (DCACHE_SHRINK_LIST) or an explicit invalidation unhashes it. dentry_kill() sets DCACHE_DENTRY_KILLED, d_iput (or the default iput()) releases the inode and removes the dentry from the inode’s i_dentry alias list, d_release runs, and the object is freed — after an RCU grace period unless DCACHE_NORCU is set, because a concurrent RCU walk may still be reading it.

stateDiagram-v2
  [*] --> Alloc: __d_alloc() from dentry_cache<br/>count = 1, d_inode = NULL
  state "Allocated, unparented" as Alloc
  state "In lookup (DCACHE_PAR_LOOKUP)" as InLookup
  state "Negative (DCACHE_MISS_TYPE)" as Neg
  state "Positive, in use (count > 0)" as Pos
  state "Unused, on LRU (DCACHE_LRU_LIST)" as Lru
  state "On shrink list (DCACHE_SHRINK_LIST)" as Shrink
  state "Killed (DCACHE_DENTRY_KILLED)" as Killed

  Alloc --> InLookup: d_alloc_parallel() publishes it<br/>into the parent in-lookup hash
  InLookup --> Neg: i_op-&gt;lookup() found nothing<br/>d_add(dentry, NULL)
  InLookup --> Pos: i_op-&gt;lookup() found an inode<br/>d_add / d_splice_alias
  Neg --> Pos: create, mkdir, mknod, link<br/>d_instantiate() fills d_inode
  Pos --> Neg: d_delete() on unlink<br/>when count == 1 (sole user)
  Pos --> Killed: d_delete() on unlink when others<br/>hold it - __d_drop leaves it unhashed
  Pos --> Lru: dput() to count 0<br/>d_delete absent or returns 0
  Neg --> Lru: dput() to count 0<br/>negatives are cached too
  Lru --> Pos: a later lookup hits<br/>lockref_get, DCACHE_REFERENCED
  Lru --> Shrink: shrinker isolates a batch
  Shrink --> Killed: dentry_kill()
  Pos --> Killed: d_delete() returns 1<br/>or the dentry is unhashed
  Killed --> [*]: d_iput, d_release,<br/>RCU-deferred free

The lifecycle of a single struct dentry. What it shows: a dentry is born negative, spends a window in the in-lookup state where rival lookups block on it, and thereafter oscillates between in-use and LRU — the two transitions that actually dominate a running system. Insight: there is no arrow from “negative” to “freed” that the kernel takes eagerly. A failed lookup does not clean up after itself: it leaves a cached object that only the shrinker will ever reclaim, which is exactly why probing for names that do not exist is a memory-growth workload. Note also the two unlink arrows: d_delete() is documented as “turn the dentry into a negative dentry if possible, otherwise remove it from the hash queues so it can be deleted later” (fs/dcache.c lines 2387–2404), and the test for “possible” is d_lockref.count == 1 — we are the only user. If anything else holds the dentry (an open file, say), it is __d_drop()ped instead: unhashed, invisible to every future lookup, and freed only when the last reference goes. That unhashed-but-alive state is what d_path() renders with the (deleted) suffix.

Watching It On a Running System

The measurements below were taken on Linux 7.1.13-200.fc44.x86_64 (Fedora, x86-64, btrfs on /home) on 2026-09-12, as uid 1000. That kernel is newer than the v6.12 source this note is pinned to, so the numbers corroborate the object’s shape and behaviour, not the v6.12 internals specifically.

/proc/sys/fs/dentry-state is the cache’s dashboard. Six numbers: nr_dentry, nr_unused, age_limit, want_pages, nr_negative, and a reserved zero.

$ cat /proc/sys/fs/dentry-state
2267321	1550505	45	0	669169	0

2.27 million dentries, 1.55 million of them unused (on an LRU), and 669,169 of those unused ones negative — on an idle desktop. Roughly 30 % of the cache is a record of names that do not exist. (The precise semantics of nr_negative — it counts only unused negatives, not all of them — belong to Dentry States and Negative Dentries.)

A failed lookup permanently allocates an object. 20,000 stat() calls on distinct names that do not exist, in a directory holding 20,000 files that do:

before cold pass : nr_dentry=2287354  nr_unused=1570527  nr_negative=669186
after  cold pass : nr_dentry=2307353  nr_unused=1590526  nr_negative=689186
nr_dentry   delta over the cold pass: +19999
nr_negative delta over the cold pass: +20000

cold (first lookup of 20000 absent names):  34.750 ms  ->  1738 ns/lookup
warm (same 20000 absent names, cached)   :  19.500 ms  ->   975 ns/lookup
speedup: 1.78x

Two readings, and it is worth being careful about which is which. The timing difference is modest — 1.78× — and that number is a lower bound on the value of caching, not a measure of it, because /proc/sys/vm/drop_caches is mode --w------- and root-only on this host, so the “cold” pass was never genuinely cold: the directory’s own metadata was already in the page cache, so even the first pass avoided disk. On a real cold cache the miss would include I/O and the ratio would be orders of magnitude, not 1.78. Reading 1.78 as “negative dentries barely help” would be exactly the error of drawing a conclusion from a measurement whose sampling conditions you have not accounted for.

The counter difference is the unambiguous one: nr_negative rose by exactly 20,000 — one object per failed lookup, with no rounding and no reclaim in between. That is the fact the timing cannot obscure.

Deriving the per-dentry cost without /proc/slabinfo. The obvious way to size a dentry is grep dentry /proc/slabinfo. That is blocked here:

$ ls -l /proc/slabinfo
-r--------. 1 root root 0 Sep 12 13:49 /proc/slabinfo
$ head -2 /proc/slabinfo
head: cannot open '/proc/slabinfo' for reading: Permission denied

Mode 0400, owned by root — unreadable as uid 1000, and no unshare trick recovers it, because the restriction is file permissions on a procfs node, not a namespace boundary. The substitute is cgroup v2 memory.stat: dentry_cache is created with SLAB_ACCOUNT (fs/dcache.c line 3185), so every dentry allocation is charged to the allocating cgroup and shows up in that cgroup’s slab_reclaimable. Sampling it around a known number of allocations gives bytes per object directly:

300,000 stat() calls on distinct NON-EXISTENT names in one directory
  elapsed                      : 0.629 s
  nr_dentry      2307374 ->    2607384  (+300010)
  nr_negative     689204 ->     989204  (+300000)
  cgroup slab_reclaimable 487907560 -> 547917272  (+60009712 bytes)
  => 200.0 bytes of reclaimable slab per negative dentry

60,009,712 bytes over exactly 300,000 new negative dentries is 200.0 bytes each, to four significant figures. Compare that to the v6.12 header’s stated design target of 192 bytes (DNAME_INLINE_LEN 40 /* 192 bytes */): the measured figure is 8 bytes larger, on a kernel eight major releases later than the pin. The gap is real but not explained here — it could be a growth in struct dentry since v6.12, slab-level per-object overhead, or SLAB_ACCOUNT’s own metadata — so the honest statement is: the object costs about 200 bytes on this kernel, and the v6.12 source targets 192.

The practical consequence is arithmetic anyone can do: the 669,169 unused negative dentries on this idle machine are roughly 134 MB of reclaimable slab devoted entirely to remembering absences. Scale that to the 52-million-failed-lookup kernel build described in the negative-dentry literature and the number becomes gigabytes. That is the whole argument for why negatives are the dangerous half of the dcache, and the argument itself is developed in Dentry States and Negative Dentries and Shrinkers and Slab Reclaim.

Hard links seen from the name side. The same experiment that proves “one inode, many names” in VFS Inode Object reads, from the dentry’s point of view, as “three dentries, one d_inode”:

$ echo hi > a.txt && ln a.txt b.txt && mkdir sub && ln a.txt sub/c.txt
$ stat -c 'name=%n ino=%i nlink=%h' a.txt b.txt sub/c.txt
name=a.txt     ino=1400095 nlink=3
name=b.txt     ino=1400095 nlink=3
name=sub/c.txt ino=1400095 nlink=3

Note that sub/c.txt has a different parent dentry from the other two — the three dentries do not even share a d_parent — yet all three d_inode pointers are equal, and all three appear on that one inode’s i_dentry alias list. That is the shape a path-based cache on the inode would get wrong, and it is the reason d_path() is documented as reconstructing a path rather than the path.

sequenceDiagram
  autonumber
  participant App as stat("dir/ghost")
  participant Walk as path walk
  participant DC as dcache hash
  participant D as struct dentry
  participant FS as filesystem
  Note over App,FS: First call - the name has never been looked up
  App->>Walk: resolve "ghost" in dir
  Walk->>DC: __d_lookup_rcu(parent, qstr)
  DC-->>Walk: miss
  Walk->>D: d_alloc_parallel() under i_rwsem shared<br/>DCACHE_PAR_LOOKUP set
  Walk->>FS: i_op->lookup(dir_inode, dentry)
  FS-->>Walk: NULL inode (the name is absent)
  Walk->>D: d_add(dentry, NULL) -> DCACHE_MISS_TYPE
  Walk->>D: d_lookup_done() clears PAR_LOOKUP, wakes waiters
  Walk-->>App: -ENOENT
  Note over D: a negative dentry now exists and is hashed
  Note over App,FS: Second call - identical path
  App->>Walk: resolve "ghost" in dir
  Walk->>DC: __d_lookup_rcu(parent, qstr)
  DC-->>Walk: hit, d_inode == NULL
  Walk-->>App: -ENOENT without touching the filesystem

A failed lookup, twice. What it shows: the first ENOENT costs a dentry allocation, a directory-inode read lock and a call into the filesystem; the second costs one hash probe. Insight: the second path never reaches the FS participant at all — the kernel answers “that file does not exist” from cache, which is the point of a negative dentry and simultaneously the reason nr_negative rose by exactly 20,000 in the measurement above. Note step 5: even a doomed lookup takes the parent’s i_rwsem shared, so concurrent lookups of other absent names in the same directory proceed in parallel.

Failure Modes and Common Misunderstandings

  • “A dentry is the on-disk directory entry.” No. The on-disk directory entry (an ext4 dir_entry, an XFS dir record) is the persistent (name → inode#) mapping; the struct dentry is its cached, in-memory form, augmented with parent pointers, a refcount, locks, and a seqlock for lockless lookup. The dcache can be dropped entirely under memory pressure and rebuilt from disk; the on-disk entries cannot.
  • “Each file has one dentry.” Only single-named files. Hard-linked files have one dentry per name, all aliasing one inode. Code that assumes inode → dentry is one-to-one (e.g. naïvely caching a path on the inode) breaks on hard links.
  • “Looking up a non-existent file doesn’t cache anything.” It usually creates a negative dentry (d_inode == NULL) so the next failed lookup is also a cache hit. A pathological workload that stat()s millions of distinct non-existent paths can bloat the dcache with negatives; the kernel caps and reclaims them (see Dentry States and Negative Dentries).
  • “d_parent gives the path.” It gives the path as currently cached, reconstructed by d_path(). For a file open across a rename or with multiple hard links, the reconstructed path may differ from the one used to open it, and a deleted-but-open file’s path is rendered with a (deleted) suffix. d_path is explicitly documented as “quite tricky.”
  • Treating d_op as always present. d_op is NULL for most local filesystems; code must null-check it (the VFS uses DCACHE_OP_* flags in d_flags to avoid even dereferencing it on the fast path). Assuming d_revalidate exists is wrong for ext4/XFS, where the dcache is authoritative and d_revalidate is intentionally absent.
  • Reading d_delete backwards. There are two d_deletes and they are easy to conflate. d_op->d_delete is a filesystem policy hook asked at the last dput(): return 1 to drop immediately, 0 to cache, default NULL meaning always cache. d_delete() (the VFS function, fs/dcache.c line 2394) is what unlink/rmdir call, and its rule is the counter-intuitive one: it makes the dentry negative only when d_lockref.count == 1 — when nothing else holds it. If something does hold it, the dentry is __d_drop()ped instead. Assuming “unlink always leaves a negative dentry” is wrong exactly in the case people care about, the deleted-but-open file. Neil Brown gives the rationale in LWN’s Pathname lookup in Linux (2015): “When a file is deleted, this can be reflected in the cache either by setting d_inode to NULL, or by removing it from the hash table… If the dentry is still in use, the second option is used, as it is perfectly legal to keep using an open file after it has been deleted — having the dentry around helps. If the dentry is not otherwise in use (i.e. if the refcount in d_lockref is one), only then will d_inode be set to NULL. Doing it this way is more efficient for a very common case.”
  • Assuming a negative dentry means the lookup failed cheaply. It means the next lookup will be cheap. The first one allocated a ~200-byte object that persists until the shrinker runs, so a workload that probes a large space of absent names is an allocation workload, not a read-only one. Measured above: 300,000 failed stat()s produced exactly 300,000 negative dentries and 60 MB of reclaimable slab.
  • Treating DNAME_INLINE_LEN as a filename-length limit. It is not: names longer than it simply get an external_name allocation. It is a cache-line constant, chosen so sizeof(struct dentry) lands on 192 bytes on 64-bit — the header says so on the same line as the number.
  • Dentry vs inode locking confusion. d_lock (inside d_lockref) protects the dentry’s own fields (rename/unlink, d_parent); the inode’s i_rwsem protects directory contents. Holding one is not holding the other; the dcache-locking and path-lookup docs spell out which protects what.

See Also