Chapter 3: The Inode – Anatomy of a File
Chapter 3: The Inode – Anatomy of a File
In Unix filesystems, a file’s name and its underlying metadata are strictly decoupled. A directory is merely a table linking human-readable strings to integer indices. The true essence of a file—its file type, ownership, access permissions, size, modification timestamps, extended attributes, and physical storage block addresses—resides within the Inode (index node).
In ext4, every file, directory, symlink, and device node is represented on disk by a dedicated inode structure. This chapter dissects the on-disk anatomy of the 256-byte ext4_inode, examines the historical direct and indirect block pointer array, explores the transition to inline extent trees within i_block, and examines nanosecond timestamps, file birth times (crtime), and the Year 2038 fix.
The ext4_inode Structure (256 Bytes)
While legacy ext2 and ext3 filesystems used a compact 128-byte inode, modern ext4 standardizes on a 256-byte inode (configured via s_inode_size). The first 128 bytes preserve compatibility with legacy Unix fields, while the remaining 128 bytes provide an expandable structure for nanosecond timestamps, inode checksums, and inline extended attributes.
struct ext4_inode {
/* 0x00 */ __le16 i_mode; /* File type and access permissions */
/* 0x02 */ __le16 i_uid; /* Low 16 bits of Owner User ID */
/* 0x04 */ __le32 i_size_lo; /* Lower 32 bits of file size in bytes */
/* 0x08 */ __le32 i_atime; /* Access time (seconds since 1970) */
/* 0x0C */ __le32 i_ctime; /* Inode Change time */
/* 0x10 */ __le32 i_mtime; /* Modification time */
/* 0x14 */ __le32 i_dtime; /* Deletion time (0 if active) */
/* 0x18 */ __le16 i_gid; /* Low 16 bits of Group ID */
/* 0x1A */ __le16 i_links_count; /* Hard link counter */
/* 0x1C */ __le32 i_blocks_lo; /* Lower 32 bits of allocated 512-byte blocks */
/* 0x20 */ __le32 i_flags; /* Inode flags (e.g. 0x80000 = EXTENTS) */
/* 0x24 */ union {
struct { __le32 l_i_version; } linux1;
__u8 l_i_reserved1[4];
} osd1; /* OS-dependent field 1 */
/* 0x28 */ __le32 i_block[15]; /* 60 bytes: Block mapping or Extent Tree Root */
/* 0x64 */ __le32 i_generation; /* File version (used for NFS export locks) */
/* 0x68 */ __le32 i_file_acl_lo; /* Lower 32 bits of extended attribute block */
/* 0x6C */ __le32 i_size_high; /* Upper 32 bits of file size (for files > 4 GiB) */
/* 0x70 */ __le32 i_obso_faddr; /* Obsolete fragment address */
/* 0x74 */ union {
struct {
__le16 l_i_blocks_high; /* Upper 16 bits of block count */
__le16 l_i_file_acl_high;/* Upper 16 bits of EA block */
__le16 l_i_uid_high; /* Upper 16 bits of UID */
__le16 l_i_gid_high; /* Upper 16 bits of GID */
__le16 l_i_checksum_lo; /* Lower 16 bits of inode CRC32c */
__le16 l_i_reserved;
} linux2;
} osd2; /* OS-dependent field 2 (completes 128 bytes) */
/* --- Modern ext4 Extended Fields (Bytes 0x80 - 0xFF) --- */
/* 0x80 */ __le16 i_extra_isize; /* Extra bytes in this inode (typically 32 or 128) */
/* 0x82 */ __le16 i_checksum_hi; /* Upper 16 bits of inode CRC32c checksum */
/* 0x84 */ __le32 i_ctime_extra; /* Extra change time: 2 bits epoch + 30 bits nsec */
/* 0x88 */ __le32 i_mtime_extra; /* Extra modification time */
/* 0x8C */ __le32 i_atime_extra; /* Extra access time */
/* 0x90 */ __le32 i_crtime; /* Creation time (File birth time - seconds) */
/* 0x94 */ __le32 i_crtime_extra;/* Creation time nanoseconds + epoch bits */
/* 0x98 */ __le32 i_version_hi; /* 64-bit inode version support */
/* 0x9C */ __le32 i_projid; /* Project Quota ID */
/* 0xA0 - 0xFF */ __u8 i_inline_xattr[96]; /* Inline extended attributes space */
};Direct, Indirect, Double and Triple Indirect Blocks
To understand why ext4 transitioned to extents, one must analyze the limitations of the classic indirect block addressing scheme inherited from Unix and ext2/3.
In the legacy scheme, the 60-byte i_block field was treated as an array of fifteen 32-bit block addresses (uint32_t i_block[15]):
┌────────────────────────────────────────────────────────────────────────┐
│ LEGACY INDIRECT BLOCK MAPPING │
│ │
│ i_block[0..11] ──► Direct Blocks (0 to 11) ──► 12 x 4 KiB = 48 KiB│
│ │
│ i_block[12] ──► Single Indirect Block │
│ └─► 1,024 Block Pointers ──► 1,024 x 4 KiB = 4 MiB│
│ │
│ i_block[13] ──► Double Indirect Block │
│ └─► 1,024 Pointers │
│ └─► 1,024 Pointers ──► 1,024² x 4 KiB = 4 GiB│
│ │
│ i_block[14] ──► Triple Indirect Block │
│ └─► 1,024³ Pointers ──► 1,024³ x 4 KiB = 4 TiB│
└────────────────────────────────────────────────────────────────────────┘
The Cost of Indirect Addressing
While elegant, indirect pointers imposed severe performance penalties on large files: - Mapping a \(100\text{ GiB}\) database file required over \(26\text{ million}\) individual 32-bit block pointers spread across thousands of indirect blocks. - Sequential read operations were forced to execute constant metadata lookups through multi-tiered indirect pointer trees. - Truncating or deleting a large file required walking and freeing every individual indirect pointer block, causing lock contention and I/O freezes.
Extent Trees (ext4_extent_header and ext4_extent)
In modern ext4, files are created with the EXT4_EXTENTS_FL flag (0x00080000) set in i_flags. When this flag is active, the kernel reinterprets the 60 bytes of i_block[15] not as indirect pointers, but as the root node of a B+ Extent Tree.
An extent describes a contiguous run of up to 32,768 physical blocks using a single 12-byte descriptor:
┌────────────────────────────────────────────────────────────────────────┐
│ EXTENT ROOT IN i_block (60 BYTES) │
│ │
│ Byte 0 ────────────── Byte 11 Byte 12 ────────────────── Byte 59 │
│ ┌─────────────────────────────┐ ┌───────────────────────────────────┐ │
│ │ ext4_extent_header (12 B) │ │ 4 x ext4_extent records (4 x 12 B)│ │
│ │ Magic: 0xF30A, Entries: 1-4 │ │ [Extent 0] [Extent 1] ... │ │
│ └─────────────────────────────┘ └───────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
The 60 bytes inside i_block accommodate exactly: - One 12-byte ext4_extent_header - Four 12-byte ext4_extent records (or ext4_extent_idx index records)
Because one extent can represent up to \(128\text{ MiB}\) of contiguous storage (\(32,768 \times 4096\text{ bytes}\)), a file up to \(512\text{ MiB}\) in size can often be described entirely within the inode itself, requiring zero external metadata blocks. (Chapter 5 explores multi-level extent trees in detail).
Inline Data for Small Files
Many filesystems suffer from internal fragmentation when storing millions of tiny files (such as web icons, miniature configuration files, or tiny logs under 100 bytes). Allocating a full \(4\text{ KiB}\) disk block for a 50-byte file wastes over 98% of the allocated block.
With the inline_data feature enabled, ext4 stores file contents directly inside the unused space of the inode itself: - The 60 bytes of i_block - The extra inode space (typically up to 96 bytes in i_inline_xattr)
A file smaller than \(156\text{ bytes}\) consumes zero physical data blocks and zero bitmap allocations. Reading the file requires only reading the inode table, resulting in instantaneous, single-I/O file access.
Timestamps: Birth Time, Nanosecond Precision and Y2038
Legacy 32-bit Unix timestamps count seconds elapsed since the Unix epoch (January 1, 1970 00:00:00 UTC). On January 19, 2038, a signed 32-bit integer overflows into negative values, causing dates to wrap around to 1901.
ext4 resolves the Y2038 problem and introduces nanosecond resolution by expanding timestamps into two fields:
┌────────────────────────────────────────────────────────────────────────┐
│ EXT4 64-BIT TIMESTAMP ENCODING │
│ │
│ Standard Field (32 bits in base inode): │
│ [31 .............................................................. 0]│
│ Seconds since 1970 (Classic Unix timestamp) │
│ │
│ Extra Field (32 bits in extended inode, e.g. i_mtime_extra): │
│ [31 30] [29 ...................................................... 0]│
│ Epoch Nanoseconds (0 to 999,999,999) │
└────────────────────────────────────────────────────────────────────────┘
- Epoch Extension (2 bits): Extends the signed 32-bit seconds field by 2 additional epoch bits, extending the timestamp cutoff by 400 years to May 2446.
- Nanosecond Resolution (30 bits): Stores the fractional second component (\(0 \le \text{nsec} \le 999,999,999\)), providing precise sub-millisecond timestamps essential for database transaction ordering, high-frequency trading logs, and make-based software build systems.
- File Creation Time (
crtime/ btime): Unlike legacy Unix filesystems that only tracked access (atime), data modification (mtime), and metadata change (ctime), ext4 records the true birth time of the file ini_crtimeandi_crtime_extra.
# View file birth time (crtime) using stat
stat /etc/passwd
# Birth: 2026-04-12 09:14:22.482019482 +0000