8. 字符设备驱动框架.md 11 KB

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 结构体

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 命令编码

#include <linux/ioctl.h>

// 宏定义(自动编码方向、大小、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

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

// 设备号相关
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);

数据交互

#include <linux/uaccess.h>

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

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>

#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

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

编译与运行

# 编译
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)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#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_createdevice_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_usercopy_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 根据规则自动创建/命名设备节点,支持设备热插拔事件通知。