4.3 存储映射
POSIX提供了相关调用,能使我们将文件映射到内存中,借由此机制我们可以方便的从内存中读取文件数据,也可以修改内存中的数据来改变文件内容,或实现父子进程间通信。
4.3.1 mmap()
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr-开发人员建议的映射文件的内存起始地址。一般来说传NULL让系统自行决议。
length-用来映射的内存大小,单位是字节。由于内存最小可寻址单位是页,因此不足一页的长度也会占用一页。
prot-内存区域的访问权限,有如下参数,可以或运算,注意要和打开文件的访问权限一致:
参数 | 含义 |
---|---|
PROT_EXEC | 页内容可以被执行 |
PROT_READ | 页内容可以被读取 |
PROT_WRITE | 页可以被写入 |
PROT_NONE | 页不可访问,无法读写,没有意义 |
注意如果文件的读写权限是只写的,那么无法保证共享内存的功能可用,因为PROT_WRITE隐含了PROT_READ。
flags-共享内存的行为,常见的有如下参数:
参数 | 含义 |
---|---|
MAP_FIXED | 使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。该参数可以用来将不同的文件映射到同一个连续空间内 |
MAP_SHARED | 与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()被调用,文件不保证被更新。 |
MAP_PRIVATE | 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和MAP_SHARED标志是互斥的,只能使用其中一个。使用该标志时内存的访问权限无需与文件访问权限一致 |
MAP_ANONYMOUS/MAP_ANON | 创建一个匿名映射 |
MAP_LOCKED | 将映射分页锁定在内存,防止被交换到交换分区 |
fd-要映射到内存中的文件描述符。
offset-文件描述符的偏移量,必须是页大小的整数倍。
在调用成功后返回指向映射区的指针,失败时返回MAP_FAILED,errno也会被设置,此处不再列举错误码类型。当程序试图访问无效的内存映射区域时,会收到SIGBUS信号;程序试图写入不可写的区域时,收到SIGSEGV信号。
下图显示了信号的可能情况:
2200-4095的区间内写入不能同步到文件,这段区域是为了保证页对齐空余出来的范围。4095-8191是映射的长度,访问这段区域会收到SIGBUS信号,超过8192是未映射的区域,访问这段区域会收到SIGSEGV信号。
4.3.1.1 页大小
前面说到offset参数必须指定为页大小的整数倍(addr参数也一般由系统保证分配,也是页面对齐的,下面相关的调用都是如此,不再赘述)。对于length,不足一页的长度会占用一页,读取多占用部分获得0,向多占用部分写入也不会影响文件。
POSIX规定了通过sysconf调用可以获得页大小:
#include <unistd.h>
long sysconf(int name);//返回name对应的值
long lPageSize = sysconf(_SC_PAGESIZE);//获取页大小,字节
Linux提供了系统调用,也能获取页大小:
#include <unistd.h>
int getpagesize (void);
此外我们还可以使用宏来在编译期获取页大小:
#include <asm/pages.h>
int iPageSize = PAGE_SIZE;
为了二进制文件的移植性考虑,建议还是使用运行期获取页大小的方式,同时将offset设置为0,addr由系统分配。
4.3.2 munmap()
在mmap()打开内存映射后,可以使用munmap()来关闭。
#include <sys/mman.h>
int munmap (void *addr, size_t len);
关闭addr起始,长度是len个字节的映射区域。
当我们mmap()映射一个文件到内存时,该文件的引用计数会增加1,当我们的进程结束、执行exec()系列函数或者关闭内存映射时,文件的引用计数会减1。为了保证数据完整性,在munmap()之前需要调用msync()来写入硬盘。
调用成功返回0,失败返回-1。
4.3.4 mmap()的优点
对比read()/write()等系统调用,mmap()有以下优点:
- read()/write()需要内核在用户空间内存做读写操作,mmap()直接操作内核的页缓冲,可以避免一次数据拷贝,对大文件操作来说优势明显
- 由于对映射的内存区域做操作,因此不像read()/write()那样需要频繁的系统调用
- 多个进程将同一个文件映射到内存可以实现进程间通信
- 在文件上移动读写位置只需要指针操作而不是lseek()
4.3.5 mmap()的缺陷
映射的区域必须是页的整数倍大小,对于映射小文件,很容易造成内存浪费。
使用mmap()来写入文件时,需要提前知道写入文件的大小,不像write()等系统调用能动态扩展文件大小。
4.3.6 调整映射大小
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/mman.h>
void * mremap (void *addr, size_t old_size, size_t new_size, unsigned long flags);
将addr对应的映射从old_size调整到new_size。flags的取值可以是0或者MREMAP_MAYMOVE,0代表不允许内核移动映射区域,MREMAP_MAYMOVE则表示内核可以根据实际情况移动映射区域以找到一个符合new_size大小要求的内存区域。len和prot参数都会被向上取整到页大小的整数倍。调用成功返回映射的地址,失败则返回MAP_FAILED。
4.3.7 改变映射区域的权限
#include <sys/mman.h>
int mprotect (const void *addr, size_t len, int prot);
将len长度,addr起始对应的映射的访问权限设置为prot。该调用在Linux上可以修改任意的内存段,而不仅仅是mmap()创建的,但是要保证addr是页对齐的。prot的取值和mmap()提供的参数一致。
4.3.8 使用映射机制同步文件
我们对可写存储映射的修改在显式调用同步操作之前是不会保证立即同步到文件中的。
#include <sys/mman.h>
int msync (void *addr, size_t len, int flags);
将以addr起始,长度为len的映射立即同步到内存中。flags取值含义如下:
取值 | 含义 |
---|---|
MS_ASYNC | msync调用会立即返回,不等到更新的完成 |
MS_SYNC | msync调用会等到更新完成之后返回 |
MS_INVALIDATE | 在共享内容更改之后,使得文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值。 |
调用成功返回0,失败返回-1。
下面代码演示如何将数据通过存储映射的方式写入硬盘:
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#define WIRTE_SIZE 50
int main ()
{
int fd = open("test.txt", O_CREAT|O_RDWR);
ftruncate (fd, WIRTE_SIZE);//新创建的文件必须要形成空洞文件,否则写入的数据会被放弃
//映射的长度必须大于等于后面访问的范围,否则会收到SIGBUS信号
void * p =mmap(NULL, WIRTE_SIZE, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
perror("mmap");
return -1;
}
memcpy(p, "123", 3);
msync(p, WIRTE_SIZE, MS_SYNC);
return 0;
}
下面是一道练习题:使用mmap()实现命令cp
类似的功能:
//使用方式 a.out xxx xxx
#include <stdio.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
//使用存储映射实现cp命令功能
int main(int argc,char *argv[])
{
if(argc < 3)
{
printf("please start as ‘a.out srcfile destfile‘\n");
return 0;
}
int fd = open(argv[1], O_RDONLY);
if(fd < 0)
{
perror("open");
return -1;
}
//文件存在则不覆盖
/*if(access(argv[2], F_OK) != -1)
{
printf("please change a destfile name\n");
return 0;
}*/
struct stat buf;
fstat(fd, &buf);
printf("file size:%ld\n", buf.st_size);
//不能用creat,因为creat打开文件的模式是只写,无法与mmap配合。前面说了mmap只能和有读权限的文件描述符配合
//int fdDest = creat(argv[2], S_IRWXU);
int fdDest = open(argv[2], O_RDWR|O_CREAT, S_IRWXU);
if(fdDest < 0)
{
perror("creat");
}
if(ftruncate(fdDest, buf.st_size)<0)
{
perror("ftruncate");
return 0;
}
char* s = (char*)mmap(NULL, buf.st_size, PROT_WRITE|PROT_READ, MAP_PRIVATE, fd, 0);
if(s == MAP_FAILED)
{
perror("mmap1");
return 0;
}
char* d = (char*)mmap(NULL, buf.st_size, PROT_WRITE, MAP_SHARED, fdDest, 0);
if(d == MAP_FAILED)
{
perror("mmap2");
return 0;
}
memcpy(d, s, buf.st_size);
if(msync(d, buf.st_size, MS_ASYNC)<0)
{
perror("mmap");
return 0;
}
return 0;
}