本系列再次回到文件 I/O 相关话题的讨论,将会介绍文件 I/O 当中的一些高级用法,以应对不同应用场合的需求,主要包括:非阻塞 I/O、I/O 多路复用、异步 I/O、存储映射 I/O 以及文件锁。
非阻塞 I/O
关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了 CPU 控制权。前面所学习过的函数,譬如 wait()、pause()、sleep()等函数都会进入阻塞,本文来聊一聊关于阻塞式 I/O 与 非阻塞式 I/O。
阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的 I/O 操作是非阻塞的。这样说大家可能不太明白,这里举个例子,譬如对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O 进行操作。
阻塞 I/O 与非阻塞 I/O 读文件
本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 对文件进行读操作,在调用 open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行; 这就是非阻塞 I/O 的打开方式,如果未指定 O_NONBLOCK 标志,则默认使用阻塞式 I/O 进行操作。
对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的,前面已经给大家进行了说明。
本小节我们将以读取鼠标为例,使用两种 I/O 方式进行读取,来进行对比,鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下,如下所示:
通常情况下是 mouseX(X 表示序号 0、1、2),但也不一定,也有可能是 eventX,如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,譬如使用 od 命令:
sudo od -x /dev/input/event3
Tips:需要添加 sudo,在 Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作。
当执行命令之后,移动鼠标或按下鼠标、松开鼠标都会在终端打印出相应的数据,如下所示:
如果没有打印信息,那么这个设备文件就不是鼠标对应的设备文件,那么就换一个设备文件再次测试, 这样就会帮助你找到鼠标设备文件。笔者使用的 Ubuntu 系统,对应的鼠标设备文件是/dev/input/event3。接 下来我们编写一个测试程序,使用阻塞式 I/O 读取鼠标。
示例代码演示了以阻塞方式读取鼠标,调用 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);
}
编译上述示例代码进行测试:
执行程序之后,发现程序没有立即结束,而是一直占用了终端,没有输出信息,原因在于调用 read()之后进入了阻塞状态,因为当前鼠标没有数据可读;如果此时我们移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read()会成功读取到数据并返回,如下所示:
打印信息提示,此次 read 成功读取了 48 个字节,程序当中我们明明要求读取的是 100 个字节,为什么 这里只读取到了 48 个字节?这里暂时先不去理会这个问题。 接下来,我们将示例代码修改成非阻塞式 I/O,如下所示:
#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);
}
修改方法很简单,只需在调用 open()函数时指定 O_NONBLOCK 标志即可,对上述示例代码进行编译测试:
执行程序之后,程序立马就结束了,并且调用 read()返回错误,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有 发生输入事件),没有数据可读,故而导致失败返回,这就是非阻塞 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 | 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);
}
}
}
阻塞 I/O 的优点与缺点
当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!
所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU 资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!
执行非阻塞示例代码对应的程序时,通过 top 命令可以发现该程序的占用了非常高的 CPU 使用率,如下所示:
其 CPU 占用率达到了 100%!在一个系统当中,一个进程的 CPU 占用率这么高是一件非常危险的事情。而阻塞式方式的代码中,其 CPU 占用率几乎为 0,所以就本文所举例子来说,阻塞式 I/O 绝地要优于非阻塞式 I/O,那既然如此,我们为何还要介绍非阻塞式 I/O 呢?下一节我们将通过一个例子给大家介绍,阻塞式 I/O 的困境!
使用非阻塞 I/O 实现并发读取
上一小节给大家所举的例子当中,只读取了鼠标的数据,如果要在程序当中同时读取鼠标和键盘,那又该如何呢?本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 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);
}
上述程序中先读了鼠标,再接着读键盘,所以由此可知,在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。
这就是阻塞式 I/O 的一个困境,无法实现并发读取(同时读取),主要原因在于阻塞,那如何解决这个问题呢?当然大家可能会想到使用多线程,一个线程读取鼠标、另一个线程读取键盘,亦或者创建一个子进程,父进程读取鼠标、子进程读取键盘等方法,当然这些方法自然可以解决,但不是我们要学习的重点。
既然阻塞 I/O 存在这样一个困境,那我们可以使用非阻塞式 I/O 解决它,将代码修改为非 阻塞式方式同时读取鼠标和键盘。使用 open()打开得到的文件描述符,调用 open()时指定 O_NONBLOCK 标志将其设置为非阻塞式 I/O;因为标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用 open()打开得到的,那如何将标准输入设置为非阻塞 I/O,可以使用fcntl()函数。通过如下代码将标准输入(键盘) 设置为非阻塞方式:
int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag
fcntl(0, F_SETFL, flag); //重新设置 flag
则代码修改为:
#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);
}
将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码 进行编译,测试结果:
这样就解决了阻塞所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数 据。 虽然使用非阻塞 I/O 方式解决了问题,但由于程序当中使用轮训方式,故而会使得该程序的 CPU 占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用,如何解决这样的问题呢?我们将在下一篇文章向大家介绍。