4. 字符设备驱动实战:实现 open/read/write/ioctl/release 接口
本章将带领你从零实现一个完整的字符设备驱动,重点掌握 file_operations 结构体中核心接口的注册与实现,并深入理解用户态与内核态之间的数据拷贝机制。通过本章实战,你将能够独立编写具备基本交互能力的字符设备驱动。
4.1 字符设备驱动核心框架回顾
字符设备驱动的核心是围绕 struct file_operations 展开的。该结构体定义了驱动能够响应的系统调用接口。本章将实现以下五个最常用的接口:
| 接口 | 系统调用 | 触发时机 | 典型用途 |
|---|---|---|---|
open |
open() |
用户程序打开设备文件时 | 初始化设备、递增引用计数、分配资源 |
release |
close() |
用户程序关闭设备文件时 | 释放资源、递减引用计数、关闭硬件 |
read |
read() |
用户程序读取设备数据时 | 从内核缓冲区拷贝数据到用户空间 |
write |
write() |
用户程序写入设备数据时 | 从用户空间拷贝数据到内核缓冲区 |
unlocked_ioctl |
ioctl() |
用户程序发送控制命令时 | 设备参数配置、状态查询、特殊操作 |
关键点: 在 Linux 5.x 及以上内核中,
ioctl 接口推荐使用 unlocked_ioctl,它不需要调用大内核锁(BKL),性能更好且是未来的标准方向。
4.2 用户态与内核态数据拷贝机制
用户空间和内核空间运行在不同的虚拟地址空间中,驱动程序不能直接解引用用户空间指针。必须使用内核提供的专用拷贝函数,这些函数会进行地址合法性检查并处理缺页异常。
4.2.1 核心拷贝函数
| 函数 | 方向 | 原型 | 返回值 |
|---|---|---|---|
copy_from_user |
用户 → 内核 | unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) |
0 成功;非0 未拷贝成功的字节数 |
copy_to_user |
内核 → 用户 | unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) |
0 成功;非0 未拷贝成功的字节数 |
安全红线: 永远不要使用
memcpy、sprintf 等标准库函数直接操作用户空间指针。这会导致内核崩溃或严重的安全漏洞。必须使用 copy_from_user / copy_to_user 系列函数。
4.2.2 数据拷贝流程示意图
下面的 SVG 图展示了用户态调用 read() 和 write() 时,数据在内核态与用户态之间的流动路径:
4.3 驱动代码实战实现
下面是一个完整的字符设备驱动模板,实现了 open、release、read、write 和 unlocked_ioctl 接口。代码中包含了详细的注释和错误处理。
4.3.1 驱动头文件与全局变量
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h> // copy_from_user, copy_to_user
#include <linux/slab.h> // kmalloc, kfree
#include <linux/ioctl.h>
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mychar"
#define BUFFER_SIZE 1024
static int major_num;
static struct class *mychar_class = NULL;
static struct device *mychar_device = NULL;
static struct cdev my_cdev;
// 设备结构体
struct mychar_dev {
char *buffer;
size_t size;
struct mutex lock;
};
static struct mychar_dev *dev_ptr;
4.3.2 open 与 release 接口
static int mychar_open(struct inode *inodep, struct file *filep)
{
struct mychar_dev *dev = container_of(inodep->i_cdev,
struct mychar_dev, cdev);
filep->private_data = dev;
mutex_lock(&dev->lock);
dev->size = 0; // 每次打开重置缓冲区
mutex_unlock(&dev->lock);
printk(KERN_INFO "mychardev: device opened\n");
return 0;
}
static int mychar_release(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "mychardev: device closed\n");
return 0;
}
4.3.3 read 与 write 接口(核心数据拷贝)
static ssize_t mychar_read(struct file *filep, char __user *buffer,
size_t len, loff_t *offset)
{
struct mychar_dev *dev = filep->private_data;
ssize_t retval;
mutex_lock(&dev->lock);
// 检查读取位置是否超出当前数据长度
if (*offset >= dev->size) {
mutex_unlock(&dev->lock);
return 0; // EOF
}
// 计算实际可读取的字节数
if (*offset + len > dev->size)
len = dev->size - *offset;
// 从内核缓冲区拷贝到用户空间
if (copy_to_user(buffer, dev->buffer + *offset, len)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*offset += len;
retval = len;
mutex_unlock(&dev->lock);
printk(KERN_INFO "mychardev: read %zu bytes\n", len);
return retval;
}
static ssize_t mychar_write(struct file *filep, const char __user *buffer,
size_t len, loff_t *offset)
{
struct mychar_dev *dev = filep->private_data;
ssize_t retval;
mutex_lock(&dev->lock);
// 检查缓冲区剩余空间
if (*offset + len > BUFFER_SIZE) {
len = BUFFER_SIZE - *offset;
if (len == 0) {
mutex_unlock(&dev->lock);
return -ENOSPC; // 设备已满
}
}
// 从用户空间拷贝到内核缓冲区
if (copy_from_user(dev->buffer + *offset, buffer, len)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*offset += len;
dev->size = *offset;
retval = len;
mutex_unlock(&dev->lock);
printk(KERN_INFO "mychardev: written %zu bytes\n", len);
return retval;
}
4.3.4 unlocked_ioctl 接口
// 定义 ioctl 命令
#define MYCHAR_IOCTL_MAGIC 'k'
#define MYCHAR_IOCTL_RESET _IO(MYCHAR_IOCTL_MAGIC, 0)
#define MYCHAR_IOCTL_GETSIZE _IOR(MYCHAR_IOCTL_MAGIC, 1, int)
#define MYCHAR_IOCTL_SETSIZE _IOW(MYCHAR_IOCTL_MAGIC, 2, int)
static long mychar_ioctl(struct file *filep, unsigned int cmd,
unsigned long arg)
{
struct mychar_dev *dev = filep->private_data;
int tmp;
switch (cmd) {
case MYCHAR_IOCTL_RESET:
mutex_lock(&dev->lock);
memset(dev->buffer, 0, BUFFER_SIZE);
dev->size = 0;
mutex_unlock(&dev->lock);
printk(KERN_INFO "mychardev: buffer reset via ioctl\n");
break;
case MYCHAR_IOCTL_GETSIZE:
tmp = dev->size;
if (copy_to_user((int __user *)arg, &tmp, sizeof(tmp)))
return -EFAULT;
break;
case MYCHAR_IOCTL_SETSIZE:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
return -EFAULT;
if (tmp < 0 || tmp > BUFFER_SIZE)
return -EINVAL;
mutex_lock(&dev->lock);
dev->size = tmp;
mutex_unlock(&dev->lock);
break;
default:
return -ENOTTY; // 不支持的 ioctl 命令
}
return 0;
}
4.3.5 file_operations 结构体与模块初始化
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
.read = mychar_read,
.write = mychar_write,
.unlocked_ioctl = mychar_ioctl,
};
static int __init mychar_init(void)
{
// 分配设备结构体内存
dev_ptr = kmalloc(sizeof(struct mychar_dev), GFP_KERNEL);
if (!dev_ptr)
return -ENOMEM;
// 分配内核缓冲区
dev_ptr->buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!dev_ptr->buffer) {
kfree(dev_ptr);
return -ENOMEM;
}
memset(dev_ptr->buffer, 0, BUFFER_SIZE);
dev_ptr->size = 0;
mutex_init(&dev_ptr->lock);
// 注册字符设备
// ... (cdev_init, cdev_add, class_create, device_create 等标准流程)
printk(KERN_INFO "mychardev: driver loaded\n");
return 0;
}
static void __exit mychar_exit(void)
{
// 注销设备、释放资源
if (dev_ptr) {
kfree(dev_ptr->buffer);
kfree(dev_ptr);
}
printk(KERN_INFO "mychardev: driver unloaded\n");
}
module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A complete char device driver example");
4.4 用户态测试程序
编写一个简单的用户态程序来验证驱动的所有接口:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define DEVICE_PATH "/dev/mychardev"
#define MYCHAR_IOCTL_MAGIC 'k'
#define MYCHAR_IOCTL_RESET _IO(MYCHAR_IOCTL_MAGIC, 0)
#define MYCHAR_IOCTL_GETSIZE _IOR(MYCHAR_IOCTL_MAGIC, 1, int)
#define MYCHAR_IOCTL_SETSIZE _IOW(MYCHAR_IOCTL_MAGIC, 2, int)
int main() {
int fd;
char write_buf[] = "Hello from userspace!";
char read_buf[128] = {0};
int size;
// 1. 打开设备
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
// 2. 写入数据
write(fd, write_buf, strlen(write_buf));
// 3. 读取数据
lseek(fd, 0, SEEK_SET);
read(fd, read_buf, sizeof(read_buf));
printf("Read from device: %s\n", read_buf);
// 4. 通过 ioctl 获取当前数据大小
ioctl(fd, MYCHAR_IOCTL_GETSIZE, &size);
printf("Current buffer size: %d\n", size);
// 5. 通过 ioctl 重置缓冲区
ioctl(fd, MYCHAR_IOCTL_RESET);
// 6. 关闭设备
close(fd);
return 0;
}
4.5 常见错误与调试技巧
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
copy_from_user 返回非0 |
用户空间地址无效或不可读 | 检查传入的 __user 指针是否合法;使用 access_ok() 预检查 |
read 返回 -EFAULT |
copy_to_user 失败 |
确认用户缓冲区可写;检查 len 参数是否过大 |
ioctl 返回 -ENOTTY |
命令码不匹配或未注册 | 检查 _IO/_IOR/_IOW 宏生成的命令码是否与用户态一致 |
| 内核崩溃(Oops) | 直接解引用用户空间指针 | 确保所有用户数据访问都通过 copy_from_user / copy_to_user |
| 数据读取不全 | 未正确处理 loff_t *offset |
在 read 和 write 中正确更新 *offset 值 |
调试建议: 在开发阶段,可以在
read 和 write 函数中加入 printk 打印 len、offset 和拷贝返回值,帮助定位数据流问题。使用 dmesg 查看内核日志。
4.6 本章小结
- 字符设备驱动的核心是填充
struct file_operations结构体,实现open、release、read、write和unlocked_ioctl接口。 - 用户态与内核态之间的数据拷贝必须使用
copy_from_user和copy_to_user,禁止直接解引用用户空间指针。 ioctl命令码使用_IO/_IOR/_IOW/_IOWR宏生成,确保用户态和内核态的命令码一致。- 驱动中必须使用互斥锁(
mutex)保护共享数据,防止并发访问导致的数据竞争。 - 错误处理是驱动开发的关键,每个系统调用接口都应返回适当的错误码(如
-EFAULT、-ENOMEM、-ENOTTY)。