在支持轮询操作的 globalfifo 驱动中,核心在于实现 struct file_operations 中的 .poll 方法。
首先,需要在设备数据结构中定义等待队列头,用于管理等待可读或可写事件的进程。
#define GLOBALFIFO_SIZE 4096
struct globalfifo_dev {
// ...
unsigned int current_len; // 当前数据长度
struct mutex lock; // 并发控制锁
wait_queue_head_t read_wait; // 可读等待队列
wait_queue_head_t write_wait; // 可写等待队列
};
globalfifo_poll 函数的实现需要完成两个核心任务:注册等待队列和返回设备当前状态掩码。
static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
struct globalfifo_dev *dev = filp->private_data;
unsigned int mask = 0;
mutex_lock(&dev->lock);
// 1. 注册等待:将当前进程注册到 read/write 等待队列上
poll_wait(filp, &dev->read_wait, wait);
poll_wait(filp, &dev->write_wait, wait);
// 2. 检查设备状态并设置掩码
if (dev->current_len > 0) {
// FIFO中有数据,可读
mask |= POLLIN | POLLRDNORM;
}
if (dev->current_len < GLOBALFIFO_SIZE) {
// FIFO有空间,可写
mask |= POLLOUT | POLLWRNORM;
}
mutex_unlock(&dev->lock);
return mask;
}
接下来,需要将实现的 poll 方法集成到驱动的文件操作表中。
static const struct file_operations globalfifo_fops = {
// ...
.read = globalfifo_read,
.write = globalfifo_write,
.poll = globalfifo_poll, // 注册 poll 方法
// ...
};
最后,在用户空间可以通过 poll() 系统调用来验证驱动是否工作正常。以下是一个示例程序,它监控 /dev/globalfifo 设备的可读事件。当设备 FIFO 为空时,poll() 会阻塞;当在另一个终端向设备写入数据后,等待的进程会被唤醒,poll() 返回并指示设备可读。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <poll.h> // 轮询相关的头文件
#include <errno.h>
#define FIFO_DEVICE "/dev/globalfifo"
#define BUF_SIZE 1024
#define TIMEOUT_MS 5000 // 5秒超时
int main(void)
{
int fd;
int ret;
struct pollfd fds[1];
char buf[BUF_SIZE];
// 1. 打开设备
fd = open(FIFO_DEVICE, O_RDWR | O_NONBLOCK); // 建议使用非阻塞模式配合 poll
if (fd < 0) {
perror(“Failed to open FIFO_DEVICE”);
return 1;
}
// 2. 设置 pollfd 结构体
fds[0].fd = fd;
fds[0].events = POLLIN; // 我们关注可读事件 (POLLIN)
printf(“Monitoring %s for data (Timeout: %dms).\n”, FIFO_DEVICE, TIMEOUT_MS);
printf(“Please open another terminal and write data to the device (e.g., ‘echo hello > %s’).\n”, FIFO_DEVICE);
// 3. 调用 poll 系统调用
ret = poll(fds, 1, TIMEOUT_MS);
if (ret < 0) {
perror(“Poll error”);
close(fd);
return 1;
} else if (ret == 0) {
// ret == 0 表示超时
printf(“Poll timeout (%dms) reached. No data arrived.\n”, TIMEOUT_MS);
} else {
// ret > 0 表示有文件描述符就绪
printf(“\nDevice state changed! Poll returned %d event(s).\n”, ret);
// 4. 检查具体事件
if (fds[0].revents & POLLIN) {
printf(“>>> %s is ready for reading (POLLIN event received).\n”, FIFO_DEVICE);
// 读取数据
ret = read(fd, buf, BUF_SIZE - 1);
if (ret > 0) {
buf[ret] = ‘\0’;
printf(“Read %d bytes: [%s]\n”, ret, buf);
} else if (ret == 0) {
printf(“End of file.\n”);
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf(“Read would block (Non-blocking mode).\n”);
} else {
perror(“Read error”);
}
}
}
close(fd);
return 0;
}
阻塞与非阻塞I/O核心对比
在 Linux系统编程与网络原理 中,阻塞访问与非阻塞访问是设备驱动与应用程序交互的两种基本模式,核心区别在于I/O条件不满足时的处理逻辑。
| 特性 |
阻塞访问 (Blocking I/O) |
非阻塞访问 (Non-blocking I/O) |
| 应用行为 |
等待。条件不满足时,进程暂停并进入休眠。 |
立即返回。条件不满足时,进程立即返回错误(-EAGAIN),不会休眠。 |
| 内核机制 |
基于等待队列,使用 wait_event_interruptible 休眠,wake_up_interruptible 唤醒。 |
基于轮询(.poll方法)和 O_NONBLOCK 标志。 |
| 打开标志 |
默认模式,无需特殊标志。 |
需在 open() 中设置 O_NONBLOCK 标志。 |
| 性能/效率 |
CPU效率高:进程休眠不占CPU,适用于I/O较慢、并发需求不高的场景。 |
灵活性高:进程可继续其他工作,但需要应用层主动轮询或使用多路复用机制。 |
| 典型用途 |
简单串行任务;必须等待数据就绪的场景。 |
I/O 多路复用(select/poll/epoll);高并发网络服务;需同时监控多个I/O流的场景,这也是解决 高并发场景下I/O效率 的关键技术之一。 |
实现要点总结:
- 阻塞访问:依赖 等待队列。驱动在
read/write 中调用 wait_event_interruptible() 使进程休眠,在条件满足时(如数据到达)调用 wake_up_interruptible() 唤醒进程。
- 非阻塞访问:依赖
O_NONBLOCK 标志 和 .poll 方法。驱动检查 filp->f_flags & O_NONBLOCK,条件不满足则返回 -EAGAIN。.poll 方法是支持用户空间 select/poll/epoll 多路复用的核心,实现“事件驱动”的轮询。
简而言之,阻塞访问让内核替你等待,而非阻塞访问则需要应用程序通过轮询机制主动判断I/O是否就绪。