2.2 字符设备注册:设备号与 cdev


2.2 字符设备注册:设备号与 cdev

本节摘要:字符设备在内核"上户口"要走三步:申请设备号、注册 cdev 操作表、在设备模型里登记设备实例。本节逐步实现这三步并讲清撤销顺序,让 LED 驱动从孤立模块变成内核字符设备框架的在册成员。

上一节的模块还只是一段"可以被装入内核的代码",内核并不知道它代表一台设备。要让 1.1 节旅程中"VFS 按主设备号定位驱动"那一站成立,驱动必须完成登记:告诉内核"这个主设备号归我,操作入口在 file_operations 里,设备实例叫这个名字"。本节把这套手续一次办齐。

第一步:申请设备号

设备号是 1.3 节讲过的"身份证"——主号指驱动、次号分实例。内核提供静态与动态两种申请方式:静态写死主号,适合设备号早已约定俗成的老设备;动态由内核分配空闲主号,是现代驱动的默认选择。我们的 LED 用动态申请:

#include <linux/fs.h> #include <linux/cdev.h> static dev_t led_devt; /* 设备号:主次号的合成值 */ static struct cdev led_cdev; static int __init led_cdev_init(void) { int ret; /* 动态申请 1 个设备号,led 为设备名,仅用于 /proc/devices 展示 */ ret = alloc_chrdev_region(&led_devt, 0, 1, "led"); if (ret < 0) { pr_err("led: 申请设备号失败\n"); return ret; } pr_info("led: 获得主设备号 %d 次设备号 %d\n", MAJOR(led_devt), MINOR(led_devt)); return 0; }

申请成功后,日志会打出分到的主设备号——重启后这个号码可能不同,这个"漂移"问题留给 2.4 节的 udev 解决。申请失败的处理体现了内核编程的基本礼仪:每个失败分支都要有日志与错误码出口,让上层调用者能定位与回滚。

💡 关键直觉:设备号申请就像开工前领工牌——没有工牌(设备号),后面的注册、节点创建统统无从谈起;而领了工牌不干活(忘记注销),工牌池就会被占满。

第二步:把操作表挂进 cdev

cdev 是内核字符设备框架的核心结构,它把"设备号"与"操作入口表"绑定在一起。绑定分三拍:初始化 cdev、填入操作表、按设备号注册:

static const struct file_operations led_fops = { .owner = THIS_MODULE, /* 属主指向本模块,防使用中被卸出 */ .open = led_open, /* 以下三个回调在 2.3 节实现 */ .write = led_write, .release = led_release, }; /* 紧接上一步,init 函数内继续 */ cdev_init(&led_cdev, &led_fops); led_cdev.owner = THIS_MODULE; ret = cdev_add(&led_cdev, led_devt, 1); if (ret < 0) { pr_err("led: cdev 注册失败\n"); goto err_cdev; }

cdev_add 执行完毕的瞬间,"某主设备号上的操作请求会路由到这张表"这件事就在内核里生效了。owner 字段值得多看一眼:它建立模块引用计数——只要有进程还开着这台设备,卸载模块的请求就会被拒绝,防止"函数还在被调用,代码却已卸出"的灾难。

第三步:在设备模型里登记

到这里字符设备框架已经认识驱动了,但系统整体(包括用户态的 udev)还看不到它。最后一步是在内核设备模型里创建设备实例,这一步会同时在 sys 文件系统的设备树上生成条目,并向用户态广播"设备到来"事件——2.4 节 udev 靠的正是这个事件:

#include <linux/device.h> static struct class *led_class; /* init 函数内继续 */ led_class = class_create("led"); /* 设备类别:/sys/class/led */ if (IS_ERR(led_class)) { ret = PTR_ERR(led_class); goto err_class; } /* 设备实例:名字会出现在 /dev 与 sys 中,LED0 为次序 0 */ device_create(led_class, NULL, led_devt, NULL, "led%d", 0);

执行后,sys 文件系统里出现类别目录与设备符号链接,内核向用户态发出热插拔事件。设备模型这条线在第 4 章会全面展开——它也是平台总线匹配的基础设施。

注册解剖图

三步注册与对应撤销

三步注册与对应撤销

拆除顺序:错误的代价

卸载时的清理必须严格反向:先销毁设备实例、再注销 cdev、最后归还设备号。顺序颠倒会出大事——比如先归还设备号再销毁设备:设备实例还引用着这个号,销毁动作本身可能触发对已释放编号的访问;更危险的是 cdev 还在册时归还设备号,另一个驱动可能立刻拿到这个号并注册,两套操作表争夺同一主号,系统行为不可预测。

正确的清理函数与错误回滚路径写在一起:

static void __exit led_cdev_exit(void) { device_destroy(led_class, led_devt); /* 撤三 */ class_destroy(led_class); /* 撤类别 */ cdev_del(&led_cdev); /* 撤二 */ unregister_chrdev_region(led_devt, 1); /* 撤一 */ } /* init 中的错误回滚,同样遵循反向原则 */ err_device: device_destroy(led_class, led_devt); err_class: class_destroy(led_class); err_cdev: cdev_del(&led_cdev); unregister_chrdev_region(led_devt, 1); return ret;

⚠️ 常见坑:初始化函数在中间某步失败后直接 return,前面已成功的步骤没有回滚——每次装入失败都泄漏一点资源,最终设备号池枯竭。错误路径的资源回收不是可选项,代码评审时这是必查项。

常见追问

追问一:设备号、cdev、设备实例三步,能不能合并成一步省点心? 内核确实为简单场景准备了"套餐":混杂设备把默认主号、cdev 注册与类别登记打包成一个注册函数,一次调用三步并作一步,适合只有一两个实例的小驱动。但套餐换走了灵活性——主号固定、命名规则受限、多实例管理要自己想办法。LED 驱动故意走全流程,正是因为这三步各自的语义会在第 4 章的平台模型里改头换面再次出现:先看清楚每一步在做什么,后面才分得清哪些由框架代劳、哪些仍归你管。

追问二:装入后重复执行注册,会发生什么? 典型症状是注册函数返回"文件已存在"类错误——设备号区间或设备名已被占用。重复装入同一个模块会被内核直接拒绝(同名模块不允许并存),但两个不同模块抢同一个名字、或同一模块的初始化函数被并发触发,都会走到这个报错。处理方式一律是检查返回值并走回滚,而不是假设注册必然成功——这也是本节错误路径练习的另一半用意。

本节要点回顾

  • 三步上户:设备号、cdev、设备实例,每步产出明确、缺一不可。
  • owner 字段保平安:引用计数防止"边被调用边被卸出"。
  • 设备模型是广播站:device_create 触发的热插拔事件是 udev 自动创建节点的信号源。
  • 拆除严格反向:注册顺序的镜像就是唯一正确的撤销顺序,错误回滚同理。

户口办好了,下一节填那张操作表——实现 openwriterelease,让命令行的字符第一次变成引脚上的电平。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U