注册设备号:从主次设备号到成对注销
课程位置:驱动初级 → 字符设备 → 注册设备号;参照子节"主次设备号"。
主要依据:2026-10-05 15:23《注册设备号》课堂录音转写,以及老师的《注册设备号》《主次设备号》笔记和
Documentation/devices.txt截图。录音转写中的口误、同音字按代码与上下文校正。实验状态:下文的
500:0注册和/proc/devices现象来自老师的课堂演示 。你此前已在 Jetson 板上验证led.ko的加载与卸载,但尚未在本次对话中提交"注册设备号"版本的板端实测结果。
一、这一课到底完成哪一步
上一节把字符设备入门概括为:注册设备号 → 建立字符设备与文件操作的关联 → 编写操作函数。这节只完成第一步:为驱动向内核登记一段设备编号,卸载时把编号还回去。(录音约 00:31---03:13)
#mermaid-svg-XdsZS2wdnwzwPdLb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XdsZS2wdnwzwPdLb .error-icon{fill:#552222;}#mermaid-svg-XdsZS2wdnwzwPdLb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XdsZS2wdnwzwPdLb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XdsZS2wdnwzwPdLb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XdsZS2wdnwzwPdLb .marker.cross{stroke:#333333;}#mermaid-svg-XdsZS2wdnwzwPdLb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XdsZS2wdnwzwPdLb p{margin:0;}#mermaid-svg-XdsZS2wdnwzwPdLb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster-label text{fill:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster-label span{color:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster-label span p{background-color:transparent;}#mermaid-svg-XdsZS2wdnwzwPdLb .label text,#mermaid-svg-XdsZS2wdnwzwPdLb span{fill:#333;color:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb .node rect,#mermaid-svg-XdsZS2wdnwzwPdLb .node circle,#mermaid-svg-XdsZS2wdnwzwPdLb .node ellipse,#mermaid-svg-XdsZS2wdnwzwPdLb .node polygon,#mermaid-svg-XdsZS2wdnwzwPdLb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XdsZS2wdnwzwPdLb .rough-node .label text,#mermaid-svg-XdsZS2wdnwzwPdLb .node .label text,#mermaid-svg-XdsZS2wdnwzwPdLb .image-shape .label,#mermaid-svg-XdsZS2wdnwzwPdLb .icon-shape .label{text-anchor:middle;}#mermaid-svg-XdsZS2wdnwzwPdLb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XdsZS2wdnwzwPdLb .rough-node .label,#mermaid-svg-XdsZS2wdnwzwPdLb .node .label,#mermaid-svg-XdsZS2wdnwzwPdLb .image-shape .label,#mermaid-svg-XdsZS2wdnwzwPdLb .icon-shape .label{text-align:center;}#mermaid-svg-XdsZS2wdnwzwPdLb .node.clickable{cursor:pointer;}#mermaid-svg-XdsZS2wdnwzwPdLb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XdsZS2wdnwzwPdLb .arrowheadPath{fill:#333333;}#mermaid-svg-XdsZS2wdnwzwPdLb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XdsZS2wdnwzwPdLb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XdsZS2wdnwzwPdLb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XdsZS2wdnwzwPdLb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XdsZS2wdnwzwPdLb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XdsZS2wdnwzwPdLb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster text{fill:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb .cluster span{color:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XdsZS2wdnwzwPdLb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XdsZS2wdnwzwPdLb rect.text{fill:none;stroke-width:0;}#mermaid-svg-XdsZS2wdnwzwPdLb .icon-shape,#mermaid-svg-XdsZS2wdnwzwPdLb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XdsZS2wdnwzwPdLb .icon-shape p,#mermaid-svg-XdsZS2wdnwzwPdLb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XdsZS2wdnwzwPdLb .icon-shape .label rect,#mermaid-svg-XdsZS2wdnwzwPdLb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XdsZS2wdnwzwPdLb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XdsZS2wdnwzwPdLb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XdsZS2wdnwzwPdLb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 负数
0
确定主号、次号和数量
MKDEV 合成 dev_t
register_chrdev_region 登记编号范围
返回值
报告错误,模块加载失败
模块保持加载,/proc/devices 可查
rmmod 时 unregister_chrdev_region
本节的边界: register_chrdev_region() 只保留设备号范围。它不会自动创建 /dev/led,也不会自动注册 struct cdev、绑定 file_operations 或操作 GPIO。/proc/devices 中能看到名字,只证明编号范围已登记;能否通过设备文件 open/write 使用 LED,还要等后续步骤完成。Linux 内核字符设备 API
二、先认识主号、次号与 dev_t
老师用 500:0 举例:主设备号 500、次设备号 0。主号通常对应驱动取得的一组设备号,次号由该驱动用于区分这组编号中的设备或实例。课堂说"主号区分设备种类、次号区分同类设备",可用作入门记忆;实际不能据此断定"所有 LED 都必须使用同一个主号"。(录音约 04:21---08:40)
| 课堂定义 | 值 | 含义 |
|---|---|---|
LED_MA |
500 |
本次演示选择的主设备号。 |
LED_MI |
0 |
本次范围的起始次设备号。 |
LED_NUM |
1 |
从起始编号连续登记 1 个 设备号,即 500:0。 |
内核用 dev_t 表示一个完整设备号,使用 MKDEV(major, minor) 合成;反过来可用 MAJOR(devno) 和 MINOR(devno) 取出两个部分。老师所用内核的实现中,低 20 位表示次号、高 12 位表示主号;写驱动时使用这些宏,避免自己手写移位和掩码。Linux 内核 kdev_t.h
c
#define LED_MA 500
#define LED_MI 0
#define LED_NUM 1U
dev_t devno = MKDEV(LED_MA, LED_MI); /* 得到起始设备号 500:0 */
若从 500:0 开始登记 3 个连续编号,且不跨越次号边界,范围就是 500:0、500:1、500:2;count 不是"主号数量",也不是"已经创建了几个 /dev 文件"。
老师查看 Documentation/devices.txt 时看到 1 char 对应 Memory devices,下面列出次号 1 的 /dev/mem、次号 2 的 /dev/kmem、次号 3 的 /dev/null 等。这是理解"一个主号下有多个次号"的例子。该文档记录设备号分配信息,不能凭它推断某台板上的 500 当前一定空闲 ;是否能取得编号,要以注册函数的返回值为准。Linux Allocated Devices(录音约 18:55---23:20)
三、静态注册的完整过程:以 500:0 为例
1. 确定要登记的范围
老师先定下 LED_MA=500、LED_MI=0、LED_NUM=1,这是指定编号的静态注册 。静态的意思是代码明确要求 500:0;若该编号已被占用,内核不会自动换一个主号。(录音约 04:21---05:59)
2. 合成起始编号
调用 MKDEV(LED_MA, LED_MI),把主、次号组成一个 dev_t。这个值作为下一步的 from 参数,代表连续范围的起点。(录音约 06:31---08:40)
3. 向内核登记
c
ret = register_chrdev_region(devno, LED_NUM, "yhai_led");
| 参数 | 本例实参 | 它让内核知道什么 |
|---|---|---|
from |
devno,即 500:0 |
连续范围从哪里开始。 |
count |
LED_NUM,即 1 |
要登记多少个连续设备号。 |
name |
"yhai_led" |
登记在内核中的名字;可在 /proc/devices 的字符设备部分看到。 |
name 是内核登记名 ,不等同于自动生成 /dev/yhai_led;本节也没有调用 mknod。函数成功返回 0,失败返回负错误码。Linux 内核字符设备 API(录音约 08:40---11:01)
4. 检查返回值,决定模块能否加载
c
if (ret < 0) {
pr_err("register_chrdev_region failed: %d\n", ret);
return ret;
}
- **返回
0:**编号登记成功,初始化函数继续执行,最后return 0;insmod才算加载成功。 - **返回负数:**登记未成功,原样返回错误码,让本次模块初始化失败;不要在这条失败路径上调用
unregister_chrdev_region(),因为本次并未取得编号。
不能只打印"失败"却仍让初始化函数返回 0,否则外部会误以为模块已准备好。老师正是通过返回值让"编号被占用"在第二次 insmod 时表现为加载失败。(录音约 11:24---13:31)
5. 加载后核对登记结果
在运行目标内核的板子上,老师先 insmod,再查看 /proc/devices。预期在 Character devices: 区域看到类似 500 yhai_led:
sh
sudo insmod ./led.ko
cat /proc/devices
sudo dmesg | tail -n 20
/proc/devices 列出主号和登记名 ,不会把 500:0 中的次号列出来。因此这项检查能证明编号登记,不足以证明设备节点、读写回调或 LED 已可用。dmesg 可看驱动打印的初始化信息与失败原因。(录音约 15:15---17:38)
6. 卸载时释放同一范围
c
unregister_chrdev_region(devno, LED_NUM);
from 和 count 要与成功注册时相同:这里仍是从 500:0 开始的 1 个编号。正常执行 rmmod led 时,内核调用模块退出函数,释放此前取得的编号;随后再次查看 /proc/devices,预期 500 yhai_led 消失。Linux 内核字符设备 API(录音约 13:40---18:11)
sh
sudo rmmod led
cat /proc/devices
sudo dmesg | tail -n 20
**配对规则:成功注册一次,就在正常卸载路径注销一次。**若初始化后续还有其他资源要申请,后续步骤失败时也要按取得的逆序释放此前已取得的资源;本节示例尚无这些后续资源。
四、把步骤放进老师的 led.c
下面保留老师讲授的静态注册路线,整理成一个只验证设备号生命周期的最小模块。代码中的 LED_MI 修正了老师笔记清单里偶见的 LEM_MI 笔误;MODULE_LICENSE("GPL") 表示模块声明的许可证,并非"免费开源声明"。
c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#define LED_MA 500
#define LED_MI 0
#define LED_NUM 1U
static int __init led_init(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);
int ret;
ret = register_chrdev_region(devno, LED_NUM, "yhai_led");
if (ret < 0) {
pr_err("register_chrdev_region failed: %d\n", ret);
return ret;
}
pr_info("led: registered %u:%u, count=%u\n",
MAJOR(devno), MINOR(devno), LED_NUM);
return 0;
}
static void __exit led_exit(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);
unregister_chrdev_region(devno, LED_NUM);
pr_info("led: unregistered %u:%u\n", MAJOR(devno), MINOR(devno));
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
这段代码只有"登记、打印、释放"。**即使编译成 led.ko 并成功加载,也不能用它点亮板上的 LED。**后续还要初始化并添加 cdev、设置 file_operations、准备设备节点与 GPIO 控制逻辑。
五、课堂故障实验:为什么第二次加载失败
老师故意先不写 unregister_chrdev_region(),观察到第一次加载时 /proc/devices 能看到 500 yhai_led;卸载模块后,登记仍留在内核中。再次加载同一模块时,它又申请 500:0,发生冲突,register_chrdev_region() 返回负数,insmod 失败。老师随后补上注销,重新编译、传到板端再验证。录音中老师也通过修改日志标记来避免误测旧 .ko。(录音约 13:40---18:11)
text
第一次 insmod:注册 500:0 成功
↓
rmmod:若退出函数漏掉 unregister,编号仍被占用
↓
第二次 insmod:再注册 500:0 → 冲突 → 初始化失败
若旧模块已经卸载 而编号被漏放,后来再给源码补上退出函数,也不会让旧模块的退出函数重新运行;老师在演示中通过重启清理残留状态。正确做法是在第一次正确加载时就把注册与注销写成一对,避免让实验板留下这类状态。不要把这段课堂故障现象误记成你自己的板端测试结果。
六、静态注册与动态分配怎么区分
| 方式 | 关键调用 | 主号由谁决定 | 本课位置 |
|---|---|---|---|
| 静态指定 | register_chrdev_region(MKDEV(500, 0), 1, "yhai_led") |
驱动代码先指定 500,内核检查可否占用。 |
老师本节实际演示。 |
| 动态分配 | alloc_chrdev_region(&devno, 0, 1, "yhai_led") |
内核选可用主号,成功后从 devno 取 MAJOR()。 |
老师提到,留待后面展开。 |
两种方式成功后都要用 unregister_chrdev_region(devno, count) 释放;动态分配尤其不能把主号预先写死到测试命令里,而要读取成功返回的 devno。Linux 内核字符设备 API
七、复习与验收
先用四句话讲清注册流程:选起点与数量 → 用 MKDEV() 得到 dev_t → 调用 register_chrdev_region() 并检查返回值 → 卸载时用同一范围 unregister_chrdev_region()。
| 要检查的事 | 预期结果 | 不能据此推断的事 |
|---|---|---|
insmod ./led.ko |
初始化返回 0,模块加载成功。 |
GPIO 已配置或 LED 已亮。 |
cat /proc/devices |
字符设备区域出现 500 yhai_led。 |
次号 0 已列出来,或 /dev/led 已创建。 |
rmmod led 后再看 /proc/devices |
500 yhai_led 消失。 |
其他驱动资源已经自动清理;它们仍要各自释放。 |
| 再次加载 | 在无其他占用的前提下可再次登记。 | 500 在所有机器、所有时刻都空闲。 |
**接到下一课:**登记设备号解决"这组编号归谁";cdev_init()、cdev_add() 和 file_operations 才继续解决"应用操作这个编号时,内核调用哪些驱动函数"。