Mapping Files into Memory
作为标准文件I/O的一种替代,内核提供了一个接口,允许应用程序将文件映射到内存中,这意味着内存与文件内容之间存在一对一的对应关系。然后,程序员可以通过内存直接访问文件,与任何其他内存块相同。甚至允许写入内存,可以透明地映射回磁盘上的文件。
mmap()
#include
void * mmap(void *addr, size_t len, int prot, int flags,
int fd, off_t offset);
对mmap()的调用要求内核映射文件描述符fd表示的对象的len字节。
从offset字节处开始进入文件,映射进内存。
如果包含addr,则表示在内存中使用地址作为推荐起始。
addr参数向内核提供了映射文件的最佳位置的建议。这只是一个提示;大多数用户传递0。
调用返回映射开始的内存中的实际地址。
访问权限由prot控制。可以或在一起:
- PROT_READ
这几页可以阅读。 - PROT_WRITE
这几页可以写入。 - PROT_EXEC
这几页可以执行。
注意不要和文件的打开方式冲突
其他行为可以由flag提供: - MAP_FIX
指示mmap()将addr视为需求,而不是提示。如果内核无法将映射放置在给定地址,则调用失败。如果地址和长度参数重叠 一个现有的映射,重叠的页面被丢弃并被新的映射所取代。由于此选项需要对进程地址空间有深入的了解,所以它是不可移植的,不建议使用这个。 - MAP_PRIVATE
映射的状态时不被共享的。文件被映射为写复制,并且此进程在内存中所做的任何更改都不会反映在实际文件中,也不会反映在其他进程。 - MAP_SHARED
与映射该文件的所有其他进程共享映射。写入映射相当于写入文件。从映射中读取将反映其他过程对它的写。
映射文件描述符时,文件的引用计数将增加。因此,您可以在映射文件之后关闭文件描述符,您的进程仍然可以访问它。取消映射文件或进程终止时,将导致文件引用计数减少。
//example
void *p;
p = mmap(0, len, PORT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
perror("mmap");
The page size
page是内存管理单元(MMU)的粒度单元。
准确地说,它是最小的内存单位,可以有不同的权限和行为。
获取page大小的标准POSIX方法是使用sysconf(),它可以检索各种特定于系统的信息:
#include
long sysconf(int name);
对sysconf()的调用返回配置项name的值,如果名称无效,则返回−1。
long page_size = sysconf(_SC_PAGESIZE);
Linux还提供了getpagesize()函数:
#include
int getpagesize(void);
对getpagesize()的调用同样将返回页的大小(以字节为单位)。使用甚至比sysconf()更简单:
int page_size = getpagesize();
page大小也静态地存储在宏PAGE_SIZE中,该宏在
int page_size = PAGE_SIZE;
sysconf()方法是可移植性和未来兼容性的最佳选择。
Return values and error codes
成功后,对mmap()的调用返回映射的位置。在失败时,调用返回MAP_FAILED并适当地设置errno。对mmap()的调用永远不会返回0。
Assocaited signals
两个信号与映射区域相关联:
- SIGBUS
这个信号是当进程试图访问一个不再有效的映射区域时产生的,例如,因为文件被截断,在它被映射之后。 - SIGSEGV
此信号是在进程试图写入映射为只读的区域时生成的。
munmap()
Linux提供了munmap()系统调用,用于删除用mmap()创建的映射:
#include
int munmap(void *addr, size_t len);
通常,munmap()用之前的mmap()调用中传递的返回值和len参数。
成功时,munmap()返回0;如果失败,则返回−1,并适当设置errno。
if(munmap(addr, len) == -1)
perror("munmap");
Mapping Example
int mappingExample(int argc, char*argv[]) {
struct stat sb;
off_t len;
char *p;
int fd;
if(argc < 2){
fprintf(stderr, "usage: %s \n", argv[0]);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1){
perror("open");
return 1;
}
if(fstat(fd, &sb) == -1){
perror("fstat");
return 1;
}
if(!S_ISREG(sb.st_mode)){
fprintf(stderr, "%s is not a file\n", argv[1]);
return 1;
}
p = (char*)mmap(0, sb.st_size,
PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
perror("mmap");
return 1;
}
if(close(fd) == -1){
perror("close");
return 1;
}
for(len = 0; len < sb.st_size; ++len)
putchar(p[len]);
if(munmap(p, sb.st_size) == -1){
perror("munmap");
return 1;
}
return 0;
}
int main(int argc, char*argv[])
{
mappingExample(argc, argv);
return 0;
}
Advantages of mmap()
- 读取和写入内存映射文件避免了在使用read()或write()系统调用时发生的无关副本,在这种情况下,数据必须复制到和从用户空间缓冲区复制。
- 除了任何潜在的页面错误之外,读取和写入内存文件不会引起任何系统调用或上下文切换开销。它就像访问内存一样简单。
- 当多个进程将同一个对象映射到内存中,数据在所有进程之间共享。只读映射和共享可写映射是完全共享的;私有可写映射有copy-on-write。
- 在映射周围进行搜索涉及到琐碎的指针操作。不需要lseek()系统调用。
Disadvantages of mmap()
- 内存映射始终是大小为整数的页数。因此,备份文件的大小与整数页数之间的差异被“浪费”为空闲空间。对于小文件, 很大一部分映射可能会被浪费掉。例如,对于4KB页,7字节映射浪费了4089字节。
- 内存映射必须适合进程的地址空间。具有32位地址空间,
大量不同大小的映射会导致地址空间的分割,使得很难找到大的自由毗连区域。当然,这个问题在64位地址空间中就不那么明显了。 - 在内核内创建和维护内存映射和关联数据结构时存在开销。这种开销通常通过消除双重副本来消除,特别是大型和频繁访问的文件。
由于这些原因,mmap()的好处在映射大文件时(因此,任何浪费的空间占映射总数的很小百分比),或者当mmap()的总大小时,最大程度地实现了mmap()的好处。 映射的文件可以被页面大小平分(因此不会浪费空间)。
Resizing a Mapping
Linux提供mremap()系统调用,用于扩展或缩小给定映射的大小。这个函数是linux特定的:
#define _GNU_SOURCE
#include
void *mremap(void *addr, size_t old_size,
size_t new_size, unsigned long flags);
flag参数可以是0,也可以是MREMAP_MAYMOVE,它指定内核可以自由地移动映射(如果需要的话)以执行请求的调整大小。 如果内核可以移动映射,较大的调整大小更有可能导致失败。
Return values and error codes
成功后,mremap()返回指向新调整大小的内存映射的指针。失败时,它返回map_false并设置errno。
Changing the Protection of a Mapping
POSIX定义了mprotect()接口,以允许程序更改现有内存区域的权限:
#include
int mprotect(const void *addr, size_t len, int port);
成功后,mprotect()返回0。如果失败,它返回−1,并将设置errno。
Synchronizing a File with a Mapping
POSIX提供了与fsync()系统调用对应的内存映射。
#include
int msync(void *arrd, size_t len, int flags);
对msync()的调用会将对通过mmap()映射的文件所做的任何更改刷新回磁盘,从而使映射的文件与映射同步。
如果不调用MSync(),就无法保证在文件unmapped之前将脏映射写回磁盘。
flags参数控制同步操作的行为。它是按位或以下值:
- MS_SYNC
指定同步应该同步进行。在将所有页面写回磁盘之前,msync()调用不会返回。 - MS_ASYNC指定同步应该异步发生。更新是按计划的,但是msync()调用会立即返回,而不会等待写操作的发生。
- MS_INVALIDATE
指定映射的所有其他缓存副本都为无效的。对此文件的任何映射的任何未来访问都将反映新同步的磁盘内容。也就是说其他进程对次文件的映射都要重新进行。
成功后,msync()返回0。如果失败,调用将返回−1并设置errno。
Giving Advice on a Mapping
Linux提供了一个名为madvise()的系统调用,让进程向内核提供建议和提示,说明它们打算如何使用映射。
#include
int madvise(void *addr, size_t len, int advice);
如果len为0,内核将于从addr开始的整个映射采用建议。
参数advice描述了建议,它可以是以下内容之一:
- MADV_NORMAL
应用程序没有关于这个内存范围的具体建议。它应该被视为正常。
内核像往常一样运行,执行适量的readahead操作。 - MADV_RANDOM
应用程序打算在指定的范围内随机(非顺序)访问。
内核禁用readahead,只读取每个物理读取操作的最小数据量。 - MADV_SEQUENTIAL
应用程序打算从较低到更高的地址依次访问指定范围内的页面。
内核执行咄咄逼人的readahead。 - MADV_WILLNEED
应用程序打算。 在不久的将来访问指定范围内的页。
内核启动readahead,将给定的页面读入内存。 - MADV_DONTNEED
应用程序不打算在不久的将来访问指定范围内的页面
内核释放与给定页面相关的任何资源,并丢弃任何脏的和尚未同步的页面。对映射数据的后续访问将导致数据从备份文件或(用于匿名映射)零填充所请求的页面。 - MADV_DONTFORK
不会将这些页面复制到子进程中。此标志仅在Linux内核2.6.16及更高版本中可用,在管理DMA页面时是必需的,其他情况很少。 - MADV_DOFORK
撤销MADV_DONTFORK的行为
//typical usage
int ret;
ret = madvise(addr, len, MADV_SEQUENTIAL);
if(ret < 0)
perror("madvise");
成功后,madvise()返回0。如果失败,则返回−1,并适当设置errno。