Linux 高级IO

学习任务:
高级 I/O:select、poll、epoll、mmap、munmap

要求:
学习高级 I/O 的用法,并实操

1、高级 I/O:
前置知识:
阻塞、I/O 多路复用
PS: 非阻塞 I/O ------ 非阻塞 I/O
阻塞其实就是进入了休眠状态,交出了 CPU 控制权

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

普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作

当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!
所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率
通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也
就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。

I/O 多路复用:
I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。
我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用 select()和 poll()。
这两个函数基本是一样的,细节特征上存在些许差别!

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

///
将以读取鼠标为例
鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下
在这里插入图片描述
通常情况下是 mouseX(X 表示序号 0、1、2),但也不一定,也有可能是 eventX,如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,譬如使用 od 命令:

sudo od -x /dev/input/event2

在这里插入图片描述
如果没有打印信息,那么这个设备文件就不是鼠标对应的设备文件,那么就换一个设备文件再次测试,这样就会帮助你找到鼠标设备文件

以下代码以阻塞方式读取鼠标,调用 open()函数打开鼠标设备文件"/dev/input/event2",以
只读方式打开,没有指定 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/event2", 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()会成功读取到数据并返回
在这里插入图片描述
修改方法很简单,只需在调用 open()函数时指定 O_NONBLOCK 标志即可

执行程序之后,程序立马就结束了,并且调用 read()返回错误,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有发生输入事件),是没有数据可读,故而导致失败返回,这就是非阻塞 I/O
在这里插入图片描述
使用轮询方式不断地去读取,直到鼠标有数据可读,read()将会成功返回

memset(buf, 0, sizeof(buf));for ( ; ; ) {ret = read(fd, buf, sizeof(buf));if (0 < ret) {printf("成功读取<%d>个字节数据\n", ret);close(fd);exit(0);}}

在这里插入图片描述
其 CPU 占用率几乎达到了 100%

/
使用阻塞式 I/O 和非阻塞式 I/O 同时读取鼠标和键盘

读完鼠标读不了键盘

#define MOUSE "/dev/input/event2"
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 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。

用for轮询则与前面一样效果,不管是先动鼠标还是先按键盘都可以成功读取到相应数据:
在这里插入图片描述

为何没打开键盘的设备文件也能读?

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

可以看出 select()函数的参数比较多,其中参数 readfds、writefds 以及 exceptfds 都是 fd_set 类型指针,指向一个 fd_set 类型对象,fd_set 数据类型是一个文件描述符的集合体,所以参数 readfds、writefds 以及exceptfds 都是指向文件描述符集合的指针,这些参数按照如下方式使用:

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

如果这三个参数都设置为 NULL,则可以将 select()当做为一个类似于 sleep()休眠的函数来使用,通过 select()函数的最后一个参数 timeout 来设置休眠时间

  • 返回 -1 表示有错误发生
  • 返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时,在这种情况下,readfds,writefds 以及 exceptfds 所指向的文件描述符集合都会被清空
  • 返回一个正整数表示有一个或多个文件描述符已达到就绪态


select()函数将阻塞直到有以下事情发生:

  • readfds、writefds 或 exceptfds 指定的文件描述符中至少有一个称为就绪态;
  • 该调用被信号处理函数中断;
  • 参数 timeout 中指定的时间上限已经超时。

使用 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/event2"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); //先获取原来的 flagflag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flagfcntl(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)printf("鼠标: 成功读取<%d>个字节数据\n", ret);}}
out:/* 关闭文件 */close(fd);exit(ret);
}

flag = fcntl(0, F_GETFL); :获取标准输入(键盘,文件描述符为 0)的当前文件状态标志,并存储在flag变量中。
flag |= O_NONBLOCK; :通过按位或运算将O_NONBLOCK标志添加到flag变量中,从而将键盘设置为非阻塞模式。
fcntl(0, F_SETFL, flag); :使用更新后的flag变量重新设置标准输入(键盘)的文件状态标志,完成非阻塞模式的设置

FD_ZERO(&rdfds);:初始化rdfds集合,将其中所有的文件描述符位清零,表示初始时没有任何文件描述符在集合中。
FD_SET(0, &rdfds);:将标准输入(键盘,文件描述符为 0)添加到rdfds集合中,表示要监视键盘是否有数据可读。
FD_SET(fd, &rdfds);:将鼠标设备文件的文件描述符fd添加到rdfds集合中,表示要监视鼠标是否有数据可读。

select函数用于监视多个文件描述符的状态变化。这里select函数的参数含义如下:
fd + 1:select函数监视的文件描述符范围是0到fd(包括fd),这里fd是鼠标设备文件的文件描述符,fd + 1指定了监视范围的上限。
&rdfds:指向包含要监视的可读文件描述符集合的指针,这里是rdfds,它包含了键盘(文件描述符 0)和鼠标(文件描述符fd)。
NULL(后三个参数):分别用于指定可写文件描述符集合、异常文件描述符集合和超时时间,这里都不需要,所以设置为NULL。

select函数会阻塞,直到rdfds集合中的某个文件描述符有数据可读,或者发生错误,它返回值ret表示准备好的文件描述符数量。

本程序对数据进行了 5 次读取,通过 while 循环来实现。由于在 while 循环中会重复调用 select()函数,所以每次调用之前需要对 rdfds 进行初始化以及添加鼠标和键盘对应的文件描
述符。

该程序中,select()函数的参数 timeout 被设置为 NULL,并且我们只关心鼠标或键盘是否有数据可读,所以将参数 writefds 和 exceptfds 也设置为 NULL。执行 select()函数时,如果鼠标和键盘均无数据可读,则select()调用会陷入阻塞,直到发生输入事件(鼠标移动、键盘上的按键按下或松开)才会返回。
在这里插入图片描述
select()函数的返回值 ret,只有当 ret 大于 0 时才表示有文件描述符处于就绪态,并将这些
处于就绪态的文件描述符通过 rdfds 集合返回出来,程序中使用 FD_ISSET()宏检查返回的rdfds 集合中是否包含鼠标文件描述符以及键盘文件描述符,如果包含则表示可以读取数据了

PS:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()
文件描述符集合的所有操作都可以通过这四个宏来完成

  • 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

PS:fcntl 函数
fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。

int fcntl(int fd, int cmd, ... /* arg */ )

在这里插入图片描述列举出来,并不需要全部学会每一个 cmd 的作用

fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使用
(例如:cmd=F_GETFL时不需要传入第三个参数,返回值成功表示获取到的文件状态标志;cmd=F_SETFL 时,需要传入第三个参数,此参数表示需要设置的文件状态标志)

执行失败情况下,返回 -1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命
令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等

fcntl 使用示例
(1)复制文件描述符 F_DUPFD
(2)获取/设置文件状态标志 F_GETFL

1.2 poll
系统调用 poll()与 select()函数很相似,但函数接口有所不同。在 select()函数中,我们提供三个 fd_set 集合,在每个集合中添加我们关心的文件描述符;而在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

在这里插入图片描述

struct pollfd 结构体
struct pollfd {
int fd; /* file descriptor /
short events; /
requested events /
short revents; /
returned events */
};

fd 是一个文件描述符,struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件。当 poll()函数返回时,revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件(注意,poll()没有更改 events 变量),我们可以对 revents 进行检查,判断文件描述符 fd 发生了什么事件。

poll 的 events 和 revents 标志:
在这里插入图片描述
第一组标志(POLLIN、POLLRDNORM、POLLRDBAND、POLLPRI、POLLRDHUP)与
数据可读相关;第二组标志(POLLOUT、POLLWRNORM、POLLWRBAND)与可写数据相关;而第三组标志(POLLERR、POLLHUP、POLLNVAL)是设定在 revents 变量中用来返回有关文件描述符的附加信息,如果在 events 变量中指定了这三个标志,则会被忽略

如果我们对某个文件描述符上的事件不感兴趣,则可将 events 变量设置为 0;另外,将 fd 变量设置为文件描述符的负值(取文件描述符 fd 的相反数-fd),将导致对应的 events 变量被 poll()忽略,并且 revents变量将总是返回 0,这两种方法都可用来关闭对某个文件描述符的检查

在实际应用编程中,一般用的最多的还是 POLLIN 和 POLLOUT
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define MOUSE "/dev/input/event3"
int main(void)
{char buf[100];int fd, ret = 0, flag;int loops = 5;struct pollfd fds[2];/* 打开鼠标设备文件 */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); //重新设置 flag/* 同时读取键盘和鼠标 */fds[0].fd = 0;fds[0].events = POLLIN; //只关心数据可读fds[0].revents = 0;fds[1].fd = fd;fds[1].events = POLLIN; //只关心数据可读fds[1].revents = 0;while (loops--) {ret = poll(fds, 2, -1);if (0 > ret) {perror("poll error");goto out;}else if (0 == ret) {fprintf(stderr, "poll timeout.\n");continue;}/* 检查键盘是否为就绪态 */if(fds[0].revents & POLLIN) {ret = read(0, buf, sizeof(buf));if (0 < ret)printf("键盘: 成功读取<%d>个字节数据\n", ret);}/* 检查鼠标是否为就绪态 */if(fds[1].revents & POLLIN) {ret = read(fd, buf, sizeof(buf));if (0 < ret)printf("鼠标: 成功读取<%d>个字节数据\n", ret);}}out:/* 关闭文件 */close(fd);exit(ret);
}

struct pollfd 结构体的 events 变量和 revents 变量都是位掩码,所以可以使用"revents & POLLIN"按位与的方式来检查是否发生了相应的 POLLIN 事件,判断鼠标或键盘数据是否可读

/
1.3epoll
/
使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在;譬如示例代码中,调用 select()函数监测鼠标和键盘这两个文件描述符,当 select()返回时,通过 FD_ISSET()宏判断文件描述符上是否可执行 I/O 操作;如果可以执行 I/O 操作时,应在应用程序中对该文件描述符执行 I/O 操作,以清除文件描述符的就绪态,如果不清除就绪态,那么该状态将会一直存在,那么下一次调用 select()时,文件描述符已经处于就绪态了,将直接返回。
同理对于 poll()函数来说亦是如此,当 poll()成功返回时,检查文件描述符是否称为就绪态,如果文件描述符上可执行 I/O 操作时,也需要对文件描述符执行 I/O 操作,以清除就绪状态。

异步 IO
在 I/O 多路复用中,进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。所以要使用异步 I/O,还得结合前面所学习的信号相关的内容,所以异步 I/O 通常也称为信号驱动 I/O

存储映射 I/O
存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。

1.4mmap
为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

在这里插入图片描述
参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系
统页大小的整数倍,可通过 sysconf()函数获取页大小

存储映射 I/O 示意图
在这里插入图片描述
虽然对 addr 和 offset 有这种限制,但对于参数 length 长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?
对于这个问题的答案,我们首先需要了解到,对于 mmap()函数来说,当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数 length并不是页大小的整数倍。如果文件大小为 96 个字节,我们调用 mmap()时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为0,可以修改后面的这 4000 个字节,但是并不会影响到文件。但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。

对于参数 length 任需要注意,参数 length 的值不能大于文件大小,即文件被映射的部分不能超出文件。

1.5munmap

通过 open()打开文件,需要使用 close()将将其关闭;同理,通过 mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射,使用 munmap()解除映射关系

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc, char *argv[])
{int srcfd, dstfd;void *srcaddr;void *dstaddr;int ret;struct stat sbuf;if (3 != argc) {fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);exit(-1);}/* 打开源文件 */srcfd = open(argv[1], O_RDONLY);if (-1 == srcfd) {perror("open error");exit(-1);}/* 打开目标文件 */dstfd = open(argv[2], O_RDWR |O_CREAT | O_TRUNC, 0664);if (-1 == dstfd) {perror("open error");ret = -1;goto out1;}/* 获取源文件的大小 */fstat(srcfd, &sbuf);/* 设置目标文件的大小 */ftruncate(dstfd, sbuf.st_size);/* 将源文件映射到内存区域中 */srcaddr = mmap(NULL, sbuf.st_size,PROT_READ, MAP_SHARED, srcfd, 0);if (MAP_FAILED == srcaddr) {perror("mmap error");ret = -1;goto out2;}/* 将目标文件映射到内存区域中 */dstaddr = mmap(NULL, sbuf.st_size,PROT_WRITE, MAP_SHARED, dstfd, 0);if (MAP_FAILED == dstaddr) {perror("mmap error");ret = -1;goto out3;}/* 将源文件中的内容复制到目标文件中 */memcpy(dstaddr, srcaddr, sbuf.st_size);/* 程序退出前清理工作 */out4:/* 解除目标文件映射 */munmap(dstaddr, sbuf.st_size);out3:/* 解除源文件映射 */munmap(srcaddr, sbuf.st_size);out2:/* 关闭目标文件 */close(dstfd);out1:/* 关闭源文件并退出 */close(srcfd);exit(ret);
}

当执行程序的时候,将源文件和目标文件传递给应用程序,该程序首先会将源文件和目标文件打开,源文件以只读方式打开,而目标文件以可读、可写方式打开,如果目标文件不存在则创建它,并且将文件的大小截断为 0。
然后使用 fstat()函数获取源文件的大小,接着调用 ftruncate()函数设置目标文件的大小与源文件大小保持一致。
然后对源文件和目标文件分别调用 mmap(),将文件映射到内存当中;对于源文件,调用mmap()时将参数 prot 指定为 PROT_READ,表示对它的映射区会进行读取操作;对于目标文件,调用 mmap()时将参数 port指定为 PROT_WRITE,表示对它的映射区会进行写入操作。最后调用 memcpy()将源文件映射区中的内容复制到目标文件映射区中,完成文件的复制操作。

普通 I/O 与存储映射 I/O 比较:

普通 I/O 方式的缺点
普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写,使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。同样使用标准 I/O(库函数 fread()、fwrite())也是如此,本身标准 I/O 就是对普通 I/O 的一种封装。
那既然效率较低,为啥还要使用这种方式呢?原因在于,只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的 I/O 方式还是非常方便的

存储映射 I/O 的优点
存储映射 I/O 的实质其实是共享,与 IPC 之内存共享 (IPC共享内存是一种允许不同进程访问同一段物理内存的机制) 很相似。譬如执行一个文件复制操作来说,对于普通 I/O 方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中

普通 I/O 实现文件复制示例图:
在这里插入图片描述
存储映射 I/O 实现文件复制:
在这里插入图片描述
使用存储映射 I/O 减少了数据的复制操作,所以在效率上会比普通 I/O 要高,其次上面也讲了,普通 I/O 中间涉及到了很多的函数调用过程,这些都会导致普通 I/O 在效率上会比存储映射 I/O 要低。

前面提到存储映射 I/O 的实质其实是共享,如何理解共享呢?其实非常简单,我们知道,应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用库函数与内核进行数据交互,包括操作硬件。通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区直接将磁盘文件直接与映射区关联起来,不用调用 read()、write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。

存储映射 I/O 的不足
存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的区域已经在调用mmap()函数时通过 length 参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍,譬如映射文件的大小为 96 字节,假定系统页大小为 4096 字节,那么剩余的 4000 字节全部填充为 0,虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来,由此可知,使用存储映射 I/O 在进行大数据量操作时比较有效;对于少量数据,使用普通 I/O 方式更加方便!

存储映射 I/O 会在视频图像处理方面用的比较多
(Framebuffer 编程,就是 LCD 编程,就会用到存储映射 I/O)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/59550.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JAVA WEB — HTML CSS 入门学习

本文为JAVAWEB 关于HTML 的基础学习 一 概述 HTML 超文本标记语言 超文本 超越文本的限制 比普通文本更强大 除了文字信息 还可以存储图片 音频 视频等标记语言 由标签构成的语言HTML标签都是预定义的 HTML直接在浏览器中运行 在浏览器解析 CSS 是一种用来表现HTML或XML等文…

ASRPRO 日历2

为避免与天问的ID冲突 ID前加10000 为使识别更顺畅 将 日期-月份 12月21日 合并 ;时间 10点25分 合并 通过串口获取日期 为使用常用词 计倒时 下周 明天,需通过串口获取当前日期 + 命令词 增加 我的 A的 B的 关系词 与任务 生日 买菜 增加 可自定义 任务 执行程序 双进…

Linux——Linux基础指令

Linux基本指令 文章目录 Linux基本指令1. 基础五指令(1) whoami(2) who(3) pwd(4) ls(5) clear 2. 文件常见命令(1) touch(2) mkdir(3) cp(4) mv(5) rm(6) cd 3. 常见IO命令(1) cat(2) tac(3) head(4) tail(5) more(6) less 4. 拓展命令(1) man手册(2) which(3) file(4) date(5…

雷池社区版 7.1.0 LTS 发布了

LTS&#xff08;Long Term Support&#xff0c;长期支持版本&#xff09;是软件开发中的一个概念&#xff0c;表示该版本将获得较长时间的支持和更新&#xff0c;通常包含稳定性、性能改进和安全修复&#xff0c;但不包含频繁的新特性更新。 作为最受欢迎的社区waf&#xff0c…

出海企业如何借助云计算平台实现多区域部署?

云计算de小白 如需进一步了解&#xff0c;请单击链接了解有关 Akamai 云计算的更多信息 在本文中我们将告诉大家如何在Linode云计算平台上借助VLAN快速实现多地域部署。 首先我们需要明确一些基本概念和思想&#xff1a; 部署多区域 VLAN 为了在多区域部署中在不同的 VLAN …

RDD转换算子:【map】

功能&#xff1a; 对RDD中每个元素调用一次参数中的函数&#xff0c;并将每次调用的返回值放入一个新的RDD中&#xff08;一对一&#xff09; 语法&#xff1a; def map(self , f: T -> U ) -> RDD[U]f&#xff1a;代表参数是一个函数 T&#xff1a;代表RDD中的每个元…

如何更好的crud

一、DDD是什么&#xff1f; DDD全名叫做Domins drives Design&#xff1b;领域驱动设计。再说的通俗一点就是&#xff1a;通过领域建模的方式来实现软件设计。 问题来了&#xff1a;什么是软件设计&#xff1f;为什么要进行软件设计&#xff1f; 软件开发最主要的目的就是&…

AI赋能酒店设计|莱佛士学生成功入围WATG设计大赛

近日&#xff0c;由Wimberly Allison Tong & Goo&#xff08;WATG&#xff09;主办的“用人工智能重新构想酒店行业的未来”设计比赛正式拉开帷幕。这场设计比赛&#xff0c;不仅是为了庆祝WATG即将步入80周年&#xff0c;更是为了激发年轻设计师们的创造力和探索实践精神&…

Netty原来就是这样啊(二)

前言: Netty其实最大的特点就是在于对于对NIO进行了进一步的封装,除此以外Netty的特点就是在于其的高性能 高可用性,下面就会一一进行说明。 高性能: 我在Netty原来就是这样啊(一)-CSDN博客 解释了其中的零拷贝的技术除此以外还有Reactor线程模型,这个Reactor线程模型的思想…

对于相对速度的重新理解

狭义相对论速度合成公式如下&#xff0c; 现在让我们尝试用另一种方式把它推导出来。 我们先看速度的定义&#xff0c; 常规的速度合成方式如下&#xff0c; 如果我们用速度的倒数来理解速度&#xff0c; 原来的两个相对速度合成&#xff0c; 是因为假定了时间单位是一样的&am…

idea 导入Spring源码遇到的坑并解决

1.下载相关文件 通过百度网盘分享的文件&#xff1a;Spring 链接&#xff1a;https://pan.baidu.com/s/1r9rkGOCaY9SFn9ecng5cIg?pwd8888 提取码&#xff1a;8888 2.配置gradle环境 gradle下载地址 需要翻墙下 https://services.gradle.org/distributions/ 我选择的是 grad…

红队-linux基础(1)

声明 通过学习 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频,做出的文章如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 一.openssl 1、openssl passwd -1 123 openssl是一个开源的…

迈入国际舞台,AORO M8防爆手机获国际IECEx、欧盟ATEX防爆认证

近日&#xff0c;深圳市遨游通讯设备有限公司&#xff08;以下简称“遨游通讯”&#xff09;旗下5G防爆手机——AORO M8&#xff0c;通过了CSA集团的严格测试和评估&#xff0c;荣获国际IECEx及欧盟ATEX防爆认证证书。2024年11月5日&#xff0c;CSA集团和遨游通讯双方领导在遨游…

[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作空洞骑士独有的EventSystem和InputModule总结 前言 hello大家好久没见&#xff0c;之所以隔了这么久才更新并不是因为我又放弃了这个项目&#xff0c;而…

你们要的App电量分析测试来了

Batterystats 是包含在 Android 框架中的一种工具&#xff0c;用于收集设备上的电池数据。您可以使用 adb 将收集的电池数据转储到开发计算机&#xff0c;并创建一份可使用 Battery Historian 分析的报告。Battery Historian 会将报告从 Batterystats 转换为可在浏览器中查看的…

<项目代码>YOLOv8 学生课堂行为识别<目标检测

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

如何在家庭网络中设置静态IP地址:一份实用指南

在家庭网络环境中&#xff0c;IP地址扮演着至关重要的角色。大多数家庭用户依赖路由器的DHCP&#xff08;动态主机配置协议&#xff09;来自动分配IP地址&#xff0c;但在某些情况下&#xff0c;手动设置静态IP地址能为家庭网络带来更多的便利性与稳定性&#xff0c;尤其是在涉…

编译cartographer和cartographer_ros 过程

环境 ros 版本 : noetic 工控机版本: firefly 工控机cpu类型: arm64 工控机系统: ubuntu 20.04 关于cartographer 如果是ros1中cartographer和cartographer_ros 都需要编译安装&#xff0c;并且在实际运行中cartographer和cartographer_ros 是有交互的。 而如果是在ros2中只…

vue系列==vue组件

vue系列vue组件 1、组件样式控制 1.1全局样式控制 1.2局部样式控制 1.3深度样式控制 2、组件通信 2.1组件父与子之间的通信props 2.1.1简单数组接收模式 2.1.2简单对象和复杂对象接受模式 2.2 组件通信之ref与defineExpose ref 的作用 defineExpose 的作用 运用 re…

智慧水肥一体化:道品科技现代农业的智能管理模式

智慧水肥一体化是现代农业中一种重要的管理模式&#xff0c;它通过信息技术和物联网技术的结合&#xff0c;实现对水资源和肥料的智能化管理。这一系统的主要功能包括环境监测、集中管理、智能控制、主动报警和数据管理。以下将分别对这些功能进行详细阐述&#xff0c;并探讨智…