select, poll 和 epoll
Unix I/O 模型
select, poll 和 epoll 都是 I/O多路复用技术的一种,I/O多路复用是 Unix I/O模型中的一种。下面简述下 unix 五种 I/O模型(以调用 recvfrom 函数读取数据为例):
(1)阻塞式(blocking I/O):应用程序调用 recvfrom 函数后,发起系统调用,一直阻塞等待数据准备好及复制到用户空间才返回。
(2)非阻塞式(non-blocking I/O):应用程序调用 recvfrom 函数后,发起系统调用,如数据未准备后不会阻塞,而是返回 EWOULDBLOCK 错误码;不断轮询直到数据准备好后,复制数据到用户空间再返回。
(3)多路复用式(synchronous I/O multiplexing):应用程序首先调用 select、poll 或 epoll_wait 函数,发起系统调用,阻塞监听多个socket;当某个 socket 可读时返回可读条件,随后再调用 recvfrom 函数,复制数据到用户空间再返回。
(4)信号驱动式:应用程序首先建立信号处理程序,发起 sigcation系统调用;当数据准备好后,内核会发送 SIGIO 信号到信号处理程序,接着应用程序调用 recvfrom 函数,发起系统调用,复制数据到用户空间后返回。
(5)异步式(asynchronous I/O):应用程序调用 recvfrom 函数后,会调用 aio_read 函数(使用 glibc 情况下),发起系统调用并返回。当数据准备好并复制到用户空间时,内核会递交信号到指定的信号处理程序进行处理。
Linux 中的异步I/O 实现有:
- glibc 中的 aio,又称之为 asynchronous POSIX I/O,Linux 2.5 及以上支持,使用用户态多线程模拟异步,问题比较多,已经被抛弃;
- libaio,真正的异步I/O,Linux原生aio,Linux内核aio,Linux 2.6.22 及以上支持,只支持 Direct I/O 进行磁盘读写,无法利用页缓存;
- libeio,自称为 truly asynchronous POSIX I/O,使用多线程模拟异步,实现比glibc 较高效,且支持页缓存;
- io_uring,Linux 5.1 及以上支持,并有 liburing 库可使用。
Java 中的 AIO 在 Windows 平台上使用 I/O Completion Ports(IOCP),在 Linux 上使用多线程模拟实现。
select
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
select 监听多个 fd_set,包括:
- readfds,可读的文件描述符集合;
- writefds,可写的文件描述符集合;
- exceptfds,异常的文件描述符集合。
API:
#include <sys/select.h>
// 监控文件描述符集合,阻塞等待 I/O 就绪
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict exceptfds,
struct timeval *restrict timeout);
// 从 set 中清除一个 fd
void FD_CLR(int fd, fd_set *set);
// 检查一个 fd 是否在 set 中
int FD_ISSET(int fd, fd_set *set);
// 添加一个 fd 到 set 中
void FD_SET(int fd, fd_set *set);
// 清空 set 中的所有 fd
void FD_ZERO(fd_set *set);
int pselect(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict exceptfds,
const struct timespec *restrict timeout,
const sigset_t *restrict sigmask);
特点:
- 最多能监听 FD_SETSIZE(32位系统下为1024,64位系统下为2048)个文件描述符;
- 文件描述符集合(fd_set)会在用户空间和内核空间之间拷贝;
- 线性遍历 fd,寻找 ready 的 fd。
poll
poll, ppoll - wait for some event on a file descriptor
poll,Linux 2.1.23 引入,跟 select 类似都是等待 fd 集合中的一个 fd 就绪;不同的是,poll 采用一个 pollfd array 来作为 fd 集合,没有 FD_SETSIZE 限制,但受限于 RLIMIT_NOFILE(resource limit number opened files,一个进程能打开的最大文件数)。pollfd 结构如下:
struct pollfd {
// 文件描述符
int fd;
// 请求的事件,作为输入参数,以位掩码方式
short events;
// 返回的事件,作为输出参数,以位掩码方式
short revents;
};
API:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
特点:
- 没有最大文件描述符限制;
- 仍然有内核空间和用户空间之间的 fd 集合拷贝;
- 仍然是线性扫描 fd 集合。
epoll
epoll - I/O event notification facility
epoll,Linux 2.5.45 引入,其中一个关键的概念就是 epoll instance,其位于内核中,从用户空间的视角可以认为其由两个 list 构成:
- interest list(也称之为 epoll set),保存注册的待监控的文件描述符集合,采用红黑树管理(red-black tree),添加和删除 fd 的复杂度都是 O(log(n));
- ready list,就绪的文件描述符集合,是 interest list 的子集,采用双向链表管理,并使用 mmap技术将其映射到进程的地址空间,减少不必要的拷贝。
#include <sys/epoll.h>
// 创建一个 epoll 实例并返回一个文件描述符指向该实例
int epoll_create(int size);
int epoll_create1(int flags);
// 注册、删除和修改 interest list 中的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待 I/O事件(读取 ready list 中的文件描述符),如无会阻塞调用线程
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
int epoll_pwait2(int epfd, struct epoll_event *events,
int maxevents, const struct timespec *timeout,
const sigset_t *sigmask);
epoll 支持两种 I/O 事件触发方式:
- 水平触发(level-triggered,LT),默认方式,监控的文件描述符处于可读或可写的状态时,一直报告可读或可写信号,直到变为不可读或不可写状态。
- 边缘触发(edge-triggered,ET),对应 flag 为 EPOLLET,监控的文件描述符状态改变时仅报告一次信号。例如:从不可读状态转变成可读状态时报告一次可读信号。处理边缘触发的事件要求使用非阻塞的文件描述符,轮询 read 或 write,直到 read 或 write 返回 EAGAIN 再退出。nginx、netty 使用边缘触发方式。
// 水平触发
ret = read(fd, buf, sizeof(buf));
// 边缘触发
while(true) {
ret = read(fd, buf, sizeof(buf);
if (ret == EAGAIN) break;
}
参考
- 《Unix网络编程》第一卷
- 五种IO模型介绍和对比
- Linux异步IO实现方案总结