阻塞式 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); //先获取原来的 flag
flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag
fcntl(0, F_SETFL, flag); //重新设置 flag
for ( ; ; ) {
// /* 读鼠标
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。