IO复用技术
IO复用使得程序能同时监听多个文件描述符,这对提高程序性能至关重要。Linux下实现IO复用主要由select、poll、epoll。
select
在一段时间内监听用户感兴趣的文件描述符上的可读可写和异常事件。
select API
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,
如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视
xxxfds...:三组不同的需要监听的IO操作,分别是读、写、异常的文件描述符集合。
timeout:设置select的超时时间。如传NULL则一直阻塞,直到有文件描述符就绪。
成功时返回就绪的文件描述符总数,如超时后没有任何文件描述符就绪则返回0
失败返回-1并设置errno
void FD_CLR(int fd, fd_set *set);//从文件描述符集合中移除该文件描述符
int FD_ISSET(int fd, fd_set *set);//判断文件描述符是否在文件描述符集合中
void FD_SET(int fd, fd_set *set);//添加文件描述符到文件描述符集合
void FD_ZERO(fd_set *set);/清空文件描述符集合
struct timeval{
long tv_sec;//秒数
long tv_usec;//微秒数
}
下面是高级用法
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
select可同时监听的文件描述符数量是通过FS_SETSIZE来限制的,在Linux系统中,该值为1024,当然我们可以增大这个值,但随着监听的文件描述符数量增加,select的效率会降低.
select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。
poll
poll和select类似,也是在指定时间内轮询一定数量的文件描述符,测试其中是否有就绪者。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds是一个pollfd机构类型的数组,指定我们所有感兴趣的文件描述符上发生的可读可写异常事件。
nfds 指定被监听事件集合fds的大小
timeout 指定poll的超时值,单位是毫秒 ,为-1时永远阻塞,直到某个事件发生。
返回值同select
struct pollfd {
int fd;
short events; 注册的事件
short revents; 实际发生的时间,由内核填充
};
poll事件类型
POLLIN 数据可读
POLLPRI 高优先级数据可读,比如TCP带外数据
POLLOUT 数据可写
POLLDRHUP TCP连接被对方关闭,或者对方关闭了写操作
下面是高级用法
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
epoll
- epoll是linux特有的IO复用函数,他在实现上与select、poll有很大差异。
- epoll使用一组函数来完成任务,而不是单个函数。
- epoll把用户关心的文件描述符放在内核的一个事件表里,无须像select和poll一样每次需要重复传文件描述符集或事件集。
- epoll需要使用一个额外的文件描述符来唯一标识内核中的这个事件表。
#include <sys/epoll.h>
int epoll_create(int size);
创建一个epoll内核事件表文件描述符,size只是给内核一个提示,告诉他事件表需要多大。
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
操作内核的事件表
fd是要操作的文件描述符,
op指定操作类型,主要有三种:
EPOLL_CTL_ADD 向事件表中注册fd上的事件
EPOLL_CTL_MOD 修改fd上的注册事件
EPOLL_CTL_DEL 删除fd上的注册事件
event指定事件,是epoll_event结构指针
struct epoll_event{
_uin32_t events;//事件类型
epoll_data_t data;//用户数据
};
基本事件类型 EPOLL_IN、EPOLL_OUT、EPOLLET、EPOLLONESHOT
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数:
epfd:
events:所有就绪的事件
maxevents 指定最多能监听多少事件,必须大于0。
timeout指定超时时间。
下面是高级用法
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
// 保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 感兴趣的事件和被触发的事件
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
ET模式和LT模式
-
LT表示只要有IO操作可以进行比如某个文件描述符有数据可读,每次调用epoll_wait都会返回以通知程序可以进行IO操作
-
ET表示只有在文件描述符状态发生变化时,调用epoll_wait才会返回,如果第一次没有全部读完该文件描述符的数据而且没有新数据写入,再次调用epoll_wait都不会有通知给到程序,因为文件描述符的状态没有变化。
-
select和poll都是状态持续通知的机制,且不可改变,只要文件描述符中有IO操作可以进行,那么select和poll都会返回以通知程序。而epoll两种通知机制可选。
EPOLLONESHOT 事件
即使使用ET模式,一个socket上的事件还是可能触发多次,这可能会引发一个问题,比如,一个线程读取某个socket后开始处理他的数据,这时这个socket又有新数据可读 ,此时另一个线程来读取这个socket,造成两个线程同时操作一个socket,这是不被期望的,我们希望一个socket在同一时刻只能被一个线程操作,可以用EPOLLONESHOT实现。
注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读可写异常事件,且只触发一次,除非使用epoll_ctl 重置该文件描述符上的EPOLLONESHOT事件。一旦处理完毕就应该立刻重置,以便下次触发时有线程处理它。
三组IO复用的比较
- 相同点:这三组系统调用都能同时监听多个文件描述符,可以设置超时时间,返回值是就绪的文件描述符数量。
- 使用上的比较:select的fd_set没有将文件描述符和事件绑定,而是需要提供三种事件类型的fd_set。poll的pollfd将文件描述符和事件都定义在其中,事件被统一处理,更简洁,并且每次调用后无需重置pollfd。epoll则在内核中维护一个事件表,用epoll_ctl操作事件表。
- 性能上的比较:select和poll每次调用都需要返回整个用户注册的事件集合,应用程序需要以O(n)复杂度来索引,epoll仅返回就绪的事件,应用程序以O(1)复杂度来索引。select和poll只能工作在效率较低的LT模式,epoll可以工作在更高效的ET模式和EPOLLONESHOT模式。原理上,select和poll都采用轮询的方式,即每次调用都需要遍历每个注册事件的文件描述符,内核检测就绪事件的复杂度为O(n),而epoll_wait采用回调的方式,内核检测到就绪的文件描述符就触发回调函数,将文件描述符对应的事件插入内核就绪事件队列,复杂度为O(1)。
- 适用场合:epoll 适合连接数较多,但活动连接数较少的情况。select和poll适合连接数较少的情况。
后记:不要浪费时间,学编程的人都知道效率对程序很重要,这对人来说也一样。