1. 阻塞 IO
通常来说,从普通文件读数据,无论你是采用 fscanf,fgets 也好,read 也好,一定会在有限的时间内返回。但是如果你从设备,比如终端(标准输入设备)读数据,只要没有遇到换行符(’\n’),read 一定会“堵”在那而不返回。还有比如从网络读数据,如果网络一直没有数据到来,read 函数也会一直堵在那而不返回。
read 的这种行为,称之为 block,一旦发生 block,本进程将会被操作系统投入睡眠,直到等待的事件发生了(比如有数据到来),进程才会被唤醒。
系统调用 write 同样有可能被阻塞,比如向网络写入数据,如果对方一直不接收,本端的缓冲区一旦被写满,就会被阻塞。
1.1 阻塞读终端实验
- 代码
- 编译
- 运行
如果你不向终端键入任何字符,程序将永远阻塞在 read 系统调用处。
1.2 阻塞调用面临的问题
假设有这样一个场景,我要从 2 个不同设备读取数据进行数据,分别进行处理。伪代码如下。
上面有什么问题呢?仔细想想,假如设备1一直没有数据到来,那么程序就一直停在 read(设备1)这一行,即使设备2有数据到来,也将得不到处理。
经验很丰富的同学肯定想出了各种方案,比如什么多进程多线程什么的。抱歉,我们是新手,目前只会单线程单进程。
既然如此,可否有一种方案,让 read 不阻塞?不管有没有数据到来,read 执行完立即返回,然后再通过某种特殊的变量来判断本次调用到底有没有数据到来?
实际上,这个方案是可行的。请续读下文。
2. 非阻塞 IO
- 如何解决从不同设备读数据而造成的干扰
现在,把刚刚上面面临问题的代码改成这样。
且不论这样的代码执行效率如何,我们先看看它是否解决了前面的问题。
如果设备1没有数据到来,read(设备1)也会立即返回,有数据就处理数据,没数据接着执行 read(设备2),有数据就处理数据,没有的话紧接着又去 read(设备1)……如此往复。
我们会发现,设备1和设备2之间,不论有没有数据到来,都不会互相影响,而不像之前阻塞IO那样,如果设备1没有数据,将会影响到设备2的数据处理。
这种方案非常不错,总之目前来说是这样的,先辈们给这种解决方案取了一个很好听的名字——Poll (轮询)。
- 效率
现在,是时候把效率搬上来谈谈了。
如果设备1和设备2一直没有数据到来,这个 while 循环将不断空转,CPU将面临高负荷。这是一种极大的浪费。不像阻塞方式,没有数据,就直接被操作系统投入睡眠。
那么,我们把上面的代码再改改。
- 修改方案
添加 sleep,主动让出 CPU。
这种方案仍然有问题,虽然可以每次让出一定时间的CPU,但是也导致了设备的数据得不到及时处理。可是以目前的知识,我们只能做到这个份上。未来,我们有机会学习更加先进的技术,来完美解决这个问题。提前预告一下,它的大名是——select。
2.1 非阻塞IO实验
有几个需要注意的地方:
- 阻塞非阻塞是文件本身的特性,不是系统调用read/write本身可以控制的。
- 终端默认是阻塞的,我们可以重新 open 设备文件 /dev/tty(表示当前终端),打开的时候指定 O_NONBLOCK 标志就行了。
- 非阻塞 read,如果有数据到到来,返回读取到的数据的字节数。如果没有数据到来,返回 -1,这时候我们没有办法判断到底是因为出错而返回,还是因为没有数据返回。所以需要借助 errno 全局变量,来判断是什么原因。如果 errno 的值为 EWOULDBLOCK或 EAGAIN(这两个宏的值是一样的),表示当前没有数据到达,希望你再尝试一次。因为 read 返回 -1 前,linux 系统会在 read 返回前给 errno 赋值,来告诉应用层,到底是什么原因。
- 非阻塞IO读终端数据
- 非阻塞IO读终端数据结合等待超时
3. 总结
本文简单介绍了阻塞与非阻塞IO的概念,并给出一个实际生产环境可能遇到的例子,利用单线程来解决多设备数据处理的方法。
因为还没有学习多进程与多线程,我们只能借助非阻塞IO来完成这个功能。在后面的深入学习中,我们将出给出更加完美的解决方案,解决因为没有数据到来而使 CPU 空转的问题。