2. 字符设备驱动基础

任务目标

编写一个最小字符设备驱动,实现:动态分配主设备号、向内核注册设备、卸载时自动注销,并验证设备节点可正常创建与访问。

环境依赖

  • Linux内核源码树(当前案例基于5.15版本验证)
  • 内核头文件与编译工具链(gcc、make)
  • 开发机需具备root权限(用于加载模块与创建设备节点)

核心实现

驱动代码包含完整的注册/注销逻辑,不依赖任何外部未定义函数。设备号动态分配,通过class_create自动创建设备节点,卸载时全部清理。

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

#define DEVICE_NAME "demo_char"
#define CLASS_NAME  "demo_class"

static dev_t dev_num;
static struct cdev demo_cdev;
static struct class *demo_class;
static struct device *demo_device;

static int demo_open(struct inode *inode, struct file *filep)
{
    return 0;
}

static ssize_t demo_read(struct file *filep, char __user *buf,
                         size_t len, loff_t *offset)
{
    return 0;
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .read    = demo_read,
};

static int __init demo_init(void)
{
    int ret;

    /* 1. 动态分配主设备号 */
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }
    pr_info("Allocated major=%d minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    /* 2. 初始化并添加cdev */
    cdev_init(&demo_cdev, &fops);
    demo_cdev.owner = THIS_MODULE;
    ret = cdev_add(&demo_cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        goto err_unregister;
    }

    /* 3. 创建device class,用于自动生成设备节点 */
    demo_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(demo_class)) {
        pr_err("Failed to create class\n");
        ret = PTR_ERR(demo_class);
        goto err_cdev_del;
    }

    /* 4. 创建设备节点 /dev/demo_char */
    demo_device = device_create(demo_class, NULL, dev_num,
                                NULL, DEVICE_NAME);
    if (IS_ERR(demo_device)) {
        pr_err("Failed to create device\n");
        ret = PTR_ERR(demo_device);
        goto err_class_destroy;
    }

    pr_info("Demo char driver initialized\n");
    return 0;

err_class_destroy:
    class_destroy(demo_class);
err_cdev_del:
    cdev_del(&demo_cdev);
err_unregister:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

static void __exit demo_exit(void)
{
    device_destroy(demo_class, dev_num);
    class_destroy(demo_class);
    cdev_del(&demo_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("Demo char driver removed\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Demo");
MODULE_DESCRIPTION("Minimal char device driver with dynamic major");

Makefile

obj-m := demo_char.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

编译与加载

  1. 将上述驱动代码保存为demo_char.c,Makefile保存为同名文件(无后缀)
  2. 执行make,生成demo_char.ko
  3. 加载模块:sudo insmod demo_char.ko
  4. 查看设备号分配:dmesg | tail,应看到类似Allocated major=240 minor=0的输出
  5. 确认设备节点:ls -l /dev/demo_char,应显示字符设备文件

验证方法

  • 注册验证:cat /proc/devices | grep demo_char,能看到设备名与动态分配的主设备号
  • 访问验证:sudo cat /dev/demo_char,命令不报错即说明设备节点可正常打开(read返回0,无数据输出属正常)
  • 注销验证:sudo rmmod demo_char,然后确认/dev/demo_char已消失,dmesg | tail显示Demo char driver removed

常见问题

  • insmod报错“Operation not permitted”:需root权限,或检查内核是否禁用模块加载
  • 设备节点未自动生成:确认内核配置了CONFIG_DEVTMPFS且/dev挂载了devtmpfs;否则需手动mknod /dev/demo_char c 主设备号 0
  • rmmod提示“Module in use”:先关闭所有打开该设备的进程(如正在运行的cat)