Advanced File I/O Mapping Files into Memory

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()

  1. 读取和写入内存映射文件避免了在使用read()或write()系统调用时发生的无关副本,在这种情况下,数据必须复制到和从用户空间缓冲区复制。
  2. 除了任何潜在的页面错误之外,读取和写入内存文件不会引起任何系统调用或上下文切换开销。它就像访问内存一样简单。
  3. 当多个进程将同一个对象映射到内存中,数据在所有进程之间共享。只读映射和共享可写映射是完全共享的;私有可写映射有copy-on-write。
  4. 在映射周围进行搜索涉及到琐碎的指针操作。不需要lseek()系统调用。

Disadvantages of mmap()

  1. 内存映射始终是大小为整数的页数。因此,备份文件的大小与整数页数之间的差异被“浪费”为空闲空间。对于小文件, 很大一部分映射可能会被浪费掉。例如,对于4KB页,7字节映射浪费了4089字节。
  2. 内存映射必须适合进程的地址空间。具有32位地址空间,
    大量不同大小的映射会导致地址空间的分割,使得很难找到大的自由毗连区域。当然,这个问题在64位地址空间中就不那么明显了。
  3. 在内核内创建和维护内存映射和关联数据结构时存在开销。这种开销通常通过消除双重副本来消除,特别是大型和频繁访问的文件。

由于这些原因,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。

你可能感兴趣的:(Advanced File I/O Mapping Files into Memory)