IO模型
IO:即输入(Input)和输出(Output),以内存为目标,数据进内存叫In,数据出内存叫Out
五种IO模型
同步阻塞 BIO
阻塞IO就是当应用发起读取数据申请时,在内核数据没有准备好之前,应用会一直处于等待数据状态,直到内核把数据准备好了交给应用才结束。
当调用线程发起 read
系统调用时,如果此时内核数据还没有 Ready,调用线程会阻塞住,等待内核 Buffer 的数据。内核数据准备就绪之后,会将内核态 Buffer 的数据复制到用户态 Buffer 中,这个过程中调用线程仍然是阻塞的,直到数据复制完成>
异步非阻塞 NIO (Non-Blocking IO)
非阻塞IO就是当应用发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用,不会让在这里等待。
数据准备阶段。此时用户线程发起 read 系统调用,此时内核会立即返回一个错误,告诉用户态数据还没有 Read,然后用户线程不停地发起请求,询问内核当前数据的状态。
数据复制阶段。此时用户线程还在不断的发起请求,但是当数据 Ready 之后,用户线程就会陷入阻塞,直到数据从内核态复制到用户态。
如果内核态的数据没有 Ready,用户线程不会阻塞;但是如果内核态数据 Ready 了,即使当前的 IO 模型是同步非阻塞,用户线程仍然会进入阻塞状态,直到数据复制完成,并不是绝对的非阻塞。
那 NIO 的好处:实时性好,内核态数据没有 Ready 会立即返回。但是频繁的轮询内核,会占用大量的 CPU 资源,降低效率。
IO复用模型
NIO模型虽然是异步非阻塞的,但是如果服务器一瞬间接收到成百上千万的请求,那么就需要有成百上千万的请求去轮询查询内核的数据有没有准备好。这样服务器未必扛得住,而且非常的浪费资源。所以IO复用模型就诞生了。由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识)这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路.
select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。
非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。
但是呢,select有几个缺点:
- 监听的IO最大连接数有限,在Linux系统上一般为1024。
- select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流)
poll
因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。
epoll
多路复用模型epoll诞生,它采用事件驱动来实现
epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的操作,而是采用监听事件回调的机制。这就是epoll的亮点。
信号驱动IO模型
信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。
信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是你细看上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。
异步IO AIO
AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。
异步IO的优化思路是解决了应用程序需要先后发送询问请求、发送接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。
同步和异步
在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
阻塞和非阻塞
他们不同的只是发起读取请求的时候一个请求阻塞(结果返回之前线程挂起),一个请求不阻塞(不会阻塞当前线程),但是相同的是,他们都需要应用自己监控整个数据完成的过程。
- 老张把水壶放到火上,站立着等水开。(同步阻塞)
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
- 于是买了一个水开会响笛的水壶。
- 老张把响水壶放到火上,站立等水开。 (异步阻塞)
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)