跳转至

Day 10–11 · 字符设备驱动

预计时长:2 小时 / 天,共 4 小时
类型:实验


做什么

实现一个功能完整的字符设备驱动,覆盖设备号申请、cdev 注册、file_operations 实现、自动创建 /dev 节点、用户空间数据交换。结束时你能从用户态 open/read/write/close 这个设备,数据在内核缓冲区里被正确读写。


要了解什么

1. 设备号:主设备号 + 次设备号

Linux 用 dev_t(32位)表示设备号:高 12 位是主设备号(标识驱动),低 20 位是次设备号(标识同驱动下的具体设备)。

MAJOR(dev)  // 提取主设备号
MINOR(dev)  // 提取次设备号
MKDEV(ma, mi)  // 组合成 dev_t

动态分配(现代推荐做法):

dev_t devno;
alloc_chrdev_region(&devno, 0, 1, "mydev");
// 参数:&devno(输出),baseminor(起始次设备号),count(设备数量),name

静态指定(老做法,可能冲突):

register_chrdev_region(MKDEV(240, 0), 1, "mydev");

用动态分配,主设备号在 /proc/devices 中可查。

2. cdev 体系(不用 register_chrdev

register_chrdev 是老接口,一个主设备号只能对应一套 file_operationscdev 体系更灵活,可以给不同次设备号绑定不同 file_operations

struct cdev my_cdev;

cdev_init(&my_cdev, &my_fops);      // 绑定 file_operations
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, devno, 1);       // 注册到内核(之后设备就可访问了)
// 注意:cdev_add 之后驱动必须准备好响应,因为用户可能立刻 open

3. copy_to_user / copy_from_user

绝对不能在内核态直接 dereference 用户指针。 理由: 1. 用户指针可能是无效地址(用户故意传 NULL 或非法地址攻击内核) 2. 用户空间内存可能被换出(page fault 在某些内核上下文不合法) 3. 32/64 位混合模式下地址空间不同

// 从用户空间读取数据到内核缓冲区
ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
    char kbuf[256];
    if (count > sizeof(kbuf))
        count = sizeof(kbuf);
    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;  // 返回 -EFAULT 表示地址无效
    // 处理 kbuf...
    return count;
}

// 向用户空间写数据(从内核角度是"读"操作)
ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    char *data = "hello from kernel\n";
    size_t len = strlen(data);
    if (*pos >= len) return 0;  // EOF
    if (copy_to_user(buf, data + *pos, min(count, len - *pos)))
        return -EFAULT;
    *pos += min(count, len - *pos);
    return min(count, len - *pos);
}

4. class_create + device_create:自动生成 /dev 节点

没有 device_create,加载驱动后需要手动 mknod /dev/mydev c 主设备号 次设备号。用 class/device 机制,udev 会自动创建节点:

// 在 init 函数中
struct class *my_class = class_create(THIS_MODULE, "mydev_class");
struct device *my_device = device_create(my_class, NULL, devno, NULL, "mydev");
// 之后 /dev/mydev 自动出现

// 在 exit 函数中
device_destroy(my_class, devno);
class_destroy(my_class);

5. open 中的私有数据传递

struct my_dev {
    char buffer[1024];
    size_t buf_len;
    struct cdev cdev;
    // ...
};

static int my_open(struct inode *inode, struct file *filp)
{
    struct my_dev *dev;
    // inode->i_cdev 指向我们注册的 cdev
    dev = container_of(inode->i_cdev, struct my_dev, cdev);
    filp->private_data = dev;  // 存入 file,后续 read/write 直接用
    return 0;
}

练习

练习:完整字符设备驱动

mkdir -p ~/labs/chardev
cd ~/labs/chardev

chardev.c(完整实现,带详细注释):

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("learner");
MODULE_DESCRIPTION("Character device driver practice");

#define DEVICE_NAME "mychardev"
#define BUF_SIZE    4096

struct chardev_data {
    char *buffer;
    size_t buf_len;
    struct mutex lock;     // 保护 buffer 的互斥锁
    struct cdev cdev;
};

static dev_t devno;
static struct class *chardev_class;
static struct chardev_data *chardev;

/* ——— file_operations 实现 ——— */

static int chardev_open(struct inode *inode, struct file *filp)
{
    struct chardev_data *dev;
    dev = container_of(inode->i_cdev, struct chardev_data, cdev);
    filp->private_data = dev;
    pr_info(DEVICE_NAME ": opened (pid=%d)\n", current->pid);
    return 0;
}

static int chardev_release(struct inode *inode, struct file *filp)
{
    pr_info(DEVICE_NAME ": released\n");
    return 0;
}

static ssize_t chardev_read(struct file *filp, char __user *buf,
                             size_t count, loff_t *pos)
{
    struct chardev_data *dev = filp->private_data;
    ssize_t ret;

    if (mutex_lock_interruptible(&dev->lock))
        return -ERESTARTSYS;  // 被信号中断,让系统调用重启

    if (*pos >= dev->buf_len) {
        ret = 0;  // EOF
        goto out;
    }

    count = min(count, dev->buf_len - (size_t)*pos);
    if (copy_to_user(buf, dev->buffer + *pos, count)) {
        ret = -EFAULT;
        goto out;
    }

    *pos += count;
    ret = count;
    pr_debug(DEVICE_NAME ": read %zu bytes at offset %lld\n", count, *pos - count);

out:
    mutex_unlock(&dev->lock);
    return ret;
}

static ssize_t chardev_write(struct file *filp, const char __user *buf,
                              size_t count, loff_t *pos)
{
    struct chardev_data *dev = filp->private_data;
    ssize_t ret;

    if (count > BUF_SIZE)
        count = BUF_SIZE;

    if (mutex_lock_interruptible(&dev->lock))
        return -ERESTARTSYS;

    if (copy_from_user(dev->buffer, buf, count)) {
        ret = -EFAULT;
        goto out;
    }

    dev->buf_len = count;
    *pos = count;
    ret = count;
    pr_info(DEVICE_NAME ": wrote %zu bytes\n", count);

out:
    mutex_unlock(&dev->lock);
    return ret;
}

static const struct file_operations chardev_fops = {
    .owner   = THIS_MODULE,
    .open    = chardev_open,
    .release = chardev_release,
    .read    = chardev_read,
    .write   = chardev_write,
    .llseek  = default_llseek,
};

/* ——— 模块初始化 / 退出 ——— */

static int __init chardev_init(void)
{
    int ret;

    /* 1. 动态分配设备号 */
    ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err(DEVICE_NAME ": alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }
    pr_info(DEVICE_NAME ": major=%d, minor=%d\n", MAJOR(devno), MINOR(devno));

    /* 2. 分配设备数据结构 */
    chardev = kzalloc(sizeof(*chardev), GFP_KERNEL);
    if (!chardev) {
        ret = -ENOMEM;
        goto err_region;
    }

    chardev->buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!chardev->buffer) {
        ret = -ENOMEM;
        goto err_chardev;
    }

    mutex_init(&chardev->lock);

    /* 3. 初始化并注册 cdev */
    cdev_init(&chardev->cdev, &chardev_fops);
    chardev->cdev.owner = THIS_MODULE;
    ret = cdev_add(&chardev->cdev, devno, 1);
    if (ret < 0) {
        pr_err(DEVICE_NAME ": cdev_add failed: %d\n", ret);
        goto err_buffer;
    }

    /* 4. 创建设备类和设备节点(生成 /dev/mychardev) */
    chardev_class = class_create(THIS_MODULE, DEVICE_NAME "_class");
    if (IS_ERR(chardev_class)) {
        ret = PTR_ERR(chardev_class);
        goto err_cdev;
    }

    if (IS_ERR(device_create(chardev_class, NULL, devno, NULL, DEVICE_NAME))) {
        ret = PTR_ERR(device_create(chardev_class, NULL, devno, NULL, DEVICE_NAME));
        goto err_class;
    }

    pr_info(DEVICE_NAME ": initialized, /dev/%s created\n", DEVICE_NAME);
    return 0;

err_class:
    class_destroy(chardev_class);
err_cdev:
    cdev_del(&chardev->cdev);
err_buffer:
    kfree(chardev->buffer);
err_chardev:
    kfree(chardev);
err_region:
    unregister_chrdev_region(devno, 1);
    return ret;
}

static void __exit chardev_exit(void)
{
    device_destroy(chardev_class, devno);
    class_destroy(chardev_class);
    cdev_del(&chardev->cdev);
    kfree(chardev->buffer);
    kfree(chardev);
    unregister_chrdev_region(devno, 1);
    pr_info(DEVICE_NAME ": removed\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

配套用户态测试程序 test_chardev.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd;
    char wbuf[] = "Hello from userspace! Testing chardev.\n";
    char rbuf[256] = {0};
    ssize_t n;

    fd = open("/dev/mychardev", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    /* 写入 */
    n = write(fd, wbuf, strlen(wbuf));
    printf("Wrote %zd bytes\n", n);

    /* 重置文件偏移 */
    lseek(fd, 0, SEEK_SET);

    /* 读回 */
    n = read(fd, rbuf, sizeof(rbuf) - 1);
    printf("Read %zd bytes: %s", n, rbuf);

    /* 验证数据一致 */
    if (strcmp(wbuf, rbuf) == 0)
        printf("Data integrity: OK\n");
    else
        printf("Data integrity: MISMATCH!\n");

    close(fd);
    return 0;
}

Makefile(驱动 + 测试程序)

KDIR ?= ~/kernel/linux-stable
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-

obj-m += chardev.o

all: module usertest

module:
    make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

usertest:
    $(CROSS_COMPILE)gcc -o test_chardev test_chardev.c

clean:
    make -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
    rm -f test_chardev

在 QEMU 中验证

  • [ ] 把 chardev.kotest_chardev 打包进 initramfs,启动 QEMU
  • [ ] insmod chardev.ko,检查 dmesg,确认主设备号
  • [ ] ls -la /dev/mychardev,确认设备节点存在
  • [ ] ./test_chardev,验证写入读出
  • [ ] 用 cat /dev/mychardev 验证读接口
  • [ ] 用 echo "test" > /dev/mychardev 验证写接口
  • [ ] rmmod chardev,确认 /dev/mychardev 消失

延伸阅读

资料 具体位置 说明
Linux Device Drivers Corbet 等 Ch.3 "Char Drivers" 字符驱动经典讲解,LDD3
《Linux 设备驱动开发详解》宋宝华 第 4–5 章 字符设备详细讲解,例子丰富
Linux Device Driver Development John Madieu Ch.3 基于 5.x 内核的字符驱动
内核源码 drivers/char/mem.c 内核自带的经典字符驱动(/dev/null, /dev/zero 等)
内核源码 include/linux/fs.h file_operations 结构体定义,每个函数指针的文档在这