Linux高性能服务器编程:I/O复用教程
1. select系统调用
在一定时间内监听用户感兴趣的文件描述符上的可读、可写和异常事件。
int select(int nfds, fd\_set* readfds, fd\_set* writefds, fd\_set* exceptfds, struct timeval* timeout);
nfds:指定被监听的文件描述符的总数。
readfds、writefds、exceptfds分别指向可读、可写和异常等事件对应的文件描述符集合。
FD\_ZERO 清除fdset所有位
FD\_SET 设置fdset的位fd
FD\_CLR 清除fdset的位fd
FD\_ISSET fdset的位fd是否设置
select成功时返回就绪(可读、可写和异常)文件描述符的总数。
可读:
内核接收缓冲区的字节数大于或等于其低水位标记SO\_RCVLOWAT.可以无阻塞的读该socket。
socket通信的对方关闭连接,此时对该socket操作将返回9
监听socket上有新的连接请求
socket上有未处理的错误
可写:
socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO\_SNDLOWAT,可以无阻塞地写该socket。
socket写操作被关闭,对写操作关闭的socket执行写操作会触发一个SIGPIPE信号。
socket使用非阻塞connect连接成功或者失败之后。
socket上有未处理的错误。
2. poll调用
和select系统调用类似,在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
int poll(struct pollfd* fds, nfds\_t nfds, int timeout);
fds是一个pollfd结构类型的数组,指定所有我们感兴趣的文件描述符上发生的可读、可写和异常事件。pollfd结构体定义如下:
struct pollfd {
int fd; //文件描述符
short events; // 注册的事件
short revents; //实际发生的事件,有内核填充
};
events告诉poll监听fd上的哪些事件。
POLLIN: 数据可读(普通和优先数据)
POLLRDNORM: 普通数据可读
POLLOUT:数据可写(普通和优先数据)
3. epoll系统调用
epoll使用一组函数来完成任务。epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
int epoll\_create(int size);
返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
int epoll\_ctl(int epfd, int op, int fd, struct epoll\_event* event);
op参数指定操作类型:
EPOLL\_CTL\_ADD: 往事件表中注册fd上的事件。
EPOLL\_CTL\_MOD: 修改fd上的事件。
EPOLL\_CTL\_DEL: 删除fd上的注册事件。
epoll\_ctl成功返回0,失败返回-1并设置errno.
epoll\_wait(int epfd, struct epoll\_event* events, int maxevents, int timeout); 在一段时间内等待一组文件描述符上的事件。
epoll\_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中(由epfd参数指定)复制到它的第二个参数events指向的数组中。此数组只用于输出epoll\_wait检测到的就绪事件,而不像
poll和select的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。
poll和epoll在使用上的区别:
//poll
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < MAX_EVENT_NUMBER; ++i) {
if (fds[i].revents & POLLIN) {
int sockfd = fds[i].fd;
//处理socket
}
}
//epoll
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < ret; i++) {
int sockfd = events[i].data.fd;
//sockfd肯定准备就绪
}
LT和ET模式
LT:采用LT工作模式的文件描述符,当epoll\_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll\_wait时,epoll\_wait还会再次向应用程序通告该事件,直到该事件被处理。
ET:对于采用ET模式的文件描述符,当epoll\_wait检测到其上有事件发生并将此事件通知给应用程序后,应用程序必须立即处理该事件,后续的epoll\_wait调用将不再向应用程序通知这一事件。(注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式操作该文件描述符), 每个使用ET模式的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态。
EPOLLONESHOT:对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次。当一个线程在处理某个socket时,其他线程是不能有机会操作该socket的。
4.三组I/O复用函数的比较
select和poll都只能工作在相对低效的LT模式,而epoll则可以工作在ET高效模式。并且epoll还支持EPOLLONESHOT事件。
实现原理上看:select和poll采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,时间复杂度为O(n)。
epoll\_wait采用回调的方式,内核检测到就绪的文件描述,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列,内核最后在适当的时候将该就绪事件队列中的内容拷贝到用户空间,时间复杂度为O(1)。
5. I/O复用的高级应用:非阻塞的connect
connect连接出错时的一种errno值为EINPROGRESS,这种错误发生在对非阻塞的socket调用connect,而连接有没有建立时,这种情况下,可以调用select、poll等函数来监听这个连接失败的socket上的可写事件。当select、poll等函数返回后,
再利用getsockopt来读取错误码并清除该socket上的错误,如果错误码是0,表示连接成功建立,否则连接失败。