一、为什么需要设备号
在上一节中,已经看到 /dev 目录下包含大量设备文件。例如:
ls -l /dev/null /dev/tty0 /dev/mmcblk02
示例输出:
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/mmcblk02
3
其中有两个重要信息:
- 第一列首字符:
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
.......................2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
说明:
- 字符设备主设备号
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
........2
3
4
5
6
7
8
9
10
11
我们的串口资源有这么多同一个串口类型的设备,可能有几十上百个,主次结合就能很快定位:
- 主设备号确定设备的类型为串口设备
- 次设备号确定为串口设备下面的那个设备
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_t2
3
典型用法示例:
dev_t dev;
/* 由主设备号 240、次设备号 0 构造一个 dev_t */
dev = MKDEV(240, 0);
/* 提取主、次设备号 */
unsigned int ma = MAJOR(dev);
unsigned int mi = MINOR(dev);2
3
4
5
6
7
8
在字符设备驱动中,通常会定义:
static dev_t dev_num; // 完整设备号
static int major; // 主设备号
static int minor = 0; // 起始次设备号2
3
四、设备号的两种分配
在编写字符设备驱动时,驱动需要向内核登记自己要使用的设备号范围。 这一过程与后续的 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);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
说明:
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);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
说明:
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);dev:返回的起始dev_t。baseminor:起始次设备号。count:从baseminor开始,连续申请多少个次设备号。name:设备名称标识。
优点:
- 不需要手动管理主设备号的分配。
- 跨平台、跨内核版本时更不易产生冲突。
- 是新驱动开发中推荐的做法。