02_注册设备号_从主次设备号到成对注销

注册设备号:从主次设备号到成对注销

课程位置:驱动初级 → 字符设备 → 注册设备号;参照子节"主次设备号"。

主要依据: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 才继续解决"应用操作这个编号时,内核调用哪些驱动函数"。

相关推荐
@codercjw1 小时前
企业实战电机和传感器选型
学习
zhangrelay2 小时前
用ROS2Lyrical实验报告册思路重做蓝桥云课ROS1实验并改进
linux·笔记·学习·ubuntu·机器人
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(91):THEANINE——不删除过时记忆,让 Agent 记住事情是如何变化的
论文阅读·人工智能·学习·开源·github
我命由我123453 小时前
Photoshop - Photoshop 面板和菜单
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·photoshop
一木 之林3 小时前
RAG开发学习总结:从 LangChain 入门到检索增强生成链路的全栈实战-4/6
人工智能·学习·计算机视觉·langchain
LHR_friendship3 小时前
HCIP课程全部内容
网络·笔记·学习·hcip·华为ensp
海绵宝宝转agent3 小时前
Pico 学习笔记 Harness 设计、历史压缩、三类目录与缓存复用
笔记·学习·缓存
我命由我123454 小时前
Photoshop - Photoshop 使用仿制图章工具移除对象
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·学习方法
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer