IO多路复用与高性能IO编程接口详解
1. IO多路复用基础
IO多路复用(Multiplexing)是一种允许单个进程/线程监视多个文件描述符的机制,可以同时检测多个文件描述符是否处于可读、可写或异常状态。
水平触发LT(Level-Triggered):状态可I/O则通知
边缘触发ET(Edge-Triggered):不可I/O变成可I/O则通知(导致一次通知后必须读完)
2. select系统调用
2.1 select函数
功能:返回哪些文件描述符不会阻塞,注意只是不会阻塞,分为有数据或出错情况。可以设置超时时间。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明:
• nfds
:所有被监视的文件描述符中最大的文件描述符加1
• readfds
:指向可读文件描述符集合的指针,传入函数前需要指定为你想要监控读操作的文件描述符集合,返回时这里会放置可以让read不阻塞的文件描述符集合
• writefds
:指向可写文件描述符集合的指针,,传入函数前需要指定为你想要监控写操作的文件描述符集合,返回时这里会放置可以让write不阻塞的文件描述符集合
• exceptfds
:指向异常(并非错误)文件描述符集合的指针,传入函数前需要指定为你想要监控异常的文件描述符集合,当连接到处于信包模式下的伪终端主设备上的从设备状态发生了改变或流式套接字上接收到了带外数据时会被记录在这个集合。
• timeout
:超时时间结构体指针。该参数可指定为NULL,此时select()会一直阻塞。如果结构体timeval的两个域都为0的话,此时 select()不会阻塞,它只是简单地轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。否则,timeout将为select()指定一个等待时间的上限值
timeval结构体:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
相关宏定义:
FD_SETSIZE // 通常为1024,定义了fd_set能容纳的最大文件描述符数量
FD_ZERO(fd_set *set) // 清空文件描述符集合
FD_SET(int fd, fd_set *set) // 将fd添加到集合中
FD_CLR(int fd, fd_set *set) // 从集合中移除fd
FD_ISSET(int fd, fd_set *set) // 检查fd是否在集合中
返回值:
• 成功:返回就绪的文件描述符总数,即三个集合内文件描述符的数量相加的结果,有可能重复
• 超时:返回0
• 出错:即任意一个或多个文件描述符被关闭则返回-1,并设置errno
3. poll系统调用
3.1 poll函数
功能:与select类似,但使用不同的文件描述符集合表示方式,没有最大文件描述符数量限制。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
• fds
:指向pollfd结构体数组的指针
• nfds
:fds数组中的元素个数
• timeout
:超时时间(毫秒),-1表示阻塞等待,0表示立即返回
pollfd结构体:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
};
事件标志(events/revents):
宏定义 | 值(十六进制) | 说明 | 依赖宏定义 |
---|
POLLIN | 0x0001 | 普通或优先级带数据可读(等效于 POLLRDNORM \ POLLRDBAND ) | 标准 POSIX |
POLLPRI | 0x0002 | 高优先级数据可读(如 TCP 带外数据) | 标准 POSIX |
POLLOUT | 0x0004 | 数据可写(等效于 POLLWRNORM ) | 标准 POSIX |
POLLRDNORM | 0x0040 | 普通数据可读(需定义 _XOPEN_SOURCE ) | _XOPEN_SOURCE |
POLLRDBAND | 0x0080 | 优先级带数据可读(需定义 _XOPEN_SOURCE ) | _XOPEN_SOURCE |
POLLWRNORM | 0x0100 | 普通数据可写(需定义 _XOPEN_SOURCE ) | _XOPEN_SOURCE |
POLLWRBAND | 0x0200 | 优先级带数据可写(需定义 _XOPEN_SOURCE ) | _XOPEN_SOURCE |
POLLERR | 0x0008 | 发生错误(输出事件,不可在 events 中设置) | 标准 POSIX |
POLLHUP | 0x0010 | 连接挂起(输出事件,不可在 events 中设置) | 标准 POSIX |
POLLNVAL | 0x0020 | 无效文件描述符(输出事件,不可在 events 中设置) | 标准 POSIX |
POLLRDHUP | 0x2000 | 对端关闭连接或半关闭(需定义 _GNU_SOURCE ,Linux 特有) | _GNU_SOURCE |
POLLMSG | 0x0400 | 系统消息(Linux 未使用,保留) | _XOPEN_SOURCE |
POLLREMOVE | 0x1000 | 从监控集中移除(Linux 特有,已废弃) | _GNU_SOURCE (已弃用) |
POLLONESHOT | 0x4000 | 一次性监控(触发后自动移除,需定义 _GNU_SOURCE ,Linux 特有) | _GNU_SOURCE |
POLLWRITE | 0x10000 | 可写(非标准扩展,某些平台使用) | 非标准 |
总结以上要点,poll()真正关心的标志位就是POLLIN、POLLOUT、POLLPRI、POLLRDHUP、
POLLHUP以及POLLERR。
返回值:
• 成功:返回就绪的文件描述符数量
• 超时:返回0
• 出错:返回-1,并设置errno
4.信号驱动I/O技术深度解析
4.1、两种建立通知的方法
4.1.1方法1:基于SIGIO的异步通知
实现步骤
- 配置可重启的信号处理器
#include <signal.h>
struct sigaction sa;
sa.sa_flags = SA_RESTART; // 默认自动重启被中断的系统调用
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
关键参数:
• SA_RESTART
:当信号中断低速系统调用时自动重启
• sigio_handler
:自定义信号处理函数原型void handler(int sig)
- 设置文件属主
fcntl(fd, F_SETOWN, getpid()); // 设置进程为属主
// 或设置进程组
fcntl(fd, F_SETOWN, -getpgrp());
F_SETOWN
参数规则:
• 正数:进程ID,
• 负数:进程组ID,注意这里进程组id若过小函数会返回错误,这是glibc实现的不足之处,具体可参考相关文档的解决办法
设置线程为属主可参考相关文档,也可避免进程组id若过小函数会返回错误
- 启用异步通知
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
标志位说明:
• O_ASYNC
:启用信号驱动I/O(原O_SYNC为笔误,正确应为O_ASYNC)
• O_NONBLOCK
:保证read()在无数据时立即返回EAGAIN
4.1.2方法2:基于实时信号的高级通知
优势对比
• 信号队列化:实时信号(SIGRTMIN-SIGRTMAX)支持队列化存储,避免信号丢失
• 事件信息传递:通过siginfo_t
结构携带文件描述符和事件类型
• 精确控制:可区分不同文件描述符的就绪事件
实现步骤
- 配置siginfo处理器
struct sigaction sa;
sa.sa_flags = SA_SIGINFO; // 必须启用此标志
sa.sa_sigaction = rt_signal_handler; // 三参数处理函数
sigemptyset(&sa.sa_mask);
sigaction(SIGRTMIN, &sa, NULL);
处理函数原型:
void handler(int sig, siginfo_t *info, void *ucontext)
- 绑定实时信号
fcntl(fd, F_SETSIG, SIGRTMIN); // 指定实时信号
F_SETSIG
特性:
• 参数需≥SIGRTMIN的实时信号编号
• 设置为0时恢复默认SIGIO行为
- 可选同步等待方式
sigset_t waitset;
sigaddset(&waitset, SIGRTMIN);
sigprocmask(SIG_BLOCK, &waitset, NULL);
siginfo_t info;
int sig = sigwaitinfo(&waitset, &info); // 同步阻塞等待
siginfo_t
关键成员:
siginfo_t {
int si_signo; // 信号编号
int si_fd; // 产生信号的文件描述符
int si_band; // 事件掩码(POLLIN/POLLOUT等)
}
4.2取消文件监控的方法
4.2.1 取消SIGIO监控
- 解除信号关联
fcntl(fd, F_SETOWN, 0); // 清除文件属主设置
- 关闭异步标志
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags & ~O_ASYNC);
- 重置信号处理器
signal(SIGIO, SIG_DFL); // 恢复默认处理
4.2.2 取消实时信号监控
- 解除信号绑定
fcntl(fd, F_SETSIG, 0); // 还原为SIGIO
- 清空信号队列
sigqueue_t qinfo;
while(sigwaitinfo(&waitset, &qinfo) > 0); // 排空已排队信号
- 解除信号阻塞
sigprocmask(SIG_UNBLOCK, &waitset, NULL);
4.3高并发场景应对策略
4.3.1 方法一:扩展信号队列容量
// 查看当前限制
cat /proc/sys/kernel/rtsig-max
// 临时修改限制(需root权限)
echo 1000000 > /proc/sys/kernel/rtsig-max
// 永久修改(在/etc/sysctl.conf添加)
kernel.rtsig-max = 1000000
相关内核参数:
• rtsig-max
:最大排队信号数(默认值通常为1024)
• rtsig-nr
:当前排队信号数(通过/proc/sys/kernel/rtsig-nr
查看)
4.3.2 方法二:混合信号与轮询机制
- 配置SIGIO兜底处理
signal(SIGIO, overflow_handler); // 信号队列满时触发
- 在处理器中启用轮询
void overflow_handler(int sig) {
struct pollfd pfds[MAX_FDS];
// 填充所有监控的fd
poll(pfds, num_fds, 0); // 非阻塞检查
}
- 事件检测逻辑
// 在poll返回后遍历所有fd
for(int i=0; i<num_fds; i++) {
if(pfds[i].revents & POLLIN) {
// 处理读就绪事件
}
}
4.4关键函数技术规格
fcntl() 扩展说明
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
命令 | 参数类型 | 作用域 |
---|
F_SETOWN | int | 设置进程/进程组属主 |
F_SETSIG | int | 绑定实时信号 |
F_GETFL | void | 获取文件状态标志 |
F_SETFL | int | 设置文件状态标志 |
sigaction() 结构体详解
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_flags
关键标志组合:
• SA_RESTART | SA_SIGINFO
:支持自动重启并携带扩展信息
• SA_NODEFER
:不自动阻塞当前信号类型
事件掩码宏定义
标志位 | 值(十六进制) | 对应事件 |
---|
POLLIN | 0x0001 | 普通或高优先级数据可读 |
POLLPRI | 0x0002 | 高优先级数据可读 |
POLLOUT | 0x0004 | 写数据不会导致阻塞 |
POLLERR | 0x0008 | 发生错误 |
POLLHUP | 0x0010 | 连接挂起 |
POLLNVAL | 0x0020 | 无效的文件描述符 |
5. epoll接口
select与poll是库函数进行循环调用系统调用来返回就绪的fd,而epoll直接是内核根据其维护的打开文件句柄表来创建红黑树实现监控。select与poll干活的人是库,epoll干活的是内核。
5.1 epoll_create/epoll_create1
功能:创建epoll实例。
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
参数说明:
• size
:提示内核期望监控的文件描述符数量(已废弃),随便你怎么指定
• flags
:
• EPOLL_CLOEXEC
:设置close-on-exec标志,目前只实现了这个
返回值:
• 成功:返回epoll文件描述符
• 失败:返回-1,并设置errno
5.2 epoll_ctl
功能:向epoll实例添加、修改或删除文件描述符。当内核维护的打开文件描述的一项文件句柄没有任何进程的fd指向它时,内核在删除该文件句柄的同时删除epoll实例对其的监控(从红黑树中删除)
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
• epfd
:epoll文件描述符
• op
:操作类型:
• EPOLL_CTL_ADD
:添加文件描述符
• EPOLL_CTL_MOD
:修改文件描述符
• EPOLL_CTL_DEL
:删除文件描述符
• fd
:要操作的目标文件描述符
• event
:指向epoll_event结构体的指针
epoll_event结构体:
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd; //监控文件描述符需要用这个
uint32_t u32;
uint64_t u64;
} epoll_data_t;
使用时既设置参数fd为要监控的fd,又设置struct event结构体的epoll_data_t为要监控的fd
事件标志(events)(仅仅较poll的事件前面加了一个'e'):
EPOLLIN // 可读事件
EPOLLOUT // 可写事件
EPOLLRDHUP // 对端关闭连接或关闭写操作
EPOLLPRI // 紧急数据可读
EPOLLERR // 错误条件发生
EPOLLHUP // 挂起发生
EPOLLET // 设置边缘触发模式(注意要读完数据的同时防止一个文件描述符上源源不断有数据导致其他文件描述符饥饿)
EPOLLONESHOT // 设置一次性监听
返回值:
• 成功:返回0
• 失败:返回-1,并设置errno
5.3 epoll_wait
功能:等待epoll文件描述符上的IO事件。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数说明:
• epfd
:epoll文件描述符
• events
:用于返回事件的数组
• maxevents
:events数组的大小,起码大于你往epoll实例里面添加的文件描述符个数
• timeout
:超时时间(毫秒),-1表示阻塞,0表示立即返回
返回值:
• 成功:返回就绪的文件描述符数量
• 超时:返回0
• 出错:返回-1,并设置errno
5.4 epoll_pwait
功能:与epoll_wait类似,但允许指定信号掩码。
#include <sys/epoll.h>
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
参数说明:
• sigmask
:指向信号掩码的指针
返回值:同epoll_wait
6. 性能比较
- select:
• 优点:跨平台支持好
• 缺点:文件描述符数量有限(FD_SETSIZE),每次调用需要重置参数为自己想要监控的文件描述符集合,函数也需循环nfds次来先判断该文件描述符是否在用户指定的集合中,函数执行的时间复杂度为O(n),对进程内存造成的空间复杂度为O(n) - poll:
• 优点:无最大文件描述符限制,较select可以指定每个文件描述符的监控偏好,函数可以不用循环0到nfds。
• 缺点:函数需要遍历整个fds数组,时间复杂度O(n),对进程内存造成的空间复杂度为O(n) - epoll:
• 优点:时间复杂度O(1),支持边缘触发模式,无文件描述符数量限制
• 缺点:Linux特有,不跨平台 - 信号驱动IO:
• 优点:真正的异步通知机制
• 缺点:编程模型复杂,信号处理有诸多限制