Appendix J — How a disk is divided
Using a computer you meet these words. “Partition the disk”, “format it”, “the MBR is damaged”, “you need to convert to GPT”. You half know what they mean, and usually get by without knowing what is actually written on the disk, and how.
This appendix opens those bytes. The structures are given as tables. Their numbers were checked by examples that build the bytes to the specification and decode them again, and the page shows only the output of those checks. The code on the page comes from one program shaped like a bootloader: it reads a disk’s partition table, finds a file on a FAT partition and loads it into memory. The disks it reads were made by other people’s tools (sfdisk, mkfs.fat, QEMU), not by this book.
Platform note. what this appendix rests on, and its limits
The authorities for these structures are their specifications — for the MBR, convention and each operating system’s documentation; for GPT, the UEFI specification; for FAT, Microsoft’s FAT specification. What is written here belongs to their opening pages, and building real tools means reading the specifications.
This machine’s disk was not read. The discipline is to leave nobody’s machine details in the document. Instead the checking examples build the bytes to the specification and decode them again, and the bootloader demonstration attaches five disk images, made on the spot by the tools, to a RISC-V machine model of QEMU 9.2 and reads them.
Words to know first#
These words are used later without explanation. There is no need to memorise them now — come back to the table.
| Word | Meaning | By analogy |
|---|---|---|
| sector | the smallest cell a disk reads and writes; usually 512 bytes | one line of a notebook |
| LBA | the number given to a sector, from 0 (logical block addressing) | the line number |
| CHS | the old way of naming a place by cylinder, head and sector | “volume, page, line” |
| partition | one piece the disk is divided into. Merely “from this sector, this many” | a divider drawn in the notebook |
| partition table | the list of partitions, written at the front of the disk | the contents page |
| volume | what the operating system treats as one storage space | “the D drive” |
| filesystem | how files and folders are managed inside a partition (FAT32, ext4 …) | the tidying rules within a section |
| format | writing a filesystem’s basic structures afresh inside a partition | ruling a new form over a section |
| mount | attaching a partition’s filesystem somewhere in the operating system | laying the section open on the desk |
Table 115.1 — The words used in this appendix
A common misconception. formatting erases the data
A disk is a row of numbered cells#
What the machine knows is very simple. A disk is a row of sectors, and each sector has a number from 0. “Read sector 3”, “write these 512 bytes to sector 7” — that is effectively all the device knows.
Partitions and filesystems are laid on that row by convention. The disk does not know how it is divided.
Figure 115.1 — Three layers on one disk — the row of sectors, the partition table, the filesystem.
Places used to be named by CHS: cylinder, head, sector. Names from when a disk really was several platters. Now everything is LBA — just a number.
| Called | Physical sector | Logical sector | What changes |
|---|---|---|---|
| 512n | 512 bytes | 512 bytes | old disks. The two agree |
| 512e | 4096 bytes | 512 bytes | inside it is 4 KiB while pretending to be 512 — misalignment makes it slow |
| 4Kn | 4096 bytes | 4096 bytes | 4 KiB with nothing hidden. Some older software cannot read it |
Table 115.2 — Three kinds of sector size
Q. Why is misalignment slow on 512e?
A. The disk really works in 4 KiB chunks, so if software writes 512 bytes at a misaligned place, the device must read 4 KiB, change 512 bytes inside it, and write 4 KiB back (read-modify-write). One write becomes three pieces of work. Today’s convention of starting partitions at sector 2048 (= 1 MiB) exists to avoid this.
Why divide at all#
Can a disk not be used whole? Sometimes it can. But there are reasons to divide.
- Several operating systems together. Each uses its own filesystem.
- Separating data of different character. Swap, system, personal data. One filling up leaves the others alive.
- A separate place for booting. Like UEFI’s ESP (EFI system partition), some places must be a filesystem the firmware can read.
- A unit for recovery and backup. Imaging one partition whole, for instance.
MBR — everything written into the first 512 bytes#
The oldest scheme. One sector, sector 0 of the disk, holds both the boot code and the partition table.
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
0x000 | 446 | bootstrap code | the first code the CPU runs when booting. Only the first piece of a bootloader fits | machine instructions |
0x1B8 | 4 | disk signature | the number by which Windows recognises the disk (absent in the oldest MBRs) | four arbitrary bytes |
0x1BC | 2 | reserved | usually zero | 00 00 |
0x1BE | 16 | partition entry 1 | see Table 115.4 below | — |
0x1CE | 16 | partition entry 2 | likewise | — |
0x1DE | 16 | partition entry 3 | likewise | — |
0x1EE | 16 | partition entry 4 | likewise | — |
0x1FE | 2 | boot signature | without it the BIOS does not accept this as a boot sector | 55 AA |
Table 115.3 — The whole layout of the MBR’s 512 bytes
★ The first and last rows tell the character of this scheme entirely. The boot code and the layout of the disk (the partition table) share the same 512 bytes. Which is why the code gets 446 bytes and there are only four partitions.
The 16-byte partition entry#
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
+0 | 1 | boot flag | 0x80 means “boot here”, 0x00 means not. Anything else is wrong | 80 or 00 |
+1 | 3 | starting CHS | the old start position. Today it is not read | FE FF FF |
+4 | 1 | partition type | one byte hinting what is inside (Table 115.6) | 83, 07, 0C … |
+5 | 3 | ending CHS | the old end position. Also unread | FE FF FF |
+8 | 4 | starting LBA | the value actually used. The sector this partition starts at | 2048 |
+12 | 4 | sector count | the value actually used. The length of this partition | 2097152 (= 1 GiB) |
Table 115.4 — The 16 bytes of an MBR partition entry
How the values pack into the three CHS bytes is worth recording too. This cramping is the source of the old limits.
| Byte | Bits | What | Range |
|---|---|---|---|
+0 | 7–0 | head | 0–255 |
+1 | 7–6 | the top 2 bits of the cylinder | the cylinder is 10 bits in all |
+1 | 5–0 | sector | 1–63 — there is no 0 |
+2 | 7–0 | the low 8 bits of the cylinder | 0–1023 |
Table 115.5 — Bit layout of the three CHS bytes
Output
== the MBR partition table (LBA 0) ==
entry 1 bootable=yes type=0x0c (FAT32 (LBA))
start LBA=2048 (relative) -> 2048 (absolute) size=204800 sectors = 100.0 MiB
CHS start=(0,32,33) end=(12,223,19)
entry 2 bootable=no type=0x83 (Linux)
start LBA=206848 (relative) -> 206848 (absolute) size=2097152 sectors = 1024.0 MiB
CHS start=(12,223,20) end=(143,106,27)
entry 3 bootable=no type=0x82 (Linux swap)
start LBA=2304000 (relative) -> 2304000 (absolute) size=262144 sectors = 128.0 MiB
CHS start=(143,106,28) end=(159,187,28)
entry 4 bootable=no type=0x0f (extended (LBA))
start LBA=2566144 (relative) -> 2566144 (absolute) size=4194304 sectors = 2048.0 MiB
CHS start=(159,187,29) end=(420,208,44)
signature 0x55aa --- accepted as a boot sector
== the EBR chain inside the extended partition (extended start LBA=2566144) ==
[EBR 1] absolute LBA of this EBR = 2566144
logical bootable=no type=0x83 (Linux)
start LBA=2048 (relative) -> 2568192 (absolute) size=1048576 sectors = 512.0 MiB
CHS start=(0,32,33) end=(65,101,36)
next EBR bootable=no type=0x0f (extended (LBA))
start LBA=1050624 (relative) -> 3616768 (absolute) size=2097152 sectors = 1024.0 MiB
CHS start=(65,101,37) end=(195,239,44)
[EBR 2] absolute LBA of this EBR = 3616768
logical bootable=no type=0x07 (NTFS / exFAT)
start LBA=2048 (relative) -> 3618816 (absolute) size=2095104 sectors = 1023.0 MiB
CHS start=(0,32,33) end=(130,138,8)
next EBR (empty)
-> end of the chain
== the limit of what CHS can express ==
1024 cylinders x 255 heads x 63 sectors x 512 bytes = 8422686720 bytes = 8.4 GB
beyond that the LBA field (4 bytes) takes over: 2^32 sectors x 512 = 2.2 TB
-> the MBR 2 TiB limit comes from here (the sector number is 32 bits).
The demonstration built exactly what the table describes and read it back. Three things to read in the output: the CHS fields, the MBR’s size limits and the type byte.
First, CHS is already abandoned. Positions too large for CHS are filled with the idiomatic FE FF FF, meaning “a position CHS cannot express”, and the reading side ignores it.
Second, the arithmetic shows where the limits come from. 1024 cylinders × 255 heads × 63 sectors × 512 bytes = 8.4 GB. And since the LBA field is four bytes, 232 sectors × 512 bytes ≈ 2.2 TB — the MBR 2 TiB limit people speak of.
Third, the type byte is only a hint. The values in the table below are convention, “usually this”; what is really inside is known only by opening it.
| Value | Meaning | Note |
|---|---|---|
0x00 | empty | the entry is unused |
0x05 | extended (CHS) | the old way; only below 8.4 GB |
0x07 | NTFS / exFAT | both use the same value — only opening it tells |
0x0B | FAT32 (CHS) | |
0x0C | FAT32 (LBA) | today’s FAT32 is usually this |
0x0E | FAT16 (LBA) | |
0x0F | extended (LBA) | today’s extended partitions are this |
0x82 | Linux swap | |
0x83 | Linux | ext4, xfs, everything takes this value |
0xEE | GPT protective MBR | “this disk is GPT” |
0xEF | EFI system partition | when an ESP is used on an MBR disk |
Table 115.6 — Partition type bytes commonly seen
The code with which a bootloader reads this table is short. Below is a piece of the demonstration run in the section “how a bootloader reads one file”. It checks the signature, moves to GPT if the first entry is 0xEE (to the alternate GPT at the end of the disk if the primary fails its checks), and otherwise walks the four entries. The bytes are assembled with le32 instead of overlaying a struct because the numbers on the disk are little-endian and do not sit on 4-byte boundaries.
examples-en/apx-disk/fat_boot/part.c (excerpt)
int partitions(struct vblk *d, struct part *out, int max, uint64_t *gpt_at)
{
*gpt_at = 0;
if (vblk_read(d, 0, sec) != 0) return -2; /* the device did not answer */
if (sec[510] != 0x55 || sec[511] != 0xAA) return -1; /* no signature, no MBR */
if (sec[446 + 4] == 0xEE) { /* protective MBR: GPT */
/* If the primary GPT (sector 1) fails its checks, read the backup GPT in the
disk's last sector. The backup's entry array sits just before the backup
header, which records where. */
*gpt_at = 1;
int n = gpt(d, *gpt_at, out, max);
if (n < 0) {
*gpt_at = d->sectors - 1;
n = gpt(d, *gpt_at, out, max);
}
return n;
}
int n = 0;
for (int i = 0; i < 4 && n < max; i++) {
const uint8_t *e = sec + 446 + 16 * i; /* four entries of 16 bytes */
if (e[4] == 0) continue; /* type 0 = empty entry */
out[n++] = (struct part){ .scheme = 'M', .mbr_type = e[4],
.lba = le32(e + 8), .count = le32(e + 12) };
}
return n;
}When four partitions are not enough — extended partitions and the EBR chain#
With only four entries there can be no fifth partition. The workaround devised was the extended partition. One entry is used as a marker (0x05/0x0F) saying “there are more divisions inside”, and inside the partition it describes lives a linked list.
Figure 115.2 — The EBR chain — the two entries of one EBR count from different bases.
The partitions made inside the extended partition are called logical partitions. Before each one sits one sector called an EBR (extended boot record), and only two entries of its partition table are used.
| Entry | What it points at | What the relative LBA is relative to | Type byte |
|---|---|---|---|
1 (0x1BE) | the logical partition immediately behind it | this EBR itself | the real type (0x83 and so on) |
2 (0x1CE) | the next EBR | the start of the extended partition | 0x05 or 0x0F |
| 3 and 4 | unused | — | 0x00 |
Table 115.7 — The two EBR entries — their bases differ
★ The third column of Table 115.7 is the trap. Two entries of the same EBR’s partition table with different bases. The latter part of the demonstration above walks that chain and computes the absolute LBAs — built deliberately, because it is the place most often misread by hand.
Counter-example. deleting an EBR from the middle of the chain
GPT — rebuilt out of an array, a check and a copy#
GPT (the GUID Partition Table) is the same job designed again. What changed, in one line: what was crammed into one sector was spread over an array of sectors, check values were attached so damage can be seen, and the whole thing was duplicated.
| MBR | GPT | |
|---|---|---|
| where the layout lives | one sector, number 0 | sector 1 (the header) + an array from sector 2 |
| number of partitions | 4 (worked around with extended) | the header decides — usually 128 |
| partition size limit | 2 TiB (32-bit sector numbers) | effectively none (64-bit) |
| partition names | none | yes — 36 characters (UTF-16) |
| type identification | a one-byte hint | a 16-byte GUID |
| damage detection | none (only two bytes, 55 AA) | two CRC32s — the header and the entry array |
| copies | none | a second set at the end of the disk |
| protection from old tools | — | a protective MBR (type 0xEE) |
Table 115.8 — MBR and GPT
The protective MBR — sector 0 is still an MBR#
Sector 0 of a GPT disk still holds an MBR. But one partition entry, of type 0xEE, covers the whole disk. It is a decoy, to stop an old tool that does not know GPT from seeing an empty disk and overwriting it. The size field holds the whole disk (or the largest value 32 bits can express).
The 92-byte GPT header#
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
0 | 8 | signature | the eight characters EFI PART | 45 46 49 20 50 41 52 54 |
8 | 4 | revision | the specification version | 00 00 01 00 (= 1.0) |
12 | 4 | header size | the range over which the CRC is computed | 92 |
16 | 4 | header CRC32 | the hash of the header with this field zeroed | computed |
20 | 4 | reserved | must be zero | 00 00 00 00 |
24 | 8 | LBA of this header | where it is itself | 1 |
32 | 8 | LBA of the alternate header | where the backup is | the last sector |
40 | 8 | first usable LBA | the earliest a partition may start | 34 |
48 | 8 | last usable LBA | the latest a partition may end | end − 33 |
56 | 16 | disk GUID | the unique number of this disk | 16 bytes |
72 | 8 | LBA of the entry array | the sector where the entries begin | 2 |
80 | 4 | number of entries | how many slots the array has | 128 |
84 | 4 | size of one entry | usually 128 bytes | 128 |
88 | 4 | entry array CRC32 | the hash of the whole array (count × size) | computed |
Table 115.9 — Every field of the GPT header (LBA 1, 92 bytes)
★ Look at offset 72. While writing this appendix it was written as 64 by mistake, which overwrote the tail of the disk GUID and made the output strange. That a mismatch between table and code shows itself is how this appendix verifies things.
The 128-byte GPT partition entry#
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
0 | 16 | type GUID | “what this partition is for” (Table 115.13) | C12A7328-… for an ESP |
16 | 16 | unique GUID | the label of this one partition — cloning it causes a collision | different for each |
32 | 8 | first LBA | the starting sector | 2048 |
40 | 8 | last LBA | the ending sector — this sector is included | 206847 |
48 | 8 | attribute bits | Table 115.11 | usually 0 |
56 | 72 | name | up to 36 characters in UTF-16LE | “EFI System” |
Table 115.10 — Every field of a GPT partition entry (128 bytes)
| Bit | Meaning | Who reads it |
|---|---|---|
| 0 | a system partition — do not touch | partitioning tools |
| 1 | the firmware ignores it | UEFI firmware |
| 2 | bootable by legacy BIOS | legacy booting |
| 60 | read only | Windows |
| 62 | hidden | Windows |
| 63 | do not automount (no drive letter) | Windows |
Table 115.11 — GPT attribute bits worth knowing
A GUID is written one way and stored another#
This is the first trap when reading GPT by hand.
| Group | Size | Storage order | Example (C12A7328-F81F-11D2-BA4B-00A0C93EC93B) |
|---|---|---|---|
| 1 | 4 bytes | little-endian — reversed | 28 73 2A C1 |
| 2 | 2 bytes | little-endian — reversed | 1F F8 |
| 3 | 2 bytes | little-endian — reversed | D2 11 |
| 4 | 2 bytes | as written | BA 4B |
| 5 | 6 bytes | as written | 00 A0 C9 3E C9 3B |
Table 115.12 — The five groups of a GUID and their storage order
Output
== the GPT header (LBA 1, 92 bytes) ==
signature : EFI PART
revision : 1.0
header size : 92 bytes (the sector is 512; the header uses only 92)
header CRC32 : 0x3e0d10e0
this / alternate : LBA 1 / LBA 4194303
usable range : LBA 34 - 4194270
disk GUID : 01234567-89AB-CDEF-0123-456789ABCDEF
entry array : from LBA 2, 128 x 128 bytes = 32 sectors
entry array CRC32: 0xb43cd5f9
== checking it ==
header CRC recomputed (that field zeroed) : 0x3e0d10e0 -> matches
entry array CRC recomputed : 0xb43cd5f9 -> matches
== entries (128 bytes each) ==
[1] name "EFI System"
type GUID (as text) : C12A7328-F81F-11D2-BA4B-00A0C93EC93B
type GUID (byte order): 28 73 2a c1 1f f8 d2 11 ba 4b 00 a0 c9 3e c9 3b
LBA 2048 ~ 206847 = 100.0 MiB
attributes 0x0000000000000000
[2] name "root"
type GUID (as text) : 0FC63DAF-8483-4772-8E79-3D69D8477DE4
type GUID (byte order): af 3d c6 0f 83 84 72 47 8e 79 3d 69 d8 47 7d e4
LBA 206848 ~ 3358719 = 1539.0 MiB
attributes 0x0000000000000000
[3] name "swap"
type GUID (as text) : 0657FD6D-A4AB-43C4-84E5-0933C84B4F4F
type GUID (byte order): 6d fd 57 06 ab a4 c4 43 84 e5 09 33 c8 4b 4f 4f
LBA 3358720 ~ 4194270 = 408.0 MiB
attributes 0x8000000000000000 (bit 63 = do not automount)
== where the alternate (backup) GPT is ==
disk 4194304 sectors = 2.0 GiB
alternate header: the last sector, LBA 4194303
alternate array : LBA 4194271 - 4194302 (32 sectors just before the header)
-> damage at the front is repaired from the back, and the back from the front.
The demonstration gives evidence for three things. The header CRC is computed with that field zeroed (otherwise its own value would enter its own hash and the computation would not stand). The entry array CRC covers count × size in full — including unused slots. And the alternate GPT is at the very end of the disk, so damage at the front can be repaired from the back.
The bootloader demonstration checks GPT as below. It zeroes the header’s CRC field before computing, and computes the entry array’s CRC sector by sector while picking out the used entries. The sector number the header records for itself must match too. If anything disagrees, that header is distrusted and the alternate GPT in the disk’s last sector is read with the same checks (the branch is in the MBR piece above). On a fourth disk with one byte of the primary header damaged, the loader read the alternate and went on to the file — the primary GPT failed its checks line in the output of “how a bootloader reads one file” below.
examples-en/apx-disk/fat_boot/part.c (excerpt)
/* CRC-32 (IEEE 802.3, reflected). One bit at a time, no table ---
in a bootloader size is worth more than speed. */
static uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
for (uint32_t i = 0; i < n; i++) {
crc ^= p[i];
for (int k = 0; k < 8; k++) crc = crc >> 1 ^ (0xEDB88320u & -(crc & 1));
}
return crc;
}
/* … */
/* Read the GPT header at lba and the entry array it points to. */
static int gpt(struct vblk *d, uint64_t lba, struct part *out, int max)
{
if (vblk_read(d, lba, sec) != 0) return -1;
static const char sig[8] = { 'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T' };
for (int i = 0; i < 8; i++) if (sec[i] != (uint8_t)sig[i]) return -1;
uint32_t hsize = le32(sec + 12);
if (hsize < 92 || hsize > 512) return -1;
if (le64(sec + 24) != lba) return -1; /* its recorded own LBA must match */
/* The header CRC is computed *with its own field set to zero*. */
uint32_t want = le32(sec + 16);
sec[16] = sec[17] = sec[18] = sec[19] = 0;
if (~crc32_update(~0u, sec, hsize) != want) return -1;
uint64_t entries_lba = le64(sec + 72);
uint32_t count = le32(sec + 80), esize = le32(sec + 84);
uint32_t entries_crc = le32(sec + 88);
if (esize != 128 || count > 1024) return -1;
/* Compute the CRC of the whole entry array sector by sector, picking out the used
entries on the way. */
uint32_t crc = ~0u;
int n = 0;
for (uint32_t s = 0; s < (count * esize + 511) / 512; s++) {
if (vblk_read(d, entries_lba + s, sec) != 0) return -1;
uint32_t bytes = count * esize - s * 512 < 512 ? count * esize - s * 512 : 512;
crc = crc32_update(crc, sec, bytes);
for (uint32_t e = 0; e < bytes; e += 128) {
const uint8_t *p = sec + e;
int used = 0;
for (int k = 0; k < 16; k++) used |= p[k]; /* zero type GUID = empty */
if (!used || n >= max) continue;
out[n] = (struct part){ .scheme = 'G', .lba = le64(p + 32) };
/* the ending LBA is inclusive */
out[n].count = le64(p + 40) - out[n].lba + 1;
for (int k = 0; k < 16; k++) out[n].guid[k] = p[k];
n++;
}
}
return ~crc == entries_crc ? n : -1;
}| GUID | What | Note |
|---|---|---|
C12A7328-F81F-11D2-BA4B-00A0C93EC93B | EFI system partition (ESP) | formatted as FAT. The firmware reads .efi from here — see the appendix on booting |
21686148-6449-6E6F-744E-656564454649 | BIOS boot partition | where GRUB puts its core image when a GPT disk boots from a legacy BIOS |
0FC63DAF-8483-4772-8E79-3D69D8477DE4 | Linux filesystem | |
0657FD6D-A4AB-43C4-84E5-0933C84B4F4F | Linux swap | |
E6D6D379-F507-44C2-A23C-238F2A3DF928 | Linux LVM | |
EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 | Microsoft basic data | NTFS, exFAT and FAT all take this value |
DE94BBA4-06D1-4D40-A16A-BFD50179D6AC | Windows recovery environment |
Table 115.13 — Type GUIDs commonly seen
Alignment — why exactly sector 2048#
Today’s tools start the first partition at LBA 2048. 2048 × 512 bytes = 1 MiB.
| What | Chunk size | If misaligned |
|---|---|---|
| 512e disks (4Kn always lines up) | 4 KiB | every write gains a read-modify-write |
| an SSD’s erase unit | hundreds of KiB to several MiB | more erasing and rewriting, cutting life and speed |
| RAID stripes | 64 KiB to 1 MiB | one write spans two devices |
| virtual disk blocks | around 1 MiB | the same problem again, one layer up on the host |
Table 115.14 — What 1 MiB alignment lines up with
★ 1 MiB is a multiple of every common chunk size up to 1 MiB (each a power of two), so lining up with one lines up with all. Compare the old tools that started at sector 63 (“just past the first track” of the CHS era): 63 sectors are not a multiple of 4 KiB, so it was always misaligned.
What is inside a partition — a filesystem’s first sector#
A partition is only “from here, this many”, and what goes on inside is the filesystem’s business. A filesystem usually writes its own description into its first sector (or first block).
| Filesystem | Name | Where | How it is recognised |
|---|---|---|---|
| FAT12/16/32 | BPB (the boot sector) | sector 0 of the partition | 55 AA at the end, and the FAT32 label |
| exFAT | boot sector | sector 0 of the partition | EXFAT at offset 3 |
| NTFS | boot sector | sector 0 of the partition | NTFS at offset 3 |
| ext2/3/4 | the superblock | 1024 bytes past the start of the partition | 53 EF at superblock offset 0x38 |
Table 115.15 — Where a filesystem’s self-description lives
FAT32 — the easiest example to read#
The name FAT comes from the file allocation table this filesystem keeps near the start of the volume, and in the table below “the FAT” means that table. FAT is old and simple, which makes it a good way to learn what a filesystem must record.
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
0 | 3 | jump instruction | it jumps over to the code when booting | EB 58 90 |
3 | 8 | OEM name | characters. It means nothing | MSWIN4.1 |
11 | 2 | sector size | in bytes | 512 |
13 | 1 | sectors per cluster | the unit in which file space is handed out | 8 (= 4 KiB) |
14 | 2 | reserved sectors | space before the FAT (the boot sector included) | 32 |
16 | 1 | number of FATs | how many copies | 2 |
17 | 2 | root entry count | 0 on FAT32 (the root lives in clusters too) | 0 |
19 | 2 | total sectors (16-bit) | for small volumes; zero if unused | 0 |
21 | 1 | media type | an old trace | F8 (fixed disk) |
22 | 2 | FAT size (16-bit) | 0 on FAT32 | 0 |
28 | 4 | hidden sectors | where this partition starts on the disk | 2048 |
32 | 4 | total sectors (32-bit) | the size of this volume | 2097152 |
36 | 4 | FAT size (32-bit) | sectors in one FAT — a computed value | 2046 |
44 | 4 | root cluster | the cluster the root directory starts at | 2 |
48 | 2 | FSInfo sector | where the free cluster count is cached | 1 |
50 | 2 | backup boot sector | where a copy sits in case the boot sector is damaged | 6 |
66 | 1 | extended signature | 0x29 means the next three are valid | 29 |
67 | 4 | volume serial number | decided at format time | arbitrary |
71 | 11 | volume label | characters | NO NAME |
82 | 8 | filesystem label | not to be trusted — it is only characters | FAT32 |
510 | 2 | signature | 55 AA | 55 AA |
Table 115.16 — The main fields of the FAT32 boot sector (BPB)
Output
== the FAT32 BPB (first sector of the partition) ==
sector size : 512 bytes
cluster size : 8 sectors = 4096 bytes (4 KiB)
reserved sectors : 32
FAT copies / size : 2 x 2046 sectors
total sectors : 2097152 = 1024.0 MiB
root cluster : 2
volume label : "NO NAME ", filesystem label "FAT32 "
hidden sectors : 2048 <- where this partition starts on the disk
== how the partition divides up ==
reserved area : sectors 0 - 31 (boot sector, FSInfo, copies)
FAT copy 1 : sectors 32 - 2077
FAT copy 2 : sectors 2078 - 4123 <- a duplicate of the same content
data area : from sector 4124 (cluster numbers start here)
clusters : 261628 --- FAT32 needs at least 65525 -> it is FAT32
== changing the cluster size on the same volume ==
cluster clusters one FAT usable as FAT32
512 B 2064606 16257 sectors yes
1 KiB 1040400 8160 sectors yes
2 KiB 522236 4088 sectors yes
4 KiB 261628 2046 sectors yes
8 KiB 130942 1024 sectors yes
16 KiB 65503 512 sectors no --- too few clusters
32 KiB 32759 256 sectors no --- too few clusters
-> this is why a small volume ends up FAT16. Larger clusters mean fewer
of them to manage, and the count falls below 65525.
== cluster number -> sector number ==
formula: start of data area + (N - 2) x sectors per cluster
(why subtract 2: 0 and 1 are used as labels and have no real place)
cluster 2 -> sector 4124 in the partition -> absolute LBA 6172 <- the root directory
cluster 3 -> sector 4132 in the partition -> absolute LBA 6180
cluster 4 -> sector 4140 in the partition -> absolute LBA 6188
cluster 5 -> sector 4148 in the partition -> absolute LBA 6196
== when one file is scattered --- the FAT chain ==
the FAT is an array of cluster number -> next cluster number (4 bytes each).
if a file starts at cluster 3:
cluster 3 (LBA 6180 , 4 KiB) -> next is 4
cluster 4 (LBA 6188 , 4 KiB) -> next is 7
cluster 7 (LBA 6212 , 4 KiB) -> end of chain
so a file that is not contiguous can still be read. And damage to one FAT
removes the way to find the rest of a file --- which is why there are two copies.
Three things to learn from this demonstration: the arithmetic of places, the structure of the FAT, and the cluster size.
First, the arithmetic of places is only addition. Start of the data area = reserved sectors + (number of FATs × FAT size). And the sector of cluster N = start of data area + (N − 2) × sectors per cluster. Two is subtracted because clusters 0 and 1 are reserved entries in the FAT and have no real place.
Second, the FAT is an array of “the next cluster number”. However scattered a file is across the disk, recording the next cluster’s number in each cluster lets it be read in order. So damage to the FAT removes the way to reach the rest of a file while the data is still there — which is why there are two copies.
| Value | Meaning | Note |
|---|---|---|
0x0000000 | a free cluster | available |
0x0000002 to 0xFFFFFEF | the next cluster number | the chain continues |
0xFFFFFF7 | a bad cluster | not used |
0xFFFFFF8 and above | end of chain | the file’s last cluster |
Table 115.17 — FAT32 entry values (only the low 28 bits are used)
Third, the cluster size even settles which filesystem it can be. As the demonstration’s table shows, on the same 1 GiB volume, raising the cluster to 16 KiB drops the cluster count below 65,525 and it cannot be FAT32. That is where a small USB stick formatted as FAT16 comes from.
How a bootloader reads one file#
A bootloader uses the tables so far in turn. Fetching sectors is the job of a driver for a virtio block device. virtio is the specification for the devices that virtual machines such as QEMU offer, and this driver only “reads one sector”. It touches registers only through the same four functions as the drivers in the “how machines talk” appendix, and requests and answers travel through rings kept in memory (the virtqueue). The device reads and writes those rings and the sector buffer itself, by DMA (direct memory access: a device reaching memory without the CPU).
examples-en/apx-disk/fat_boot/virtio_blk.c (excerpt)
int vblk_read(struct vblk *d, uint64_t lba, void *buf512)
{
req = (struct req){ .type = 0, .sector = lba };
req_status = 0xFF;
/* header (what to read) -> data (512 bytes) -> status (1 byte), chained */
q_desc[0] = (struct desc){ (uintptr_t)&req, sizeof req, F_NEXT, 1 };
q_desc[1] = (struct desc){ (uintptr_t)buf512, 512, F_WRITE | F_NEXT, 2 };
q_desc[2] = (struct desc){ (uintptr_t)&req_status, 1, F_WRITE, 0 };
uint16_t seen = q_used.idx;
q_avail.ring[q_avail.idx % QN] = 0; /* one request, chained from descriptor 0 */
/* raise idx only *after* the ring is written */
atomic_thread_fence(memory_order_seq_cst);
q_avail.idx++;
atomic_thread_fence(memory_order_seq_cst);
wr(d, Q_NOTIFY, 0); /* "look at queue 0" */
/* The device answers in memory (the used ring), not in a register. */
for (long i = 0; i < 10000000; i++) {
if (*(volatile uint16_t *)&q_used.idx != seen) {
atomic_thread_fence(memory_order_seq_cst);
wr(d, INT_ACK, rd(d, INT_STATUS));
return req_status == 0 ? 0 : -1;
}
}
return -2; /* no answer */
}On top of it sits the code that reads FAT: it works out the places from the BPB, settles the FAT type, and looks for the name in the root directory. A FAT12 entry is a byte and a half, so two entries share three bytes and one entry can straddle a sector boundary of the FAT. The name looked for, boot-kernel.bin, does not fit the 8.3 form (eight characters of name, three of extension), so the tool that put the file there added long-name (LFN, long file name) entries along with a short name (BOOT-K~1.BIN or similar). A long name is stored in reverse as 32-byte pieces right before the short-name entry, and every piece carries the short name’s checksum to point at its partner.
examples-en/apx-disk/fat_boot/fat.c (excerpt)
int fat_mount(struct fat *fs, struct vblk *d, uint32_t part_lba)
{
if (vblk_read(d, part_lba, sec) != 0) return -1;
if (le16(sec + 11) != 512 || sec[13] == 0 || sec[510] != 0x55 || sec[511] != 0xAA)
return -1;
uint16_t reserved = le16(sec + 14);
uint8_t nfats = sec[16];
uint16_t rootents = le16(sec + 17); /* 0 on FAT32 */
uint32_t fatsz = le16(sec + 22) ? le16(sec + 22) : le32(sec + 36);
uint32_t total = le16(sec + 19) ? le16(sec + 19) : le32(sec + 32);
uint32_t rootsecs = (rootents * 32u + 511) / 512;
*fs = (struct fat){ .disk = d, .spc = sec[13], .root_sectors = rootsecs };
fs->fat_lba = part_lba + reserved;
fs->root_lba = fs->fat_lba + nfats * fatsz; /* right after the FATs */
fs->data_lba = fs->root_lba + rootsecs; /* the same place on FAT32 */
fs->clusters = (total - (reserved + nfats * fatsz + rootsecs)) / fs->spc;
/* The FAT type is decided by the cluster count, not by the label ("FAT16 ") (the
specification's rule). */
fs->bits = fs->clusters < 4085 ? 12 : fs->clusters < 65525 ? 16 : 32;
if (fs->bits == 32) fs->root_cluster = le32(sec + 44);
return 0;
}
/* … */
/* The FAT is an array that says "after this cluster comes number ...". An entry is
a byte and a half on FAT12, 2 bytes on FAT16, 4 bytes on FAT32 (top 4 bits unused).
The end mark is returned as 0x0FFFFFFF for all three. */
static uint32_t next_cluster(struct fat *fs, uint32_t c)
{
uint32_t at = fs->bits == 12 ? c + c / 2 : c * (fs->bits / 8);
if (vblk_read(fs->disk, fs->fat_lba + at / 512, sec) != 0) return 0x0FFFFFFF;
if (fs->bits == 12) {
/* Two 12-bit entries share 3 bytes, so one entry can straddle a sector
boundary. */
uint32_t v = sec[at % 512];
uint32_t next = fs->fat_lba + at / 512 + 1;
if (at % 512 == 511 && vblk_read(fs->disk, next, sec) != 0) return 0x0FFFFFFF;
v |= (uint32_t)sec[(at + 1) % 512] << 8;
v = c & 1 ? v >> 4 : v & 0xFFF; /* odd numbers take the top 12 bits */
return v >= 0xFF8 ? 0x0FFFFFFF : v;
}
if (fs->bits == 16) {
uint16_t v = le16(sec + at % 512);
return v >= 0xFFF8 ? 0x0FFFFFFF : v;
}
return le32(sec + at % 512) & 0x0FFFFFFF;
}
static uint32_t cluster_lba(const struct fat *fs, uint32_t c)
{ return fs->data_lba + (c - 2) * fs->spc; } /* cluster numbers start at 2 */
/* … */
/* A long name (LFN) is pieces stored in reverse *before* the short-name entry. Each
piece holds 13 UCS-2 characters, a sequence number (0x40 on the last piece) and the
checksum of the short name it belongs to. Pieces can cross directory sectors, so the
state lives outside a sector. */
struct lfn { char name[261]; uint8_t sum; int valid; };
static void lfn_piece(struct lfn *l, const uint8_t *e)
{
static const uint8_t at[13] = { 1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30 };
unsigned ord = e[0] & 0x1F;
if (e[0] & 0x40) { /* the last piece comes first */
l->valid = ord >= 1 && ord <= 20;
l->sum = e[13];
if (l->valid) l->name[ord * 13] = '\0';
}
if (!l->valid || e[13] != l->sum || ord < 1 || ord > 20) { l->valid = 0; return; }
for (unsigned k = 0; k < 13; k++) {
uint16_t ch = le16(e + at[k]);
char *p = &l->name[(ord - 1) * 13 + k];
if (ch == 0x0000) *p = '\0'; /* end of the name */
else if (ch != 0xFFFF) *p = ch < 0x80 ? (char)ch : '?'; /* 0xFFFF is padding */
}
}
/* Checksum of the 11-byte short name: rotate right by one bit and add, repeatedly. */
static uint8_t short_sum(const uint8_t *e)
{
uint8_t sum = 0;
for (int k = 0; k < 11; k++) sum = (uint8_t)(((sum & 1) << 7) + (sum >> 1) + e[k]);
return sum;
}
/* … */
/* The root directory: on FAT12 and FAT16 a fixed area after the FATs,
on FAT32 a cluster chain like any file. */
static int find(struct fat *fs, const char *name, struct hit *h)
{
struct lfn l; /* = { 0 } would call memset (no libc) */
l.valid = 0;
int r = 0;
if (fs->bits != 32)
for (uint32_t s = 0; s < fs->root_sectors && r == 0; s++)
r = scan(fs, fs->root_lba + s, name, &l, h);
else
for (uint32_t c = fs->root_cluster; !is_end(c) && r == 0;
c = next_cluster(fs, c))
for (uint32_t s = 0; s < fs->spc && r == 0; s++)
r = scan(fs, cluster_lba(fs, c) + s, name, &l, h);
return r == 1 ? 0 : -1;
}Output
== this machine: the commands the driver sends (trace backend) ==
R 0x10001000 -> 0x00000000
R 0x10002000 -> 0x00000000
... 5 more like this, every 0x1000, up to 0x10007000
R 0x10008000 -> 0x74726976
R 0x10008004 -> 0x00000002
R 0x10008008 -> 0x00000002
W 0x10008070 <- 0x00000000
W 0x10008070 <- 0x00000001
W 0x10008070 <- 0x00000003
W 0x10008014 <- 0x00000001
R 0x10008010 -> 0x00000001
W 0x10008024 <- 0x00000000
W 0x10008020 <- 0x00000000
W 0x10008024 <- 0x00000001
W 0x10008020 <- 0x00000001
W 0x10008070 <- 0x0000000b
R 0x10008070 -> 0x0000000b
W 0x10008030 <- 0x00000000
R 0x10008034 -> 0x00000100
W 0x10008038 <- 0x00000004
W 0x10008080 <- 0x005064c0
W 0x10008084 <- 0x00000000
W 0x10008090 <- 0x005064a8
W 0x10008094 <- 0x00000000
W 0x100080a0 <- 0x00506480
W 0x100080a4 <- 0x00000000
W 0x10008044 <- 0x00000001
W 0x10008070 <- 0x0000000f
R 0x10008100 -> 0x00020000
R 0x10008104 -> 0x00000000
# virtio-blk at 0x10008000, 131072 sectors
W 0x10008050 <- 0x00000000
# sector 0: the device did not answer
W 0x00100000 <- 0x00005555
== QEMU virt (emulated RISC-V): the same loader on five virtio disks ==
virtio-blk at 0x10004000, 8192 sectors
MBR partition 1: sectors 2048 + 6144, type 0x01
FAT12: 1525 clusters of 4 sectors, FAT at sector 2049, data at sector 2091
boot-kernel.bin, 770042 bytes: hello from sfdisk + mkfs.fat (MBR, FAT12)
110000 numbered lines, all in order
virtio-blk at 0x10005000, 131072 sectors
primary GPT failed its checks; using the backup at sector 131071
GPT partition 1: sectors 2048 + 126976, type c12a7328-f81f-11d2-ba4b-00a0c93ec93b
FAT32: 127006 clusters of 1 sectors, FAT at sector 2080, data at sector 4066
boot-kernel.bin, 770042 bytes: hello from sfdisk + mkfs.fat (GPT, FAT32)
110000 numbered lines, all in order
virtio-blk at 0x10006000, 131072 sectors
GPT partition 1: sectors 2048 + 126976, type c12a7328-f81f-11d2-ba4b-00a0c93ec93b
FAT32: 127006 clusters of 1 sectors, FAT at sector 2080, data at sector 4066
boot-kernel.bin, 770042 bytes: hello from sfdisk + mkfs.fat (GPT, FAT32)
110000 numbered lines, all in order
virtio-blk at 0x10007000, 131072 sectors
MBR partition 1: sectors 2048 + 129024, type 0x0c
FAT32: 127006 clusters of 1 sectors, FAT at sector 2080, data at sector 4066
boot-kernel.bin, 770042 bytes: hello from sfdisk + mkfs.fat (MBR, FAT32)
110000 numbered lines, all in order
virtio-blk at 0x10008000, 1032192 sectors
MBR partition 1: sectors 63 + 1032129, type 0x06
FAT16: 64474 clusters of 16 sectors, FAT at sector 64, data at sector 600
boot-kernel.bin, 770035 bytes: hello from QEMU vvfat (MBR, FAT16)
110000 numbered lines, all in order
The QEMU half found boot-kernel.bin by its long name on all five disks and read it. The three made by sfdisk and mkfs.fat are GPT with FAT32, MBR with FAT32 and MBR with FAT12. The fourth is a copy of the GPT disk with its primary header damaged, and was read through the alternate GPT. The remaining one, made by QEMU’s vvfat (a feature that shows a directory as a FAT disk), is MBR with FAT16. The file is about 750 KiB of 110,000 numbered lines, so one wrong step along the cluster chain breaks the numbering; on all five the numbers ran to the end. On the FAT12 disk the chain runs over 376 clusters, past the entry that straddles a sector boundary (cluster 341); built without the lines that handle that boundary, the chain broke on that disk alone.
This driver settles the FAT type not by the label but by the cluster count, the rule of the third point above — 1,525 is below 4,085 and so FAT12, 64,474 is below 65,525 and so FAT16, and 127,006 is FAT32. Asked for FAT32, vvfat still produced a FAT16 boot sector (QEMU itself warns that “FAT32 has not been tested”), so the vvfat disk was left as FAT16.
This machine’s half stops after asking for sector 0. Setting up the rings and writing Q_NOTIFY are register commands and get printed, but the sector contents are written into memory by the device through DMA, which a reply table cannot imitate. The ring addresses written to 0x10008080 and its neighbours in the output are where the program’s variables sit on this machine. Also, QEMU’s virt model is a machine on which DMA and caches do not disagree (dma-coherent), so this driver has no cache-flushing code. On a machine that is not, the cache handling listed in the DMA section of the “how machines talk” appendix is needed as well.
How ext4 and NTFS differ from FAT#
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
0x00 | 4 | total inodes | one inode per file — fixed at format time | computed |
0x04 | 4 | total blocks | the volume size in blocks | computed |
0x18 | 4 | block size exponent | block size = 1024 << this value | 2 (= 4 KiB) |
0x20 | 4 | blocks per group | the size of a block group | 32768 |
0x28 | 4 | inodes per group | the number of inodes in one block group | computed |
0x38 | 2 | magic | 53 EF — how ext is recognised | 53 EF |
0x3A | 2 | state | whether it was unmounted cleanly | 1 |
0x58 | 2 | inode size | usually 256 bytes | 256 |
0x60 | 4 | feature flags (compatible) | the journal and so on | — |
0x64 | 4 | feature flags (incompatible) | features that forbid mounting if unknown | — |
0x68 | 16 | UUID | the volume’s unique number — what /etc/fstab uses | 16 bytes |
0x78 | 16 | volume name | characters | — |
Table 115.18 — The main fields of the ext4 superblock (start of partition + 1024 bytes)
Where FAT is “an array of next-slot numbers”, ext4 centres on the inode. One file holds one inode, in which its size, permissions, times and where its data is are recorded. And places are recorded not one number at a time but as extents (contiguous runs), so a large file is described compactly.
| Offset | Size | Name | What | Typical value |
|---|---|---|---|---|
3 | 8 | label | recognised by NTFS | NTFS |
11 | 2 | sector size | 512 | |
13 | 1 | sectors per cluster | 8 | |
28 | 4 | hidden sectors | where the partition starts | 2048 |
40 | 8 | total sectors | 64-bit — hence large volumes | computed |
48 | 8 | cluster of $MFT | where the list of everything begins | computed |
56 | 8 | cluster of $MFTMirr | a copy of that list | computed |
64 | 1 | MFT record size | negative means 2|value| bytes (usually 1024) | F6 |
510 | 2 | signature | 55 AA |
Table 115.19 — The main fields of the NTFS boot sector
In NTFS everything is a file — even the list of files ($MFT) is a file, and the boot sector is a file called $Boot. Record only where that list is in the boot sector, and everything else is found by following the list.
| Name | Space management | Journal | File size limit | Mostly where |
|---|---|---|---|---|
| FAT32 | FAT chains | none | under 4 GiB | USB, SD, ESP — readable everywhere |
| exFAT | FAT chains (64-bit) | none | effectively none | large SD cards, cameras |
| NTFS | MFT + extents | metadata | very large | Windows |
| ext4 | inodes + extents | yes (optional) | around 16 TiB | Linux |
| btrfs, ZFS, APFS | copy-on-write + checksums | the structure itself replaces it | very large | where snapshots and integrity are needed |
Table 115.20 — Filesystems commonly met
★ The “journal” column is where these filesystems differ most in practice. The FAT family, with no journal, can be left with the filesystem itself inconsistent if power is lost during a write. That is why you should use “safely remove” before pulling out a USB stick.
The physics of the medium — why random writes are slow#
So far we have read the structures written on the disk (partition tables and filesystems’ first sectors). Now the thing underneath, where the bytes actually live. The same word “storage” covers a spinning platter and a semiconductor, and their characters differ enormously — a difference that settles the design of filesystems and databases.
The spinning platter — the time to reach a place#
The time for one read on a hard disk divides into three parts.
| What | What is waited for | Typical value | Can it be reduced |
|---|---|---|---|
| seek | the head moving to that track | 5–10 ms | reading nearby places reduces it |
| rotational latency | the platter turning until that sector arrives | at 7200 rpm, 4.2 ms on average | hard to reduce |
| transfer | actually moving the bytes | tens of microseconds | very small if sequential |
Table 115.21 — The time for one read on a hard disk
★ The first two are the cost of finding a place and the third is the cost of the work, and the first two are about a hundred times the third. That is where “random access is slow” comes from — reading 4 KiB or 1 MiB, the cost of finding the place once is the same.
Q. Why is the average rotational latency 4.2 ms at 7200 rpm?
A. 7200 per minute is s, or 8.33 ms, per turn. On average the wanted sector arrives half a turn later, so half of that: 4.17 ms. The rotation speed alone settles the latency — software can do nothing about it.
The semiconductor — writing and erasing in different units#
An SSD has no moving parts, so there is no seek. In its place another property appears: it cannot be overwritten where it stands.
Figure 115.3 — An SSD’s pages and blocks — where write amplification comes from.
| Unit | Size (roughly) | Used for | Constraint |
|---|---|---|---|
| cell | 1 to 4 bits | the smallest thing holding a value | it wears a little with every write |
| page | 4 to 16 KiB | the unit of reading and writing | only a free page can be written |
| block | several MiB (hundreds of pages) | the unit of erasing | erasing means the whole block |
Table 115.22 — Three units inside an SSD
Every property of an SSD follows from this.
| What | What it is | What the programmer feels |
|---|---|---|
| the FTL (mapping layer) | the device keeps a “logical address → real page” table itself | writing the same address lands somewhere different each time |
| write amplification | changing 4 KiB rewrites and erases a block | small random writes are unusually expensive |
| garbage collection | tidying blocks full of dead pages into free ones | writes suddenly slow down now and then |
| TRIM | the filesystem saying “this page is no longer used” | after deleting, speed returns |
| wear levelling | rotating use so the same block does not wear out | “with no free space the life shortens” |
| over-provisioning | spare capacity invisible to the user | the real chips are larger than the advertised size |
Table 115.23 — What “it cannot be overwritten” produces
A common misconception. on an SSD, random access is as fast as sequential
What was measured on this machine#
examples-en/apx-disk/storage_cost.c (excerpt)
fd = open(path, O_WRONLY);
double s[9];
for (int r = 0; r < 9; r++) {
t0 = ns();
for (int i = 0; i < 20; i++) {
if (write(fd, buf, BLK) != (ssize_t)BLK) { perror("write"); return 1; }
fsync(fd);
}
t1 = ns();
s[r] = (t1 - t0) / 20.0 / 1e6; /* milliseconds per fsync */
}
qsort(s, 9, sizeof *s, cmp_d);
/* … */
for (int cold = 0; cold < 2; cold++) {
if (cold) posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); /* drop only this file's cache */
lseek(fd, 0, SEEK_SET);
t0 = ns();
for (unsigned i = 0; i < FILE_MiB; i++)
if (read(fd, buf, MiB) != (ssize_t)MiB) { perror("read"); return 1; }
t1 = ns();
double mbs = FILE_MiB / ((t1 - t0) / 1e9);
if (cold) seq_cold = mbs; else seq_cached = mbs;
}Output
== what is being measured ==
measured with one 64 MiB temporary file. This is not the figure of one device
but of the filesystem, the cache and the device together --- the whole stack.
== 1. sequential writing ==
writing 64 MiB (into cache) : 6444.6 MiB/s
one fsync after it : 41.6 ms <- this much had not reached the disk
4 KiB write + fsync : 0.319 ms each -> about 3133 per second
* that is the cost of one database commit. Which is why designs gather several
commits and flush them together (group commit).
== 2. reading --- with the cache, and after asking to drop it ==
sequential read (cached) : 10442.7 MiB/s
sequential read (dropped) : 11043.0 MiB/s -> a factor of 0.9
random 4 KiB (cached) : 2.8 us each -> 356261 per second
random 4 KiB (dropped) : 2.7 us each -> 369139 per second
* the two are nearly equal --- the request to drop the cache was not honoured.
This filesystem keeps its own cache, which `posix_fadvise` does not
empty. So read the numbers above not as the speed of the device but as
the speed of reading a file that is already in memory.
(11043 MiB/s is the speed of memory, not of storage.)
Really measuring the device needs bypassing the cache (O_DIRECT) or the
privilege to drop the system cache, and this environment has neither ---
so what could not be measured is left unmeasured.
== 3. placed on the ladder from earlier ==
what each against L1 (1.4 ns)
L1 cache read 1.4 ns 1 x
main memory read 75.0 ns 54 x
random 4 KiB (from cache) 2.8 us 2005 x
4 KiB write + fsync 319.2 us 227992 x
* the ladder does not end at the cache. Several more orders of magnitude lie below.
And on those lower rungs, reading in order matters far more.
★ The most important thing in this measurement’s conclusion is not what was measured but what was not. The request to drop the cache (posix_fadvise) is not honoured by this filesystem, so “sequential read” came out at 11 GiB/s — which is the speed of memory, not of storage. The demonstration checks that itself and says so.
In practice. meaning to measure the device and measuring the cache
This is the commonest mistake in storage measurement. The filesystem cache sits in between, so measuring without preparation gives the speed of memory. Measuring properly means bypassing the cache (O_DIRECT) or emptying the whole system’s cache, and both need privilege.
So this appendix leaves it as “the device could not be isolated in this environment”. What could be isolated is clear enough — the cost of fsync, because that is a cost no cache can hide.
fsync — the cost no cache can hide#
A write usually reaches only the cache. Lose power and it is gone. To be promised “it really is stored” you must call fsync, and that costs orders of magnitude more than a write that stops at the cache.
| What | Each | Against L1 | What spends the time |
|---|---|---|---|
| L1 cache read | about 1.4 ns | 1× | — |
| main memory read | about 75 ns | about 54× | the distance to memory |
| reading 4 KiB from a file (cached) | about 2.3 µs | about 1,600× | the system call and the copy |
4 KiB write + fsync | about 0.3 ms | about 200,000× | it really goes down to the device |
Table 115.24 — The lower rungs of the memory ladder (measured on this machine)
★ The last row explains database design entirely. At 0.3 ms per commit, three thousand per second is the ceiling. So databases gather several commits and flush them together (group commit), and write first to a log that is only ever appended sequentially (WAL).
| Situation | What to do | Why |
|---|---|---|
| small writes, very frequent | gather and write once | the cost of one fsync barely depends on size |
| you must promise “it is saved” | call fsync and budget for its cost | data written only to the cache is not yet saved |
| many random reads | fine on an SSD; rearrange the layout on a platter | on a platter, finding the place costs about a hundred times the transfer |
| keeping a log | an append-only file | sequential writing is the cheapest on any medium |
| measuring the device | bypass or empty the cache first | otherwise you measure memory |
Table 115.25 — Design guidance that comes from this layer
What breaks where#
The symptom alone can tell which layer was damaged. That is this appendix’s practical use.
| Symptom | Layer damaged | The data | What to look at first |
|---|---|---|---|
| it will not boot, but every file appears when plugged into another machine | the bootstrap code (446 bytes) | intact | reinstall the bootloader |
| the disk is seen, but it asks “initialise this disk?” | the partition table | usually intact | write nothing. The alternate GPT, or a partition recovery tool |
| the partition is seen, but it asks “format this?” | the filesystem’s first sector | usually intact | the backup boot sector (sector 6 for FAT), or a superblock copy |
| the file list appears but the contents are corrupt | the FAT, MFT or similar structure | partly | a checking tool (fsck, chkdsk) on an image of the disk |
| only certain files cannot be read | sectors in the data area | only those files | the health of the disk itself (SMART) |
Table 115.26 — Symptom → which layer was damaged
In practice. saved by not clicking "initialise"
What to take from this#
Recap
- All a disk knows is numbered sectors. Partitions and filesystems are conventions above them.
- The MBR put boot code and layout into one 512-byte sector — hence 446 bytes of code, four partitions and the 2 TiB limit.
- The extended partition invented to get past four is a linked list, and each EBR’s two entries have different bases, which confuses people.
- GPT rebuilt the same job out of an array, a CRC and a copy. And it left a protective MBR in sector 0 to hold off old tools.
- A GUID’s byte order when stored differs from how it is written.
- Inside a partition is the filesystem’s world. FAT is an array of next-slot numbers, ext4 centres on inodes, and in NTFS everything is a file.
- Because the layers are distinct, the symptom tells which layer was damaged. And when the damaged layer is the layout, the data is usually still there — at which point the most dangerous thing is a hasty write.