阻塞IO和非阻塞IO
在讨论IO复用之前,最好先了解一下阻塞IO和非阻塞IO;
阻塞是指:是否阻塞应用进程
;更具体的说,在执行I/O系统调用的时候,若文件描述符没有准备好,内核是阻塞当前进程还是返回错误;若返回错误就是非阻塞;
阻塞IO调用的整个过程包括:发起请求->内核数据准备->准备好后复制到调用者缓冲区->完成;整个过程应用阻塞;
非阻塞IO的过程:(轮询)发起请求->内核数据准备(还没好则返回错误)——>准备好后复制到调用者缓冲区->完成;前面内核准备不阻塞,后面复制数据会阻塞;
IO多路复用
从前面可以看到,如果我们需要这个进程去处理很多事情,那么肯定希望在内核准备数据这段时间是不阻塞的;但是如果我们有很多文件描述符需要进行监听怎么办?是开个循环,然后逐个检查所有的文件描述符是否准备就绪,一旦就绪的就执行io操作?这样确实也可以实现,但是实际处理起来可能会存在很多问题;这个时候就可以借助select,poll和epoll来为我们做这件事情,他们将帮我们监控所有的文件描述符,在文件描述符就绪时便会返回,平时则会进入阻塞;而这也是io多路复用的思想,即同时监控多个文件描述符,以查看哪些文件描述符可以执行io操作;
select和poll
select和poll都是需要传递所有待监听的文件描述符给内核,之后内核返回一个就绪的文件描述符集合,我们需要遍历所有描述符以查看是否在返回的集合中;处理完之后重新调用时还需要再一次传递完整的文件描述符;当检查大量的文件描述符时,需用从用户态到内核态来回拷贝这些数据将会占用大量的CPU时间;
这两种方法的触发模式都是水平触发(LT)
epoll
epoll能提供高效得性能;他区别于select和poll的最大特点是内核能够“记住”文件描述符,不需要每次调用都epoll_wait()的时候都重新传递一遍完整的文件描述符,需要监听的文件描述符添加一次便可,还可设置监听的事件,而他内部是有一颗红黑树和一个ready链表组成的,当添加文件描述符时,如果存在就直接返回,不存在就添加到树干上;当有描述符准备就绪时,就会添加到队列上,并返回就绪的文件描述符;
epoll支持水平触发(LT)和边缘触发(ET);
水平触发和边缘触发
这里用电信号来解释会比较清楚一点;
假如信号为1便发送通知,那么只要一直为1便会一直发送通知,这就是水平触发;
而边缘触发则是只要信号发生变化时才会通知,即从0变为1时才触发,变后即便一直为1也不发生通知,只有重新变化时才通知;
类似上面的情况,在文件描述符准备就绪时,两者都会触发;如果是水平触发的话,在文件描述符若不处理或还未处理完成,就会一直触发;边缘触发就只是一开始变化触发一次,后续便不会再次触发;
文件描述符就绪条件
- 读写缓存区准备就绪
- 读半部关闭(对方已不在写数据),即收到了fin分组;进行读操作时返回0;
- 写半部关闭(对方已不在读数据),即写的时候收到RST分组;
- 套接字上有一个待处理的错误;