IO=等待+拷贝
读IO=等待读事件就绪+内核数据拷贝至用户空间
写IO=等待写事件就绪+数据从用户空间拷贝至内核
高效IO就是让等的时间减少
操作系统收到数据就会向对应进程发送SIGIO信号
5种IO模型
钓鱼例子:
自己等自己钓(同步IO,需要自己拷贝)
- 张三:一个人一个杆,一直盯着鱼竿,阻塞IO
- 李四:一个人一个杆,看一会书看一下鱼竿,非阻塞IO
- 王五:一个人一个杆一个铃铛,干自己的事情,铃铛不响不用管,信号驱动IO
- 赵六:一个人一批杆,循环的看鱼竿(鱼上钩的概率最大,效率最高),多路转接、多路复用
自己不用等(异步IO,不用自己拷贝)
- 田七:让他的司机小刘钓鱼,他自己干别的事,鱼钓够了打电话叫田七,异步IO
IO多路复用
下面的系统调用只负责IO过程中的等,一次可以等一批文件描述符,更擅长处理长连接的情况
本质:IO时等待,就绪事件的一种通知方式
套接字的连接请求是被当成读事件来处理的
一个进程同时能打开的文件描述符是32
1 select
select过程
-
定义一个数组,将需要需要等待事件就绪的文件描述符填入数组中。
-
遍历数组将数组中的文件描述符通过FD_SET()设置进readfds中,调用select系统调用(此时readfds作为输入参数表示用户告诉操作系统需要关心那些文件描述符上面的读事件),若返回值大于0,则遍历数组,同时通过FD_ISSET()函数判断数组中的文件描述符是否被设置进readfds(此时readfds作为输出参数表示操作系统告诉用户哪些文件描述符上面的读事件已经就绪),若该文件描述符的事件就绪了,则对该文件描述符进行事件的处理
-
重复2过程
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
/*返回值:大于0,表示有文件描述符就绪了。等于0,表示超时了即在等的过程中没有文件描述符就绪。小于0,表示等待失败
nfds:要等的最大文件描述符的值+1,例:要等1和7,nfds=8
fd_set是位图结构,比特位位置表示哪一个文件描述符,比特位的内容:
输入时:是否需要关心特定文件描述符上面的读/写事件,为1需要关心,为0不需要关心
输出时:那些文件描述符上面的读/写事件就绪了,为1就绪,0没就绪
readfds:既是输入型参数又是输出型参数,输入时代表用户想告诉操作系统需要关心那些文件描述符的读事件,输出时代表操作系统想告诉用户那些文件描述符读事件已经就绪。每次调用select都要重置,因为输出时已经被改变
writefds:需要写的文件描述符集合
exceptdfds:哪几个文件描述符出异常了。
timeout:NULL表示以阻塞方式等,0表示以非阻塞方式等,其他值例如5表示以阻塞方式等5秒,5秒后返回一次
优点
- 一个进程可同时等待多个文件描述符,减少等待时间
缺点
- 等待的文件描述符是有上限的,因为等待的文件描述符使用位图来表示,每次最多等sizeof(fd_set)*8个文件描述符,fd_set的大小可以调整,在内核源码中调整后重新编译内核即可
- 输入和输出参数是用同一个参数表示,每次select结束后,下次select前都需要重新设置fd_set
- 调用select时,都需要把fd_set集合拷贝到内核,随着集合中fd增多,开销会增大
- select等待中,内核需要不断轮询检测每个fd的某个事件是否就绪,当fd很多时,开销会增大
- select等到就绪事件后,需要遍历整个fd集合,通过FD_ISSET判断是否是该文件描述符的事件就绪,当fd很多时,效率不高
2 poll
poll过程
- 定义struct pollfd数组fds,该结构体中包含文件描述符、作为输入参数的事件、作为输出参数的事件
- 将需要等待的文件描述符的对应事件建立一个struct pollfd结构体添加到上面定义的数组fds中
- 调用poll,如果返回值大于0,则遍历fds数组,判断每个fds的revents是被设置,若被设置,则处理该文件描述符对应的事件
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
优点
- 理论上等待的文件描述符没有上限,struct pollfd数组有多大,就能等待多少个文件描述符
- 输入和输出参数分离,不用每次调用poll时都重新设置struct pollfd数组
缺点
- 调用poll时,都需要把struct pollfd数组拷贝到内核,随着数组中fd增多,开销会增大
- poll等待中,内核需要不断轮询检测每个fd的某个事件是否就绪,当fd很多时,开销会增大
- poll等到就绪事件后,需要遍历整个数组,判断每个文件描述符的事件是否就绪,当fd很多时,效率不高
3 epoll
epoll过程
- 创建epoll模型
- 向epoll模型中添加需要关心文件描述符的某些事件
- 调用epoll_wait()进行等待,如果返回值大于0,则遍历epoll_event数组,数组中的元素就是某个文件描述符就绪的事件,处理该事件
int epoll_create(int size);
在内核创建epoll模型
创建epoll过程:
- 建立红黑树:红黑树中的节点为fd和events
- 建立回调机制:便于当文件描述符中的某事件就绪时通知操作系统
- 建立就绪队列:当某文件描述符的某事件就绪,通过回调机制通知操作系统时,操作系统为对应的数据创建一个结构体,挂在就绪队列中,便于epoll_wait时取出就绪事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用户向对应的epoll模型中添加对应的文件描述符及其对应的事件
epfd:某个epoll模型
op:要给该epoll模型中添加/删除/修改,某个文件描述符或事件
作用:
- 添加、删除、修改epoll模型中红黑树节点
- 建立对应文件描述符的回调函数:当文件描述符的事件就绪时,回调函数创建就绪节点,连接在就绪队列中
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
等待内核通知那些文件描述符的那些事件就绪
返回值表示实际有多少个事件就绪
epfd:要等待的epoll模型
events:操作系统将就绪的事件放在该结构体中返回给用户,该结构体中包含了就绪的事件和
maxevents:events结构体数组的大小,期望有多少个事件就绪
- 检测就绪队列是否有节点,时间复杂度O(1),select和poll等待事件就绪的事件复杂度为O(n),n越大效率越低
- 将就绪队列中的节点信息拷贝到events数组中
优点
- epoll不用每次wait都把所有文件描述符和需要关心的事件拷贝到内核,因为epoll模型中有一个红黑树来存储文件描述符及其需要等待的事件,只在需要时调用EPOLL_CTL_ADD将文件描述符及其需要关心的事件添加到内核epoll模型的红黑树中
- 等待时不需要内核轮询检测所有需要关心的文件描述符,因为epoll建立了回调机制,事件就绪时,硬件驱动会调用回调函数自己来通知操作系统事件就绪,将就绪的信息添加到epoll模型的就绪队列中
- 有事件就绪时,不需要遍历所有文件描述符来判断哪个文件描述的事件就绪,只需要将就绪队列中的信息拿出来直接进行就绪事件处理即可
- 由于2/3过程不需要遍历所有文件描述符信息,因此就算有大量需要等待的文件描述符,也不会影响效率
两种工作模式
- 水平触发(LT模式)更可靠
默认为LT模式,只要接受缓冲区有数据就会一直处于就绪队列中,每次epoll_wait都会拿出该就绪事件
- 边缘触发(ET模式)更高效、没有重复通知
只通知一次,这一次没读取完缓冲区中的数据,下次不再会处于就绪队列,除非硬件驱动再次通过回调函数通知操作系统有事件就绪
需要循环读取文件描述符,如果实际读取到的个数小于想要读到的个数,说明读完了,否则没有读完继续循环
一种特殊情况:当客户端发送的字节数是当前服务器每次recv字节数的整数倍,最后一次刚好读取完所有数据,但是由于实际读取的大小等于想要读的大小,循环不退出,下次再读的时候内核缓冲区已经没有数据了,此时如果是阻塞读,该进程就会在这里挂起(阻塞住了),对方如果不发数据,服务器就会在这里永远挂起。解决该问题需要将fd设置为非阻塞
三者对比
select | poll | epoll | |
---|---|---|---|
参数 | 输入和输出参数为同一个参数,每次传参都得重新设置输入参数的值 | 输入输出参数互不影响 | 输入输出参数互不影响 |
用户告诉内核 | 每次都需要将fd_set集合拷贝至内核,fd越多效率越低 | 每次都需要将rfd数组拷贝至内核,fd越多效率越低 | 只在需要时将文件描述符及其需要关心的事件设置进红黑树 |
内核等待 | 轮询检查所有文件描述符的对应事件是否就绪,fd越多效率越低 | 轮询检查所有文件描述符的对应事件是否就绪,fd越多效率越低 | 回调机制,当事件就绪时硬件驱动自动调用回调函数通知操作系统,把就绪信息挂在就绪链表,不需要轮询检测 |
内核告诉用户 | 就绪事件设置在fd_set中,需要用户遍历所有文件描述符并与设置的fd_set进行对比,才能得出哪个文件描述符的事件就绪了,fd越多效率越低 | 遍历rfd数组,如果文件描述符对应的revent字段被设置说明该事件就绪,fd越多效率越低 | 就绪的信息存放在epoll_event数组中,直接使用 |