存储映射 I/O
存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操 作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。
mmap()和 munmap()函数
为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现。其函数原型如下所示:
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
使用该函数需要包含头文件<sys/mman.h>。
函数参数和返回值含义如下:
addr:参数 addr 用于指定映射到内存区域的起始地址。通常将其设置为 NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数 addr 不为 NULL,则表示由自己指定映射区的起始地址,此函数的返回值是该映射区的起始地址。
length:参数 length 指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位,譬如 length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。
offset:文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射;因此参数 offset 和参数 length 确定了文件的起始位置和长度,将文件的这部分映射到内存区域中,如下图所示。
fd:文件描述符,指定要映射到内存区域中的文件。 prot:参数 prot 指定了映射区的保护要求,可取值如下:
- PROT_EXEC:映射区可执行;
- PROT_READ:映射区可读;
- PROT_WRITE:映射区可写;
- PROT_NONE:映射区不可访问。
可将prot指定为PROT_NONE,也可设置为PROT_EXEC、PROT_READ、PROT_WRITE中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限,譬如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。
flags:参数 flags 可影响映射区的多种属性,参数 flags 必须要指定以下两种标志之一:
- MAP_SHARED:此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
- MAP_PRIVATE:此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-on-write)对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。
除此之外,还可将以下标志中的 0 个或多个组合到参数 flags 中,通过按位或运算符进行组合:
- MAP_FIXED:在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定 映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃!通常,不建议使用此标志,因为这不利于移植。
- MAP_ANONYMOUS:建立匿名映射,此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域无法和其它进程共享。
- MAP_ANON:与 MAP_ANONYMOUS 标志同义,不建议使用。
- MAP_DENYWRITE:该标志被忽略。
- MAP_EXECUTABLE:该标志被忽略。
- MAP_FILE:兼容性标志,已被忽略。
- MAP_LOCKED:对映射区域进行上锁。 除了以上标志之外,还有其它一些标志,这里便不再介绍,可通过 man 手册进行查看。在众多标志当中,通常情况下,参数 flags 中只指定了 MAP_SHARED。
返回值:成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,通常使用 MAP_FAILED 来表示,并且会设置 errno 来指示错误原因。
对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,如下所示(以字节为单位):
sysconf(_SC_PAGE_SIZE)
或
sysconf(_SC_PAGESIZE)
虽然对 addr 和 offset 有这种限制,但对于参数 length 长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?对于这个问题的答案,我们首先需要了解到,对于 mmap()函数来说, 当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数 length 并不是页大小的整数倍。如果文件大小为 96 个字节,我们调用 mmap()时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为 0,可以修改后面的这 4000 个字节,但是并不会影响到文件。但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。
对于参数 length 任需要注意,参数 length 的值不能大于文件大小,即文件被映射的部分不能超出文件。
与映射区相关的两个信号
- SIGSEGV:如果映射区被 mmap()指定成了只读的,那么进程试图将数据写入到该映射区时,将会产生 SIGSEGV 信号,此信号由内核发送给进程。该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
- SIGBUS:如果映射区的某个部分在访问时已不存在,则会产生 SIGBUS 信号。例如,调用 mmap() 进行映射时,将参数 length 设置为文件长度,但在访问映射区之前,另一个进程已将该文件截断 (譬如调用 ftruncate()函数进行截断),此时如果进程试图访问对应于该文件已截去部分的映射区, 进程将会受到内核发送过来的SIGBUS 信号,同样,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
munmap()解除映射
通过 open()打开文件,需要使用 close()将将其关闭;同理,通过 mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射,使用 munmap()解除映射关系,其函数原型如下所示:
#include <sys/mman.h>int munmap(void *addr, size_t length);
使用该函数需要包含头文件<sys/mman.h>。
munmap()系统调用解除指定地址范围内的映射,参数 addr 指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数 length 是一个非负整数,指定了待解除映射区域的大小(字节数),被解除映射的区域对应的大小也必须是系统页大小的整数倍,即使参数 length 并不等于系统页大小的整数倍,与 mmap()函数相似。
需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close() 关闭文件时并不会解除映射。
通常将参数 addr 设置为 mmap()函数的返回值,将参数 length 设置为 mmap()函数的参数 length,表示解除整个由 mmap()函数所创建的映射。
使用示例
通过以上介绍,接下来我们编写一个简单地示例代码,使用存储映射 I/O 进行文件复制。
代码演示了使用存储映射 I/O 实现文件复制操作,将源文件中的内容全部复制到另一个目标文件中,其效果类似于 cp 命令。
#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()将源文件映射区中的内容复 制到目标文件映射区中,完成文件复制操作。
接下来我们进行测试,笔者使用当前目录下的 srcfile 作为源文件,dstfile 作为目标文件,先看看源文件 srcfile 的内容,如下所示:
由打印信息可知,程序运行完之后,生成了目标文件 dstfile,使用 cat 命令查看到其内容与源文件 srcfile 相同,本测试程序成功实现了文件复制功能!