02.设备号详解
一、为什么需要设备号
在上一节中,已经看到/dev目录下包含大量设备文件。例如:
ls -l /dev/null /dev/tty0 /dev/mmcblk0示例输出:
crw-rw-rw- 1 root root 1, 3 12月 9 19:02 /dev/null crw--w---- 1 root tty 4, 0 12月 9 19:02 /dev/tty0 brw-rw---- 1 root disk 179, 0 12月 9 19:02 /dev/mmcblk0其中有两个重要信息:
- 第一列首字符:
c表示字符设备,b表示块设备。 - 倒数第三、倒数第二列两个数字:例如
1, 3、4, 0、179, 0。
这两个数字组合在一起,构成该设备的设备号(device number):
- 前一个数字是主设备号(major number)。
- 后一个数字是次设备号(minor number)。
当用户空间程序执行:
int fd = open("/dev/tty0", O_RDWR);内核需要根据/dev/tty0找到对应的驱动以及具体设备实例。 设备号在这里起到了桥梁作用:
- 通过主设备号确定由哪个驱动负责处理;
- 通过次设备号确定该驱动下的哪一个设备实例。
可以概括为:
设备号用于实现 “设备文件 ↔ 驱动程序 ↔ 具体设备实例” 的映射。
二、主设备号与次设备号
1. 主设备号(major number)
主设备号的作用是标识设备所属的驱动或设备类别。
- 在内核内部,字符设备子系统会维护按主设备号索引的表结构。
- 每个主设备号与某个驱动(或某个子系统)相关联。
- 当内核解析
/dev/xxx的设备号时,会根据主设备号找到对应驱动的入口。
例如(以/proc/devices为例):
cat /proc/devices输出(节选):
lckfb@linaro-alip:~$ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 81 video4linux 89 i2c 90 mtd .......................说明:
- 字符设备主设备号
4对应tty类设备。 - 块设备主设备号
13对应input设备(鼠标/键盘)。
主设备号的范围和分配策略由内核维护,部分主设备号在文档中有约定用途,其他范围可供动态分配给各种驱动。
2. 次设备号(minor number)
次设备号用于标识同一主设备号下的不同设备实例。
- 一个驱动(对应某个主设备号)可以管理多路设备。
- 每一路设备具有不同次设备号,用于区分。
例如,假设某字符设备驱动使用主设备号 240,并管理两个实例:
/dev/mychar0:主设备号 240,次设备号 0 →<240, 0>/dev/mychar1:主设备号 240,次设备号 1 →<240, 1>
驱动在处理open/read/write等操作时,可以根据次设备号选择对应的数据结构或硬件资源。
我们继续来看tty串口的例子:
lckfb@linaro-alip:~$ ls -l /dev/tty* crw-rw-rw- 1 root tty 5, 0 12月 9日 19:02 /dev/tty crw--w---- 1 root tty 4, 0 12月 9日 19:02 /dev/tty0 crw--w---- 1 root tty 4, 1 12月 9日 19:02 /dev/tty1 crw--w---- 1 root tty 4, 10 12月 9日 19:02 /dev/tty10 crw--w---- 1 root tty 4, 11 12月 9日 19:02 /dev/tty11 crw--w---- 1 root tty 4, 12 12月 9日 19:02 /dev/tty12 crw--w---- 1 root tty 4, 13 12月 9日 19:02 /dev/tty13 crw--w---- 1 root tty 4, 14 12月 9日 19:02 /dev/tty14 crw--w---- 1 root tty 4, 15 12月 9日 19:02 /dev/tty15 ........我们的串口资源有这么多同一个串口类型的设备,可能有几十上百个,主次结合就能很快定位:
- 主设备号确定设备的类型为串口设备
- 次设备号确定为串口设备下面的那个设备
tty10:
- 主设备号为
4- 次设备号为
10
tty11:
- 主设备号为
4- 次设备号为
11
tty12:
- 主设备号为
4- 次设备号为
12……
三、设备号的数据类型与布局
在内核中,设备号使用dev_t类型表示。 在多数架构上,它是一个 32 位无符号整数,其位布局大致为:
- 高 12 位:主设备号
- 低 20 位:次设备号
即:
- 主设备号理论范围:0 ~ 2¹² - 1(0 ~ 4095)
- 次设备号理论范围:0 ~ 2²⁰ - 1(0 ~ 1,048,575)
内核提供了一组宏用于操作dev_t:
MAJOR(dev_t dev); // 从 dev 中取得主设备号 MINOR(dev_t dev); // 从 dev 中取得次设备号 MKDEV(unsigned int major, unsigned int minor); // 构造 dev_t典型用法示例:
dev_t dev; /* 由主设备号 240、次设备号 0 构造一个 dev_t */ dev = MKDEV(240, 0); /* 提取主、次设备号 */ unsigned int ma = MAJOR(dev); unsigned int mi = MINOR(dev);在字符设备驱动中,通常会定义:
static dev_t dev_num; // 完整设备号 static int major; // 主设备号 static int minor = 0; // 起始次设备号四、设备号的两种分配
在编写字符设备驱动时,驱动需要向内核登记自己要使用的设备号范围。 这一过程与后续的cdev注册、/dev节点创建是分离的,本节仅讨论设备号本身的注册与释放。
内核提供了两种常用方式:
- 静态注册(指定主设备号):
register_chrdev_region - 动态分配(自动分配主设备号):
alloc_chrdev_region
1. 静态注册
驱动可以直接指定期望使用的主设备号和起始次设备号,然后向内核注册:
static dev_t dev_num; static int major = 240; /* 希望使用主设备号 240 */ static int minor = 0; /* 起始次设备号 */ static int __init mydrv_init(void) { int ret; dev_num = MKDEV(major, minor); /* 注册从 dev_num 开始,连续 1 个设备号 */ ret = register_chrdev_region(dev_num, 1, "mychar_static"); if (ret < 0) { pr_err("register_chrdev_region failed\n"); return ret; } pr_info("mychar_static: registered with major=%d, minor=%d\n", MAJOR(dev_num), MINOR(dev_num)); return 0; } static void __exit mydrv_exit(void) { unregister_chrdev_region(dev_num, 1); }说明:
register_chrdev_region(dev_t from, unsigned count, const char *name);from:起始设备号(包含主、次设备号)。count:连续设备号的数量。name:名称,用于/proc/devices等位置显示。- 对于字符设备,主设备号必须尚未被其他驱动使用。
适用场景:
- 某些需要固定主设备号的旧系统或特定应用场景。
局限性:
- 需要开发者手动选择主设备号,并确保不与系统中已有设备冲突。
- 在不同内核版本或不同平台上,该主设备号可能已经被占用。
对于一般新开发的驱动,更推荐使用动态分配方式。
2. 动态分配
动态分配由内核选择尚未被使用的主设备号,驱动只需指定:
- 起始次设备号;
- 需要的连续设备数量;
- 名称。
static dev_t dev_num; static int major; static int minor = 0; static int __init mydrv_init(void) { int ret; /* 请求动态分配 1 个设备号,从次设备号 0 开始 */ ret = alloc_chrdev_region(&dev_num, minor, 1, "mychar_dynamic"); if (ret < 0) { pr_err("alloc_chrdev_region failed\n"); return ret; } major = MAJOR(dev_num); minor = MINOR(dev_num); pr_info("mychar_dynamic: registered with major=%d, minor=%d\n", major, minor); return 0; } static void __exit mydrv_exit(void) { unregister_chrdev_region(dev_num, 1); }说明:
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);dev:返回的起始dev_t。baseminor:起始次设备号。count:从baseminor开始,连续申请多少个次设备号。name:设备名称标识。
优点:
- 不需要手动管理主设备号的分配。
- 跨平台、跨内核版本时更不易产生冲突。
- 是新驱动开发中推荐的做法。