What are Block Device Major Minor Numbers-Free Linux Device Drivers Course

Block Device Major Minor Numbers

A free Linux kernel development course lecture on how the block layer identifies storage devices and their partitions on modern Linux

Free Linux Kernel Development Course
Free Linux Device Drivers Course
Hands-on Demo Included

If you have ever run lsblk or ls -l /dev/sda* and wondered why every storage device on your Linux box shows up with a pair of numbers separated by a comma, this lecture answers that question in depth. This is part of our free Linux kernel development course and free Linux device drivers course, and it builds directly on the character device numbering you learned in the previous lecture, extending the same major/minor idea to block devices — the class of drivers behind hard disks, SSDs, eMMC, SD cards, and USB storage.

What You Will Learn

Lecture Roadmap
1. How block devices differ from character devices in the numbering scheme 2. What the major number means for a block driver 3. How the minor number encodes both the whole disk and its partitions 4. Reading /proc/partitions and /sys/dev/block to inspect live numbering 5. A minimal ep_blockdemo driver showing registration on a modern kernel 6. How partitioning tools and udev turn numbers into /dev nodes

Prerequisites

You should already be comfortable with character device major/minor numbers, mknod, and reading /proc/devices, all covered in the previous lecture of this free embedded Linux course. Basic familiarity with building and inserting a kernel module is assumed.

Block Devices Are a Separate Namespace

The single most important fact to internalize before going further: character major numbers and block major numbers live in completely separate namespaces. A character driver registered with major 8 has no relationship whatsoever to a block driver also registered with major 8. The kernel keeps two independent tables, one for chrdev registrations and one for blkdev registrations, and a number in one table says nothing about the other. This trips up a lot of people learning Linux kernel development for the first time, so it is worth repeating: always check whether you are looking at the character or the block table before drawing conclusions from a major number.

For block devices specifically, the major number identifies the driver — the piece of software that knows how to talk to a particular class of storage controller — while the minor number identifies which physical device and, further, which partition on that device you are addressing. This two-level minor number scheme is what makes block numbering more involved than character numbering.

How Minor Numbers Encode Disks and Partitions

Rather than one minor number per device, block drivers reserve a contiguous range of minor numbers per physical disk, and then subdivide that range: the first minor number in the range addresses the whole disk as raw sectors, and the remaining numbers in the range address up to that many partitions on the disk. The size of the range is a decision made by the driver author, and it directly caps how many partitions a single disk of that type can expose.

Minor Number Range Layout (example: 16 minors per disk)
Disk 0 -> minors 0-15 minor 0 = whole disk -> /dev/xda minor 1 = partition 1 -> /dev/xda1 minor 2 = partition 2 -> /dev/xda2 … minor 15 = partition 15 -> /dev/xda15 Disk 1 -> minors 16-31 minor 16 = whole disk -> /dev/xdb minor 17 = partition 1 -> /dev/xdb1 …

This is exactly the pattern you will see on real hardware. On a typical desktop or server, the SCSI/SATA/USB-mass-storage/UFS class of disks is handled by the sd driver, which reserves 16 minors per disk: the first disk gets device nodes sda through sda15, the second gets sdb through sdb15, and so on, cycling through the alphabet. On an embedded board using eMMC or SD, the mmcblk driver instead reserves 8 minors per device, giving you mmcblk0 as the raw device and mmcblk0p1 through mmcblk0p7 for up to seven partitions, then mmcblk1 starting the next device at the next multiple of 8. Both drivers solve the same problem — mapping one physical disk to many logical partitions — with a range size chosen to suit how many partitions that class of device realistically needs.

There is one notable exception worth knowing about: raw NAND/NOR flash handled by the MTD subsystem does not use this partition-numbering scheme at all. MTD partition layout is described statically, either on the kernel command line or in the device tree, rather than being created dynamically through a partitioning tool. We will cover MTD in a dedicated lecture later in this free embedded Linux course.

Inspecting Numbering on a Running System

Three files give you a live view of the block numbering scheme without writing a single line of code:

# List currently registered block drivers and their major numbers
cat /proc/devices

# List every block device and partition currently known to the kernel,
# along with its major:minor pair and size in 1K blocks
cat /proc/partitions

# List the same information from sysfs, one directory per device
ls /sys/dev/block/

A typical /proc/partitions output on a board with one eMMC device looks like this:

major minor  #blocks  name

  179        0   15558144 mmcblk0
  179        1     262144 mmcblk0p1
  179        2   15294976 mmcblk0p2

Notice that major 179 (the real-world mmcblk major) appears three times: once for the raw device and once for each of the two partitions on it — exactly the layout described above, just with an 8-minor range instead of 16, so mmcblk0 is minor 0, mmcblk0p1 is minor 1, and mmcblk0p2 is minor 2.

Registering a Minimal Block Driver

Writing a full block driver capable of real I/O is beyond the scope of one lecture, but you can absolutely see the numbering mechanics in isolation with a tiny driver that registers itself, reserves a minor range, and creates a disk object, without wiring up any actual read/write logic. On a current mainline kernel (6.x), block drivers use blk_mq_alloc_disk() to obtain a struct gendisk tied to a request queue, set the disk’s name and minor range, then call add_disk() to make it visible to user space.

#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>

#define EP_MINORS_PER_DISK 4   /* raw disk + 3 partitions max */

static int ep_major;
static struct gendisk *ep_disk;
static struct blk_mq_tag_set ep_tag_set;

static blk_status_t ep_queue_rq(struct blk_mq_hw_ctx *hctx,
                                 const struct blk_mq_queue_data *bd)
{
    /* No real transfer logic -- this demo only proves out numbering */
    blk_mq_end_request(bd->rq, BLK_STS_OK);
    return BLK_STS_OK;
}

static const struct blk_mq_ops ep_mq_ops = {
    .queue_rq = ep_queue_rq,
};

static const struct block_device_operations ep_fops = {
    .owner = THIS_MODULE,
};

static int __init ep_blockdemo_init(void)
{
    ep_major = register_blkdev(0, "ep_blockdemo");
    if (ep_major major = ep_major;
    ep_disk->first_minor = 0;
    ep_disk->minors = EP_MINORS_PER_DISK;
    ep_disk->fops = &ep_fops;
    sprintf(ep_disk->disk_name, "ep_blockdemo");
    set_capacity(ep_disk, 2048); /* 1MB, in 512-byte sectors */

    add_disk(ep_disk);
    pr_info("ep_blockdemo: registered with major %d\n", ep_major);
    return 0;
}

static void __exit ep_blockdemo_exit(void)
{
    del_gendisk(ep_disk);
    put_disk(ep_disk);
    blk_mq_free_tag_set(&ep_tag_set);
    unregister_blkdev(ep_major, "ep_blockdemo");
}

module_init(ep_blockdemo_init);
module_exit(ep_blockdemo_exit);
MODULE_LICENSE("GPL");

Build and load it with the same Makefile pattern from earlier lectures, then check the result:

$ sudo insmod ep_blockdemo.ko
$ dmesg | tail -1
[ 1234.567890] ep_blockdemo: registered with major 253

$ cat /proc/devices | grep ep_blockdemo
253 ep_blockdemo

$ ls -l /dev/ep_blockdemo
brw-rw---- 1 root disk 253, 0 Aug 19 10:00 /dev/ep_blockdemo

$ sudo rmmod ep_blockdemo

Because we passed 0 as the requested major to register_blkdev(), the kernel allocated the next free dynamic major on your machine — expect a different number on your own system, which is exactly the same dynamic-allocation behavior you saw with character devices.

From Numbers to Partitions: The Role of Partitioning Tools

The kernel’s job stops at exposing the raw disk as minor 0 of its range. Turning that raw space into partitions is the job of user-space tools such as fdisk, sfdisk, or parted, which write a partition table (MBR or GPT) to the start of the disk. Once the kernel re-reads that partition table — either automatically on boot or on demand with partprobe — it creates one gendisk minor per partition found, and udev (or mdev on smaller systems) creates the matching /dev nodes using the naming convention the driver registered.

DriverMajorMinors per diskMax partitionsExample nodes
sd (SCSI/SATA/USB/UFS)81615sda, sda1..sda15
mmcblk (eMMC/SD)17987mmcblk0, mmcblk0p1..p7
MTD (raw NAND/NOR)90static, via DT/cmdlinedefined externallymtdblock0, mtdblock0p1..

Common Mistakes

  • Assuming a character and block major with the same number are related — they are not, they live in separate tables.
  • Choosing a minor range too small for the expected number of partitions, then having to redesign the driver later.
  • Forgetting that MTD devices do not follow this dynamic partitioning model at all.
  • Hardcoding a major number instead of requesting dynamic allocation with register_blkdev(0, ...), which risks collisions on different systems.

Best Practices

  • Always request a dynamic major unless you have a Linux Assigned Names And Numbers Authority (LANANA) reservation for a fixed one.
  • Size your minor-per-disk range with real headroom for future partitions, since it cannot be changed without breaking existing device nodes.
  • Use /proc/partitions and /sys/dev/block as your first debugging step whenever a partition fails to appear — the kernel-side numbering is almost always correct even when a userspace tool is confused.
  • Release the gendisk and tag set cleanly in your exit path to avoid leaving stale entries in /sys/dev/block.

Summary

Block device numbering extends the major/minor idea you already know from character devices, but adds a second layer: a contiguous minor range per physical disk, with the first minor addressing the raw device and the rest addressing partitions. The sd and mmcblk drivers are the two you will meet most often in real embedded Linux development, and MTD is the deliberate exception that skips dynamic partitioning entirely. With register_blkdev(), blk_mq_alloc_disk(), and add_disk() you can see this mechanism end to end on a current kernel, even before writing any real I/O logic.

Frequently Asked Questions

Why do block devices need more than one minor number per disk?

Because a single physical disk is almost always subdivided into partitions, and the kernel needs a distinct device node for the raw disk plus each partition. Reserving a range of minors per disk lets one major/driver pair cover a whole disk’s partition table.

Do character major numbers and block major numbers ever collide?

They can share the same number without conflict, because character and block devices are tracked in entirely separate kernel tables. The number 8 as a block major (sd) has nothing to do with number 8 as a character major.

How many partitions can an sd or mmcblk device have?

With the standard ranges, sd supports up to 15 partitions per disk and mmcblk supports up to 7, because one minor in each range is reserved for the raw whole-disk device.

Why doesn’t MTD flash use this same partitioning scheme?

Raw NAND/NOR flash has no standard on-media partition table the kernel can read at boot, so MTD partition layout is instead declared statically through the device tree or kernel command line.

What tool actually creates the /dev nodes for new partitions?

The kernel exposes the numbering through sysfs, and a device manager such as udev or mdev listens for those uevents and creates the matching /dev nodes using the driver’s naming convention.

Is this free Linux kernel development course suitable for someone with no prior driver experience?

Yes, this free Linux device drivers course builds concepts in order starting from character devices, so working through the lectures sequentially is the recommended path for someone new to embedded Linux development.

Continue Your Free Linux Kernel Development Course

Next up: how network devices get their names without any major or minor numbers at all.

Next Lecture Back to Course Index