《从系统调用到驱动回调:read() 如何映射到 chrdev_read()》
Linux 驱动中,为什么我们在用户程序里执行
read(fd, buf, size),内核就会自动去调用驱动中的chrdev_read()呢?本文记录了我在学习字符设备驱动时,对用户态 read() / write() 如何触发内核态驱动回调函数的理解与分析。
一、字符设备驱动的基本组成
在 Linux 中,字符设备驱动(Character Device Driver) 是最常见的驱动类型之一。
每个字符设备驱动文件,基本都由以下几个核心模块组成:
| 模块 | 作用 | 示例 |
|---|---|---|
1. #include 头文件 | 包含内核API与结构定义 | #include <linux/module.h> |
| 2. 模块许可(License) | 指定驱动许可证,否则加载时会警告 | MODULE_LICENSE("GPL"); |
3. 文件操作集 (file_operations) | 定义驱动如何响应 open/read/write 等系统调用 | static struct file_operations my_fops = {...}; |
| 4. 主设备号与次设备号 | 标识驱动类型与设备实例 | register_chrdev(200, "chrdev", &my_fops); |
| 5. 模块加载与卸载函数 | 定义驱动加载和卸载时的动作 | module_init() / module_exit() |
| 6. 实际操作函数 | open、read、write、release 的实现 | ssize_t my_read(...) {...} |
| 7. 元信息 | 描述、作者等辅助信息 | MODULE_AUTHOR("Yunes"); |
这七个部分几乎出现在所有字符设备驱动中,是字符设备框架的“标准模板”。
二、内核态驱动代码示例
下面我们写一个最小可运行的字符设备驱动。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user#define CHRDEV_MAJOR 200 // 主设备号
#define CHRDEV_NAME "chrdev" // 设备名static char write_buf[100]; // 写缓冲(保存用户写入)
static char read_buf[100]; // 读缓冲(内核要返回的数据)
static char kernel_data[] = "kernel data!";// 打开设备
static int chrdev_open(struct inode *inode, struct file *filp)
{printk("chrdev open!\n");return 0;
}// 读取设备
static ssize_t chrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int ret;memcpy(read_buf, kernel_data, sizeof(kernel_data));ret = copy_to_user(buf, read_buf, cnt);if (ret == 0)printk("send successful!\n");elseprintk("send error!\n");return cnt; // 返回实际读取的字节数
}// 写入设备
static ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int ret;ret = copy_from_user(write_buf, buf, cnt);if (ret == 0)printk("write successful: %s\n", write_buf);elseprintk("write error!\n");return cnt;
}// 关闭设备
static int chrdev_release(struct inode *inode, struct file *filp)
{printk("chrdev released.\n");return 0;
}// 设备操作函数集
static struct file_operations chrdev_fops = {.owner = THIS_MODULE,.open = chrdev_open,.read = chrdev_read,.write = chrdev_write,.release = chrdev_release,
};// 模块加载
static int __init chrdev_init(void)
{int ret = register_chrdev(CHRDEV_MAJOR, CHRDEV_NAME, &chrdev_fops);if (ret < 0)printk("register error!\n");elseprintk("chrdev init successful!\n");return 0;
}// 模块卸载
static void __exit chrdev_exit(void)
{unregister_chrdev(CHRDEV_MAJOR, CHRDEV_NAME);printk("chrdev exit.\n");
}module_init(chrdev_init);
module_exit(chrdev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yunes");
MODULE_DESCRIPTION("Simple character device driver demo");
三、用户态测试程序
下面的程序用于测试 open/read/write/close 是否与驱动正确对应。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>int main(int argc, char const *argv[])
{char write_buf[100], read_buf[100];static char user_data[] = "user data!";int fd, tmp;if (argc != 3) {printf("Usage: %s <device> <0=read | 1=write>\n", argv[0]);return -1;}char *filename = (char *)argv[1];fd = open(filename, O_RDWR);if (fd < 0) {perror("open");return -1;}if (atoi(argv[2]) == 0) {tmp = read(fd, read_buf, sizeof(read_buf) - 1);if (tmp > 0) {read_buf[tmp] = '\0';printf("Read successful: %s\n", read_buf);} else if (tmp == 0)printf("EOF reached.\n");elseperror("read error");}if (atoi(argv[2]) == 1) {memcpy(write_buf, user_data, sizeof(user_data));tmp = write(fd, write_buf, sizeof(user_data));if (tmp < 0)perror("write error");}close(fd);return 0;
}
四、系统调用是如何映射到驱动函数的?
有人会问:
“为什么我在用户态执行
read(),驱动的chrdev_read()就自动被调用了?”
这是因为——内核在 file_operations 结构体里维护了函数指针映射关系。
内核中的调用流程如下:
用户态调用 read(fd, buf, size)↓
系统调用入口 sys_read()↓
根据 fd 查找 struct file 对象↓
找到 file->f_op 指针↓
调用 file->f_op->read(file, buf, size, &file->f_pos)↓
最终调用 chrdev_read()
所以,file_operations 就像是驱动的“函数接口表”,
内核通过它找到对应的驱动回调函数。
五、系统调用与驱动函数的对应关系
| 用户态函数 | 驱动回调函数 | 作用 |
|---|---|---|
open() | .open = chrdev_open | 打开设备 |
read() | .read = chrdev_read | 从设备读取数据 |
write() | .write = chrdev_write | 写入数据到设备 |
close() | .release = chrdev_release | 关闭设备文件 |
file_operations就是把这些系统调用“挂接”到驱动的核心机制。
六、总结
Linux 字符设备驱动的核心机制,就是通过
struct file_operations
将用户态系统调用(open/read/write/close)映射到内核态驱动函数。
🔹 register_chrdev() 告诉内核这个主设备号对应哪个函数集;
🔹 用户态执行 read(),系统调用进入内核;
🔹 内核通过 file_operations 找到 chrdev_read();
🔹 最终完成一次用户态与内核态的数据交互。
一句话总结:
file_operations是用户态与内核态的“桥梁”,
每一个系统调用,最终都要在驱动中找到它的影子。
