存储映射I/O(memory-mapped I/O)是一种基于内存区域的高级I/O操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行read操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行write操作)。这样就可以在不使用系统I/O操作函数read和write的情况下执行I/O操作。
普通I/O方式一般是通过调用read和write函数来实现对文件的读写,使用read和 write读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间传递,效率会比较低。
对于存储映射I/O来说,由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映射区来实现文件复制。使用存储映射I/O减少了数据的复制操作,所以在效率上会比普通I/O要高。
然而只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的I/O方式还是非常方便的。
1.4.4.1 mmap
用于将一个给定的文件映射到进程地址空间中的一块内存区域中。
1. 头文件
#include <sys/mman.h>
2. 函数原型
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
3. 参数
1)addr:用于指定映射到内存区域的起始地址。通常将其设置为NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数addr不为NULL,则表示由自己指定映射区的起始地址,此函数的返回值是该映射区的起始地址。
2)length:指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位。
3)prot:指定了映射区的保护要求,可取值如下:
⚫PROT_EXEC:映射区可执行;
⚫PROT_READ:映射区可读;
⚫PROT_WRITE:映射区可写;
⚫PROT_NONE:映射区不可访问。
可将 prot 指定为为 PROT_NONE,也可将其设置为PROT_EXEC、PROT_READ、PROT_WRITE 中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限。
4)flags:可影响映射区的多种属性,参数flags必须要指定以下两种标志之一:
⚫MAP_SHARED:此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
⚫MAP_PRIVATE:此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-onwrite),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。
除此之外,还可将以下标志中的0个或多个组合到参数flags中,通过按位或运算符进行组合:
⚫MAP_FIXED:在未指定该标志的情况下,如果参数addr不等于 NULL,表示由调用者自己指定映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了MAP_FIXED标志,则表示要求必须使用参数addr指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃。通常,不建议使用此标志,因为这不利于移植。
⚫MAP_ANONYMOUS:建立匿名映射,此时会忽略参数fd和 offset,不涉及文件,而且映射区域无法和其它进程共享。
⚫MAP_ANON:与MAP_ANONYMOUS 标志同义,不建议使用。
⚫MAP_LOCKED:对映射区域进行上锁。
5)fd:指定要映射到内存区域中的文件。
6)offset:文件映射的偏移量,通常将其置为0,表示从文件头部开始映射。
所以参数 offset 和参数 length 就确定了文件的起始位置和长度,将文件的这部分映射到内存区域中。
参数addr和offset在不为NULL和0的情况下,addr和offset的值通常被要求是系统页大小的整数倍,可通过sysconf函数获取页大小:
sysconf(_SC_PAGE_SIZE) 或 sysconf(_SC_PAGESIZE)
需要注意参数length的值不能大于文件大小,即文件被映射的部分不能超出文件。
4. 返回值
成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,通常使用MAP_FAILED来表示,并且会设置errno来指示错误原因。
1.4.4.2 munmap
用于解除存储映射。
1. 头文件
#include <sys/mman.h>
2. 函数原型
int munmap(void *addr, size_t length);
3. 参数
addr:指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍。
length:指定了待解除映射区域的大小(字节数)。
需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用munmap),但调用close关闭文件时并不会解除映射。
通常将参数addr设置为mmap函数的返回值,将参数length设置为mmap函数的参数length,表示解除整个由mmap函数所创建的映射。
4. 返回值
会返回一个指向描述该错误的字符串的指针。
5. 示例
基于前面2.1.7练习的程序,将其改为存储映射的方式。
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <stdio.h> #include <stdlib.h> #include <string.h> //strlen的头文件 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> int main() { int fda, fdb; char buf[30] = "123456\n"; size_t length = strlen(buf); void *addrA, *addrB; fda = open("./testA", O_RDWR | O_CREAT | O_TRUNC, 0777); if ( fda < 0 ) { printf("error: A open\n"); return -1; } if ( write(fda, buf, strlen(buf)) != strlen(buf) ) { //通过strlen计算buf的实际字节长度 printf("error: testA write\n"); close(fda); return -1; } fdb = open("./testB", O_RDWR | O_CREAT | O_TRUNC, 0777); if ( fdb < 0 ) { printf("error: testB open\n"); close(fda); return -1; } ftruncate(fdb,length); //由于是存储映射拷贝,需要设置testB文件的长度 addrA = mmap(NULL, length, PROT_READ, MAP_SHARED, fda, 0); if (addrA == MAP_FAILED) { perror("mmap A"); close(fda); close(fdb); return -1; } addrB = mmap(NULL, length, PROT_WRITE, MAP_SHARED, fdb, 0); if (addrB == MAP_FAILED) { perror("mmap B"); close(fda); close(fdb); munmap(addrA, length); return -1; } memcpy(addrB, addrA, length); //内存数据拷贝 munmap(addrA, length); munmap(addrB, length); close(fda); close(fdb); return 0; } |
6.编译运行并查看测试结果
|---------------------------------------|
| cat testA 123456 cat testB 123456 |
存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的区域已经在调用mmap函数时通过length参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍,比如映射文件的大小为100字节,假定系统页大小为1000字节,那么剩余的900字节全部填充为0,虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来,由此可知,使用存储映射I/O在进行大数据量操作时比较有效;对于少量数据,使用普通I/O方式更加方便。