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)