0. 一个小故事
需求:小黄是一个收信员,需要去A,B两家收信。他先去了A, 但A磨磨唧唧的就不给小黄信,小黄只能干等着,而B的信早就写好了一直等小黄来收,但是由于小黄只能等A给信了才能再去B收,结果让B等了几个小时。最后,小黄被生气的B投诉扣工资。
bio :经过上次的教训,小黄决定为每个客户都安排一个收信员1对1服务,这样虽然投入增加了,但是每个客户都非常满意。于是小黄的客户越来越多,可是随着客户增加,小黄发现收信员也越来越多,渐渐入不敷出。而且,很多收信员根本不忙,大部分时间都在等着客户写信,于是小黄决心优化收信的流程。
io多路复用 : 这次,小黄辞退了所有收信员,自己一个人收信,只不过,这次他不再等客户写完信再走了,而是给客户留了个电话,告诉客户说:如果你写好了信,就给我发短信,我就过来取信。之后小黄每天只要不停的盯着手机看,只要有短信通知,就立马过去收件,一个人就服务了所有客户
1. bio的一些背景
1.1 数据读取的两个阶段
对我们用户来说,当调用read从硬盘里读取一个文件时,我们会认为是直接从硬盘里把数据读取进内存的,其实不是这样,数据的读取分为以下两个阶段:
- 数据准备阶段(写信):把数据从硬件(例如硬盘,网卡)读取到内核缓冲区(不需要内核参与)
- 数据传输阶段(收信):把数据从内核缓冲区复制到进程缓冲区(内核参与)
1.2 bio是怎么阻塞的
前面我们提到,数据的读取有两个阶段,bio的阻塞,其实发生在第一个阶段,让我们列一个简单的读取过程
- 用户发起一个read系统调用
- 检查内核缓冲区有没有想要的数据,此时有两种情况.
- 内核缓冲区有数据,那么内核就直接把数据从内核缓冲区复制到调用read方法的线程缓冲区,不阻塞线程(如果信已经写好了,就直接取走)
- 内核缓冲区没有数据,那么内核先挂起线程,然后通过dma去把数据从硬件读到内核缓冲区(不需要内核去参与,此时内核就干别的去了),当数据全部读取到内核缓冲区后,内核唤醒再线程,然后再把数据拷贝到线程缓冲区(如果信没写好,客户当场写信,收信员在旁边等着信写完)
可以从上面的过程中看到,阻塞,其实就发生在硬件到内核缓冲区这一步
1.3 bio的缺点
缺点:每个连接都需要一个线程去处理
从开头的小故事就能了解,为了服务好每个客户,小黄为每个客户都一定要配置一个专属收信员去服务,但是客户人一多,收信员也就多了,成本就高了
2.io多路复用
简单过程
nio模式下,基本流程如下
- 收信员(线程)只有一个,但是收信员多了个工具:手机短信(select,poll,epoll)。
- 小黄告诉客户,如果信写好了就给自己发短信(将io注册到select上),然后就溜了去下一家继续通知(数据准备阶段不阻塞了!)
- 当通知完所有客户要发短信后,小黄一整天都盯着手机,看是否有人给自己发短信(不断轮询select去看是否有注册的io状态就绪)
- 如果有人发短信了,就跑去他那取(如果某io状态就绪,这个线程就去取数据)
io多路复用的一些解惑
在刚看io多路复用时,我有些疑惑,记录下来
问题1: io多路复用为什么是非阻塞的?
答:前面已经提到过了,io其实分为两步:数据准备阶段和数据传输阶段。数据传输阶段要内核参与的,所以无论如何也会阻塞住,而非阻塞,指的就是数据准备阶段不阻塞。由于io多路复用在得知内核缓冲区没有数据后会直接返回,所以不会阻塞线程。
问题2: io多路复用的用途?
答:一开始我也没想通,比如我要读一个文件,就算你在数据准备阶段返回了,我又能干啥呢,我还不是要等文件都读到线程内存了才进行下一步,而且就算我同时读n个文件,我还不是要开n个线程去分别处理,因为我们对每个文件对处理方式很多时候是不一样的。后来理解了,io多路复用多用于socket编程,用于服务端处理长连接。