4.2.35 · D3Operating Systems

Worked examples — ext4 structure — superblock, block groups, inodes

3,700 words17 min readBack to topic

The scenario matrix

Every question about ext4 geometry is really one of a small handful of shapes. Here is the full list — each later example is tagged with the cell it fills.

# Case class What makes it tricky Example
A Inode → block group (normal) the 1-based offset Ex 1
B Inode → block group (boundary) inode exactly at a group edge Ex 2
C Degenerate input: inode 1 smallest legal inode, off-by-one trap Ex 3
D Capacity from geometry metadata overhead vs raw size Ex 4
E Zero/limiting: tiny disk disk smaller than one group Ex 5
F Inode table sizing inodes are a separate fixed pool Ex 6
G Real-world word problem "millions of tiny files" — runs out of inodes not space Ex 7
H Extents vs old pointers how many extent records for a fragmented file Ex 8
I Exam twist: different block size geometry scales with ; don't hardcode 4096 Ex 9
J Usable data blocks per group subtracting ALL metadata blocks Ex 10

Two symbols recur, so let's pin them down once, in plain words:

Recall The one formula that solves cells A, B, C

For an inode numbered (numbering starts at 1) with inodes per group:

  • Group number :::
  • Index inside that group's inode table :::
  • Why subtract 1? ::: inode numbers are 1-based, array slots are 0-based; skipping this puts you one group off at every boundary.

A picture of the mapping (read this before Case A)

The figure below is the whole page in one image: on the left, the inode-number line chopped into equal groups of ; on the right, the physical layout of a single block group (superblock backup → bitmaps → inode table → data). Every worked example is just "which arrow am I following?"

Figure — ext4 structure — superblock, block groups, inodes

In words: inode numbers live in group 0, in group 1, and so on — the vertical dashed lines are the group boundaries that Case B lands exactly on. Inside one group (right panel), the first few blocks are metadata; the big green stretch at the end is where file data actually goes (that green stretch is what Case J counts).


Case A — Inode to block group, the ordinary case

Follow the blue arrow in the figure above: it takes inode from the left number-line into some group box.

Step 1 — Shift to 0-based. Why this step? The formula counts how many inodes come before . Inode 1 is the "zeroth" inode, so we subtract 1 to turn a 1-based label into a 0-based position.

Step 2 — Divide by the group size, keep the whole part. Why this step? Every group holds exactly inodes. Integer division "how many full groups fit before us" is the group number. The floor throws away the fractional part because you can't be in "group 6.1".

Step 3 — Remainder gives the slot. Why this step? The remainder is "how far into group 6 we are" — the row of the inode table (the right panel of the figure).

Verify: reconstruct the inode from (group, index): . ✅ (the undoes the 0-based shift).


Case B — Boundary: an inode sitting exactly on a group edge

In the figure, these two inodes sit on either side of the first dashed boundary line.

Step 1 — Do inode 8192. Why this step? , so the whole part is . Inode 8192 is the last inode of group 0 (index ).

Step 2 — Do inode 8193. Why this step? Now the numerator equals the group size exactly, so the whole part ticks over to . Inode 8193 is the first inode of group 1 (index ).

Verify: group 0 holds inodes (that's exactly inodes), group 1 starts at . Count checks out. ✅


This is the very first slot on the left of the figure.

Step 1 — Apply the formula literally. Why this step? This is the whole reason the formula subtracts 1: the first inode must map to slot 0 of group 0. If we forgot the we'd still get group 0 here (since ), so this case hides the bug — that's why you must test boundaries (Ex 2), not just inode 1.

Verify: reconstruct . ✅ The smallest inode maps to the first slot, as it must.


Case D — Capacity from pure geometry

Step 1 — Put both sizes in the same unit (MiB). Why this step? You can't divide a size by a group-size unless the units match. Mixing GiB and MiB is the #1 arithmetic error here.

Step 2 — Divide by group size. Why this step? Number of groups is just total-size ÷ size-per-group. The metadata (bitmaps, inode table, superblock backups) is carved out of each group, so it does not change the group count — it eats into the usable data blocks within a group.

Verify: MiB GiB exactly. ✅ Clean because is a multiple of .


Case E — Limiting case: a disk smaller than one group

Step 1 — Compute the raw ratio. Why this step? The disk is half a group. But you cannot have half a block group — a group is the unit of layout.

Step 2 — Round up with ceiling. Why this step? We use ceiling (defined above) because even a fraction of a group still needs one whole group to live in. ext4 makes one group and simply tracks fewer blocks in it: the block bitmap still occupies a full block, but many of its bits mark "beyond end of device" as used. So a small disk pays proportionally more metadata overhead — the degenerate case where "one bitmap block" is oversized for the data.

Verify: blocks actually present blocks. That's fewer than the bits a full bitmap holds, so one bitmap covers them all with room to spare. ✅ Confirms exactly one group.


Case F — Inode table sizing (inodes are a separate pool)

Step 1 — Total bytes of the inode table. Why this step? The inode table is a plain array: (count) × (size of one record).

Step 2 — Convert bytes to blocks, rounding up. Why this step? Disk is allocated in whole blocks, so in general you must round up (ceiling) — if the array were, say, bytes you'd still need blocks because the leftover bytes demand one more whole block. Here it divides evenly, so ceiling and floor agree at . blocks dwarfs the bitmap blocks — inode tables are the bulk of fixed metadata.

Verify: . ✅ And blocks MiB, out of a MiB group — about of the group is inode table.


Case G — Real-world word problem: running out of inodes

Step 1 — Total inodes on the volume. Why this step? Inodes are pre-allocated at mkfs time — a fixed pool. Every file needs exactly one. Once all are used, no new file can be created regardless of free bytes.

Step 2 — Check the space side (upper bound). Take the worst case where each file still occupies one full -byte block: Convert to GiB (1 GiB bytes): Why this step? Even in this worst case only GiB of the GiB is touched — confirming the two resources decoupled: files (inodes) hit their limit while bytes did not.

Step 3 — Diagnose. df -i would show inodes used; df shows space free. Fix: delete files or reformat with more inodes (mkfs.ext4 -N or -i bytes-per-inode).

Verify: the failure count equals the inode pool: , and the worst-case space bytes GiB exactly. ✅


Case H — Extents vs old block pointers

Figure — ext4 structure — superblock, block groups, inodes

Reading the figure: the top blue bar is a -block file stored contiguously — it collapses into a single extent record (start , length ), shown by the one arrow on the right. The bottom three bars (green / orange / red) are the same file broken into 3 separated runs on disk — now you need 3 extent records, one per contiguous run, still fitting in the inode's 4 inline slots. The gray line at the bottom reminds you that ext2/3 would instead store one pointer per block ( of them) plus an indirect block. The pedagogical punch: extent count grows with the number of fragments, not the number of blocks.

Step 1 — Contiguous file. One run of blocks 1 extent (top bar in the figure). Why this step? An extent's whole point is that contiguity collapses into a single (start, length) pair. blocks, one record.

Step 2 — 3-piece file. Three runs 3 extents, all fitting in the inode's inline slots (no B-tree needed). Why this step? One extent per contiguous run, not per block. , so no external tree.

Step 3 — Contrast ext2/3. The old scheme stores one 4-byte pointer per data block: pointers. The first are direct; the rest () live in a single indirect block. That indirect block holds pointers (we divide by because each pointer is bytes), which is , so one indirect block suffices. Net: ext2/3 needs extra indirect block of metadata plus pointer entries — versus ext4's extent record. Why this step? Shows why extents win: metadata grows with the number of fragments, not the number of blocks.

Verify: contiguous ⇒ extent; ext2/3 indirect block holds pointers needed, so exactly one indirect block suffices. ✅


Case I — Exam twist: change the block size

Step 1 — Recompute blocks per group from the definition. Why this step? was never a constant — it was evaluated at . The bitmap constraint (one bit per block, one bitmap block per group) scales with . Memorising the number instead of the formula is the trap.

Step 2 — Group size in bytes. Why this step? Group size is (blocks/group) × (bytes/block) . Quadratic in — shrinking by shrinks group size by ().

Verify: . And the ratio to the 4 KiB case: . ✅ Quadratic scaling confirmed.


Case J — Usable data blocks per group

This is the green data stretch in the right panel of the very first figure — everything after the metadata blocks.

Step 1 — List every metadata block per group. Why this step? You cannot just subtract "the bitmaps" — every fixed structure the group carries eats blocks. Missing one term is the usual exam slip. (Groups without a superblock backup save those 2 blocks, but the worst case is the safe one to quote.)

Step 2 — Subtract from the group total. Why this step? Data blocks are whatever remains after all fixed metadata is carved out — this is the number a group can actually store files in.

Step 3 — Overhead as a percentage. Why this step? Turning the loss into a fraction tells you the metadata "tax". Under here — inode tables dominate it.

Verify: (accounts for every block), and bytes MiB of usable data per MiB group. ✅


The chain, at a glance

Inode number N

subtract 1 for zero-based

divide by inodes per group

floor gives block group

remainder gives table index

block size B

blocks per group equals 8 times B

group size equals 8 times B squared

subtract metadata gives usable data blocks


Self-test

Recall Quick recall

Group of inode 8192 with I=8192 ::: group 0 (last inode of group 0, since floor(8191/8192)=0) Blocks per group when B=1024 ::: 8×1024 = 8192 blocks (8 MiB group) Extents for a perfectly contiguous 400-block file ::: exactly 1 extent Why does a 64 MiB disk still get a full bitmap block ::: the bitmap is one whole block regardless; unused bits are marked used df shows free space but files won't create — cause ::: inode pool exhausted (separate fixed pool); check df -i Usable data blocks per group (B=4096, 512-block inode table, 4 other metadata blocks) ::: 32768 − 516 = 32252 data blocks What does equal precisely ::: , always in the range Why ceiling (not floor) for "how many blocks to store X bytes" ::: any leftover bytes still need one more whole block, so you round up

See also: Bitmaps for allocation · Hard links vs Symbolic links · ext2 vs ext3 vs ext4 · Journaling · Virtual File System · Disk Scheduling · Memory Management · back to Operating Systems.