从设备读数据:字符设备 read / write 的完整流程
课程位置:驱动初级 → 字符设备 → 实现读写文件操作(应用程序、驱动程序添加、验证测试)。
依据:2026-10-06 11:27《从设备读数据》录音转写;学习系统
05_实现读写文件操作下的四份笔记;老师资料10.内核驱动/led/step4.实现文件操作readwrite/{app.c,led.c,Makefile}。录音中的"right""copy from you""copy to you"等按代码校正为write、copy_from_user、copy_to_user。本文是老师示例的学习笔记 ,没有改动你的驱动工程,也没有在你的板上完成本节读写测试。老师示例中的
c_buf是内核内存缓冲区,不是 LED 引脚上的数据,更不是串口或网卡硬件接收的数据。
一、这节课接在什么位置
前面的课程已经做到:
- 模块入口注册设备号
500:0,把字符设备的file_operations交给内核。 - 应用通过设备节点打开驱动,调用
ioctl()发送"开灯/关灯"命令。 - 驱动映射 GPIO 寄存器,用
readl()/writel()改变 LED 电平。
这节新增的是数据流 :应用写入一段字节,再从同一个字符设备读回。老师用一个 64 字节的内核数组 c_buf 模拟设备缓冲区,先把"应用 ↔ 驱动"的读写接口讲通,再让我们类比真实串口、传感器等设备。(录音约 00:12---01:29、07:14---08:35)
#mermaid-svg-ZAVAe5RxzrS8qvU4{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-ZAVAe5RxzrS8qvU4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .error-icon{fill:#552222;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .marker.cross{stroke:#333333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 p{margin:0;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster-label text{fill:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster-label span{color:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster-label span p{background-color:transparent;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .label text,#mermaid-svg-ZAVAe5RxzrS8qvU4 span{fill:#333;color:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .node rect,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node circle,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node ellipse,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node polygon,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .rough-node .label text,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node .label text,#mermaid-svg-ZAVAe5RxzrS8qvU4 .image-shape .label,#mermaid-svg-ZAVAe5RxzrS8qvU4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .rough-node .label,#mermaid-svg-ZAVAe5RxzrS8qvU4 .node .label,#mermaid-svg-ZAVAe5RxzrS8qvU4 .image-shape .label,#mermaid-svg-ZAVAe5RxzrS8qvU4 .icon-shape .label{text-align:center;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .node.clickable{cursor:pointer;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .arrowheadPath{fill:#333333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZAVAe5RxzrS8qvU4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZAVAe5RxzrS8qvU4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster text{fill:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .cluster span{color:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 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-ZAVAe5RxzrS8qvU4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZAVAe5RxzrS8qvU4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .icon-shape,#mermaid-svg-ZAVAe5RxzrS8qvU4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .icon-shape p,#mermaid-svg-ZAVAe5RxzrS8qvU4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .icon-shape .label rect,#mermaid-svg-ZAVAe5RxzrS8qvU4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZAVAe5RxzrS8qvU4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZAVAe5RxzrS8qvU4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZAVAe5RxzrS8qvU4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} write fd
调用 .write
copy_from_user
copy_to_user
调用 .read 的返回路径
应用 app.c 的 buff
系统调用 / VFS
驱动 led_write
内核数组 c_buf
驱动 led_read
应用 read fd 的 buff
图里的数据在用户空间缓冲区和内核空间缓冲区之间往返 。此演示没有从 Jetson 的 LED 引脚读取电平;前一节的 ioctl → GPIO 是另一条控制路径。前一节硬件控制笔记
二、先看应用程序:写入、清空、读回
老师的应用程序核心是:
c
char buff[] = " let's go ";
fd = open("/dev/uart", O_RDWR); /* 老师 step4/app.c 的原始路径 */
write(fd, buff, sizeof(buff));
memset(buff, '\0', sizeof(buff));
read(fd, buff, sizeof(buff) - 1);
printf("read buf is %s\n", buff);
close(fd);
| 顺序 | 应用做了什么 | 为什么这样做 |
|---|---|---|
open() |
打开字符设备节点,取得 fd。 |
后续 read/write 都用同一个文件描述符。 |
write() |
把 buff 的 sizeof(buff) 字节交给设备。 |
字符串 " let's go " 有 10 个可见字符,加结尾 \0,所以这里写 11 字节。 |
memset() |
把应用自己的 buff 清零。 |
防止只打印原来就留在应用缓冲区的数据,给读回验证提供一个直观条件。 |
read() |
最多读取 sizeof(buff)-1,即 10 字节。 |
用户缓冲区共 11 字节,预留末尾一个已经清零的位置,便于作为 C 字符串打印。 |
printf()、close() |
显示读回的文字并关闭文件。 | close() 归还这个 fd。 |
如果读写都成功,应用可看到类似 read buf is let's go 的输出。is 后会有两个空格:格式串本身有一个,buff 开头也有一个。输出相同只能证明这次数据穿过了应用、驱动和 c_buf;不能证明 LED 或外部硬件提供了这些字节。
/dev/uart 与 /dev/led 到底用哪个
学习系统的总笔记里,应用打开 /dev/led;老师提供的 step4/app.c 和子章节笔记打开 /dev/uart。录音测试时老师也提到按设备号重新创建节点。节点名称可以不同,真正决定由哪个字符驱动接管的是节点的主次设备号。
你此前的板端输出是 grep yhai_led /proc/devices → 500 yhai_led,而曾手动建过 /dev/led c 600 0,导致 open ... No such device or address。复现本节时,最容易与已有实验保持一致的做法是:仅把复制到虚拟机的 step4/app.c 中 open("/dev/uart", O_RDWR) 改为 open("/dev/led", O_RDWR),并确认 /dev/led 是 c 500, 0。不要把系统中可能已有的真正串口节点误认为这个课堂 LED 驱动。
三、应用的 write() 如何进入驱动
字符设备操作表需要有对应的函数指针:
c
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.read = led_read,
};
模块初始化时,cdev_init(&cdev, &led_fops) 把操作表交给 cdev,cdev_add() 把它发布到设备号 500:0。此后应用对正确节点调用 write(fd, ...),VFS 才有机会调用驱动的 .write = led_write;read(fd, ...) 同理进入 .read = led_read。Linux VFS 文档
老师的写回调签名及四个参数如下:
c
static ssize_t led_write(struct file *file,
const char __user *buf,
size_t count, loff_t *f_pos);
| 参数/返回类型 | 在本例中的意思 |
|---|---|
struct file *file |
这次打开所对应的内核文件对象;本例没有使用。 |
const char __user *buf |
应用空间 的源数据地址。__user 提醒它不是普通内核指针。 |
size_t count |
应用请求写入的字节数;是无符号类型。 |
loff_t *f_pos |
文件位置;老师示例暂时没使用。 |
ssize_t 返回值 |
成功通常返回实际写入的字节数,可小于 count;失败返回负错误码。用户态 write() 失败表现为 -1 并设置 errno。write(2) |
为什么必须用 copy_from_user()
驱动运行在内核空间,而 buf 指向应用进程的用户空间。不能把 buf 当成普通内核数组直接解引用并复制;应通过内核的用户空间访问接口处理地址检查和复制:
c
if (copy_from_user(c_buf, buf, count))
return -EFAULT;
return count;
这三个参数分别是内核目的地址、用户源地址、字节数 。返回 0 表示请求的字节全部复制完成;非零值是未复制的字节数 ,不是"成功复制数",也不是负错误码。老师的示例把任何非零结果统一转换成 -EFAULT,作为入门演示足够直观。Linux 内核用户空间复制说明(录音约 09:26---15:21)
老师的 c_buf 是 char c_buf[64],因此复制前先检查长度,避免 count 大于缓冲区。代码中的 count < 0 对 size_t 永远不成立;这个判断可以从笔记中理解为老师提醒"检查参数"的思路,不能当成有效的负数检查。
四、驱动的 read() 如何把数据送回应用
老师的读回调签名是:
c
static ssize_t led_read(struct file *file,
char __user *buf,
size_t count, loff_t *f_pos);
与 write 相比,数据方向反过来了:buf 是用户空间目的地址。核心动作是:
c
if (copy_to_user(buf, c_buf, count))
return -EFAULT;
return count;
参数依次是用户目的地址、内核源地址、字节数 。与 copy_from_user() 一样,copy_to_user() 返回 0 代表本次请求全部复制成功,非零代表尚未复制的字节数。驱动的 .read 成功时应返回实际交给应用的字节数 ;没有数据时按设计返回 0,应用便会把它理解为文件结束。Linux 内核用户空间复制说明、read(2)(录音约 21:45---29:19)
老师的演示在读时把 count 限制到最多 63 字节,避免跨出 64 字节数组;应用这次只请求 10 字节,因此会从 c_buf 取回 " let's go " 的 10 个可见字符。应用预先清零的第 11 个字节仍是 \0,所以 printf("%s") 能打印这个字符串。
五、完整的一次调用:按时间顺序复盘
text
板端加载新 led.ko
└─ register_chrdev_region(500:0) → cdev_init → cdev_add
确认 /dev/led 为字符设备 500:0
应用 open("/dev/led") → 得到 fd
应用 write(fd, buff, 11)
└─ VFS → led_fops.write → led_write → copy_from_user → 内核 c_buf
应用 memset(buff, 0, 11)
应用 read(fd, buff, 10)
└─ VFS → led_fops.read → led_read → copy_to_user → 应用 buff
应用 printf → 显示读回内容
应用 close(fd) → 关闭 fd
停止使用后 rmmod led → 模块清理资源
close(fd) 与驱动 .release 不是同名函数,也不是直接从应用跳到驱动的 C 函数调用。若操作表设置 .release = led_release,关闭这个打开文件对象的最后一个引用 时,VFS 可调用这个回调;若没有设置,应用仍可关闭 fd,只是不会执行你自定义的 led_release。老师 step4/led.c 定义了 led_open()、led_release(),但其 led_fops 只登记了 .write、.read,所以直接编译这份原始文件,不应期待自定义 open/release 日志。录音中出现这两条日志的演示状态与提供的文件快照不完全一致。(录音约 33:49---40:49)
六、跟着老师复现前,先看这几处代码边界
这些不影响理解"数据怎样跨越用户空间与内核空间",但会影响你直接照抄时的结果:
| 老师提供的代码 | 对复现的影响 |
|---|---|
step4/app.c 打开 /dev/uart,学习系统总笔记写 /dev/led。 |
应用路径必须与实际设备节点一致;你这台板建议用已经确认主次号的 /dev/led。 |
原始 step4/led.c 的 led_fops 只填 .write/.read。 |
open()/close() 可以完成,但不会打印自定义 led_open/led_release 的日志。要看这两个回调,需要把它们加入操作表。 |
step4/led.c 虽然还保留 led_ioctl() 和 GPIO 设置,但 led_fops 未登记 .unlocked_ioctl。 |
上一节的闪灯应用不能靠这份原始模块继续发送开/关命令;模块加载时 GPIO 初始化仍会把输出置高,原始卸载函数也没有明确拉低输出。这些现象与本节 c_buf 读写验证是两回事。 |
write 允许 count == 64,随后强制 c_buf[63] = '\0'。 |
若把 64 字节都当文本,第 64 字节会被覆盖。若本意是"63 字节内容 + 结尾零",就应限制有效数据最多 63 字节。 |
read 最多按用户请求复制 63 字节,却没记录上次真正写入多少字节 ,也未使用 *f_pos。 |
对短数据可能读出尾部旧字节;重复读取不会自然到达 EOF,不要用 cat /dev/led 做这个示例的测试。 |
count < 0、过长返回 -ENOMEM。 |
size_t 不会小于 0;-ENOMEM 通常表示内存不足,与"数据太长"不是同一种错误。 |
课堂应用没有检查 write()、read() 的返回值。 |
只看打印出的字符串不足以证明请求的 11/10 字节都成功传输;复现时同时看 dmesg,后续改进再检查返回值。 |
这节课先把老师的主线跑通。后续若要让它成为可复用驱动,再处理实际数据长度、文件位置、并发访问、部分传输、错误码和真实硬件接收。不要把课堂 c_buf 的读回演示描述成"从 LED 硬件读到了数据"。
七、你这台板的最短复现路径
下面是操作顺序 ;命令要在标注的机器执行。老师笔记里的 U-Boot setenv / run nfsboot 是他当时的 NFS 启动环境。你当前已经进入板上的 Linux shell,可以直接传入新文件测试,不必为了这一节重设 U-Boot 启动参数。
**Ubuntu 虚拟机:**把老师 step4.实现文件操作readwrite 的 led.c、app.c、Makefile 复制到一个独立目录,别覆盖上一节已能工作的 LED 闪烁代码。把复制后的 app.c 中 /dev/uart 改为 /dev/led;把 Makefile 的 KERNELDIR 指向你已验证的 /home/yhai/bsp/Linux_for_Tegra/source/public/kernel/kernel-4.9,再编译:
sh
make clean
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
aarch64-linux-gnu-gcc app.c
file led.ko a.out
modinfo -F vermagic led.ko
预期两个文件都是 AArch64,模块的 vermagic 与板端 uname -r 对应的 4.9.253-tegra 匹配。**只重编 a.out 而保留上一节的旧 led.ko 不会得到 .read/.write 回调。**传输老师这节新编出的 两个文件 到板端 ~/led;可用你现有的 NFS 共享,或确认板子实际 IP 后用 scp。
**Jetson 板子:**先退出正在运行的旧应用,再检查/卸载旧 led 模块,加载这节的新模块:
sh
cd ~/led
uname -r
uname -m
lsmod | grep '^led '
# 如果上一行显示旧 led,先执行:sudo rmmod led
sudo insmod ./led.ko
grep yhai_led /proc/devices
ls -l /dev/led
/proc/devices 应显示 500 yhai_led,ls -l /dev/led 应显示字符设备 500, 0。若节点仍是你此前建过的 600, 0,确认路径和类型后 执行 sudo rm /dev/led、sudo mknod /dev/led c 500 0,再核对一次 ls -l /dev/led。确认两者都正确才运行:
sh
sudo ./a.out
sudo dmesg | tail -n 30
sudo rmmod led
期待应用打印 read buf is let's go ,日志中能看到驱动写入/读出的信息。若看到 open /dev/led: No such device or address,先查节点是否仍是 600:0 ;若只看到旧的 ioctl 日志或没有读写日志,核对板上 led.ko 是否真由 step4 重建并替换。当前这些是待你板端验证的预期结果,不是已经验证通过的实测记录。
八、记住这四句话
file_operations决定应用的read/write最终能否找到这个驱动里的.read/.write。write的方向是应用 → 内核 ,用copy_from_user();read的方向是内核 → 应用 ,用copy_to_user()。copy_*_user()返回 0 表示全部复制成功 ;驱动的read/write返回实际传输字节数或负错误码。这两层返回值不能混为一谈。- 本课读回的是驱动中的
c_buf。看到" let's go "往返成功,是学会字符设备数据通路的证据;真实硬件读取还需要对应设备的数据来源和处理逻辑。