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(字符设备)是内核中表示字符设备的核心结构体。注册一个字符设备需要完成以下步骤:
- 分配设备号:使用
alloc_chrdev_region()动态分配,或register_chrdev_region()注册静态设备号。 - 初始化 cdev:调用
cdev_init()将cdev与file_operations关联。 - 添加 cdev 到内核:调用
cdev_add()将设备注册到 VFS 层。 - 自动创建设备节点(可选):使用
class_create()和device_create()在/dev/下自动生成设备文件。
以下 SVG 展示了 cdev 注册的完整流程及其与 VFS 的关系:
以下代码展示了完整的 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 展示了用户态与内核态数据交互的完整路径:
以下代码展示了 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 驱动。