2.3 file_operations:让 write 点亮 LED


2.3 file_operations:让 write 点亮 LED

本节摘要file_operations 是驱动兑现"一切皆文件"承诺的操作表。本节实现 LED 驱动的 openwriterelease 回调,掌握跨边界数据拷贝、命令解析与返回值约定,并了解 readioctlpoll 各自的适用场景。

设想这样一个翻车现场:驱动注册成功、设备文件也在,你满怀期待地敲下写入命令,系统却瞬间失去响应,串口只剩一片寄存器转储——事后发现,write 回调里直接解引用了用户传来的指针。用户指针在内核地址空间里就是一串无意义的数字,碰它等于闭眼过马路。本节把回调写对,也把背后的规矩写进肌肉记忆。

三件套:open、write、release

先看 LED 驱动最核心的三个回调。open 在进程首次打开设备时调用,做使用计数与状态初始化;write 承担点灯主逻辑;release 在最后一个引用关闭时调用,负责收尾。

#include <linux/uaccess.h> #include <asm/io.h> #define LED_GPIO_REG_BASE 0xfdd20000 /* 先写死:GPIO 控制器数据寄存器 */ static void __iomem *led_gpio_base; static int led_state; static int led_open(struct inode *inode, struct file *filp) { pr_debug("led: 设备被打开\n"); return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char kbuf[8]; /* 内核侧缓冲,栈上小而够用 */ if (count == 0 || count > sizeof(kbuf)) return -EINVAL; /* 参数边界先挡掉 */ /* 安全拷贝:失败说明用户缓冲区不可用,绝不硬闯 */ if (copy_from_user(kbuf, buf, count)) return -EFAULT; if (kbuf[0] == '1') { iowrite32(1, led_gpio_base); /* 拉高引脚,点亮 */ led_state = 1; } else if (kbuf[0] == '0') { iowrite32(0, led_gpio_base); /* 拉低引脚,熄灭 */ led_state = 0; } else { return -EINVAL; /* 不认识的命令,明确拒绝 */ } return count; /* 返回已消费字节数:内核据此告知用户态写入完成 */ } static int led_release(struct inode *inode, struct file *filp) { pr_debug("led: 设备被关闭\n"); return 0; }

这份实现里有四个值得逐条咀嚼的细节。

第一,copy_from_user 不是多余的礼节。它逐段校验用户页面并处理缺页,失败返回未拷贝字节数——宏判断非零即失败。直接把 buf 当内核指针用,就是本节开头那场翻车。

第二,返回值是协议不是习惯write 返回正数表示消费的字节数,返回负的错误码表示失败。返回值小于用户请求的长度是合法的(短写),用户库会决定是否续写。返回 0 配非零请求长度则会被视为异常。

第三,__user 标注是给检查工具看的。它标记"这是用户空间指针",静态分析工具据此揪出未经拷贝就使用用户指针的代码——第 7 章的稀疏检查会大量依赖这类标注。

第四,寄存器访问用专用函数iowrite32 而不是随手一个指针强转:这类函数带内存屏障语义,保证写序符合硬件期望,且在不同架构上有正确的展开。第 3 章讲 I/O 内存时会展开缘由。

read:把状态还给用户

LED 驱动的 read 把当前灯状态报给用户态,同时示范另一方向的拷贝:

static ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { char state = led_state ? '1' : '0'; if (count < 1) return -EINVAL; if (copy_to_user(buf, &state, 1)) return -EFAULT; /* 简化处理:每次读只回一个字符并推进偏移 */ *ppos += 1; return 1; }

copy_to_usercopy_from_user 是一对镜像:方向相反、规矩相同。注意 ppos(文件偏移)的处理——字符设备大多维护逻辑偏移,读两次应该给出不同位置的内容;对 LED 这种单值设备,重复读返回同一状态也说得通,但要保持行为自洽。

ioctl:不适合文件模型的控制命令

开关灯适合 write,但"设置闪烁频率""查询固件版本"这类控制命令塞进数据流就很别扭。ioctl 就是为"带命令字的控制通道"准备的:

#include <linux/ioctl.h> #define LED_IOC_MAGIC 'L' #define LED_IOC_SET_BLINK _IOW(LED_IOC_MAGIC, 1, int) /* 设闪烁周期毫秒 */ #define LED_IOC_GET_STATE _IOR(LED_IOC_MAGIC, 2, int) /* 查当前状态 */ static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int period, state; switch (cmd) { case LED_IOC_SET_BLINK: if (copy_from_user(&period, (int __user *)arg, sizeof(period))) return -EFAULT; if (period < 0) return -EINVAL; led_blink_period = period; /* 6.2 节会用定时器兑现闪烁 */ return 0; case LED_IOC_GET_STATE: state = led_state; if (copy_to_user((int __user *)arg, &state, sizeof(state))) return -EFAULT; return 0; default: return -ENOTTY; /* 未知命令:约定俗成的错误码 */ } }

命令码用 _IOW_IOR 宏拼装,把方向、魔数、序号、参数大小编码进一个整数——这是跨用户态与内核的命令协议,两边必须用同一组头文件定义。返回 -ENOTTY 表示"不认识的命令",是历史沿袭的标准应答。

poll:让等待变成优雅的

如果设备状态变化需要等(比如按键驱动的按键事件),让用户态空转轮询读接口既浪费又不实时。poll 回调让进程"挂起等待、事件到来时被唤醒",第 3 章中断一节会给出配合等待队列的完整实现,这里先记住它在操作表里的位置与分工。

回调 触发时机 LED 驱动中的职责
open 打开设备 状态初始化、互斥进入
read 读取 上报灯状态
write 写入 解析命令、驱动寄存器
ioctl 控制命令 设置闪烁参数等带命令字操作
poll 等待事件 配合等待队列实现阻塞式通知
release 关闭引用归零 收尾清理

本节要点回顾

  • 跨边界必走安全拷贝copy_from_usercopy_to_user,失败即返回错误码。
  • 返回值是协议:正数消费量、负数错误码,短写合法。
  • 命令走 ioctl、数据走读写:接口分工清晰,用户态用着不拧巴。
  • 专用寄存器访问函数iowrite32 一族自带屏障语义,别用裸指针。

回调齐了,命令行已能点灯。但设备文件还得手敲命令创建——下一节让 udev 接管这道工序,驱动的交付形态才算完整。


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