IO=等待+拷贝
读IO=等待读事件就绪+内核数据拷贝至用户空间
写IO=等待写事件就绪+数据从用户空间拷贝至内核
高效IO就是让等的时间减少
操作系统收到数据就会向对应进程发送SIGIO信号
钓鱼例子:
自己等自己钓(同步IO,需要自己拷贝)
- 张三:一个人一个杆,一直盯着鱼竿,阻塞IO
- 李四:一个人一个杆,看一会书看一下鱼竿,非阻塞IO
- 王五:一个人一个杆一个铃铛,干自己的事情,铃铛不响不用管,信号驱动IO
- 赵六:一个人一批杆,循环的看鱼竿(鱼上钩的概率最大,效率最高),多路转接、多路复用
自己不用等(异步IO,不用自己拷贝)
- 田七:让他的司机小刘钓鱼,他自己干别的事,鱼钓够了打电话叫田七,异步IO
下面的系统调用只负责IO过程中的等,一次可以等一批文件描述符,更擅长处理长连接的情况
本质:IO时等待,就绪事件的一种通知方式
套接字的连接请求是被当成读事件来处理的
一个进程同时能打开的文件描述符是32
定义一个数组,将需要需要等待事件就绪的文件描述符填入数组中。
遍历数组将数组中的文件描述符通过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秒后返回一次
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
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数组中
默认为LT模式,只要接受缓冲区有数据就会一直处于就绪队列中,每次epoll_wait都会拿出该就绪事件
只通知一次,这一次没读取完缓冲区中的数据,下次不再会处于就绪队列,除非硬件驱动再次通过回调函数通知操作系统有事件就绪
需要循环读取文件描述符,如果实际读取到的个数小于想要读到的个数,说明读完了,否则没有读完继续循环
一种特殊情况:当客户端发送的字节数是当前服务器每次recv字节数的整数倍,最后一次刚好读取完所有数据,但是由于实际读取的大小等于想要读的大小,循环不退出,下次再读的时候内核缓冲区已经没有数据了,此时如果是阻塞读,该进程就会在这里挂起(阻塞住了),对方如果不发数据,服务器就会在这里永远挂起。解决该问题需要将fd设置为非阻塞
select | poll | epoll | |
---|---|---|---|
参数 | 输入和输出参数为同一个参数,每次传参都得重新设置输入参数的值 | 输入输出参数互不影响 | 输入输出参数互不影响 |
用户告诉内核 | 每次都需要将fd_set集合拷贝至内核,fd越多效率越低 | 每次都需要将rfd数组拷贝至内核,fd越多效率越低 | 只在需要时将文件描述符及其需要关心的事件设置进红黑树 |
内核等待 | 轮询检查所有文件描述符的对应事件是否就绪,fd越多效率越低 | 轮询检查所有文件描述符的对应事件是否就绪,fd越多效率越低 | 回调机制,当事件就绪时硬件驱动自动调用回调函数通知操作系统,把就绪信息挂在就绪链表,不需要轮询检测 |
内核告诉用户 | 就绪事件设置在fd_set中,需要用户遍历所有文件描述符并与设置的fd_set进行对比,才能得出哪个文件描述符的事件就绪了,fd越多效率越低 | 遍历rfd数组,如果文件描述符对应的revent字段被设置说明该事件就绪,fd越多效率越低 | 就绪的信息存放在epoll_event数组中,直接使用 |