03_初始化字符设备_从cdev到应用open

初始化字符设备:从 cdev 到应用 open()

课程位置:驱动初级 → 字符设备 → 初始化字符设备;衔接上一节"注册设备号",参照本节的"应用程序"和"验证测试"。

主要依据:2026-10-05 15:48《初始化字符设备》课堂录音转写,你提供的课程笔记、老师的 led.c / app.c 示例与测试截图。转写把 cdev、file_operations 等术语多次识别错,本文按示例代码校正。

证据边界:老师截图展示了 mknod /dev/led c 500 0、运行应用和内核打印 open/release;这不等于你自己的 Jetson 板已完成本节实验。下方"修正版"整理了资源释放和失败回滚,尚未在你的板上编译运行。

一、承接上一课:为什么注册完设备号还不能 open

上一课的 register_chrdev_region(MKDEV(500, 0), 1, "yhai_led") 只让内核知道:这段设备号已被这个驱动占用。此时即使 /proc/devices 出现 500 yhai_led,内核仍未得到"打开 500:0 时应该调用哪个函数"的信息。本课通过 struct cdev 和 struct file_operations 补上这条关联。(录音 00:05---01:06)
#mermaid-svg-yjw5CejGHlyAFvkp{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-yjw5CejGHlyAFvkp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yjw5CejGHlyAFvkp .error-icon{fill:#552222;}#mermaid-svg-yjw5CejGHlyAFvkp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yjw5CejGHlyAFvkp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yjw5CejGHlyAFvkp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yjw5CejGHlyAFvkp .marker.cross{stroke:#333333;}#mermaid-svg-yjw5CejGHlyAFvkp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yjw5CejGHlyAFvkp p{margin:0;}#mermaid-svg-yjw5CejGHlyAFvkp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yjw5CejGHlyAFvkp .cluster-label text{fill:#333;}#mermaid-svg-yjw5CejGHlyAFvkp .cluster-label span{color:#333;}#mermaid-svg-yjw5CejGHlyAFvkp .cluster-label span p{background-color:transparent;}#mermaid-svg-yjw5CejGHlyAFvkp .label text,#mermaid-svg-yjw5CejGHlyAFvkp span{fill:#333;color:#333;}#mermaid-svg-yjw5CejGHlyAFvkp .node rect,#mermaid-svg-yjw5CejGHlyAFvkp .node circle,#mermaid-svg-yjw5CejGHlyAFvkp .node ellipse,#mermaid-svg-yjw5CejGHlyAFvkp .node polygon,#mermaid-svg-yjw5CejGHlyAFvkp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yjw5CejGHlyAFvkp .rough-node .label text,#mermaid-svg-yjw5CejGHlyAFvkp .node .label text,#mermaid-svg-yjw5CejGHlyAFvkp .image-shape .label,#mermaid-svg-yjw5CejGHlyAFvkp .icon-shape .label{text-anchor:middle;}#mermaid-svg-yjw5CejGHlyAFvkp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yjw5CejGHlyAFvkp .rough-node .label,#mermaid-svg-yjw5CejGHlyAFvkp .node .label,#mermaid-svg-yjw5CejGHlyAFvkp .image-shape .label,#mermaid-svg-yjw5CejGHlyAFvkp .icon-shape .label{text-align:center;}#mermaid-svg-yjw5CejGHlyAFvkp .node.clickable{cursor:pointer;}#mermaid-svg-yjw5CejGHlyAFvkp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yjw5CejGHlyAFvkp .arrowheadPath{fill:#333333;}#mermaid-svg-yjw5CejGHlyAFvkp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yjw5CejGHlyAFvkp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yjw5CejGHlyAFvkp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yjw5CejGHlyAFvkp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yjw5CejGHlyAFvkp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yjw5CejGHlyAFvkp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yjw5CejGHlyAFvkp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yjw5CejGHlyAFvkp .cluster text{fill:#333;}#mermaid-svg-yjw5CejGHlyAFvkp .cluster span{color:#333;}#mermaid-svg-yjw5CejGHlyAFvkp 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-yjw5CejGHlyAFvkp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yjw5CejGHlyAFvkp rect.text{fill:none;stroke-width:0;}#mermaid-svg-yjw5CejGHlyAFvkp .icon-shape,#mermaid-svg-yjw5CejGHlyAFvkp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yjw5CejGHlyAFvkp .icon-shape p,#mermaid-svg-yjw5CejGHlyAFvkp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yjw5CejGHlyAFvkp .icon-shape .label rect,#mermaid-svg-yjw5CejGHlyAFvkp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yjw5CejGHlyAFvkp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yjw5CejGHlyAFvkp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yjw5CejGHlyAFvkp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注册设备号 500:0
准备 file_operations
cdev_init 让 cdev 记住操作表
cdev_add 把 cdev 加入内核映射
字符设备节点 /dev/led 记录 500:0
应用 open /dev/led
驱动 led_open
应用 close 或进程退出
驱动 led_release

这节的成果是应用层调用 open(),确实走到驱动中的 .open 回调 ;还没有 .read、.write 或 GPIO 控制,所以名字叫 led 也不会真的点亮 LED。

二、先准备两个对象:cdev 和操作表

struct cdev:内核中的字符设备对象

老师在内核模块中定义 struct cdev,并引入 <linux/cdev.h>。课堂用"公司已经建好,但还没有登记到系统里"打比方:定义变量只是取得一个对象;cdev_init() 做初始化;cdev_add() 才把它加入内核的字符设备映射。(录音 01:24---03:29、08:19---09:18)

c 复制代码
#include <linux/cdev.h>

static struct cdev led_cdev;

这里把对象设为模块级 static,让它在模块加载后的整个使用期间持续存在;不能在初始化函数里定义一个随后就失效的局部 struct cdev,再把其地址交给内核。

struct file_operations:把操作入口指向自己的函数

内核预先定义了 struct file_operations 这张函数指针表。驱动只填写当前需要的成员;老师这次实现 .open 和 .release。应用调用的是用户态 open() / close(),驱动实现的是对应的内核回调,两者函数名相近,不是同一个函数。(录音 03:38---07:45、09:43---12:21)

c 复制代码
static int led_open(struct inode *inode, struct file *file)
{
    pr_info("driver led open\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
    pr_info("driver led release\n");
    return 0;
}

static const struct file_operations led_fops = {
    .owner   = THIS_MODULE,
    .open    = led_open,
    .release = led_release,
};
  • .open、.release 的右边是函数名 ,表示把函数地址放进对应成员;不是在定义操作表时就执行函数。两个回调都遵守内核规定的函数签名,成功返回 0,出错返回负错误码。
  • struct inode *inode 表示这次打开对应的文件节点信息,struct file *file 表示本次打开的文件对象。本节只打印日志,暂时不需要使用这两个参数;后续常用 file->private_data 保存每次打开所需的状态。
  • .owner = THIS_MODULE 标记这组操作属于当前模块,配合内核的模块引用管理,避免仍在使用回调时卸载代码。
  • .release 与一次打开的文件对象的最后一次引用释放 有关。在老师的简单程序中,一次 open 后关闭或退出,便会看到一次 release;有 dup()、fork() 等共享引用时,不能机械地说"每次 close(fd) 都调用一次 .release"。Linux VFS 文档

三、初始化与添加:这几个函数分别做什么

函数 本例调用 作用、返回值和关键细节
register_chrdev_region() register_chrdev_region(devno, 1, "yhai_led") 先取得 500:0 的使用权;成功返回 0,失败为负数。这里只占号,尚无 .open。
cdev_init() cdev_init(&led_cdev, &led_fops) 初始化 cdev,让它记住 led_fops;函数返回 void。调用完成不代表设备已可被打开。
cdev_add() cdev_add(&led_cdev, devno, 1) 把已经初始化的 cdev 与从 devno 开始的 1 个设备号接入内核;成功返回 0、失败返回负数。一旦成功,映射立即生效,因此回调及其依赖状态应在调用前准备好。
cdev_del() cdev_del(&led_cdev) 撤销成功添加的 cdev,不再接受新的打开。它与成功的 cdev_add() 配对。
unregister_chrdev_region() unregister_chrdev_region(devno, 1) 把先前取得的设备号退还给内核。它与成功的设备号注册配对。

cdev_init() 的第二个参数是文件操作表的地址 ,不是一个具体的普通文件,也不是 /dev/led 的路径。cdev_add() 的第二个参数是起始设备号 ,第三个参数是由此 cdev 负责的连续设备号数量;本例都对应 500:0 和 1。这些定义与"添加后立即生效"的语义可在 Linux 内核字符设备 API 和 Linux 4.9 fs/char_dev.c 源码 中核对。

四、按顺序完成加载、失败回滚和卸载

加载路径

text 复制代码
① MKDEV(500, 0) 得到 devno
② register_chrdev_region(devno, 1, "yhai_led")
   └─失败:直接返回错误;本次没有取得编号
③ 定义好 led_fops,cdev_init(&led_cdev, &led_fops)
④ cdev_add(&led_cdev, devno, 1)
   └─失败:释放 cdev_init 建立的内部引用,注销已占的设备号,再返回错误
⑤ 全部成功:模块初始化返回 0

卸载路径

text 复制代码
① cdev_del(&led_cdev)                  先撤掉"设备号 → 操作表"的入口
② unregister_chrdev_region(devno, 1) 再归还设备号

**必须按取得资源的逆序清理。**老师的文字笔记已指出 cdev_add 要配 cdev_del,录音中也发现示例代码漏了删除步骤;但笔记所附 led.c 代码仍只在退出函数里注销设备号,而且在 cdev_add() 失败时直接返回,遗漏了已注册设备号的回滚。下面的代码补齐这两处。cdev_init() 没有单独配对的"撤销初始化"API;在 Linux 4.9 中它会初始化 cdev 内嵌的 kobject,若随后的 cdev_add() 失败,还应 kobject_put() 释放这份内部引用。内核自身的 __register_chrdev() 失败路径也是如此。Linux 4.9 fs/char_dev.c 源码(录音 12:21---13:24)

五、可对照老师示例的修正版 led.c

c 复制代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define LED_MA   500
#define LED_MI   0
#define LED_NUM  1U

static dev_t led_devno;
static struct cdev led_cdev;

static int led_open(struct inode *inode, struct file *file)
{
    pr_info("driver led open\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
    pr_info("driver led release\n");
    return 0;
}

static const struct file_operations led_fops = {
    .owner   = THIS_MODULE,
    .open    = led_open,
    .release = led_release,
};

static int __init led_init(void)
{
    int ret;

    led_devno = MKDEV(LED_MA, LED_MI);
    ret = register_chrdev_region(led_devno, LED_NUM, "yhai_led");
    if (ret < 0) {
        pr_err("register_chrdev_region failed: %d\n", ret);
        return ret;
    }

    cdev_init(&led_cdev, &led_fops);
    ret = cdev_add(&led_cdev, led_devno, LED_NUM);
    if (ret < 0) {
        pr_err("cdev_add failed: %d\n", ret);
        kobject_put(&led_cdev.kobj);
        unregister_chrdev_region(led_devno, LED_NUM);
        return ret;
    }

    pr_info("led init: cdev ready for %u:%u\n",
            MAJOR(led_devno), MINOR(led_devno));
    return 0;
}

static void __exit led_exit(void)
{
    cdev_del(&led_cdev);
    unregister_chrdev_region(led_devno, LED_NUM);
    pr_info("led exit\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

这是一份学习用、只验证回调的最小模块 。led_open() 和 led_release() 只打印信息;没有 .write、GPIO 初始化或真正的点灯动作。与老师笔记一样,主设备号 500 是静态示例,实际使用时可能被占用;注册失败应根据错误信息判断,不要假定 500 在每台机器都可用。

六、应用如何走到驱动:设备节点与 app.c

先有正确类型和编号的 /dev/led

cdev_add() 建立的是内核中的编号到回调的映射,不会自动创建设备节点。课堂手动创建:

sh 复制代码
sudo mknod /dev/led c 500 0
ls -l /dev/led

c 表示字符设备节点,500 0 必须与驱动的 LED_MA、LED_MI 对应。执行前先看 /dev/led 是否已存在;若已存在,核对其类型和主、次号,不能盲目重复创建或覆盖。touch /dev/led 创建的是普通文件,不能代替这一步。这里创建的是设备节点,不是创建设备或初始化硬件。(录音 18:49---19:58)

老师的应用程序及一个清理补充

老师的 app.c 只 open("/dev/led", O_RDWR),打印 open led ok 后返回。进程结束会自动关闭文件描述符,因此截图里仍出现驱动的 release 日志。写成日常可复用的测试程序时,显式 close(fd) 更容易看清生命周期:(录音 17:28---18:49、20:22---21:38)

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/dev/led", O_RDWR);
    if (fd < 0) {
        perror("open /dev/led");
        return 1;
    }

    puts("open led ok");
    if (close(fd) < 0) {
        perror("close /dev/led");
        return 1;
    }
    return 0;
}

访问路径是:应用 open("/dev/led") → VFS 读取节点中的 500:0 → 查到 cdev_add() 加入的 led_cdev → 使用其 led_fops → 调用 led_open();显式 close() 或进程结束释放最后引用后,调用 led_release()。Linux 4.9 fs/char_dev.c 源码、Linux VFS 文档

七、复现老师测试时,每一步该在哪执行

位置 关键动作 目的
Ubuntu 虚拟机 在 led.c 所在目录编译与板上 4.9.253-tegra 匹配的 led.ko;用板子可运行的 AArch64 工具链编译 app.c。 同时得到内核模块 和用户态测试程序,两者不是同一个文件。
传输阶段 把新生成的 led.ko 和 app 放到板上的测试目录。 老师用 /nfs/rootfs;只有你的板已挂载相应 NFS 路径时才能照搬。你也可以使用先前能用的文件传输方式。
Jetson 板 加载模块,检查 /proc/devices;确认 /dev/led 是 c 500 0;运行应用并看 dmesg。 验证从应用 open/close 到驱动 .open/.release 的整条路径。

在你此前用于编译 Jetson 4.9.253-tegra 模块的 Ubuntu 虚拟机目录,可以先按已有 Makefile 重新生成本节版本,并检查输出文件:

sh 复制代码
make clean
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
aarch64-linux-gnu-gcc app.c -o app
file led.ko app
modinfo -F vermagic led.ko

这里的 led.ko 应显示为 AArch64 内核模块、vermagic 对应板上的 4.9.253-tegra;app 则应为 AArch64 用户态可执行文件。编译完成后再传新文件 到板上,避免像课堂里那样误用旧 .ko。若板上已加载旧 led,先确认并卸载旧模块,再加载新版本。(录音 13:24---17:28)

课堂示例的板端检查顺序可写成:

sh 复制代码
sudo insmod ./led.ko
grep 'yhai_led' /proc/devices
if [ ! -e /dev/led ]; then
    sudo mknod /dev/led c 500 0
fi
ls -l /dev/led                 # 核对它显示为字符设备、主号 500、次号 0
./app
sudo dmesg | tail -n 20
sudo rmmod led

若 /dev/led 原本存在,先看 ls -l 的类型和编号;若不是 c 500, 0,不要把错误节点当作通过测试。老师截图的输出顺序是内核 led open → 用户程序 open led ok → 内核 led release;因为内核控制台和用户程序的显示会交织,最终以应用的返回情况与 dmesg 中的两条驱动日志共同判断。若终端提示 open /dev/led: No such file or directory,先查设备节点;若 ./app 文件确实存在却无法启动,再查程序架构与动态加载器,二者不是同一种故障。

八、最容易记错的细节

  1. 占号、加入 cdev、创建设备节点是三件事。 /proc/devices 有名字只证实第一件;成功 cdev_add() 才建立回调入口;mknod 给应用一个可打开的路径。三者的设备号必须一致。
  2. **cdev_init() 不代表已经"注册可用"。**它返回 void,只是初始化对象与操作表;真正让编号可以查到 cdev 的是 cdev_add()。
  3. **失败时只清理已经成功取得的资源。**注册设备号失败时不要注销;注册成功而 cdev_add() 失败时释放初始化后的内部引用并注销设备号;只有 cdev_add() 成功后才在退出路径 cdev_del()。
  4. 卸载先 cdev_del(),再注销设备号。 cdev_del() 阻止新的打开;已打开的文件在最后关闭前仍可能调用回调,所以更完整的驱动还要认真处理资源生命周期。Linux 内核字符设备 API
  5. **release 不是 close 的简单改名。**应用调用 close(fd);驱动的 .release 在打开文件对象的最后引用消失时运行。老师的单进程单描述符程序中,两者现象上相连。
  6. **"能打开"不等于"能读写或点灯"。**这版 file_operations 只有 .open、.release;下一步要实现数据接口并连接到 GPIO 等硬件控制。

**本节验收句:**在板上通过正确的 /dev/led 节点运行应用,看到应用成功打开,以及驱动 open、release 两条日志,才说明本节"从应用进入驱动"的路径跑通。老师截图已有这个结果;你的 Jetson 板结果需以你自己的新 led.ko 与 app 实测为准。

相关推荐
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
一隅论数智1 小时前
RDF(Resource Description Framework)介绍和使用举例(二)
大数据·人工智能·经验分享·笔记·学习·架构·政务
海绵宝宝转agent10 小时前
learn-claude-code第1-5章开源学习笔记分享
人工智能·笔记·python·学习
每天都要写算法(努力版)11 小时前
【行业前沿报告】DAgger:让智能体在自己走到的状态上学习
人工智能·学习·机器学习
sunshine22 girl14 小时前
Java学习六 Java常见API-2--String-3-字符串的截取替换和其他方法
java·开发语言·学习
sunshine22 girl15 小时前
Java学习六 Java常见API-1-Radom随机数
java·开发语言·学习
Answer1st16 小时前
【嵌入式学习】嵌入式原理知识-DMA(九)
学习
传奇开心果编程16 小时前
【ArkUI进阶练中学】第11课:安全与合规进阶
学习·ui·华为·harmonyos
Mr_Macallon18 小时前
C++回顾(基础)(01)
c语言·开发语言·c++·学习