---
title: 'Block Devices and Partitions'
source: 'https://academia.sh/en/courses/system-administration/block-devices-and-partitions'
course: 'System Administration'
language: en
updated: '2026-08-17T18:10:05+00:00'
license: 'CC BY-SA 4.0'
---

# Block Devices and Partitions

A partition list is not a map: while the device shows 3,702,784 free sectors, the largest contiguous gap is 2,881,536 sectors, and six of the twelve placement decisions drawn from the list turn out wrong.

The Logs topic separated which indicator a bottleneck should be read from, and
reduced I/O wait to a single number. Beneath that number sits a device, and this
topic descends there: to the place where bytes are actually written. Storage is
the layer in system administration where failure looks quietest; a process's
stopping is visible, a service's not running is visible, but a disk filling up
is usually visible only once a write fails.

The first question is the lowest one. A file system is built not on an entire
device but on a part of it; a small table says where that part begins and ends.
This lesson reads that table and counts what the table **does not say**.

## Device, Sector, and Partition

A **block device** is a storage unit read from and written to in fixed-size
units. The smallest addressable unit is called a **sector**; a device is an
array of sectors numbered starting from zero, and it has no structure beyond
this array. All of the order inside it is read from a data structure sitting in
the device's first sectors.

A sector has two sizes, and the two need not be equal. The **logical sector**
the device presents to the outside and the **physical sector** actually read
and written in one operation internally are separate numbers; in a common
setup, the logical sector is 512 bytes, the physical sector 4096 bytes. So the
device accepts 512-byte addresses, but when 512 bytes are to be written, it
reads a 4096-byte block, modifies it, and writes it back. This distinction is
the source of the difference counted in the alignment section, and it sits
nowhere in the partition table; it is the device's own declaration.

That data structure is the **partition table**. For every **partition**, the
table holds a start sector, an end sector, and a type label. This is all the
table holds, and nothing more: the table does not know what is inside the
partition, whether it is full or empty, or even whether a file system exists
inside it at all. A partition is nothing but a range drawn on the device's
body.

Two table formats are common. The older format is limited to four primary
partitions and carries more only by nesting entries inside one partition; the
later format allows hundreds of entries, gives every partition an immutable
identity, and keeps a copy of the table at the end of the device. The
difference between them is not a ranking of superiority but a difference in
capacity, and which one is in use is learned by looking at the device itself.

Tools that list partitions only read, and are harmless: `lsblk` gives the
device tree, `blkid` writes partitions' identities and type labels, `parted`
dumps a table in units of sectors. The transcript below is a **sample
transcript** showing the shape of `parted`'s output; it was not run, and no
numeric claim in this lesson comes from it.

```text
Model: fictional block device
Disk /dev/sdb: 12582912s
Sector size (logical/physical): 512B/4096B
Partition Table: gpt

Number  Start     End       Size      File system  Name
 1      2048s     1048575s  1046528s  ext4         root
 2      1048576s  3344383s  2295808s  ext4         data
 3      3753984s  4636671s  882688s   ext4         log
 4      4636672s  6383615s  1746944s  linux-swap   swap
```

What stands out in the transcript is that the gap between where the fourth row
ends and where the next partition begins is **not written down**. The tool
lists partitions; it does not list what remains in between.

## Why It Is Partitioned

Building a single file system on a device is also an option, and partitioning
is the option set against it. The reason for the distinction is not capacity
but **limiting the spread of failure**. On the fictional server, the units
that write logs and the units that produce measurement files share the same
device; if both write to the same file system, the moment log rotation stalls
once, the filled space floods not just the logs but the root file system too,
and the system becomes unable to log in. A separate partition keeps the
filling within its own boundary.

The second reason is different mount options: one partition can be mounted
read-only, another closed to running programs. The third is that a
partition's file system type can be chosen independently of another's. There
is also a cost, and it is measurable: every partition's free space serves
only itself. In the layout above, while the log partition fills up, 1,320,960
sectors can sit empty in the reserved partition, and these two numbers cannot
be transferred to each other. Partitioning gives up flexibility to buy
isolation; one of the later lessons measures a layer that loosens this
trade-off.

## A Partition List Is Not a Map

The only way to compute free space from a partition list is subtraction: the
sum of the partitions is taken away from the device's sector count. This
operation gives a correct total and produces a wrong diagnosis, because free
space is not a single piece.

- **ST1** — The fictional device is **12,582,912 sectors**; every measure is
  in sectors, and no real device's size is written down.
- **ST2** — Partitions are aligned to a **2048-sector** multiple. The first
  2048 sectors are reserved for the partition table and cannot be used as a
  partition.
- **ST3** — The layout is produced from the shared definition's generator
  with seed **20260218**; six partitions carry the fictional server's root,
  data, log, swap, backup, and reserved areas.
- **ST4** — The tool's output is the **partition list**; the gap map is not
  in the output. The diagnosis is built from the list alone.
- **ST5** — A placement decision consists of two questions: does the
  requested size **fit**, and **how many** of that size fit.

```python
"""M03/K03 storage: the fictional block device's partition layout and gap map."""
SEED = 20260218
ALIGN = 2048                 # partitions start at this multiple of sectors
DEVICE = 12582912            # the fictional device's sector count
NAMES = ("root", "data", "log", "swap", "backup", "reserved")


def generator(seed):
    d = seed

    def next_value(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return next_value


def build_layout(seed=SEED):
    """Oracle: the partitions' real start and size, in sectors."""
    r = generator(seed)
    parts, cursor = [], ALIGN
    for name in NAMES:
        size = (r(900) + 300) * ALIGN
        parts.append({"name": name, "start": cursor, "size": size})
        cursor += size + r(4) * 100 * ALIGN
    return parts


def gaps(parts, device=DEVICE):
    """Oracle: the CONTIGUOUS free ranges between and after the partitions."""
    g, cursor = [], 0
    for x in sorted(parts, key=lambda z: z["start"]):
        if x["start"] > cursor:
            g.append(x["start"] - cursor)
        cursor = x["start"] + x["size"]
    if cursor < device:
        g.append(device - cursor)
    return g


def diagnose(parts, requests, device=DEVICE):
    """The tool's output is the partition LIST. The diagnosis drawn from the
    list: free space is the device minus the sum of the partitions."""
    g = gaps(parts, device)
    total_free = device - sum(x["size"] for x in parts)
    wrong, rows = 0, []
    for s in requests:
        tool_fits, actual_fits = s <= total_free, s <= max(g)
        tool_count, actual_count = total_free // s, sum(x // s for x in g)
        wrong += (tool_fits != actual_fits) + (tool_count != actual_count)
        rows.append((s, tool_fits, actual_fits, tool_count, actual_count))
    return {"total_free": total_free, "largest": max(g), "gap_count": len(g),
            "rows": rows, "wrong": wrong}


layout = build_layout()
print("partition  start         size        end")
for x in layout:
    print(f"  {x['name']:9s} {x['start']:9d} {x['size']:9d} {x['start'] + x['size']:11d}")
print("used sectors:", sum(x["size"] for x in layout), "| free sectors:",
      DEVICE - sum(x["size"] for x in layout))
print("contiguous gap count:", len(gaps(layout)), "| gaps:", gaps(layout))
print()
REQUESTS = (204800, 614400, 1228800, 2457600, 3072000, 3686400)
print("requested  tool: fits  actual: fits  tool: count  actual: count")
for s, a, ge, aa, ga in diagnose(layout, REQUESTS)["rows"]:
    print(f"{s:7d}  {str(a):11s}  {str(ge):13s}  {aa:10d}  {ga:12d}")
print()
for seed in (SEED, 20260219):
    t2 = diagnose(build_layout(seed), REQUESTS)
    print(f"seed {seed}: free {t2['total_free']:8d}  largest contiguous {t2['largest']:8d}"
          f"  gaps {t2['gap_count']:2d}  wrong diagnosis {t2['wrong']:2d} / 12")
```

```
partition  start         size        end
  root           2048   1046528     1048576
  data        1048576   2295808     3344384
  log         3753984    882688     4636672
  swap        4636672   1746944     6383616
  backup      6793216   1587200     8380416
  reserved    8380416   1320960     9701376
used sectors: 8880128 | free sectors: 3702784
contiguous gap count: 4 | gaps: [2048, 409600, 409600, 2881536]

requested  tool: fits  actual: fits  tool: count  actual: count
 204800  True         True                   18            18
 614400  True         True                    6             4
1228800  True         True                    3             2
2457600  True         True                    1             1
3072000  True         False                   1             0
3686400  True         False                   1             0

seed 20260218: free  3702784  largest contiguous  2881536  gaps  4  wrong diagnosis  6 / 12
seed 20260219: free  3633152  largest contiguous  1787904  gaps  7  wrong diagnosis  6 / 12
```

Three numbers side by side. **The oracle:** free space is four pieces —
2048, 409,600, 409,600, and 2,881,536 sectors; the largest contiguous range
is **2,881,536 sectors**. **The tool's output:** a partition list, six rows
totaling 8,880,128 sectors; the free space drawn from this is **3,702,784
sectors**. **Wrong diagnosis:** **six** of twelve decisions.

The distribution of the errors is instructive too. Two decisions say "it
fits" when it does not: partitions of 3,072,000 and 3,686,400 sectors stay
under the total free space but fit into no contiguous range. Four decisions
err in count: the tool says six 614,400-sector partitions fit; in reality
four fit. The total is correct, the diagnosis is wrong.

In the layout produced with the second seed, free space drops to 3,633,152
sectors, the largest contiguous range to 1,787,904 sectors, and the gap
count rises to seven; the wrong diagnosis is again **6**. The number staying
the same is a coincidence; its staying in the same order of magnitude is
not: as the gap count grows, the diagnosis drawn from the total keeps
breaking down.

## Alignment

Where a partition begins determines the performance of the file system built
inside it. A file system works in 4096-byte blocks, and one block is eight
sectors; the device's own physical block is also eight sectors, counted from
zero. If a partition begins at a sector that is not a multiple of eight,
every block of the file system spans two physical blocks.

- **ST6** — In sequential writing, the number of physical blocks touched
  increases by only one under misalignment; the blocks are already
  consecutive.
- **ST7** — In a random single-block write, a misaligned block touches two
  physical blocks, and because the unchanged portion of both must be
  preserved, they are read before being written: four operations.

```python
"""Alignment: does the file system block sit on the same boundary as the device block."""
SECTORS_PER_BLOCK = 8        # 4096-byte block, eight 512-byte sectors
BLOCK_COUNT = 512            # file system blocks written
PARTITION_SIZE = 1048576     # same in all six layouts, sectors


def cost(start, blocks=BLOCK_COUNT, s=SECTORS_PER_BLOCK):
    offset = start % s
    return {"aligned": offset == 0,
            "sequential": blocks + (1 if offset else 0),
            "random": blocks * (4 if offset else 1)}


print("start      partition size  aligned  sequential ops  random ops")
wrong = 0
for start in (63, 2048, 2049, 2052, 2056, 4096):
    m = cost(start)
    wrong += not m["aligned"]
    print(f"{start:9d}  {PARTITION_SIZE:12d}  {str(m['aligned']):6s}"
          f"  {m['sequential']:12d}  {m['random']:14d}")
print("the partition size in the tool's output is the same across all six layouts:", PARTITION_SIZE)
print("how many times the 'same layout' diagnosis drawn from that size is wrong:", wrong, "/ 6")
```

```
start      partition size  aligned  sequential ops  random ops
       63       1048576  False            513            2048
     2048       1048576  True             512             512
     2049       1048576  False            513            2048
     2052       1048576  False            513            2048
     2056       1048576  True             512             512
     4096       1048576  True             512             512
the partition size in the tool's output is the same across all six layouts: 1048576
how many times the 'same layout' diagnosis drawn from that size is wrong: 3 / 6
```

The six layouts' partition size is the same, and this size appears in the
tool's output; the starting sector also appears but is not read, because
whether a number divides by eight is not asked while looking at the list.
Sequential writing's cost rises from 512 to 513 under misalignment, an
immeasurable difference; random writing's rises from 512 to **2048**. The
"no difference" diagnosis given by a sequential test is wrong on **three**
of the six layouts. This is storage's counterpart to the course's second
claim: what distinguishes is not multiplying the test, but testing the right
load.

The row for 2056 in the table shows that alignment is not specific to 2048:
every start that is a multiple of eight is aligned. The reason partitioning
tools default to 2048 sectors is not eight but staying aligned on devices
that work with much larger internal units too; 2048 is a number divisible by
all plausible internal unit sizes. Tools do not make this decision silently;
they produce a warning too, and `lsblk` writes the alignment deviation in a
separate column. The warning sits in the output; reading it is a matter of
habit, and the difference that appears when it is not read shows up nowhere
until the moment it is measured.

## Modifying the Partition Table

Tools that read the table are harmless; ones that write it are irreversible.
Deleting a partition or changing its boundary overwrites the start and end
values in the table; the data inside the partition is not deleted, but the
information about how to reach it is lost, and the content does not become
reachable again until the table is rewritten. This is why commands that
write the table are **not given in a runnable, complete form** in this
lesson.

If a change is unavoidable, three precautions provide measurable safety. The
first is **taking a text dump** of the table; a reading command like
`sfdisk --dump` writes the table to a file, and restoring that same file
re-establishes the old layout. The second is a **dry run**: most tools that
write the table carry an option that shows the result without applying the
change, and the output from this option must be read before it is run for
real. The third is doing the trial **on a separate device**; mounting a file
as a **loop device** makes it possible to practice partitioning away from
real data.

There is also an ordering rule: when a command that changes the table
returns, the kernel may still be holding the old layout in memory. In that
case, `lsblk` shows the new table while mounted file systems keep working
with the old boundaries. A command returning does not mean the change has
taken effect; this is storage's counterpart to the sentence established for
signals in the Process Management topic.

## Summary

- A block device is an array of numbered sectors; all of the order inside
  it is read from the partition table sitting in the first sectors, and the
  table carries only start, end, and type information.
- The free space drawn from a partition list is a total, not a map: the
  largest contiguous piece of 3,702,784 free sectors is 2,881,536 sectors,
  and the free space is split into four pieces.
- Six of twelve placement decisions turn out wrong when only the list is
  consulted; two think something that does not fit does fit, four overcount
  how many fit. With the second seed, the gap count rises to seven and the
  wrong diagnosis is again 6.
- A partition start being a multiple of eight sectors produces a difference
  of 513 operations instead of 512 in sequential writing, and 2048 instead
  of 512 in random writing; the "no difference" diagnosis drawn from output
  showing the same partition size is wrong on three of six layouts.
- Commands that write the partition table are irreversible; three
  measurable precautions are taking a dump of the table, reading the
  dry-run option, and doing the trial on a separate device.

## Next Step

This lesson never looked inside a partition: to the table, a partition was
nothing but a range with a start and an end. Yet the moment a file system is
built on that range, decisions are made that cannot be reversed without cost
— how many files can be held, how much space metadata will take up, and
whether corrupted data will be noticed when read. The next lesson measures
how file system families make these decisions differently, and counts how
many times a design that carries no checksum silently returns corrupted
data.
