阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的I/O 操作是非阻塞的。
当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误。
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作。
1、阻塞IO读文件
在调用 open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行;这就是非阻塞 I/O 的打开方式,如果未指定 O_NONBLOCK 标志,则默认使用阻塞式 I/O 进行操作。
以读取鼠标为例,鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下,如下所示:
使用 od 命令查看是哪个设备文件:
sudo od -x /dev/input/event3
移动鼠标或滚轮会打印出信息,如果没有试下其他的设备文件
下面以阻塞方式读取鼠标,调用 open()函数打开鼠标设备文件"/dev/input/event3",以只读方式打开,没有指定 O_NONBLOCK 标志,说明使用的是阻塞式 I/O;程序中只调用了一次 read()读取鼠标。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;// /* 打开文件 fd = open("/dev/input/event3", O_RDONLY);if (-1 == fd) {perror("open error");exit(-1);}// /* 读文件 memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));if (0 > ret) {perror("read error");close(fd);exit(-1);}printf("成功读取<%d>个字节数据\n", ret);// /* 关闭文件 close(fd);exit(0);
}
可以看出在不动滚轮的情况下阻塞
当移动滚轮是输出
2、非阻塞IO读文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;// /* 打开文件 fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}// /* 读文件 memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));if (0 > ret) {perror("read error");close(fd);exit(-1);}printf("成功读取<%d>个字节数据\n", ret);// /* 关闭文件 close(fd);exit(0);
}
可以看到非阻塞状态返回错误,因为在执行程序时并没有滑动滚轮,程序就退出了
可以对上述代码修改,使用轮训方式不断地去读取,直到鼠标有数据可读,read()将会成功
返回:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;// /* 打开文件 fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}// /* 读文件 memset(buf, 0, sizeof(buf));for ( ; ; ) {ret = read(fd, buf, sizeof(buf));if (0 < ret) {printf("成功读取<%d>个字节数据\n", ret);close(fd);exit(0);}}
}
用top命令可以看出用轮训方式此进程CPU占用率非常高,将近100
阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!
3、使用阻塞 I/O 实现并发读取
使用阻塞式方式同时读取鼠标和键盘,示例代码如下所示:
键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MOUSE "/dev/input/event3"
int main(void)
{char buf[100];int fd, ret;// /* 打开鼠标设备文件 fd = open(MOUSE, O_RDONLY);if (-1 == fd) {perror("open error");exit(-1);}// /* 读鼠标 memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));printf("鼠标: 成功读取<%d>个字节数据\n", ret);// /* 读键盘 memset(buf, 0, sizeof(buf));ret = read(0, buf, sizeof(buf));printf("键盘: 成功读取<%d>个字节数据\n", ret);// /* 关闭文件 close(fd);exit(0);
}
可以看出阻塞IO无法实现同时读取,要先读鼠标再读键盘。用非阻塞就可以解决这个问题
4、使用非阻塞 I/O 实现并发读取
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define MOUSE "/dev/input/event3"
int main(void)
{char buf[100];int fd, ret, flag;// /* 打开鼠标设备文件 fd = open(MOUSE, O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}// /* 将键盘设置为非阻塞方式 flag = fcntl(0, F_GETFL); //先获取原来的 flagflag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flagfcntl(0, F_SETFL, flag); //重新设置 flagfor ( ; ; ) {// /* 读鼠标 ret = read(fd, buf, sizeof(buf));if (0 < ret)printf("鼠标: 成功读取<%d>个字节数据\n", ret);// /* 读键盘 ret = read(0, buf, sizeof(buf));if (0 < ret)printf("键盘: 成功读取<%d>个字节数据\n", ret);}// /* 关闭文件 close(fd);exit(0);
}
注意:因为标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用 open()打开得到的,使用fcntl()函数将标准输入设置为非阻塞 I/O。