logo

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 dentry represents a specific file name in a specific directory (e.g., /etc/passwd).
  • Many struct file objects can point to that same dentry.

Example: If three different programs (Vim, Cat, and Grep) all open /etc/passwd at the same time:

  1. There is one Inode (the data on disk).
  2. There is one Dentry (the name "passwd" in the folder "/etc").
  3. There are three struct file objects 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 \downarrow (owns a) File Descriptor (int) \downarrow (maps to a) struct file (The session: "I am at byte 500") \downarrow (points to a) struct dentry (The name: "I am called 'notes.txt'") \downarrow (points to a) struct inode (The metadata: "I am owned by UID 1000") \downarrow (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:

  1. If you hook vfs_read: You get a struct file *.

    • You can find the filename via file->f_path.dentry->d_name.
    • You can find the full path by walking the d_parent pointers.
    • You can find the offset via file->f_pos.
  2. If you hook an inode function (like inode_permission): You only get a struct 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 lseek on 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 single struct 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 file and 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:

  1. It creates two different struct file objects.
  2. Each gets its own FD.
  3. Result: Each struct file has 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 same struct dentry and struct 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 called f_count.
  • Every time you dup() or fork(), f_count increases.
  • Every time you close(fd), f_count decreases.
  • The kernel only destroys the struct file (and the session state/offset) when f_count hits 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.