轮询操作
在 I/O 编程领域,轮询是一种高效的 I/O 监控策略。它指程序不进入休眠,而是主动、周期性地查询一个或多个 I/O 资源(如文件描述符或设备)是否已准备好进行读写操作。
在经典的 Linux I/O模型 中,轮询操作是实现I/O多路复用 (I/O Multiplexing) 的核心,有效解决了传统阻塞I/O模式的瓶颈:
- 避免单点阻塞: 防止进程因等待单一I/O操作而被长期挂起,从而提升系统整体的并发处理能力与响应速度。
- 高效集中监控: 允许单个线程或进程同时监控大量的文件描述符(如网络套接字、设备文件),并在其中任何一个就绪时立即获得通知。
- 非阻塞I/O基石: 为用户空间的非阻塞 I/O 操作提供了就绪状态判定的基础。
用户空间应用中的轮询编程
在用户空间的应用程序中,“轮询”通常特指使用特定的系统调用来集中监控多个文件描述符的状态,这是实现I/O多路复用的主要手段。
| 系统调用 |
核心描述 |
select() |
最早的 I/O 复用机制。它使用固定大小的位图(fd_set)来传递描述符集合。主要缺陷是描述符数量有上限,且每次调用都需要在用户与内核空间之间复制整个集合,效率较低。 |
poll() |
对select()的改进。采用struct pollfd数组,解除了文件描述符数量的硬性上限,性能通常优于select。 |
epoll() |
Linux独有的高性能I/O复用机制。采用事件驱动模型,只需在初始化时注册描述符,后续调用仅返回已就绪的描述符列表,极大地提升了在高并发场景下的效率和扩展性。 |
无论使用select、poll还是epoll,应用程序的基本逻辑都是统一的:
- 将需要监控的文件描述符集合传递给系统调用。
- 程序“阻塞”在该系统调用上(这是一种可控、高效的阻塞)。
- 系统调用会等待,直到至少一个文件描述符就绪(可读、可写或出现异常)、达到指定的超时时间或被信号中断。
- 调用返回后,程序轮询检查就绪的描述符,并对它们执行相应的
read或write操作。
内核驱动中的轮询方法实现
当用户空间调用select/poll/epoll时,内核最终会调用到对应设备驱动程序的.poll方法。该方法的职责非常明确:
- 注册等待上下文:将发起调用的用户进程信息注册到驱动内部的等待队列中。
- 立即报告状态:返回设备当前的可读/可写等就绪状态。
一个完整的驱动poll方法实现包含两个核心步骤:注册等待与返回掩码。
第一步:注册等待 (poll_wait)
使用poll_wait函数将当前文件的等待队列注册到poll_table中。这并不会使进程休眠,只是建立一个“当事件发生时唤醒谁”的关联。
static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
struct xxx_dev *dev = filp->private_data;
// 注册等待:将当前进程添加到驱动的读/写等待队列
poll_wait(filp, &dev->read_wait, wait);
poll_wait(filp, &dev->write_wait, wait);
// ... 接下来检查并返回设备状态 ...
}
第二步:返回就绪掩码 (Bitmask)
根据设备当前的实际状态,返回一个位掩码,向内核报告设备是否可读、可写或存在异常。
static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
struct xxx_dev *dev = filp->private_data;
unsigned int mask = 0;
// 1. 注册等待队列
poll_wait(filp, &dev->read_wait, wait);
poll_wait(filp, &dev->write_wait, wait);
// 2. 检查并返回状态掩码
// 检查是否有数据可读
if (dev_has_data(dev)) {
mask |= POLLIN | POLLRDNORM; // 标示可读
}
// 检查是否有空间可写
if (dev_has_space(dev)) {
mask |= POLLOUT | POLLWRNORM; // 标示可写
}
return mask;
}
最终流程:
- 如果驱动返回的掩码
mask不为0(表示设备已就绪),用户空间的select/poll/epoll调用将立即返回,告知应用程序可以进行I/O操作。
- 如果掩码为0(表示设备未就绪),用户进程将在系统调用中休眠,直到驱动程序在未来的某个时刻(例如,数据到达或缓冲区空出)调用
wake_up_interruptible()等函数唤醒相关等待队列上的进程。
|