# 8 字符设备驱动框架 ## 8.1 完整概念讲解 ### 字符设备概念 字符设备是 Linux 三大设备类型之一(字符设备、块设备、网络设备),按**字节流**顺序访问,不支持随机访问。典型例子:串口、键盘、鼠标、LED、虚拟设备。 访问方式:用户空间通过 `/dev/` 下的设备文件节点(如 `/dev/myled`)进行 read/write/ioctl 操作。 ### 字符设备驱动架构 ``` 用户空间 内核空间 ───────── ───────── open("/dev/myled") → file_operations.open() read(fd, buf, n) → file_operations.read() write(fd, buf, n) → file_operations.write() ioctl(fd, cmd, arg) → file_operations.ioctl() close(fd) → file_operations.release() ``` ### 注册流程概览 ``` 1. alloc_chrdev_region() → 动态分配主/次设备号 2. cdev_init() → 初始化 cdev,绑定 file_operations 3. cdev_add() → 将 cdev 注册到内核 4. class_create() → 创建设备类(/sys/class/xxx) 5. device_create() → 创建设备节点(/dev/xxx) ``` 卸载时反向执行: ``` 5. device_destroy() 4. class_destroy() 3. cdev_del() 2. (cdev 无需显式销毁) 1. unregister_chrdev_region() ``` ## 8.2 核心API/语法 ### file_operations 结构体 ```c struct file_operations { struct module *owner; int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long); int (*mmap)(struct file *, struct vm_area_struct *); loff_t (*llseek)(struct file *, loff_t, int); __poll_t (*poll)(struct file *, struct poll_table_struct *); // ... 更多操作 }; ``` ### ioctl 命令编码 ```c #include // 宏定义(自动编码方向、大小、magic、序号) _IO(type, nr) // 无数据传输 _IOR(type, nr, datatype) // 从内核读数据 _IOW(type, nr, datatype) // 向内核写数据 _IOWR(type, nr, datatype) // 双向传输 // 示例 #define LED_MAGIC 'L' #define LED_ON _IO(LED_MAGIC, 0) #define LED_OFF _IO(LED_MAGIC, 1) #define LED_SET _IOW(LED_MAGIC, 2, int) #define LED_GET _IOR(LED_MAGIC, 3, int) ``` ### 核心注册 API ```c #include #include #include // 设备号相关 alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); void unregister_chrdev_region(dev_t dev, unsigned count); // cdev 操作 void cdev_init(struct cdev *cdev, struct file_operations *fops); int cdev_add(struct cdev *cdev, dev_t dev, unsigned count); void cdev_del(struct cdev *cdev); // 设备类和设备节点 struct class *class_create(const char *name); void class_destroy(struct class *cls); struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); void device_destroy(struct class *cls, dev_t devt); ``` ### 数据交互 ```c #include unsigned long copy_to_user(void __user *dst, const void *src, unsigned long size); unsigned long copy_from_user(void *dst, const void __user *src, unsigned long size); // 返回0成功,非0失败 ``` ## 8.3 代码示例(完整可编译,附gcc命令) ### led_chrdev.c ```c #include #include #include #include #include #include #include #include #define DEVICE_NAME "myled" #define CLASS_NAME "led_class" #define LED_MAGIC 'L' #define LED_ON _IO(LED_MAGIC, 0) #define LED_OFF _IO(LED_MAGIC, 1) #define LED_SET _IOW(LED_MAGIC, 2, int) static dev_t dev_num; static struct cdev led_cdev; static struct class *led_class; static struct device *led_device; static int led_state = 0; static int led_open(struct inode *inode, struct file *file) { printk(KERN_INFO "LED: open\n"); return 0; } static int led_release(struct inode *inode, struct file *file) { printk(KERN_INFO "LED: release\n"); return 0; } static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { int ret; if (*ppos > 0) return 0; ret = copy_to_user(buf, &led_state, sizeof(led_state)); if (ret) return -EFAULT; *ppos += sizeof(led_state); return sizeof(led_state); } static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int ret; if (count < sizeof(led_state)) return -EINVAL; ret = copy_from_user(&led_state, buf, sizeof(led_state)); if (ret) return -EFAULT; printk(KERN_INFO "LED: write state=%d\n", led_state); return sizeof(led_state); } static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int val; switch (cmd) { case LED_ON: led_state = 1; printk(KERN_INFO "LED: ON\n"); break; case LED_OFF: led_state = 0; printk(KERN_INFO "LED: OFF\n"); break; case LED_SET: if (copy_from_user(&val, (int __user *)arg, sizeof(val))) return -EFAULT; led_state = val; printk(KERN_INFO "LED: SET state=%d\n", led_state); break; default: return -ENOTTY; } return 0; } static const struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .release = led_release, .read = led_read, .write = led_write, .unlocked_ioctl = led_ioctl, }; static int __init led_init(void) { int ret; /* 1. 动态分配设备号 */ ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { printk(KERN_ERR "LED: alloc_chrdev_region failed\n"); return ret; } printk(KERN_INFO "LED: major=%d minor=%d\n", MAJOR(dev_num), MINOR(dev_num)); /* 2. 初始化并添加 cdev */ cdev_init(&led_cdev, &led_fops); led_cdev.owner = THIS_MODULE; ret = cdev_add(&led_cdev, dev_num, 1); if (ret < 0) { printk(KERN_ERR "LED: cdev_add failed\n"); goto err_unregister; } /* 3. 创建设备类 */ led_class = class_create(CLASS_NAME); if (IS_ERR(led_class)) { printk(KERN_ERR "LED: class_create failed\n"); ret = PTR_ERR(led_class); goto err_cdev; } /* 4. 创建设备节点 /dev/myled */ led_device = device_create(led_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(led_device)) { printk(KERN_ERR "LED: device_create failed\n"); ret = PTR_ERR(led_device); goto err_class; } printk(KERN_INFO "LED: driver loaded, device /dev/%s created\n", DEVICE_NAME); return 0; err_class: class_destroy(led_class); err_cdev: cdev_del(&led_cdev); err_unregister: unregister_chrdev_region(dev_num, 1); return ret; } static void __exit led_exit(void) { device_destroy(led_class, dev_num); class_destroy(led_class); cdev_del(&led_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO "LED: driver unloaded\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Example"); MODULE_DESCRIPTION("Simple LED character device driver"); MODULE_VERSION("1.0"); ``` ### Makefile ```makefile obj-m := led_chrdev.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean ``` ### 编译与运行 ```bash # 编译 make # 加载驱动 sudo insmod led_chrdev.ko # 查看设备号 dmesg | tail # [xxxxx] LED: major=240 minor=0 # 查看设备节点 ls -l /dev/myled # crw------- 1 root root 240, 0 ... /dev/myled # 用户空间测试 # 写入状态 echo -n -e '\x01\x00\x00\x00' > /dev/myled # 读取状态 cat /dev/myled # 卸载驱动 sudo rmmod led_chrdev make clean ``` ### 用户空间 ioctl 测试程序(test_led.c) ```c #include #include #include #include #define LED_MAGIC 'L' #define LED_ON _IO(LED_MAGIC, 0) #define LED_OFF _IO(LED_MAGIC, 1) #define LED_SET _IOW(LED_MAGIC, 2, int) int main(void) { int fd = open("/dev/myled", O_RDWR); if (fd < 0) { perror("open"); return 1; } ioctl(fd, LED_ON); printf("LED ON\n"); int val = 0; ioctl(fd, LED_GET, &val); printf("LED state: %d\n", val); ioctl(fd, LED_OFF); printf("LED OFF\n"); close(fd); return 0; } // 编译:gcc test_led.c -o test_led // 运行:sudo ./test_led ``` ## 8.4 注意事项与易错点 1. **设备号冲突**:静态注册设备号(`register_chrdev_region`)可能与其他驱动冲突,推荐使用 `alloc_chrdev_region` 动态分配 2. **`IS_ERR` 检查**:`class_create` 和 `device_create` 返回 `ERR_PTR` 而不是 NULL,必须用 `IS_ERR` 检查 3. **`copy_to_user` / `copy_from_user` 不可省略**:用户空间指针可能非法,直接解引用会导致内核 oops 4. **ioctl 命令编码**:使用 `_IO/_IOR/_IOW/_IOWR` 宏自动生成命令码,避免手动编码冲突 5. **`module` 的 `.owner` 字段**:`file_operations` 中的 `.owner = THIS_MODULE` 确保模块在使用中不能被卸载 6. **卸载顺序**:必须反向销毁(device → class → cdev → unregister),否则内核引用计数泄漏 7. **`cdev_del` 后不能再调用文件操作**:cdev 删除后,用户空间的 open 调用会返回 `-ENXIO` ## 8.5 面试要点(3-5个Q&A) **Q1:字符设备驱动的注册流程是什么?** A:① `alloc_chrdev_region` 动态分配设备号 → ② `cdev_init` 初始化 cdev 并绑定 `file_operations` → ③ `cdev_add` 注册到内核 → ④ `class_create` 创建设备类 → ⑤ `device_create` 创建 `/dev/` 设备节点。卸载时反向销毁。 **Q2:`file_operations` 结构体的作用是什么?它有哪些常用成员?** A:`file_operations` 是字符设备驱动的核心,定义了用户空间操作(open/read/write/ioctl/close)对应的内核函数指针。常用成员:`open`(打开设备)、`release`(关闭设备)、`read/write`(数据读写)、`unlocked_ioctl`(控制命令)。 **Q3:`copy_to_user` 和 `copy_from_user` 为什么要检查返回值?** A:它们返回未拷贝的字节数,非零表示部分或完全失败(用户空间指针非法、进程被杀死等)。不检查返回值可能导致内核使用未初始化数据,产生安全漏洞或数据不一致。 **Q4:ioctl 命令码 `_IOW(LED_MAGIC, 2, int)` 是怎么编码的?** A:内核宏 `_IOW` 将方向(写)、数据大小(`sizeof(int)`)、类型码(`'L'`)和序号(2)编码为 32 位整数。`_IO` 无数据传输,`_IOR` 读,`_IOW` 写,`_IOWR` 双向。这样不同的 ioctl 命令不会冲突。 **Q5:为什么设备类和设备节点要分开创建?** A:`class_create` 在 `/sys/class/` 下创建类,`device_create` 在类下创建具体设备节点(`/dev/xxx`)。分离设计允许 udev/mdev 根据规则自动创建/命名设备节点,支持设备热插拔事件通知。