struct file vs struct dentry
The relationship between struct file and struct dentry can be summarized as: An "Open Session" vs. a "Location on Disk."
If a dentry is the "Address" of a house, a file is a "Visitor" currently inside that house. There can be many visitors (files) in the same house (dentry) at the same time, each doing their own thing.
The Connector: struct path
In the kernel, a struct file does not point directly to a dentry. Instead, it points to a struct path, which acts as a wrapper.
struct file {
struct path f_path; /* The "Link" */
loff_t f_pos; /* Current cursor position in the file */
unsigned int f_flags; /* O_RDONLY, O_NONBLOCK, etc. */
// ...
};
struct path {
struct vfsmount *mnt; /* Which partition/mount point? */
struct dentry *dentry; /* Which file name/location? */
};
Cardinality: One-to-Many
- One
struct dentryrepresents a specific file name in a specific directory (e.g.,/etc/passwd). - Many
struct fileobjects can point to that samedentry.
Example:
If three different programs (Vim, Cat, and Grep) all open /etc/passwd at the same time:
- There is one Inode (the data on disk).
- There is one Dentry (the name "passwd" in the folder "/etc").
- There are three
struct fileobjects in the kernel (one for each program).
Why do we need both? (State vs. Identity)
The struct file stores ephemeral state that belongs to the process, while the struct dentry stores permanent state that belongs to the filesystem.
What struct file knows (The "Session"):
- Current Offset (
f_pos): Where am I currently reading? (Process A might be at byte 100, while Process B is at byte 0). - Access Mode (
f_flags): Did the process open this as Read-Only or Read-Write? - Credentials: Which user opened this file?
What struct dentry knows (The "Hierarchy"):
- The Name: The string
"passwd". - The Parent: The dentry for the directory
/etc. - The Connection: A pointer to the
struct inode(the actual data).
The Big Picture: The VFS "Chain"
When a process wants to read data, the kernel follows this chain:
Process
(owns a)
File Descriptor (int)
(maps to a)
struct file (The session: "I am at byte 500")
(points to a)
struct dentry (The name: "I am called 'notes.txt'")
(points to a)
struct inode (The metadata: "I am owned by UID 1000")
(points to)
Data Blocks (The actual 0s and 1s on the physical disk)
Why this matters for Tracing (eBPF/Kprobes)
This distinction is crucial when writing probes:
-
If you hook
vfs_read: You get astruct file *.- You can find the filename via
file->f_path.dentry->d_name. - You can find the full path by walking the
d_parentpointers. - You can find the offset via
file->f_pos.
- You can find the filename via
-
If you hook an inode function (like
inode_permission): You only get astruct inode *.- As we discussed before, you are now "lost." You know the file's size and owner, but you don't know its name or how the process opened it.
Is one FD mapped to one struct file?
No. The relationship is Many-to-One.
Multiple file descriptors (FDs) can point to the exact same struct file.
To understand this, you have to look at the File Descriptor Table inside the process.
How the Mapping Works
In the kernel, every process has a struct task_struct. Inside that, there is a pointer to a struct files_struct, which contains the File Descriptor Table.
This table is essentially an array of pointers.
- The Index of the array is the FD (0, 1, 2, 3...).
- The Value at that index is a pointer to a
struct file.
Scenario: Multiple FDs, One struct file
There are two common ways that different FDs end up pointing to the same struct file session:
A. The dup() System Call
When you run new_fd = dup(old_fd);, the kernel does not create a new struct file. It simply goes to the next empty slot in your FD table and copies the pointer from old_fd.
- FD 3 points to
struct file A. - FD 4 points to
struct file A. - Result: If you call
lseekon FD 3 to move to byte 100, and then read from FD 4, you will start at byte 100. This is because they share the same offset (stored in the singlestruct file).
B. The fork() System Call
When a process forks, the child process gets a copy of the parent's FD table.
- Parent FD 3 points to
struct file A. - Child FD 3 points to
struct file A. - Result: This is why a child process can write to the same log file as the parent without overwriting the parent's data; they share the same
struct fileand therefore the same "current offset" cursor.
Scenario: Same Physical File, Different struct file
If you call open() twice on the same file (/tmp/test.txt), the kernel behaves differently:
- It creates two different
struct fileobjects. - Each gets its own FD.
- Result: Each
struct filehas its own offset. If you read 10 bytes on FD 3, the cursor for FD 4 stays at 0. Both, however, will eventually point to the samestruct dentryandstruct inode.
Reference Counting (f_count)
Because multiple FDs can point to one struct file, the kernel uses Reference Counting to know when it is safe to delete the struct file object.
- Inside
struct file, there is a field calledf_count. - Every time you
dup()orfork(),f_countincreases. - Every time you
close(fd),f_countdecreases. - The kernel only destroys the
struct file(and the session state/offset) whenf_counthits zero.
Summary Table
| Action | New FD? | New struct file? |
Effect |
|---|---|---|---|
open() |
Yes | Yes | New session, independent offset. |
dup() |
Yes | No | Shared session, shared offset. |
fork() |
Yes (in child) | No | Shared session between processes. |