4. 字符设备驱动基础回顾

在深入网卡 NIC 驱动开发之前,必须牢固掌握 Linux 字符设备驱动的基础框架。网卡驱动虽然属于网络子系统,但其控制面(如 ethtool、ioctl)以及某些虚拟网卡(如 tun/tap)的数据面,仍然依赖字符设备模型。本章将系统回顾 file_operations 结构体、cdev 注册流程以及用户态与内核态数据拷贝这三个核心知识点。

4.1 file_operations 结构体:驱动与 VFS 的契约

file_operations 是字符设备驱动与虚拟文件系统(VFS)之间的核心接口。每个打开的设备文件在 VFS 层都对应一个 struct file,而 struct file 中的 f_op 指针指向驱动提供的 file_operations 实例。当用户态程序调用 open()、read()、write()、ioctl() 等系统调用时,VFS 会通过该结构体找到对应的驱动函数。

关键认知: file_operations 是驱动开发者向内核注册的“方法表”,内核不关心具体实现,只按契约调用。网卡驱动的控制通道(如 ethtool 的 ioctl)正是通过此机制实现的。

以下是最常用的 file_operations 成员:

成员名称 函数原型 说明
owner struct module *owner 通常设为 THIS_MODULE,防止模块被卸载时仍有打开的文件
open int (*open)(struct inode *, struct file *) 设备文件打开时调用,用于初始化设备、递增使用计数
release int (*release)(struct inode *, struct file *) 设备文件关闭时调用,用于清理资源、递减使用计数
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据,数据通过用户空间缓冲区返回
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据,数据从用户空间缓冲区获取
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令处理,网卡驱动中用于设置 MAC、链路参数等
mmap int (*mmap)(struct file *, struct vm_area_struct *) 将设备内存映射到用户空间,高性能网卡常用(如 DPDK)
注意: 在 Linux 2.6.36 之后,ioctl 已被 unlocked_ioctl 取代。驱动中应使用 .unlocked_ioctl = my_ioctl 的形式,且调用者不再持有 BKL(大内核锁)。

典型的 file_operations 定义示例:

// 定义一个简单的字符设备驱动操作集
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
};

4.2 cdev 注册流程:从内核对象到设备节点

cdev(字符设备)是内核中表示字符设备的核心结构体。注册一个字符设备需要完成以下步骤:

  1. 分配设备号:使用 alloc_chrdev_region() 动态分配,或 register_chrdev_region() 注册静态设备号。
  2. 初始化 cdev:调用 cdev_init() 将 cdev 与 file_operations 关联。
  3. 添加 cdev 到内核:调用 cdev_add() 将设备注册到 VFS 层。
  4. 自动创建设备节点(可选):使用 class_create() 和 device_create() 在 /dev/ 下自动生成设备文件。

以下 SVG 展示了 cdev 注册的完整流程及其与 VFS 的关系:

字符设备 cdev 注册流程与 VFS 关系 用户空间应用程序 系统调用层 (VFS) VFS 核心层 file_operations cdev 结构体 设备号 (dev_t) ① alloc_chrdev_region() ② cdev_init() ③ cdev_add() ④ class_create() ⑤ device_create()

以下代码展示了完整的 cdev 注册过程:

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

static dev_t my_dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static int __init my_driver_init(void)
{
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

    // 2. 初始化 cdev 并关联 file_operations
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 3. 将 cdev 添加到内核
    ret = cdev_add(&my_cdev, my_dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(my_dev_num, 1);
        return ret;
    }

    // 4. 创建 device class 和 device 节点(自动生成 /dev/my_device)
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        pr_err("Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_num, 1);
        return PTR_ERR(my_class);
    }

    device_create(my_class, NULL, my_dev_num, NULL, "my_device");

    pr_info("My device driver loaded, major=%d, minor=%d\n",
            MAJOR(my_dev_num), MINOR(my_dev_num));
    return 0;
}

static void __exit my_driver_exit(void)
{
    device_destroy(my_class, my_dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(my_dev_num, 1);
    pr_info("My device driver unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
常见错误: 忘记在 cdev_init() 后设置 my_cdev.owner = THIS_MODULE,会导致模块引用计数失效,在设备打开时卸载模块可能引发内核崩溃。

4.3 用户态与内核态数据交互:copy_from/to_user

Linux 内核运行在特权级(Ring 0),用户程序运行在非特权级(Ring 3)。内核不能直接访问用户空间指针,用户空间也不能直接访问内核内存。因此,驱动中必须使用内核提供的专用 API 来安全地拷贝数据。

两个核心函数:

函数 原型 方向 说明
copy_from_user unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) 用户空间 → 内核空间 将用户空间数据拷贝到内核缓冲区;返回未拷贝成功的字节数(0 表示成功)
copy_to_user unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) 内核空间 → 用户空间 将内核缓冲区数据拷贝到用户空间;返回未拷贝成功的字节数
核心原则: 永远不要直接解引用用户空间指针!即使是在内核上下文中,用户指针也可能指向无效地址、只读页面或已被换出的页面。必须使用 copy_from_user / copy_to_user,这些函数内部会做地址合法性检查、页面错误处理以及隐含的 access_ok() 验证。

以下 SVG 展示了用户态与内核态数据交互的完整路径:

用户态与内核态数据交互路径 用户空间 (Ring 3) 用户缓冲区 (char __user *buf) 系统调用: read() / write() 内核空间 (Ring 0) 内核缓冲区 (void *kern_buf) 驱动 read() / write() 实现 copy_from_user / copy_to_user 地址校验 + 页面错误处理 + 数据拷贝 ⚠ 禁止直接访问用户指针!必须通过 copy_from_user / copy_to_user 进行安全拷贝

以下代码展示了 read 和 write 方法中如何使用这些函数:

// 驱动中的 read 实现:从内核缓冲区拷贝数据到用户空间
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    size_t available = MY_BUF_SIZE - *f_pos;
    size_t to_copy = min(count, available);
    int ret;

    if (to_copy == 0)
        return 0;  // EOF

    // 将内核数据拷贝到用户空间
    ret = copy_to_user(buf, my_kernel_buf + *f_pos, to_copy);
    if (ret) {
        pr_err("Failed to copy data to user, %d bytes remaining\n", ret);
        return -EFAULT;
    }

    *f_pos += to_copy;
    return to_copy;
}

// 驱动中的 write 实现:从用户空间拷贝数据到内核缓冲区
static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos)
{
    size_t available = MY_BUF_SIZE - *f_pos;
    size_t to_copy = min(count, available);
    int ret;

    if (to_copy == 0)
        return -ENOSPC;  // 缓冲区满

    // 将用户空间数据拷贝到内核
    ret = copy_from_user(my_kernel_buf + *f_pos, buf, to_copy);
    if (ret) {
        pr_err("Failed to copy data from user, %d bytes remaining\n", ret);
        return -EFAULT;
    }

    *f_pos += to_copy;
    return to_copy;
}
性能提示: 对于网卡驱动中的大数据量传输(如 DMA 环形缓冲区),应避免逐字节拷贝。使用 mmap 将 DMA 缓冲区直接映射到用户空间,或使用 splice() 系统调用实现零拷贝,可以显著提升吞吐量。

4.4 本章小结

  • file_operations 是驱动与 VFS 的契约,定义了设备文件的所有操作入口。
  • cdev 注册 需要依次完成设备号分配、cdev 初始化、cdev 添加和设备节点创建。
  • copy_from_user / copy_to_user 是用户态与内核态数据交互的唯一安全途径,驱动中必须使用,禁止直接解引用用户指针。
  • 这些基础机制是网卡驱动控制面(ioctl、ethtool)和虚拟网卡数据面的基石,后续章节将在此基础上构建完整的 NIC 驱动。