VFS Dentry Object
The dentry (directory entry;
struct dentry, defined ininclude/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/envbecomes 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.”
| Job | Fields | What it means | Protected by |
|---|---|---|---|
| The binding itself | d_name, d_parent, d_inode | the 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 dentry | d_lock; d_inode and the alias linkage additionally under the inode’s i_lock |
| Inline name storage | d_iname[DNAME_INLINE_LEN] | the characters of short names, stored in the dentry itself | d_lock (via d_name); published to RCU readers with smp_store_release |
| Lockless-walk machinery | d_seq, d_hash, d_flags | per-dentry seqcount validating an RCU read; the hlist_bl_node linking into the global hash; the DCACHE_* bit set | d_seq is its own seqlock; d_hash under the per-bucket bit-spinlock; d_flags under d_lock |
| Filesystem identity | d_sb, d_op, d_fsdata, d_time | which mounted filesystem this name lives in, the optional per-fs vtable, fs-private scratch, and a timestamp for d_revalidate | d_sb/d_op set at allocation and read-only afterwards; d_fsdata/d_time are the filesystem’s business |
| Reference + lock, fused | d_lockref | a lockref: a spinlock and a reference count in one 64-bit word. d_lock is literally #define d_lock d_lockref.lock | itself — the point is that lockref_get() can bump the count with one CAS without taking the lock |
| Tree linkage | d_children, d_sib | our children (an hlist_head) and our own link in the parent’s child list | d_lock — and, for the sibling links, the parent’s d_lock |
| Reclaim position | d_lru (union with d_wait) | the per-superblock LRU list when the dentry is unused | sb->s_dentry_lru_lock for the list, d_lock for the member |
| Alias / teardown | d_u.d_alias | d_u.d_in_lookup_hash | d_u.d_rcu | link in the inode’s i_dentry list, or in the parent’s in-lookup hash, or the RCU callback head | d_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
#endifThe 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.
| Method | Contract | rename_lock | d_lock | May block | Callable in rcu-walk |
|---|---|---|---|---|---|
d_revalidate | re-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” | no | no | yes (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_revalidate | revalidate 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 is | no | no | yes | no — “only called after leaving rcu-walk mode” |
d_hash | override name hashing when adding to the hash table. First argument is the parent | no | no | no | maybe |
d_compare | override 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 guaranteed | yes | no | no | maybe |
d_delete | called 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” | no | yes | no | no |
d_init | called when a dentry is allocated | no | no | yes | no |
d_release | called when a dentry is really deallocated | no | no | yes | no |
d_prune | called before a dentry is pruned from the cache. Documented in locking.rst but absent from vfs.rst’s list | no | yes | no | no |
d_iput | called 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 it | no | no | yes | no |
d_dname | generate 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 this | no | no | no | no |
d_automount | create 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 set | no | no | yes | no |
d_manage | control transit through a managed dentry (autofs blocking, -EISDIR to fall through) | no | no | yes (ref-walk) | maybe |
d_real | resolve 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 object | no | no | yes | no |
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_deleteprototype’s constness. Reason:dcache.hat v6.12 declaresint (*d_delete)(const struct dentry *)whilelocking.rstat the same tag printsint (*d_delete)(struct dentry *)without theconst;vfs.rstagrees with the header. Likewiselocking.rstlistsd_prunein its locking table but omits it from its prototype block. Both are documentation drift, not ambiguity in the interface. To resolve: read signatures frominclude/linux/dcache.hat 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.
| Flag | Bit | Group | Meaning |
|---|---|---|---|
DCACHE_OP_HASH | 0 | ops cache | d_op->d_hash exists |
DCACHE_OP_COMPARE | 1 | ops cache | d_op->d_compare exists |
DCACHE_OP_REVALIDATE | 2 | ops cache | d_op->d_revalidate exists — the bit that makes NFS pay a per-lookup callback and ext4 not |
DCACHE_OP_DELETE | 3 | ops cache | d_op->d_delete exists |
DCACHE_OP_PRUNE | 4 | ops cache | d_op->d_prune exists |
DCACHE_OP_WEAK_REVALIDATE | 11 | ops cache | d_op->d_weak_revalidate exists |
DCACHE_OP_REAL | 26 | ops cache | d_op->d_real exists (stacking filesystems) |
DCACHE_ENTRY_TYPE | 20–22 (mask 7 << 20) | type | three bits holding the type of the thing named |
DCACHE_MISS_TYPE | 0 << 20 | type | negative dentry — the name resolves to nothing |
DCACHE_WHITEOUT_TYPE | 1 << 20 | type | a whiteout: “stop pathwalk”. This is how overlayfs renders a file deleted in the upper layer but still present below |
DCACHE_DIRECTORY_TYPE | 2 << 20 | type | a normal directory (has ->lookup) |
DCACHE_AUTODIR_TYPE | 3 << 20 | type | a “lookupless directory (presumed automount)” |
DCACHE_REGULAR_TYPE | 4 << 20 | type | a regular file |
DCACHE_SPECIAL_TYPE | 5 << 20 | type | device node, FIFO, socket |
DCACHE_SYMLINK_TYPE | 6 << 20 | type | a symlink |
DCACHE_DISCONNECTED | 5 | state | the dentry is not connected to the tree root — an NFS filehandle was resolved into an object with no known path |
DCACHE_REFERENCED | 6 | state | “recently used, don’t discard” — the second-chance bit for reclaim |
DCACHE_DONTCACHE | 7 | state | “purge from memory on final dput()” |
DCACHE_CANT_MOUNT | 8 | state | nothing 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_GENOCIDE | 9 | state | set 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_LIST | 10 | state | isolated onto the shrinker’s private list — no longer on the normal LRU |
DCACHE_NFSFS_RENAMED | 12 | state | NFS silly-rename in progress |
DCACHE_FSNOTIFY_PARENT_WATCHED | 14 | state | the parent directory is being watched, so events must be sent |
DCACHE_DENTRY_KILLED | 15 | state | dentry_kill() has committed to destroying this dentry |
DCACHE_MOUNTED | 16 | state | something is mounted on this dentry |
DCACHE_NEED_AUTOMOUNT | 17 | state | traversing this directory must call d_automount |
DCACHE_MANAGE_TRANSIT | 18 | state | traversal must call d_manage |
DCACHE_LRU_LIST | 19 | state | the dentry is on its superblock’s LRU |
DCACHE_NOKEY_NAME | 25 | state | an fscrypt name encoded without the key available |
DCACHE_PAR_LOOKUP | 28 | state | in-lookup: a lookup for this (parent, name) is in progress, with the parent locked shared |
DCACHE_DENTRY_CURSOR | 29 | state | this 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_NORCU | 30 | state | free 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.
Many Dentries, One Inode — How Hard Links Work
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:
- Allocates the
struct dentryfrom the dedicateddentry_cacheslab. - Copies the name into
d_iname(inline) or a freshexternal_nameallocation, NUL-terminating it; publishes the name pointer withsmp_store_releaseso RCU readers always see a valid string. - Initializes
d_lockref.count = 1,d_flags = 0, the seqlock, and setsd_inode = NULL(it starts negative),d_parent = dentry(self, until parented). - Inherits
d_opfrom the superblock’s default (d_set_d_op(dentry, sb->s_d_op)), and callsd_op->d_initif 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->lookup() found nothing<br/>d_add(dentry, NULL) InLookup --> Pos: i_op->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 02.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 deniedMode 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=3Note 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; thestruct dentryis 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 → dentryis 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 thatstat()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_parentgives the path.” It gives the path as currently cached, reconstructed byd_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_pathis explicitly documented as “quite tricky.” - Treating
d_opas always present.d_opisNULLfor most local filesystems; code must null-check it (the VFS usesDCACHE_OP_*flags ind_flagsto avoid even dereferencing it on the fast path). Assumingd_revalidateexists is wrong for ext4/XFS, where the dcache is authoritative andd_revalidateis intentionally absent. - Reading
d_deletebackwards. There are twod_deletes and they are easy to conflate.d_op->d_deleteis a filesystem policy hook asked at the lastdput(): return 1 to drop immediately, 0 to cache, default NULL meaning always cache.d_delete()(the VFS function,fs/dcache.cline 2394) is whatunlink/rmdircall, and its rule is the counter-intuitive one: it makes the dentry negative only whend_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 settingd_inodeto 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 ind_lockrefis one), only then willd_inodebe 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_LENas a filename-length limit. It is not: names longer than it simply get anexternal_nameallocation. It is a cache-line constant, chosen sosizeof(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(insided_lockref) protects the dentry’s own fields (rename/unlink,d_parent); the inode’si_rwsemprotects directory contents. Holding one is not holding the other; the dcache-locking and path-lookup docs spell out which protects what.
See Also
- The Dentry Cache — the dcache as a whole: the global hash, the LRU, the shrinker,
dput’s kill path - VFS Inode Object — the inode a dentry names; the
i_dentry/d_aliaslist, the other half of the hard-link story - Path Resolution and Name Lookup — how a path string is walked dentry by dentry into an inode
- RCU-Walk and Ref-Walk — the lockless (
d_seq) and reference-taking lookup modes that traverse dentries - Dentry States and Negative Dentries — negative dentries, the
DCACHE_*_TYPEstates, in-lookup dentries - VFS Operation Tables — the contents of
dentry_operationsand how the VFS dispatches throughd_op - Mount Point Traversal During Lookup — how a dentry marked
DCACHE_MOUNTEDredirects the walk - Symlink Resolution and Loop Detection — why a symlink is a separate inode, not an alias
- Shrinkers and Slab Reclaim — what actually frees the ~200 bytes each dentry costs
- VFS File Object — an open file is one of the things that pins a dentry’s
d_lockref.countabove zero - Filesystem Registration and Mounting Internals — where
sb->s_d_op(the per-superblock defaultd_op) is installed - overlayfs Union Filesystem — the consumer of
d_real,DCACHE_OP_REALandDCACHE_WHITEOUT_TYPE - XFS Filesystem Internals — a filesystem whose on-disk directories are B+trees, which the dcache caches identically to ext4’s
- Mount Namespaces — the mount table is expressed as “which dentry is mounted on which other dentry”, so namespaces are a view over these same objects
- MOC: Linux Filesystems and VFS MOC