4. 字符设备驱动基础:file_operations结构体、设备号注册与注销、cdev初始化、用户空间交互

字符设备是Linux驱动开发中最基础的设备类型,也是网卡驱动中控制通道(如ethtool、ioctl接口)的底层实现基石。本章聚焦字符设备驱动的核心骨架:设备号管理、cdev结构体初始化、file_operations操作集以及用户空间交互路径。掌握这些内容后,你就能构建一个可被用户态程序打开、读写、控制的字符设备驱动。

4.1 字符设备驱动核心数据结构

字符设备驱动的三个核心要素:设备号(dev_t)、cdev(字符设备对象)、file_operations(操作函数表)。三者关系如下:

核心关系:设备号标识设备 → cdev将设备号与操作函数绑定 → file_operations提供用户态系统调用的内核实现。
字符设备驱动核心架构 用户空间 (User Space) 系统调用接口 (open/read/write/ioctl/release) 内核空间 (Kernel Space) file_operations .open = my_open .read = my_read cdev ops → file_operations dev → 设备号 设备号 (dev_t) 主设备号 (major) 次设备号 (minor)

4.2 设备号:注册与注销

设备号是内核识别设备的唯一标识,由主设备号(major)和次设备号(minor)组成。主设备号对应驱动类型,次设备号对应同类设备的不同实例。

4.2.1 设备号数据结构

类型/宏 说明 示例
dev_t 32位无符号整数,高12位主设备号,低20位次设备号 dev_t devno;
MKDEV(major, minor) 将主次设备号组合为dev_t devno = MKDEV(240, 0);
MAJOR(dev_t) 从dev_t提取主设备号 int major = MAJOR(devno);
MINOR(dev_t) 从dev_t提取次设备号 int minor = MINOR(devno);

4.2.2 静态注册 vs 动态分配

方式 函数 优点 缺点
静态注册 register_chrdev_region() 设备号固定,便于预知 可能冲突,需申请保留
动态分配 alloc_chrdev_region() 避免冲突,推荐使用 设备号不固定,需查看/proc/devices

4.2.3 设备号注册与注销代码模板

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

#define DEVICE_NAME "my_netdev"
#define MINOR_COUNT 4

static dev_t dev_num;
static int major;

/* 动态分配设备号 */
static int __init my_driver_init(void)
{
    int ret;

    /* 动态分配:第一个次设备号为0,数量为MINOR_COUNT */
    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate chrdev region\n");
        return ret;
    }

    major = MAJOR(dev_num);
    pr_info("Allocated major number: %d, minor base: 0\n", major);
    return 0;
}

/* 注销设备号 */
static void __exit my_driver_exit(void)
{
    unregister_chrdev_region(dev_num, MINOR_COUNT);
    pr_info("Unregistered chrdev region\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
最佳实践:生产驱动优先使用alloc_chrdev_region()动态分配,避免与已有驱动冲突。静态注册仅用于已知且已预留的主设备号场景。

4.3 cdev初始化与注册

cdev结构体是字符设备在内核中的核心对象,负责将设备号与file_operations绑定。初始化流程分为三步:分配cdev → 初始化cdev → 添加cdev到内核。

4.3.1 cdev初始化步骤

  1. 分配cdev对象:静态定义或cdev_alloc()动态分配
  2. 初始化cdev:调用cdev_init(),绑定file_operations
  3. 添加cdev:调用cdev_add(),关联设备号

4.3.2 完整cdev注册代码

#include <linux/cdev.h>

static struct cdev my_cdev;

/* file_operations 定义 */
static int my_open(struct inode *inode, struct file *filp)
{
    pr_info("Device opened\n");
    return 0;
}

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

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    /* 用户空间读取实现 */
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos)
{
    /* 用户空间写入实现 */
    return count;
}

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

static int __init my_driver_init(void)
{
    int ret;

    /* 第一步:分配设备号(省略alloc_chrdev_region细节) */
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) return ret;

    /* 第二步:初始化cdev,绑定file_operations */
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    /* 第三步:添加cdev到内核 */
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to add cdev\n");
        return ret;
    }

    pr_info("cdev registered successfully\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    /* 先删除cdev,再注销设备号 */
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("cdev unregistered\n");
}
注意:cdev_add()之后设备立即可被用户空间访问。必须在cdev_add()成功后才暴露设备接口,避免未初始化完成就被打开。

4.4 file_operations结构体详解

file_operations是字符设备驱动的“系统调用映射表”,每个成员对应一个用户态系统调用。网卡驱动中常用的操作包括:

成员 对应系统调用 网卡驱动典型用途
.open open() 打开控制通道,初始化私有数据
.release close() 释放资源,关闭设备
.read read() 读取统计信息、寄存器状态
.write write() 下发配置命令、设置参数
.unlocked_ioctl ioctl() 网卡控制命令(如ethtool)
.mmap mmap() DMA缓冲区映射到用户空间
.poll poll()/select() 等待网络事件或数据就绪

4.5 用户空间交互:数据通路与关键函数

用户空间与字符设备驱动的交互通过系统调用触发,内核自动调用file_operations中对应的回调函数。数据传递的核心是用户空间缓冲区与内核空间缓冲区之间的安全拷贝。

4.5.1 数据拷贝函数

函数 方向 说明
copy_to_user() 内核 → 用户 将内核数据拷贝到用户缓冲区
copy_from_user() 用户 → 内核 从用户缓冲区安全拷贝数据到内核
put_user() 内核 → 用户 拷贝单个简单类型到用户空间
get_user() 用户 → 内核 从用户空间获取单个简单类型

4.5.2 用户空间交互示例

/* 内核驱动:read实现 */
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    char kernel_buf[64];
    int ret;

    /* 准备内核数据 */
    snprintf(kernel_buf, sizeof(kernel_buf),
             "Driver stats: packets=%lu\n", packet_count);

    /* 安全拷贝到用户空间 */
    ret = copy_to_user(buf, kernel_buf, strlen(kernel_buf) + 1);
    if (ret) {
        pr_err("copy_to_user failed: %d bytes not copied\n", ret);
        return -EFAULT;
    }

    return strlen(kernel_buf) + 1;
}

/* 用户空间测试程序 */
int main(void)
{
    int fd;
    char buf[128];

    fd = open("/dev/my_netdev", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    read(fd, buf, sizeof(buf));
    printf("Kernel says: %s\n", buf);

    close(fd);
    return 0;
}
安全要点:永远不要直接解引用用户空间指针。必须使用copy_to_user()/copy_from_user()系列函数,这些函数会检查地址合法性并处理缺页异常。
用户空间与内核空间交互流程 用户空间 fd = open("/dev/my_netdev", O_RDWR); read(fd, buf, sizeof(buf)); / write(fd, data, len); 系统调用 (sys_read / sys_write) VFS 虚拟文件系统 file_operations 回调 my_read() / my_write() / my_ioctl() copy_to_user() / copy_from_user()

4.6 完整驱动加载与测试流程

步骤 操作 命令/说明
1 编译驱动 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
2 加载模块 sudo insmod my_netdev.ko
3 查看设备号 cat /proc/devices | grep my_netdev
4 创建设备节点 sudo mknod /dev/my_netdev c major 0
5 测试读写 echo "hello" > /dev/my_netdev ; cat /dev/my_netdev
6 卸载模块 sudo rmmod my_netdev
自动化设备节点:生产驱动应使用udev或class_create()/device_create()自动创建设备节点,避免手动mknod。后续章节将详细讲解设备模型与自动节点创建。

4.7 本章小结

  • 设备号是驱动的身份证,alloc_chrdev_region()动态分配更安全。
  • cdev是字符设备的内核对象,cdev_init() + cdev_add()完成注册。
  • file_operations是系统调用与驱动函数的映射表,网卡驱动重点关注open/release/read/write/unlocked_ioctl/mmap/poll。
  • 用户空间交互必须使用copy_to_user()/copy_from_user()安全拷贝,禁止直接解引用用户指针。
  • 驱动加载后需手动或自动创建设备节点,用户态通过标准文件操作接口访问。