高级 IO(非阻塞多路复用异步存储映射文件锁)

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级 IO(非阻塞多路复用异步存储映射文件锁)相关的知识,希望对你有一定的参考价值。

目录

非阻塞I/O

关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了CPU 控制权。前面所学习过的函数,譬如wait()、pause()、sleep()等函数都会进入阻塞,本小节来聊一聊关于阻塞式I/O 与非阻塞式I/O。

对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式I/O 。如果是非阻塞式I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误

阻塞I/O 与非阻塞I/O 读文件

本小节我们将分别演示使用阻塞式I/O 和非阻塞式I/O 对文件进行读操作,在调用open()函数打开文件时,参数flags指定O_NONBLOCK 标志,open()调用成功后,后续的I/O 操作将以非阻塞式方式进行;这就是非阻塞I/O 的打开方式,如果参数flags未指定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 temporarilyunavailable",意思就是说资源暂时不可用;原因在于调用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 使用率!

执行示例代码13.1.3 对应的程序时,通过top 命令可以发现该程序的占用了非常高的CPU 使用率,如下所示:

其CPU 占用率几乎达到了100%,在一个系统当中,一个进程的CPU 占用率这么高是一件非常危险的事情。而示例代码13.1.1 这种阻塞式方式,其CPU 占用率几乎为0。

使用非阻塞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 解决它,将示例代码13.1.4 修改为非阻塞式方式同时读取鼠标和键盘。

标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用open()打开得到的,将标准输入设置为非阻塞I/O,可以使用3.10.1 小节中给大家介绍的fcntl()函数==,具体使用方法在该小节中已有详细介绍,这里不再重述!可通过如下代码将标准输入(键盘)设置为非阻塞方式

int flag;
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //将O_NONBLOCK 标志添加到flag
fcntl(0, F_SETFL, flag); //重新设置flag

示例代码13.1.5 演示了以非阻塞方式同时读取鼠标和键盘。

#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);

将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码进行编译,测试结果:

这样就解决了示例代码13.1.4 所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数据。

虽然使用非阻塞I/O 方式解决了示例代码13.1.4 出现的问题,但由于程序当中使用轮训方式,故而会使得该程序的CPU 占用率特别高,终归还是不太安全。

I/O 多路复用

上一小节虽然使用非阻塞式I/O 解决了阻塞式I/O 情况下并发读取文件所出现的问题,但依然不够完美,使得程序的CPU 占用率特别高。解决这个问题,就要用到本小节将要介绍的I/O 多路复用方法。

何为I/O多路复用

I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式I/O 场景中进程或线程阻塞到某个I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用

由此可知,I/O 多路复用一般用于并发式的非阻塞I/O,也就是多路非阻塞I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取

我们可以采用两个功能几乎相同的系统调用来执行I/O 多路复用操作,分别是系统调用select()和poll()。这两个函数基本是一样的,细节特征上存在些许差别!

I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路I/O。

select()函数

系统调用select()可用于执行I/O 多路复用操作,调用select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。其函数原型如下所示:

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set 数据类型是一个文件描述符的集合体。

  • ⚫ readfds 是用来检测读是否就绪(是否可读)的文件描述符集合;
  • ⚫ writefds 是用来检测写是否就绪(是否可写)的文件描述符集合;
  • ⚫ exceptfds 是用来检测异常情况是否发生的文件描述符集合。

Tips:异常情况并不是在文件描述符上出现了一些错误。

fd_set 数据类型是以位掩码的形式来实现的,我们并不需要关心这些细节,因为Linux 提供了四个宏用于对fd_set 类型对象进行操作:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO(),后面介绍。

如果对readfds、writefds 以及exceptfds 中的某些事件不感兴趣,可将其设置为NULL,这表示对相应条件不关心。如果这三个参数都设置为NULL,则可以将select()当做为一个类似于sleep()休眠的函数来使用,通过select()函数的最后一个参数timeout 来设置休眠时间。

  • select()函数的第一个参数nfds 通常表示最大文件描述符编号值加1,考虑readfds、writefds 以及exceptfds这三个文件描述符集合,在3 个描述符集中找出最大描述符编号值,然后加1,这就是参数nfds。

  • select()函数的最后一个参数timeout 可用于设定select()阻塞的时间上限,控制select 的阻塞行为,可将timeout 参数设置为NULL,表示select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;也可将其指向一个struct timeval 结构体对象,该结构体在示例代码5.6.3 有详细介绍,这里不再重述!

如果参数timeout 指向的struct timeval 结构体对象中的两个成员变量都为0,那么此时select()函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。否则,参数timeout 将为select()指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值,select()函数将会返回!

select()函数将阻塞直到有以下事情发生:
⚫ readfds、writefds 或exceptfds 指定的文件描述符中至少有一个称为就绪态;
⚫ 该调用被信号处理函数中断;
⚫ 参数timeout 中指定的时间上限已经超时。

宏:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()

文件描述符集合的所有操作都可以通过这四个宏来完成,这些宏定义如下所示:

#include <sys/select.h>

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

这些宏按照如下方式工作:

⚫ FD_ZERO()将参数set 所指向的集合初始化为空;
⚫ FD_SET()将文件描述符fd 添加到参数set 所指向的集合中;
⚫ FD_CLR()将文件描述符fd 从参数set 所指向的集合中移除;
⚫ 如果文件描述符fd 是参数set 所指向的集合中的成员,则FD_ISSET()返回true,否则返回false。

文件描述符集合有一个最大容量限制,由常量FD_SETSIZE 来决定,在Linux 系统下,该常量的值为1024。在定义一个文件描述符集合之后,必须用FD_ZERO()宏将其进行初始化操作,然后再向集合中添加我们关心的各个文件描述符,例如:

fd_set fset; //定义文件描述符集合

FD_ZERO(&fset); //将集合初始化为空
FD_SET(3, &fset); //向集合中添加文件描述符3
FD_SET(4, &fset); //向集合中添加文件描述符4
FD_SET(5, &fset); //向集合中添加文件描述符5

在调用select()函数之后,select()函数内部会修改readfds、writefds、exceptfds 这些集合,当select()函数返回时,它们包含的就是已处于就绪态的文件描述符集合了。

譬如在调用select()函数之前,readfds 所指向的集合中包含了3、4、5 这三个文件描述符,当调用select()函数之后,假设select()返回时,只有文件描述符4 已经处于就绪态了,那么此时readfds 指向的集合中就只包含了文件描述符4。所以由此可知,如果要在循环中重复调用select(),我们必须保证每次都要重新初始化并设置readfds、writefds、exceptfds 这些集合。

select()函数的返回值

select()函数有三种可能的返回值,会返回如下三种情况中的一种:

返回-1表示有错误发生,并且会设置errno。可能的错误码包括EBADF、EINTR、EINVAL、EINVAL以及ENOMEM,EBADF 表示readfds、writefds 或exceptfds 中有一个文件描述符是非法的;EINTR表示该函数被信号处理函数中断了,其它错误大家可以自己去看,在man 手册都有相信的记录。
返回0表示在任何文件描述符成为就绪态之前select()调用已经超时,在这种情况下,readfds,writefds 以及exceptfds 所指向的文件描述符集合都会被清空。
返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过FD_ISSET()宏进行检查,以此找出发生的I/O 事件是什么。如果同一个文件描述符在readfds,writefds 以及exceptfds 中同时被指定,且它多于多个I/O 事件都处于就绪态的话,那么就会被统计多次,换句话说,select()返回三个集合中被标记为就绪态的文件描述符的总数。

使用示例

示例代码13.2.1 演示了使用select()函数来实现I/O 多路复用操作,同时读取键盘和鼠标。程序中将鼠标和键盘配置为非阻塞I/O 方式,本程序对数据进行了5 次读取,通过while 循环来实现。由于在while 循环中会重复调用select()函数,所以每次调用之前需要对rdfds 进行初始化以及添加鼠标和键盘对应的文件描述符。

该程序中,select()函数的参数timeout 被设置为NULL,并且我们只关心鼠标或键盘是否有数据可读,所以将参数writefds 和exceptfds 也设置为NULL。执行select()函数时,如果鼠标和键盘均无数据可读,则select()调用会陷入阻塞,直到发生输入事件(鼠标移动、键盘上的按键按下或松开)才会返回。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#define MOUSE "/dev/input/event3"
int main(void)

        char buf[100];
        int fd, ret = 0, flag;
        fd_set rdfds;
        int loops = 5;
        /* 打开鼠标设备文件*/
        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
        
        /* 同时读取键盘和鼠标*/
        while (loops--) 
                FD_ZERO(&rdfds);
                FD_SET(0, &rdfds);  //添加键盘
                FD_SET(fd, &rdfds); //添加鼠标
                ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
                if (0 > ret) 
                        perror("select error");
                        goto out;
                
                else if (0 == ret) 
                        fprintf(stderr, "select timeout.\\n");
                        continue;
                
                
                /* 检查键盘是否为就绪态*/
                if(FD_ISSET(0, &rdfds)) 
                        ret = read(0, buf, sizeof(buf));
                        if (0 < ret)
                                printf("键盘: 成功读取<%d>个字节数据\\n", ret);
                
                /* 检查鼠标是否为就绪态*/
                if(FD_ISSET(fd, &rdfds)) 
                        ret = read(fd, buf, sizeof(buf));
                        if (0 < ret)以上是关于高级 IO(非阻塞多路复用异步存储映射文件锁)的主要内容,如果未能解决你的问题,请参考以下文章

IO模型--阻塞IO,非阻塞IO,IO多路复用,异步IO

I/O多路复用之select

{python之IO多路复用} IO模型介绍 阻塞IO(blocking IO) 非阻塞IO(non-blocking IO) 多路复用IO(IO multiplexing) 异步IO

阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一文搞定

python开发IO模型:阻塞&非阻塞&异步IO&多路复用&selectors

python开发IO模型:阻塞&非阻塞&异步IO&多路复用&selectors