7-文件IO

阻塞与非阻塞IO_51CTO博客_阻塞io和非阻塞io区别 · · 787 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。


1. 阻塞 IO

通常来说,从普通文件读数据,无论你是采用 fscanf,fgets 也好,read 也好,一定会在有限的时间内返回。但是如果你从设备,比如终端(标准输入设备)读数据,只要没有遇到换行符(’\n’),read 一定会“堵”在那而不返回。还有比如从网络读数据,如果网络一直没有数据到来,read 函数也会一直堵在那而不返回。

read 的这种行为,称之为 block,一旦发生 block,本进程将会被操作系统投入睡眠,直到等待的事件发生了(比如有数据到来),进程才会被唤醒

系统调用 write 同样有可能被阻塞,比如向网络写入数据,如果对方一直不接收,本端的缓冲区一旦被写满,就会被阻塞。

1.1 阻塞读终端实验

  • 代码
// 文件名:blockdemo.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
char buf[10];
int len;
while(1) {
// STDIN_FILENO 是标准输入的描述符,它的值是 0. STDOUT_FILENO 是标准输出的描述符,它的值是 1.
len = read(STDIN_FILENO, buf, 10);
write(STDOUT_FILENO, buf, len);
}

return 0;
}
  • 编译
$ gcc blockdemo.c -o blockdemo
  • 运行
$ ./blockdemo

如果你不向终端键入任何字符,程序将永远阻塞在 read 系统调用处。

1.2 阻塞调用面临的问题

假设有这样一个场景,我要从 2 个不同设备读取数据进行数据,分别进行处理。伪代码如下。

while(1) {
阻塞 read(设备1);
处理设备1数据;

阻塞 read(设备2);
处理设备2数据;
}

上面有什么问题呢?仔细想想,假如设备1一直没有数据到来,那么程序就一直停在 read(设备1)这一行,即使设备2有数据到来,也将得不到处理。

经验很丰富的同学肯定想出了各种方案,比如什么多进程多线程什么的。抱歉,我们是新手,目前只会单线程单进程。

既然如此,可否有一种方案,让 read 不阻塞?不管有没有数据到来,read 执行完立即返回,然后再通过某种特殊的变量来判断本次调用到底有没有数据到来?

实际上,这个方案是可行的。请续读下文。

2. 非阻塞 IO

  • 如何解决从不同设备读数据而造成的干扰

现在,把刚刚上面面临问题的代码改成这样。

while(1) {
非阻塞 read(设备1);
if (设备1有数据){
处理设备1数据;
}

非阻塞 read(设备2);
if (设备2有数据) {
处理设备2数据;
}
}

且不论这样的代码执行效率如何,我们先看看它是否解决了前面的问题。

如果设备1没有数据到来,read(设备1)也会立即返回,有数据就处理数据,没数据接着执行 read(设备2),有数据就处理数据,没有的话紧接着又去 read(设备1)……如此往复。

我们会发现,设备1和设备2之间,不论有没有数据到来,都不会互相影响,而不像之前阻塞IO那样,如果设备1没有数据,将会影响到设备2的数据处理。

这种方案非常不错,总之目前来说是这样的,先辈们给这种解决方案取了一个很好听的名字——Poll (轮询)。

  • 效率

现在,是时候把效率搬上来谈谈了。

如果设备1和设备2一直没有数据到来,这个 while 循环将不断空转,CPU将面临高负荷。这是一种极大的浪费。不像阻塞方式,没有数据,就直接被操作系统投入睡眠。

那么,我们把上面的代码再改改。

  • 修改方案

添加 sleep,主动让出 CPU。

while(1) {
非阻塞 read(设备1);
if (设备1有数据){
处理设备1数据;
}

非阻塞 read(设备2);
if (设备2有数据) {
处理设备2数据;
}
sleep(5); // 加了一行
}

这种方案仍然有问题,虽然可以每次让出一定时间的CPU,但是也导致了设备的数据得不到及时处理。可是以目前的知识,我们只能做到这个份上。未来,我们有机会学习更加先进的技术,来完美解决这个问题。提前预告一下,它的大名是——select。

2.1 非阻塞IO实验

有几个需要注意的地方:

  1. 阻塞非阻塞是文件本身的特性,不是系统调用read/write本身可以控制的。
  1. 终端默认是阻塞的,我们可以重新 open 设备文件 /dev/tty(表示当前终端),打开的时候指定 O_NONBLOCK 标志就行了。
  2. 非阻塞 read,如果有数据到到来,返回读取到的数据的字节数。如果没有数据到来,返回 -1,这时候我们没有办法判断到底是因为出错而返回,还是因为没有数据返回。所以需要借助 errno 全局变量,来判断是什么原因。如果 errno 的值为 EWOULDBLOCK或 EAGAIN(这两个宏的值是一样的),表示当前没有数据到达,希望你再尝试一次。因为 read 返回 -1 前,linux 系统会在 read 返回前给 errno 赋值,来告诉应用层,到底是什么原因。
  • 非阻塞IO读终端数据
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>// errno 变量的头文件
#include <stdlib.h>

char MSG_TRY[] = "try again!\n";

int main() {
char buffer[10];
int len;
int fd;

fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

while(1) {
len = read(fd, buffer, 10);
if (len < 0) {
if (errno == EAGAIN) {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(1); // 让出 CPU,避免CPU长时间空转
}
else {
perror("read");
exit(1);
}
}
else {
break;
}
}

write(STDOUT_FILENO, buffer, len);
return 0;
}
  • 非阻塞IO读终端数据结合等待超时
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

char MSG_TRY[] = "try again!\n";
char MSG_TMOUT[] = "time out!\n";

int main() {
char buffer[10];
int len;
int fd;
int i;

fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

// 超过 5 秒后,无论有没有数据都退出。
for (i = 0; i < 5; ++i) {
len = read(fd, buffer, 10);
if (len < 0) {
if (errno == EAGAIN) {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(1);
}
else {
perror("read");
exit(1);
}
}
else {
break;
}
}

if (i == 5) {
write(STDOUT_FILENO, MSG_TMOUT, strlen(MSG_TMOUT));
}
else {
write(STDOUT_FILENO, buffer, len);
}
return 0;
}

3. 总结

本文简单介绍了阻塞与非阻塞IO的概念,并给出一个实际生产环境可能遇到的例子,利用单线程来解决多设备数据处理的方法。

因为还没有学习多进程与多线程,我们只能借助非阻塞IO来完成这个功能。在后面的深入学习中,我们将出给出更加完美的解决方案,解决因为没有数据到来而使 CPU 空转的问题。


本文来自:阻塞与非阻塞IO_51CTO博客_阻塞io和非阻塞io区别

感谢作者:阻塞与非阻塞IO_51CTO博客_阻塞io和非阻塞io区别

查看原文:7-文件IO

787 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传