select, poll, 和 epoll 都是用于处理多路输入输出(IO)的系统调用,它们允许程序监视多个文件描述符(FDs),等待一个或多个FDs成为非阻塞的,即准备好进行IO操作(如读或写)。这些机制在网络编程中尤其有用,用于实现高效的并发服务器。

以下是对每个系统调用的简要说明:

select

select 系统调用允许程序监视一组文件描述符,以等待其中一个或多个FDs准备好执行IO操作,通过轮询的方式捕获io事件 它有几个限制,包括FD集合大小的限制(通常是1024)。 它使用固定大小的文件描述符集合,导致随着监控的FD数量增加,其效率降低。

1
2
3
4
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
select(sockfd + 1, &readfds, NULL, NULL, NULL);

poll

poll 提供了与 select 类似的功能,但是没有文件描述符数量的限制。通过轮询的方式捕获io事件 poll 使用 pollfd 结构数组来跟踪每个文件描述符。 相比 select,poll 的扩展性更好,但是随着FD数量增加,性能仍然会下降。

1
2
3
4
struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
poll(fds, 1, -1);

epoll

epoll 是Linux特有的,它解决了 select 和 poll 在扩展性上的问题,通过操作系统的事件通知机制,回调函数的方式来实现异步的I/O操作. epoll 可以处理大量的FDs,因为它工作方式不同,只关心活跃的FDs。 使用 epoll_create 创建一个epoll实例,然后通过 epoll_ctl 添加、修改或删除文件描述符,最后使用 epoll_wait 等待事件。

1
2
3
4
5
6
int epfd = epoll_create1(0);
struct epoll_event event, events[MAX_EVENTS];
event.data.fd = sockfd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
epoll_wait(epfd, events, MAX_EVENTS, -1);

epoll的事件触发方式

epoll 提供了两种触发模式:边缘触发(Edge-Triggered)水平触发(Level-Triggered)

  1. 边缘触发(Edge-Triggered)模式:

当文件描述符上的状态发生变化时,epoll 会通知应用程序,但只通知一次,即只在状态从未就绪变为就绪时通知。 如果应用程序没有处理完所有就绪事件,下次调用 epoll_wait 时只会返回新的就绪事件,不会返回之前已经就绪但没有处理的事件。 边缘触发模式适用于需要及时处理所有就绪事件的场景,可以减少事件通知的次数。

  1. 水平触发(Level-Triggered)模式:

当文件描述符上的状态处于就绪状态时,epoll 会通知应用程序,并在该状态保持就绪时不断通知。 即使应用程序没有处理就绪事件,下次调用 epoll_wait 时仍然会返回之前已经就绪但没有处理的事件。 水平触发模式适用于需要持续监控文件描述符状态变化的场景,可以方便地实现非阻塞 I/O 操作。

总结

selectpoll 更为通用,但随着监视的FD数量增加,效率降低。 epoll 只在Linux上可用,但提供了更好的性能,尤其是在处理大量FDs时。它也支持一些额外的特性,如边缘触发(ET)和水平触发(LT)模式。