【Linux驱动开发】多节点驱动原理 + file_operations全套接口详解(open/read/write/release)

🎯 本文核心:详解 Linux2.6 多节点字符设备驱动原理、设备号区分机制、标准文件操作接口(open/release/write/read),附带全套可编译实测源码,解决单驱动多设备节点开发核心问题。

🔧 适用场景:嵌入式Linux字符设备驱动、多设备复用驱动接口、GPIO硬件控制、用户态与内核态数据交互


一、Linux2.6 字符设备注册核心特点

1.1 设备注册规则

1、支持一次性注册多个连续次设备号,兼容标准32位dev_t设备号格式;

2、内核不会自动创建 /dev 设备文件,必须手动或代码主动创建。

1.2 两种设备文件创建方式

✅ 方式1:终端命令手动创建

# 语法:mknod /dev/设备名 设备类型 主设备号 次设备号 mknod /dev/xxx c 主设备号 次设备号

✅ 方式2:驱动代码自动创建(推荐)

通过内核 device_create 函数,驱动加载(insmod)时自动在/dev下生成设备文件,卸载自动销毁。

二、多节点驱动核心原理(单接口驱动多设备)

2.1 核心问题

一套file_operations驱动接口,注册多个字符设备节点,内核底层如何精准区分不同硬件设备?

2.2 核心本质

每个 /dev 设备文件对应唯一的inode 节点,设备的唯一设备号存储在inode->i_rdev成员中。

驱动通过解析 inode 节点的设备号,计算设备下标,实现一套接口、多设备区分控制

2.3 内核设备号获取API

内核提供专用函数快速解析主/次设备号:

// 获取次设备号 static inline unsigned iminor(const struct inode *inode) { return MINOR(inode->i_rdev); } // 获取主设备号 static inline unsigned imajor(const struct inode *inode) { return MAJOR(inode->i_rdev); }

2.4 多设备区分核心代码

两种等效写法,通过设备号差值计算设备数组下标:

// 写法1:通过完整设备号差值区分设备 int index = inode->i_rdev - dev; // 写法2:通过次设备号差值区分设备(更常用) int index = iminor(inode) - BASEMINOR;

三、驱动标准文件操作接口详解

Linux 所有字符设备驱动,均依赖file_operations结构体绑定标准接口,实现应用层系统调用映射。

3.1 open / release 打开关闭接口

3.1.1 函数原型
// 设备打开:应用open()触发 int (*open) (struct inode *, struct file *); // 设备关闭:应用close()触发 int (*release) (struct inode *, struct file *);
3.1.2 核心参数解析
  • struct inode *inode:文件节点,存放设备唯一设备号,多设备区分核心;

  • struct file *file:进程文件描述符,核心成员private_data可跨函数传参。

3.1.3 核心设计思想

open 函数中解析设备号、计算设备下标,存入file->private_data,后续 write/read/release 直接读取,无需重复解析inode,提升代码效率。

3.2 write 写入接口(应用层 → 内核层)

3.2.1 应用/驱动层函数映射

应用层系统调用

ssize_t write(int fd, const void *buf, size_t count);

驱动层接口原型

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
3.2.2 参数对应关系
  • fd --> struct file * (存放设备私有数据)

  • buf --> char __user * (用户态缓冲区,内核禁止直接访问

  • count --> size_t (传输字节数)

  • loff_t * :文件偏移量,普通设备驱动可忽略

3.2.3 关键规则:用户/内核空间数据拷贝

严禁内核直接解引用用户态指针,会触发内核内存崩溃!必须使用专用拷贝函数:

// 用户空间 --> 内核空间 int copy_from_user(void *to, const void __user volatile *from, unsigned long n); to:内核空间接收缓冲区 from:用户空间数据源 n:拷贝字节数

3.3 read 读取接口(内核层 → 应用层)

3.3.1 应用/驱动层函数映射

应用层系统调用

ssize_t read(int fd, void *buf, size_t count);

驱动层接口原型

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
3.3.2 内核上报数据函数
// 内核空间 --> 用户空间 int copy_to_user(void __user volatile *to, const void *from, unsigned long n);

作用:将内核采集的硬件数据(按键、传感器、GPIO状态)上报给应用层。


四、实战源码1:多节点LED字符设备驱动(可直接编译)

功能简介:一次性注册 2 个 LED 设备节点,共用一套驱动接口,通过设备号区分不同 GPIO 设备,应用层可独立控制两个 LED 亮灭。

#include <linux/kernel.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/gpio.h> // 设备号配置 #define BASEMINOR 15 // 起始次设备号 #define COUNT 2 // 设备数量(2 个 LED) #define NAME "myleds" // 设备名称 // 硬件信息结构体:保存 GPIO 编号、默认电平 struct INFOR { int gpio; int val; }; // 初始化 2 路 LED 硬件参数 struct INFOR infor[COUNT] = {{24, 0}, {61, 0}}; // 驱动核心全局变量 struct cdev cdev; dev_t dev; struct class *cls; struct file_operations ops; /** @brief 设备打开接口 */ int led_open(struct inode *inode, struct file *f) { // 计算当前设备下标 long index = inode->i_rdev - dev; // int index = iminor(inode) - BASEMINOR; // 等效写法 printk("内核层 open 被调用\n"); // 保存设备下标,传递给读写接口 f->private_data = (void *)index; // 初始化 GPIO 电平 gpio_set_value(infor[index].gpio, infor[index].val); return 0; } /** @brief 设备关闭接口 */ int led_close(struct inode *inode, struct file *f) { int index = inode->i_rdev - dev; printk("内核层 close 被调用\n"); // 关闭 LED,电平取反 gpio_set_value(infor[index].gpio, !infor[index].val); return 0; } /** @brief 设备写入接口:应用层控制 LED 亮灭 */ ssize_t led_write(struct file *f, const char __user *buf, size_t s, loff_t *l) { int data, ret, value; // 从私有数据获取设备下标 long index = (long)f->private_data; printk("kernel : 字节数 s = %ld 应用层数据地址: %p\n", s, buf); // 用户数据拷贝到内核 ret = copy_from_user(&data, buf, s); printk("应用层传递过来的数据: %d\n", data); // 根据数据更新 LED 状态 if (data) value = infor[index].val; else value = !infor[index].val; gpio_set_value(infor[index].gpio, value); return s; } /** @brief 驱动初始化入口 */ int __init linux26_led_init(void) { int i, ret; struct device *devRet; // 绑定设备操作接口 ops.owner = THIS_MODULE; ops.open = led_open; ops.release = led_close; ops.write = led_write; // 初始化字符设备 cdev_init(&cdev, &ops); // 动态申请设备号 ret = alloc_chrdev_region(&dev, BASEMINOR, COUNT, NAME); if (ret != 0) { printk("申请设备号失败\n"); goto ERR; } // 注册字符设备到内核 ret = cdev_add(&cdev, dev, COUNT); if (ret != 0) { printk("注册设备失败\n"); goto ERR_ADD; } // 创建设备类 cls = class_create(THIS_MODULE, NAME); if (IS_ERR(cls)) { printk("class 创建失败\n"); ret = PTR_ERR(cls); goto ERR_CLS; } // 循环创建多个设备节点 /dev/myleds_1 /dev/myleds_2 for (i = 0; i < COUNT; i++) { devRet = device_create(cls, NULL, dev + i, NULL, NAME "_%d", i + 1); if (IS_ERR(devRet)) { printk("创建设备文件失败 %d\n", i + 1); ret = PTR_ERR(devRet); goto ERR_DEV; } } // 初始化 GPIO 为输出模式 for (i = 0; i < COUNT; i++) { gpio_request(infor[i].gpio, NAME); gpio_direction_output(infor[i].gpio, !infor[i].val); } printk("多节点 LED 驱动加载成功!\n"); return 0; // 异常逐级资源释放 ERR_DEV: for (i--; i >= 0; i--) { device_destroy(cls, dev + i); } ERR_CLS: class_destroy(cls); cdev_del(&cdev); ERR_ADD: unregister_chrdev_region(dev, COUNT); ERR: return ret; } /** @brief 驱动卸载、资源清理 */ void __exit linux26_led_exit(void) { int i; // 释放设备、GPIO 资源 for (i = 0; i < COUNT; i++) { gpio_free(infor[i].gpio); device_destroy(cls, dev + i); } cdev_del(&cdev); class_destroy(cls); unregister_chrdev_region(dev, COUNT); printk("驱动卸载完成,资源释放完毕!\n"); } module_init(linux26_led_init); module_exit(linux26_led_exit); MODULE_LICENSE("GPL");

五、实战源码2:杂项设备Read读取驱动(按键采集)

功能简介:基于 Linux misc 杂项设备框架,无需手动申请设备号,自动分配设备节点,实现 GPIO 按键电平读取,内核数据上报应用层。

#include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <linux/gpio.h> /** @brief 按键读取接口:内核数据上报应用层 */ ssize_t key_read(struct file *f, char __user *buf, size_t s, loff_t *l) { int val, ret; // 读取 GPIO39 按键电平 val = gpio_get_value(39); // 内核数据拷贝至用户空间 ret = copy_to_user(buf, &val, sizeof(val)); return sizeof(val); } // 文件操作接口集 struct file_operations ops = { .owner = THIS_MODULE, .read = key_read }; // 杂项设备结构体 struct miscdevice misc = { .minor = 255, // 255 = 内核自动分配次设备号 .name = "miscTest", // 设备节点:/dev/miscTest .fops = &ops }; // 驱动初始化 static int __init misc_test_init(void) { int ret; // 申请 GPIO 并配置为输入模式 gpio_request(39, "key"); gpio_direction_input(39); // 注册杂项设备 ret = misc_register(&amp;misc); if (ret == 0) { printk("杂项设备注册成功!!!\n"); } else { printk("杂项设备注册失败####\n"); } return ret; } // 驱动卸载 static void __exit misc_test_exit(void) { gpio_free(39); misc_deregister(&misc); } module_init(misc_test_init); module_exit(misc_test_exit); MODULE_LICENSE("GPL");

六、全文核心知识点总结

1. 多节点驱动核心:通过 inode 设备号区分多设备,依靠private_data跨接口传递参数,实现单驱动多设备复用。

2. 数据交互核心规则:内核与用户空间严禁直接指针访问,必须使用copy_from_user/copy_to_user完成数据拷贝。

3. 设备文件创建方式:支持 mknod 手动创建、device_create 代码自动创建两种方式。

4. 杂项设备优势:无需手动申请、管理设备号,内核自动分配,大幅简化简单外设驱动开发流程。