字符设备驱动入门

本文是 嵌入式 Linux 学习路径 第六阶段:实现 最小字符设备驱动,通过 /devsysfsioctl 与用户态交互。完成后你应能:insmod/rmmod 干净理解 file_operations 生命周期安全处理 copy_from_user


学习目标

  • 编写 内核模块module_init/module_exitMODULE_LICENSE
  • 注册 字符设备alloc_chrdev_region / register_chrdevcdevfile_operations
  • 暴露 sysfs 属性ioctl 命令与用户态工具通信。
  • 使用 dmesg/sysstrace 调试。

架构位置

用户 open/read/write/ioctl → VFS → char dev → 你的 fops → 硬件/内存缓冲区

字符设备 按字节流 抽象;块设备、网络设备是其他子系统(概念对比即可)。


最小模块骨架

// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
 
static int __init demo_init(void)
{
	pr_info("demo: loaded\n");
	return 0;
}
 
static void __exit demo_exit(void)
{
	pr_info("demo: unloaded\n");
}
 
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("demo");

Makefile(内核外树或内核树内)

obj-m += demo.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

目标机与编译内核一致的 vermagic(交叉编译模块时用 板子内核源码树)。


字符设备注册流程(现代写法概览)

  • alloc_chrdev_regionregister_chrdev_region 获取 dev_t
  • cdev_init + cdev_add 绑定 file_operations
  • class_create(或 legacy device_create)生成 /dev/mydevsysfs 类目录。
  • unregister_chrdev_region / cdev_del / device_destroy`exit 中对称释放

file_operations 要点

方法典型用途
open分配 per-file 私有数据 filp->private_data
read/writecopy_to_user / copy_from_user
llseek可随机访问设备缓冲时
ioctl结构化命令(优先 unlocked_ioctl
release对应 close,释放 open 中资源

并发:多进程打开同一设备时,自旋锁/mutex 保护共享缓冲;考虑 wait_queue 阻塞读。


示例:环形缓冲 + read/write(思路)

  • 内核 kmalloc 环形队列;
  • write 入队,read 出队;队列空时 read 阻塞wait_event_interruptible);
  • 用户态 cat /dev/mydev / echo 测试。

(完整代码建议参考 Linux Device Drivers 示例或 Linux 内核模块开发实战。)


sysfs 属性(调试友好)

  • device_create_file + DEVICE_ATTR 暴露 /sys/class/.../mydev/state
  • show/storesprintf/kstrtointstore 返回 count 或负 errno。

适合 开关 LED 模拟值统计计数 而不定义 ioctl 命令号。


ioctl 设计

  • 命令编码:_IO / _IOR / _IOW / _IOWR<linux/ioctl.h>);
  • 用户态与内核 共用同一头文件 mydev_ioctl.h
  • ioctl 参数 若是指针,必须 copy_from_user 到内核栈/堆再使用。
#define MYDEV_IOC_MAGIC 'm'
#define MYDEV_IOC_RESET _IO(MYDEV_IOC_MAGIC, 0)
#define MYDEV_IOC_GET_VAL _IOR(MYDEV_IOC_MAGIC, 1, int)

用户态测试

int fd = open("/dev/mydev", O_RDWR);
ioctl(fd, MYDEV_IOC_GET_VAL, &v);
write(fd, "test", 4);

strace 跟踪:strace -e trace=open,read,write,ioctl ./app


与设备树结合(platform 字符设备)

  • platform_driver + .probe 里做 字符设备注册
  • .remove 里逆序注销;
  • 资源(IO 内存、IRQ)从 platform_get_resource / devm_ioremap 获取(MMIO 设备)。

GPIO LED 教学可先 gpio-leds(DT 阶段),再 自定义 misc 驱动 练习 fops


常见错误

错误后果
exit 未 device_destroyrmmod 失败或 oops
copy_to_user 未检查返回值部分拷贝仍返回成功
ioctl 未 CAP_SYS_ADMIN 等权限设计安全漏洞
中断上下文调用 copy_to_user禁止

实践练习

  • 模块加载后 /dev 节点存在,major/minor/proc/devices 一致。
  • 实现 read 阻塞 + poll 返回可读。
  • sysfs 读驱动内部计数器。
  • insmod → 运行测试 → rmmoddmesg 无 warn

阶段验收

  • open/read/release 调用顺序图(单进程)。
  • 解释 为什么必须 copy_from_user
  • 说明 字符设备与 platform_driver probe 的注册顺序

下一阶段衔接

  • I2C/SPI:数据路径变为 总线 transfer + 字符设备或 hwmon/input 子系统
  • 内核子系统深入块层、网络 sk_buff 是另一条驱动线。

参考


模块开发与 GPL 导出符号staging 驱动 规范有关;产品化前做 lockdep并发测试