IO复用 知识点梳理
- 2019 年 11 月 1 日
- 筆記
IO复用
程序可以同时监听多个fd
场景
- 需要同时监听多个socket
- 需要同时监听socket和用户输入
- 需要同时处理监听socket和连接socket
- 需要同时处理TCP和UDP
- 需要同时监听多个端口
socket就绪
- 可读
- 内核缓冲区收到的数据>=其低水位标志
- 对方关闭连接
- socket上有新的连接请求
- socket上有未处理的错误
- 可写
- 发送缓冲区的字节数大于等于SO_SNDLOWAT
- 关闭写操作
- 非阻塞connect连接成功或失败
- socket上有未处理的错误
- 异常
- socket接收到带外数据
XMind: ZEN – Trial Version
select
select API模型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 用三个fd数组分别表示要监听可读、可写、异常事件的描述符数组。
readfds、writefds、exceptfds这三个数组既是输入参数,也是输出参数。 它们也用于内核空间想用户空间传递就绪的文件描述符。
select使用模型:
while(1) { FD_ZERO(&rfds); FD_ZERO(&wfds); FD_SET(fd0, rfds); // FD_SET(fd1, wfds); // ret = select(MAX_FDS+1, &rfds, &wfds, &efds, &tv); if (FD_ISSET(fd0, &rfds)) { ... } else (FD_ISSET(fd1, &wfds)) { ... } }
select调用时,会通过三个数组参数把要监听的文件描述符(和对应的事件)传递给内核。
select因有就绪事件而返回时,内核再把相应就绪的文件描述符通过三个数组返回。
此时程序需要遍历监听文件描述符,判断其是否在相应的就绪fds中。
缺陷
select模型有如下缺陷
- 因为只有一个函数,所以每次调用文件都需要把监听的文件描述符这些数据从用户空间拷贝到内核空间。
- 由于rfds、wfds、efds既是入参也是出参,所以每次调用select都需要重新设置三个数组。
- 索引就绪fd时,仍然需要遍历所有监听的fds,做FD_ISSET
- 由fd_set能容纳的文件描述符数量限制为FD_SETSIZE,这个值被设置为1024
- select只支持 可读、可写、异常 三类事件。
poll
poll API原型:
#include <poll.h> int poll(struct pollfd fds[], nfds_t nfds, int timeout); struct pollfd { int fd; /* file descriptor */ short events; /* events to look for */ short revents; /* events returned */ };
poll函数讲描述符和事件区关联起来了。 同时讲注册到事件和就绪到事件区分开来。 内核只需要修改revents。
poll 使用:
int main() { int ret = 0; struct pollfd pollfds[1]; char buf[10]; pollfds[0].fd = 1; pollfds[0].events = POLLIN; while(1) { memset(buf, 0, sizeof(buf)); ret = poll(pollfds, 2, 0); if (pollfds[0].revents == POLLIN) { scanf("%s", buf); printf("buf:%sn", buf); } } return 0; }
可以看出,相比select函数,poll把监听描述符和事件到设置从for循环中移动出来了。
优缺点
相对select的改进
- 相对于select,不必每次调用前都重置一次监听参数pollfds,
- 相对于select,能够监听更多都事件类型。
仍然存在都不足:
- 因为只有一个函数,所以每次调用文件都需要把监听的文件描述符这些数据从用户空间拷贝到内核空间。
- 索引就绪fd时,仍然需要遍历所有监听的fds,
epoll
epoll 是linux特有的API
epoll API
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
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 */ };
与select和poll只有一个函数不同,epoll API提供了一组函数来实现IO复用。
epoll 使用模型
struct epoll_event event; struct epoll_event *events; // 用户保存内核返回的就绪事件 int efd; int listenfd = 0; // 监听标准输入 efd = epoll_create(0); event.data.fd = listenfd; event.events = EPOLLIN | EPOLLET; ret = epoll_ctl (efd, EPOLL_CTL_ADD, listenfd, &event); while(1) { int n, i; n = epoll_wait(efd, events, MAXEVENTS, -1); for (i = 0; i < n; i++) { if (events[i].events & EPOLLIN) { // handle EpollIN, 可以从events[i].data.fd获取文件描述符 } }
优缺点
epoll所做的改进
- epoll 提供了一整组函数, 将监听数据(epoll_ctl)的传递和监听本身(epoll_wait)分开了。调用epoll_wait时,不需要在将要监听都文件描述符、事件等从用户空间拷贝到内核空间。
- epoll事件返回了就绪到文件描述符和对应到事件,因而索引就绪事件时不需要遍历所有监听事件。事件复杂度变为O(1)。
- 在内部实现上,epoll使用回调取代了select和poll到轮询机制,极大提升了性能。
其他
LT和ET
文件描述符的操作有LT和ET两种方式,epoll两种模式都支持。
- LT:电平触发
epoll将就绪事件通知应用程序后,应用程序可以不用立即处理。 下次调用epoll_wait的时候,还会继续通知。
- ET: 边缘触发
epoll将就绪事件通知应用程序后,应用程序**必须**立即处理。 下次调用epoll_wait的时候,不会重复通知。
举个栗子,如果是ET模式,epoll_wait检测到事件可读性,通知应用程序后,应用程序就需要吧本次可读读数据都读完。