【奔跑吧!Linux 内核(第二版)】第6章:简单的字符设备驱动(三)
笨叔 陈悦. 奔跑吧 Linux 内核(第2版) [M]. 北京: 人民邮电出版社, 2020.
文章目录
- Linux 内核的 I/O 多路复用
- 1. `select` 系统调用
- 函数原型
- 参数说明
- 辅助宏
- 示例代码
- 2. `poll` 系统调用
- 函数原型
- 参数说明
- `struct pollfd` 结构体
- 常用事件类型
- 示例代码
- 3. 对比与选择建议
- 使用建议
- 4. 内核与用户空间的交互
- 驱动开发者需实现
- 总结
- 添加异步通知
- 一、整体架构设计
- 1. 多设备支持体系
- 2. 关键数据结构关系
- 二、核心机制实现
- 1. 阻塞 I/O 实现
- 2. poll/select 接口实现
- 3. 异步通知机制
- 三、关键代码分析
- 1. 初始化流程
- 2. 读写交互流程
- 3. 资源清理
- 四、潜在问题与优化建议
- 1. 并发安全问题
- 2. 内存泄漏风险
- 3. 性能优化建议
- 五、总结
在之前的实验中,我们分别把虚拟设备改造成了支持非阻塞模式和阻塞模式的操作,非阻塞模式和阻塞模式各有各的特点,但是在下面的场景里,它们就不能满足要求了。一个用户进程要监控多个 I/O 设备,它在访问一个 I/O 设备并进入睡眠状态之后,就不能做其他的操作了。例如,一个用户进程既要监控鼠标事件,又要监控键盘事件和读取摄像头数据,那么之前介绍的方法就无能为力了。
如果采用多线程或者多进程的方式,这种方法当然可行,缺点是在大量 I/O 多路复用场景下需要创建大量的线程或进程,造成资源浪费和不必要的进程间通信。
Linux 内核的 I/O 多路复用
Linux 内核提供了 poll、select 及 epoll 这 3 种 I/O 多路复用机制。I/O 多路复用其实就是一个进程可以同时监视多个打开的文件描述符,一旦某个文件描述符就绪,就立即通知程序进行相应的读写操作。因此,它们经常用在那些需要使用多个输入或输出数据流而不会阻塞在其中一个数据流的应用中,如网络应用等。
poll 和 select 机制在 Linux 用户空间中的接口函数定义如下:
在 Linux 用户空间中,poll
和 select
是两种用于 I/O 多路复用 的系统调用接口,它们允许进程同时监控多个文件描述符(fd),并在其中任何一个或多个 fd 就绪(可读、可写或出现异常)时返回。以下是它们的接口函数定义及核心机制分析。
1. select
系统调用
select
是较早期的 I/O 多路复用机制,通过 位掩码(fd_set) 管理文件描述符集合。
函数原型
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数说明
参数 | 说明 |
---|---|
nfds | 监控的最大 fd 值 +1(即 max_fd + 1 ) |
readfds | 监控 可读事件 的 fd 集合(传入需监控的 fd,返回就绪的 fd) |
writefds | 监控 可写事件 的 fd 集合 |
exceptfds | 监控 异常事件 的 fd 集合(如带外数据) |
timeout | 超时时间(NULL 表示阻塞,0 表示非阻塞,正值为精确超时) |
辅助宏
void FD_ZERO(fd_set *set); // 清空 fd 集合
void FD_SET(int fd, fd_set *set); // 添加 fd 到集合
void FD_CLR(int fd, fd_set *set); // 从集合移除 fd
int FD_ISSET(int fd, fd_set *set); // 检查 fd 是否就绪
示例代码
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);struct timeval timeout = {.tv_sec = 5, .tv_usec = 0}; // 5秒超时int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(sockfd, &readfds)) {// sockfd 可读
}
2. poll
系统调用
poll
是 select
的改进版,使用 结构体数组 管理 fd,解决了 select
的以下问题:
- 文件描述符数量限制(
select
默认 1024,poll
无硬性限制)。 - 无需手动计算
nfds
。 - 更细粒度的事件类型(如
POLLRDHUP
对端关闭连接)。
函数原型
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明
参数 | 说明 |
---|---|
fds | 指向 struct pollfd 数组的指针,每个元素描述一个监控的 fd |
nfds | 数组长度(监控的 fd 数量) |
timeout | 超时时间(毫秒,-1 表示阻塞,0 表示非阻塞) |
struct pollfd
结构体
struct pollfd {int fd; // 文件描述符short events; // 监控的事件(如 POLLIN、POLLOUT)short revents; // 返回的就绪事件
};
常用事件类型
事件 | 说明 |
---|---|
POLLIN | 数据可读 |
POLLOUT | 数据可写 |
POLLERR | 错误发生 |
POLLHUP | 连接挂起 |
POLLRDHUP | 对端关闭连接(Linux 特有) |
示例代码
struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN | POLLRDHUP; // 监控可读和对端关闭int ret = poll(fds, 1, 5000); // 5秒超时
if (ret > 0) {if (fds[0].revents & POLLIN) {// sockfd 可读}if (fds[0].revents & POLLRDHUP) {// 对端关闭连接}
}
3. 对比与选择建议
特性 | select | poll |
---|---|---|
fd 数量限制 | 默认 1024(可调整) | 无硬性限制 |
事件类型 | 仅读/写/异常 | 支持更多事件(如 POLLRDHUP ) |
性能(大量 fd) | 较差(线性扫描 fd_set) | 更优(数组结构) |
接口易用性 | 需手动管理 fd_set 和 nfds | 直接使用结构体数组 |
可移植性 | 广泛支持 | Linux 更推荐 |
使用建议
-
优先选择
poll
:- 需要监控大量 fd 时(如高性能服务器)。
- 需要检测对端关闭(
POLLRDHUP
)等高级事件。
-
兼容性场景用
select
:- 旧系统或需要跨平台兼容时。
-
现代替代方案:
- Linux 下更推荐使用
epoll
(适用于大规模并发连接)。
- Linux 下更推荐使用
4. 内核与用户空间的交互
当用户调用 poll/select
时,内核会:
- 注册监控:将当前进程添加到每个 fd 的等待队列(通过
file_operations.poll
)。 - 阻塞进程:如果没有 fd 就绪,进程进入睡眠状态。
- 唤醒机制:当任一 fd 就绪时(如数据到达),内核唤醒进程,并返回就绪的 fd 集合。
驱动开发者需实现
如果编写字符设备驱动,需要实现 file_operations.poll
:
unsigned int my_poll(struct file *filp, poll_table *wait) {struct mydev *dev = filp->private_data;unsigned int mask = 0;poll_wait(filp, &dev->read_queue, wait); // 注册读等待队列poll_wait(filp, &dev->write_queue, wait); // 注册写等待队列if (!kfifo_is_empty(&dev->buffer))mask |= POLLIN; // 标记可读if (!kfifo_is_full(&dev->buffer))mask |= POLLOUT; // 标记可写return mask;
}
当用户程序打开设备文件后执行 poll 或 select 系统调用时,设备驱动的 poll() 方法就会被调用。设备驱动的 poll() 方法会执行如下步骤:
- 在一个或多个等待队列中调用 poll_wait() 函数。poll_wait() 函数会把当前进程添加到指定的等待列表(poll_table)中,当请求数据准备好之后,会唤醒这些睡眠进程。
- 返回监听事件,也就是 POLLIN 或 POLLOUT 等掩码。
因此,poll() 方法的作用就是让应用程序同时等待多个数据流。
总结
select
和poll
是用户空间监控多 fd 的核心接口。poll
在功能和性能上优于select
,是 Linux 下的推荐选择。- 驱动开发者需正确实现
poll
方法以支持这些机制。
设备驱动程序
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kfifo.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>#define DEMO_NAME "my_demo_dev"
#define MYDEMO_FIFO_SIZE 64static dev_t dev; // primary device code
static struct cdev *demo_cdev;#define MYDEMO_MAX_DEVICES 8static struct mydemo_device{char name[64];struct device *dev;wait_queue_head_t read_queue;wait_queue_head_t write_queue;struct kfifo device_buffer;
} *demo_device[MYDEMO_MAX_DEVICES];struct mydemo_device_private_data{char name[64];struct mydemo_device *device;
};// KFIFO buffer
// static char *device_buffer;
// #define MAX_DEVICE_BUFFER_SIZE 32
// DEFINE_KFIFO(device_buffer, char, MAX_DEVICE_BUFFER_SIZE);static int demodrv_open(struct inode *inode, struct file *file)
{unsigned int minor = iminor(inode);struct mydemo_device_private_data *data;struct mydemo_device *device = demo_device[minor];printk("%s: major=%d, minor=%d, device=%s\n", __func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);data = kmalloc(sizeof(struct mydemo_device_private_data), GFP_KERNEL);if(!data)return -ENOMEM;data->device = device;file->private_data = data;return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{struct mydemo_device_private_data *data = file->private_data;kfree(data);return 0;
}static ssize_t demodrv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{int actual_readed;int ret;struct mydemo_device_private_data *data = file->private_data;struct mydemo_device *device = data->device;if(kfifo_is_empty(&device->device_buffer)) // kfifo_is_empty的主要功能是检测环形缓冲区是否没有任何可读数据。当返回值为真时,表示缓冲区中没有数据可供读取{if(file->f_flags & O_NONBLOCK)return -EAGAIN;printk("%s:%s pid=%d, going to sleep, %s\n", __func__, device->name, current->pid, data->name);ret = wait_event_interruptible(device->read_queue, !kfifo_is_empty(&device->device_buffer));if(ret)return ret;}ret = kfifo_to_user(&device->device_buffer, buf, count, &actual_readed);if(ret)return -EIO;if(!kfifo_is_full(&device->device_buffer)) // kfifo_is_full的核心功能是检测内核环形缓冲区是否没有剩余空间,即判断是否所有缓冲区空间都已被使用。当返回值为真时,表示无法再向缓冲区写入新数据,除非先读取部分数据腾出空间。wake_up_interruptible(&device->write_queue); // no sleeping write process.printk("%s:%s, pid=%d, actual_readed=%d, pos=%lld\n", __func__, device->name, current->pid, actual_readed, *ppos);return actual_readed;
}static ssize_t demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{unsigned int actual_write;int ret;struct mydemo_device_private_data *data = file->private_data;struct mydemo_device *device = data->device;if(kfifo_is_full(&device->device_buffer)){if(file->f_flags & O_NONBLOCK)return -EAGAIN;printk("%s:%s pid=%d, going to sleep\n", __func__, device->name, current->pid);ret = wait_event_interruptible(device->write_queue, !kfifo_is_full(&device->device_buffer));if(ret)return ret;}ret = kfifo_from_user(&device->device_buffer, buf, count, &actual_write);if(ret)return -EIO;if(!kfifo_is_empty(&device->device_buffer))wake_up_interruptible(&device->read_queue);printk("%s:%s pid=%d, actual_write=%d, ppos=%lld\n", __func__, device->name, current->pid, actual_write, *ppos);return actual_write;
}static unsigned int demodrv_poll(struct file *file, poll_table *wait)
{int mask = 0;struct mydemo_device_private_data *data = file->private_data;struct mydemo_device *device = data->device;poll_wait(file, &device->read_queue, wait);poll_wait(file, &device->write_queue, wait);if (!kfifo_is_empty(&device->device_buffer))mask |= POLLIN | POLLRDNORM;if (!kfifo_is_full(&device->device_buffer))mask |= POLLOUT | POLLWRNORM;return mask;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write,.poll = demodrv_poll,
};static int __init simple_char_init(void)
{int ret;struct mydemo_device *device;// device_buffer = kmalloc(MAX_DEVICE_BUFFER_SIZE, GFP_KERNEL);// if(!device_buffer)// return -ENOMEM;ret = alloc_chrdev_region(&dev, 0, MYDEMO_MAX_DEVICES, DEMO_NAME);if(ret){printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if (!demo_cdev) {printk("cdev_alloc failed\n");goto unregister_chrdev;} cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, MYDEMO_MAX_DEVICES);if(ret){printk("cdev_add failed\n");goto cdev_fail;}for(int i = 0; i < MYDEMO_MAX_DEVICES; i++){device = kmalloc(sizeof(struct mydemo_device), GFP_KERNEL);if(!device){ret = -ENOMEM;goto free_device;}sprintf(device->name, "%s%d", DEMO_NAME, i);demo_device[i] = device;init_waitqueue_head(&device->read_queue);init_waitqueue_head(&device->write_queue);ret = kfifo_alloc(&device->device_buffer, MYDEMO_FIFO_SIZE, GFP_KERNEL);if(ret){ret = -ENOMEM;goto free_kfifo;}printk("device_buffer=%px\n", &device->device_buffer);}printk("succeeded register char device: %s\n", DEMO_NAME);return 0;
free_kfifo:for(int i = 0; i < MYDEMO_MAX_DEVICES; i++)if(&device->device_buffer)kfifo_free(&device->device_buffer);
free_device:for(int i = 0; i < MYDEMO_MAX_DEVICES; i++)if(demo_device[i])kfree(demo_device[i]);
cdev_fail:cdev_del(demo_cdev);unregister_chrdev:unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);return ret;
}static void __exit simple_char_exit(void)
{int i;printk("removing device\n");if (demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);for (i =0; i < MYDEMO_MAX_DEVICES; i++)if (demo_device[i])kfree(demo_device[i]);
}module_init(simple_char_init);
module_exit(simple_char_exit);MODULE_AUTHOR("JJM");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simple character device");
MYDEMO_MAX_DEVICES 表示设备驱动最多支持 8 个设备,首先,在模块加载函数 simple_char_init() 里使用 alloc_chrdev_region() 函数去申请 8 个次设备号。然后,通过 cdev_add() 函数把这 8 个次设备都注册到系统里。最后,为每一个设备都分配 mydemo_device 数据结构,并且初始化其中的等待队列头和 KFIFO 环形缓冲区。
在 open() 函数中,首先会通过次设备号找到对应的 mydemo_device 数据结构,然后分配私有的 mydemo_private_data 数据结构,最后把私有数据的地址存放在 file->private_data 指针里。
用户程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <linux/input.h>
#include <unistd.h>int main(int argc, char *argv[])
{int ret;struct pollfd fds[2];char buffer0[64];char buffer1[64];fds[0].fd = open("/dev/mydemo0", O_RDWR);if (fds[0].fd == -1) goto fail;fds[0].events = POLLIN;fds[1].fd = open("/dev/mydemo1", O_RDWR);if (fds[1].fd == -1) goto fail;fds[1].events = POLLIN;while (1) {ret = poll(fds, 2, -1);if (ret == -1)goto fail;if (fds[0].revents & POLLIN) {ret = read(fds[0].fd, buffer0, sizeof(buffer0));if (ret < 0)goto fail;printf("%s\n", buffer0);}if (fds[1].revents & POLLIN) {ret = read(fds[1].fd, buffer1, sizeof(buffer1));if (ret < 0)goto fail;printf("%s\n", buffer1);}}fail:perror("poll test failed");exit(EXIT_FAILURE);
}
运行过程
root@q:/home/q/Documents/6# insmod mydemo.ko
root@q:/home/q/Documents/6# cat /proc/devices
Character devices:
...
240 my_demo_dev
...
root@q:/home/q/Documents/6# mknod /dev/mydemo0 c 240 0
root@q:/home/q/Documents/6# mknod /dev/mydemo1 c 240 1
root@q:/home/q/Documents/6# gcc-12 test.c -o test
root@q:/home/q/Documents/6# ./test &
[1] 4572
root@q:/home/q/Documents/6# dmesg | tail
[ 909.597334] device_buffer=ffff9e6d836b5d38
[ 909.597335] device_buffer=ffff9e6d836b5138
[ 909.597336] device_buffer=ffff9e6d836b58b8
[ 909.597337] device_buffer=ffff9e6d836b5378
[ 909.597337] device_buffer=ffff9e6d836b54f8
[ 909.597338] device_buffer=ffff9e6d836b5438
[ 909.597339] device_buffer=ffff9e6d836b5a38
[ 909.597340] succeeded register char device: my_demo_dev
[ 1020.301333] demodrv_open: major=240, minor=0, device=my_demo_dev0
[ 1020.301347] demodrv_open: major=240, minor=1, device=my_demo_dev1
root@q:/home/q/Documents/6# echo "i am a linuxer" > /dev/mydemo0
i am a linuxerroot@q:/home/q/Documents/6# dmesg | tail
[ 909.597337] device_buffer=ffff9e6d836b5378
[ 909.597337] device_buffer=ffff9e6d836b54f8
[ 909.597338] device_buffer=ffff9e6d836b5438
[ 909.597339] device_buffer=ffff9e6d836b5a38
[ 909.597340] succeeded register char device: my_demo_dev
[ 1020.301333] demodrv_open: major=240, minor=0, device=my_demo_dev0
[ 1020.301347] demodrv_open: major=240, minor=1, device=my_demo_dev1
[ 1063.004170] demodrv_open: major=240, minor=0, device=my_demo_dev0
[ 1063.004229] demodrv_write:my_demo_dev0 pid=2831, actual_write=15, ppos=0
[ 1063.004279] demodrv_read:my_demo_dev0, pid=4572, actual_readed=15, pos=0
root@q:/home/q/Documents/6# echo "hello, device 1" > /dev/mydemo1
hello, device 1root@q:/home/q/Documents/6# dmesg | tail
[ 909.597339] device_buffer=ffff9e6d836b5a38
[ 909.597340] succeeded register char device: my_demo_dev
[ 1020.301333] demodrv_open: major=240, minor=0, device=my_demo_dev0
[ 1020.301347] demodrv_open: major=240, minor=1, device=my_demo_dev1
[ 1063.004170] demodrv_open: major=240, minor=0, device=my_demo_dev0
[ 1063.004229] demodrv_write:my_demo_dev0 pid=2831, actual_write=15, ppos=0
[ 1063.004279] demodrv_read:my_demo_dev0, pid=4572, actual_readed=15, pos=0
[ 1099.402704] demodrv_open: major=240, minor=1, device=my_demo_dev1
[ 1099.402731] demodrv_write:my_demo_dev1 pid=2831, actual_write=16, ppos=0
[ 1099.403031] demodrv_read:my_demo_dev1, pid=4572, actual_readed=16, pos=0
添加异步通知
异步通知类似于中断,当请求的设备资源可以获取时,由驱动主动通知应用程序,再由应用程序调用 read() 或 write() 方法来发起 I/O 操作。异步通知不像是阻塞操作,它不会造成堵塞,仅在设备驱动满足条件之后才通过信号机制通知应用程序发起 I/O 操作。
异步通知使用了系统调用的 signal() 函数和 sigcation() 函数。signal() 函数会让一个信号和一个函数对应,每当接收到这个信号时就会调用相应的函数来处理。
设备驱动程序
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/kfifo.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/poll.h>#define DEMO_NAME "mydemo_dev"
#define MYDEMO_FIFO_SIZE 64static dev_t dev;
static struct cdev *demo_cdev;struct mydemo_device {char name[64];struct device *dev;wait_queue_head_t read_queue;wait_queue_head_t write_queue; struct kfifo mydemo_fifo;struct fasync_struct *fasync;
};struct mydemo_private_data {struct mydemo_device *device;char name[64];
};#define MYDEMO_MAX_DEVICES 8
static struct mydemo_device *mydemo_device[MYDEMO_MAX_DEVICES]; static int demodrv_open(struct inode *inode, struct file *file)
{unsigned int minor = iminor(inode);struct mydemo_private_data *data;struct mydemo_device *device = mydemo_device[minor];printk("%s: major=%d, minor=%d, device=%s\n", __func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);if (!data)return -ENOMEM;sprintf(data->name, "private_data_%d", minor);data->device = device;file->private_data = data;return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{struct mydemo_private_data *data = file->private_data;kfree(data);return 0;
}static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{struct mydemo_private_data *data = file->private_data;struct mydemo_device *device = data->device;int actual_readed;int ret;if (kfifo_is_empty(&device->mydemo_fifo)) {if (file->f_flags & O_NONBLOCK)return -EAGAIN;printk("%s:%s pid=%d, going to sleep, %s\n", __func__, device->name, current->pid, data->name);ret = wait_event_interruptible(device->read_queue,!kfifo_is_empty(&device->mydemo_fifo));if (ret)return ret;}ret = kfifo_to_user(&device->mydemo_fifo, buf, count, &actual_readed);if (ret)return -EIO;if (!kfifo_is_full(&device->mydemo_fifo)){wake_up_interruptible(&device->write_queue);kill_fasync(&device->fasync, SIGIO, POLL_OUT);}printk("%s:%s, pid=%d, actual_readed=%d, pos=%lld\n",__func__,device->name, current->pid, actual_readed, *ppos);return actual_readed;
}static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{struct mydemo_private_data *data = file->private_data;struct mydemo_device *device = data->device;unsigned int actual_write;int ret;if (kfifo_is_full(&device->mydemo_fifo)){if (file->f_flags & O_NONBLOCK)return -EAGAIN;printk("%s:%s pid=%d, going to sleep\n", __func__, device->name, current->pid);ret = wait_event_interruptible(device->write_queue,!kfifo_is_full(&device->mydemo_fifo));if (ret)return ret;}ret = kfifo_from_user(&device->mydemo_fifo, buf, count, &actual_write);if (ret)return -EIO;if (!kfifo_is_empty(&device->mydemo_fifo)) {wake_up_interruptible(&device->read_queue);kill_fasync(&device->fasync, SIGIO, POLL_IN);printk("%s kill fasync\n", __func__);}printk("%s:%s pid=%d, actual_write =%d, ppos=%lld, ret=%d\n", __func__,device->name, current->pid, actual_write, *ppos, ret);return actual_write;
}static unsigned int demodrv_poll(struct file *file, poll_table *wait)
{int mask = 0;struct mydemo_private_data *data = file->private_data;struct mydemo_device *device = data->device;poll_wait(file, &device->read_queue, wait);poll_wait(file, &device->write_queue, wait);if (!kfifo_is_empty(&device->mydemo_fifo))mask |= POLLIN | POLLRDNORM;if (!kfifo_is_full(&device->mydemo_fifo))mask |= POLLOUT | POLLWRNORM;return mask;
}static int demodrv_fasync(int fd, struct file *file, int on)
{struct mydemo_private_data *data = file->private_data;struct mydemo_device *device = data->device;printk("%s send SIGIO\n", __func__);return fasync_helper(fd, file, on, &device->fasync);
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write,.poll = demodrv_poll,.fasync = demodrv_fasync,
};static int __init simple_char_init(void)
{int ret;int i;struct mydemo_device *device;ret = alloc_chrdev_region(&dev, 0, MYDEMO_MAX_DEVICES, DEMO_NAME);if (ret) {printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if (!demo_cdev) {printk("cdev_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, MYDEMO_MAX_DEVICES);if (ret) {printk("cdev_add failed\n");goto cdev_fail;}for (i = 0; i < MYDEMO_MAX_DEVICES; i++) {device = kmalloc(sizeof(struct mydemo_device), GFP_KERNEL);if (!device) {ret = -ENOMEM;goto free_device;}sprintf(device->name, "%s%d", DEMO_NAME, i);mydemo_device[i] = device;init_waitqueue_head(&device->read_queue);init_waitqueue_head(&device->write_queue);ret = kfifo_alloc(&device->mydemo_fifo,MYDEMO_FIFO_SIZE,GFP_KERNEL);if (ret) {ret = -ENOMEM;goto free_kfifo;}printk("mydemo_fifo=%p\n", &device->mydemo_fifo);}printk("succeeded register char device: %s\n", DEMO_NAME);return 0;free_kfifo:for (i =0; i < MYDEMO_MAX_DEVICES; i++)if (&device->mydemo_fifo)kfifo_free(&device->mydemo_fifo);
free_device:for (i =0; i < MYDEMO_MAX_DEVICES; i++)if (mydemo_device[i])kfree(mydemo_device[i]);
cdev_fail:cdev_del(demo_cdev);
unregister_chrdev:unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);return ret;
}static void __exit simple_char_exit(void)
{int i;printk("removing device\n");if (demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);for (i =0; i < MYDEMO_MAX_DEVICES; i++)if (mydemo_device[i])kfree(mydemo_device[i]);
}module_init(simple_char_init);
module_exit(simple_char_exit);MODULE_AUTHOR("Benshushu");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
这个内核模块实现了一个支持多设备实例的字符设备驱动,集成了环形缓冲区(kfifo)、阻塞/非阻塞 I/O、poll/select 接口以及异步通知(fasync)机制。下面我将从架构设计、核心机制和优化建议三个维度进行全面分析。
一、整体架构设计
1. 多设备支持体系
模块通过静态数组 mydemo_device[MYDEMO_MAX_DEVICES]
管理多个设备实例,每个设备包含:
- 独立的等待队列(read/write)
- 专用的 kfifo 缓冲区
- 异步通知结构(fasync)
- 设备标识信息
struct mydemo_device {char name[64]; // 设备名称wait_queue_head_t read_queue; // 读等待队列 wait_queue_head_t write_queue; // 写等待队列struct kfifo mydemo_fifo; // 64字节环形缓冲区struct fasync_struct *fasync; // 异步通知结构
};
2. 关键数据结构关系
二、核心机制实现
1. 阻塞 I/O 实现
通过等待队列和 kfifo 状态判断实现经典的生产者-消费者模型:
读阻塞逻辑:
if (kfifo_is_empty(&device->mydemo_fifo)) {if (file->f_flags & O_NONBLOCK) return -EAGAIN;wait_event_interruptible(device->read_queue, !kfifo_is_empty(...));
}
写阻塞逻辑:
if (kfifo_is_full(&device->mydemo_fifo)) {if (file->f_flags & O_NONBLOCK) return -EAGAIN; wait_event_interruptible(device->write_queue,!kfifo_is_full(...));
}
2. poll/select 接口实现
demodrv_poll
方法通过 poll_wait
注册等待队列并返回就绪状态:
poll_wait(file, &device->read_queue, wait);
poll_wait(file, &device->write_queue, wait);if (!kfifo_is_empty(...)) mask |= POLLIN;
if (!kfifo_is_full(...)) mask |= POLLOUT;
3. 异步通知机制
通过 fasync_helper
实现 SIGIO 信号通知:
// 写操作触发读通知
if (!kfifo_is_empty(...)) {kill_fasync(&device->fasync, SIGIO, POLL_IN);
}// fasync回调设置
.fasync = demodrv_fasync,
三、关键代码分析
1. 初始化流程
simple_char_init()
├─ alloc_chrdev_region() // 动态分配设备号
├─ cdev_alloc() // 分配cdev结构
├─ cdev_add() // 注册字符设备
└─ 初始化多个设备实例├─ kfifo_alloc() // 为每个设备分配缓冲区└─ init_waitqueue_head() // 初始化等待队列
2. 读写交互流程
3. 资源清理
模块退出时需按顺序释放:
- 删除 cdev
- 释放设备号区域
- 释放每个设备的 kfifo
- 释放设备结构内存
四、潜在问题与优化建议
1. 并发安全问题
- 问题:未使用自旋锁保护 kfifo 操作
- 建议:
static DEFINE_SPINLOCK(fifo_lock);spin_lock(&fifo_lock); kfifo_to_user(...); spin_unlock(&fifo_lock);
2. 内存泄漏风险
- 问题:
simple_char_exit
中未释放 kfifo - 修复:
for (i = 0; i < MYDEMO_MAX_DEVICES; i++)if (mydemo_device[i]) kfifo_free(&mydemo_device[i]->mydemo_fifo);
3. 性能优化建议
- 批量传输:支持
ioctl
实现大块数据传输 - 内存池:预分配多个 kfifo 缓冲区
- 无锁设计:考虑使用原子操作优化计数器
五、总结
该驱动模块展示了 Linux 字符设备开发的多个高级特性:
- 完善的多设备实例管理
- 阻塞/非阻塞 I/O 的统一处理
- poll/select 事件通知机制
- 异步信号驱动 I/O
- 环形缓冲区的生产者-消费者模型
通过添加适当的并发保护和完善错误处理,这个驱动可以进一步发展为生产级代码。其设计模式也适用于大多数需要高效数据缓冲的设备驱动场景。
用户程序
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <signal.h>static int fd;void my_signal_fun(int signum, siginfo_t *siginfo, void *act)
{int ret;char buf[64];if (signum == SIGIO) {if (siginfo->si_band & POLLIN) {printf("FIFO is not empty\n");if ((ret = read(fd, buf, sizeof(buf))) != -1) {buf[ret] = '\0';puts(buf);}}if (siginfo->si_band & POLLOUT)printf("FIFO is not full\n");}
}int main(int argc, char *argv[])
{int ret;int flag;struct sigaction act, oldact;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, SIGIO);act.sa_flags = SA_SIGINFO;act.sa_sigaction = my_signal_fun;if (sigaction(SIGIO, &act, &oldact) == -1)goto fail;fd = open("/dev/mydemo0", O_RDWR);if (fd < 0) goto fail;/*设置异步IO所有权*/if (fcntl(fd, F_SETOWN, getpid()) == -1)goto fail;/*将当前进程PID设置为fd文件所对应驱动程序将要发送SIGIO,SIGUSR信号进程PID*/if (fcntl(fd, F_SETSIG, SIGIO) == -1)goto fail;/*获取文件flags*/if ((flag = fcntl(fd, F_GETFL)) == -1)goto fail;/*设置文件flags, 设置FASYNC,支持异步通知*/if (fcntl(fd, F_SETFL, flag | FASYNC) == -1)goto fail;while (1)sleep(1);fail:perror("fasync test");exit(EXIT_FAILURE);
}
上述代码首先通过 sigaction() 函数设置进程接收指定的信号以及接收信号之后的动作,这里指定接收 SIGIO 信号,信号处理函数是 my_signal_fun()。接下来,打开设备驱动文件,并使用 fcntl() 函数让设备驱动文件支持 FASYNC 功能。当测试程序接收到 SIGIO 信号之后,会执行 my_signal_fun() 函数并判断事件类型是否为 POLLIN。如果事件类型是 POLLIN,那么可以主动调用 read() 函数并把数据读出来。