linux c内存映射

2018-06-19 15:47:54

内存映射一般是用来做ipc进程间通信的,类似于一种共享内存技术。内存映射分为4种(假设有进程A和进程B):

1.私有文件映射:2个进程都以一个文件中的内容来初始化内存内容,共享同一物理内存页,但是2个进程之后对该内存的修改,相互是不可见的,而且对映射的内容反映到文件。系统利用的是一种写时复制技术。

2.私有匿名映射:子进程继承父进程的映射,但是父子进程对其的修改双方都是不可见的。也是利用写时复制技术,一般在分配大块内存时用代替malloc()。

3.共享文件映射:对比于私有文件映射,内容修改2个进程可见(但是不保证进程A对其修改,进程B立即就能看到变化),共享所有内存页。这个一般用于无关进程间的通信。

4.共享匿名映射:对比于私有匿名映射,父子进程对该映射的内容都可见(但是不保证进程A对其修改,进程B立即就能看到变化)。

一个进程在执行 exec() 时映射会丢失。通过Linux特有的/proc/PID/maps 文件能够查看映射有关的所有信息。


创建一个映射:mmap()

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

//Returns starting address of mapping on success, or MAP_FAILED on error

addr 参数指定了映射被放置的虚拟地址,一般设为NULL,内核会帮我们选合适的地址,指定了,内核会作为参考。
length 参数指定映射的字节数,但是内核分配的实际内存大小为length向上提升为分页大小的整数倍。
prot 参数是一个位掩码,它指定了施加于映射之上的保护信息,一旦违反了这些保护位,进程会收到 SIGSEGV 信号。
标记为 PROT_NONE 的分页内存的一个用途是作为一个进程分配的内存区域的起始位置或结束位置的守护分页。如果进程意外的访问了其中一个被标记为PROT_NONE的分页,那么内核会生成SIGSEGV 信号。 使用 mprotect() 系统调用能够修改内存保护位。

 值描述 
 PROT_NONE 区域无法访问
 PROT_READ 区域内容可读取
 PROT_WRITE 区域内容可修改
 PROT_EXEC 区域内容可执行

flags 参数是一个控制映射操作各个方面的选项的位掩码。

MAP_PRIVATE--私有映射
MAP_SHARED--共享映射
MAP_ANONYMOUS--匿名映射
MAP_FIXED--原样解释 addr 参数

剩余的参数 fd 和 offset 是用于文件映射(匿名映射将忽略它们)。fd 参数是一个标记被映射的文件描述符。offset 参数指定了映射在文件中的起点,它必须是系统分页大小的倍数。

例子-1

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>

#include <sys/mman.h>

void errExit(char *msg){
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[]){
    char *addr;
    int fd;
    struct stat sb;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s file\n", argv[0]);
        return 0;
    }
        

    fd = open(argv[1], O_RDONLY);
    if (fd == -1)
        errExit("open");


    if (fstat(fd, &sb) == -1)
        errExit("fstat");

    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size){
        printf("partial/failed write");
        return 1;
    }
        
    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# echo 'url:http://www.freecls.com' > tmp.txt
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out tmp.txt 
url:http://www.freecls.com


解除映射区域:munmap()

执行与mmap() 相反的操作,即从调用进程的虚拟地址空间中删除一个映射。

#include <sys/mman.h>

int munmap(void *addr, size_t length);

//Returns 0 on success, or –1 on error

addr 参数是待解除映射的地址范围的起始地址,它必须与一个分页边界对齐。
length 参数是一个非负整数,它指定了待解除映射区域的大小。范围为系统分页大小的

通常来讲会解除整个映射。因此,addr 指定为上一个 mmap() 调用返回的地址,并且 length 的值与 mmap() 调用中使用的 length 的值一样。

addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
    errExit("mmap");

if (munmap(addr, length) == -1)
    errExit("munmap");

或者也可以解除一个映射中的部分映射,这样原来的映射要么会收缩,要么会被分成两个,这取决于在何处开始解除映射。还可以指定一个跨越多个映射的地址范围,这样的话所有在范围内的映射都会被解除。如果在由 addr 和 length 指定的地址范围中不存在映射,那么 munmap() 将不起任何作用并返回 0 (表示成功)。

在解除映射期间,内核会删除进程持有的在指定地址范围内的所有内存锁。(内存锁是通过 mlock() 或 mlockall() 来建立的,后面文章可能会介绍。)当一个进程终止或执行了一个 exec() 之后进程中所有的映射会自动被解除。为确保一个共享文件映射的内容会被写入到底层文件中,在使用 munmap() 解除一个映射之前需要调用 msync()。


文件映射

要创建一个文件映射需要执行下面的步骤。

1.获取文件的一个描述符,通常通过调用 open() 来完成。

2.将文件描述符作为 fd 参数传入 mmap() 调用。

执行上述步骤之后 mmap() 会将打开的文件的内容映射到调用进程的地址空间中。一旦 mmap() 被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响。

除了普通的磁盘文件,使用 mmap() 还能够映射各种真实和虚拟设备的内容,如硬盘、光盘以及/dev/mem。

在打开描述符 fd 引用的文件时必须要具备与 prot 和 flags 参数值匹配的权限。特别地,文件必须总是被打开以允许读取,并且如果在 flags 中指定了 PROT_WRITE 和 MAP_SHARED, 那么文件必须总是被打开以允许读取和写入。 

offset 参数指定了从文件区域中的哪个字节开始映射,它必须是系统分页大小的倍数。将 offset 指定为 0 会导致从文件的起始位置开始映射。length 参数指定了映射的字节数。 offset 和 length 参数一起确定了文件的哪个区域会被映射进内存,如图。

在linux 上,一个文件映射的分页会在首次被访问的时候才会被加载进内存。所以当调用mmap()之后,在访问映射区域之前,该文件被修改了,那么后面当要访问这个映射时,文件内容被加载进内存,进程看到的内容就已经跟调用mmap()时看到的内容不一样。


共享文件映射

当多个进程创建了同一个文件区域的共享映射时,它们会共享同样的内存物理分页。此外,对映射内容的变更将会反应到文件上。实际上,这个文件被当成了该块内存区域的分页存储,如图。(这幅图是简化过的,它并没有指出映射分页在物理内存中通常是不连续的这个事实。)

共享文件映射一般有两个用途:内存映射I/O和 IPC。

内存映射I/O

就是用共享文件映射技术代替传统的read(),write()操作。优势是,它可以节省内和空间和用户空间之间的一次传输,也可以减少所需使用的内存来提升性能。

内存映射io所带来的性能优势在大型文件中执行重复随机访问时最能体现出来。如果顺序的访问一个文件且io缓冲区足够大以至于能够避免执行大量的io系统调用,那么性能相差不大。

内存映射io也有一些缺点,对于小量数据io来讲,内存映射io的开销(映射,分页故障,解除映射等等)实际上要比简单的read() 或 write() 大。

IPC

由于使用同样文件区域的共享映射的进程共享同样的内存物理分页,因此可以充当共享内存技术来实现进程间通信。

例子

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>

#include <sys/mman.h>

#define MEM_SIZE 30

void errExit(char *msg){
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[]){
    char *addr;
    int fd;

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s file [new-value]\n", argv[0]);
        return 0;
    }
        

    fd = open(argv[1], O_RDWR);
    if (fd == -1)
        errExit("open");

    addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (close(fd) == -1)                /* No longer need 'fd' */
        errExit("close");

    printf("Current string=%.*s\n", MEM_SIZE, addr);
                        /* Secure practice: output at most MEM_SIZE bytes */

    if (argc > 2) {
        if (strlen(argv[2]) >= MEM_SIZE){
            printf("'new-value' too large\n");
        }

        memset(addr, 0, MEM_SIZE);
        strncpy(addr, argv[2], MEM_SIZE - 1);
        if (msync(addr, MEM_SIZE, MS_SYNC) == -1)
            errExit("msync");

        printf("Copied \"%s\" to shared memory\n", argv[2]);
    }

    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# dd if=/dev/zero of=tmp.txt bs=1 count=1024
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out tmp.txt free
Current string=
Copied "free" to shared memory
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out tmp.txt freecls
Current string=free
Copied "freecls" to shared memory
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out tmp.txt
Current string=freecls


边界情况

上图没啥好解释的,6000不是系统分页大小的整数倍,所以会扩充到8192。


上图情况略复杂,映射值超过了文件结尾。比如文件大小2200,它还是会扩充到4096大小,途中2200-4095字节是可以访问和修改的,但是不会同步到文件。


同步映射区域:msync()

内核会自动将发生在 MAP_SHARED 映射内容上的变更写入到底层文件中,但在默认情况下,内核不保证这种同步何时发生。

调用msync 会强制将数据写入到磁盘上,还允许将一个应用程序确保在可写入映射上发生的更新会对该文件上执行 read() 的其他进程可见。

#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);

//Returns 0 on success, or –1 on error

addr 为起始地址,必须分页对齐,length会被向上舍入到系统分页大小的下一个整数倍。

flags 参数可取值为下列值中的一个。

MS_SYNC 执行一个同步的文件写入。这个调用会阻塞直到内存区域中所有被修改过的分页被写入到磁盘。

MS_ASYNC 执行一个异步文件写入。内存区域中被修改过的分页会在后面某个时刻被写入磁盘并立即对在相应文件区域执行read() 的其他进程可见()。换句话说,内存区域仅仅是与内核高速缓冲区同步。想要更快的刷新的磁盘可以在后面立即调用 fsync()或fdatasync()。


匿名映射

创建匿名映射有2中方法:

1.通过在flags中指定 MAP_ANONYMOUS。
2.打开文件 /dev/zero设备文件并将文件描述符传递给mmap()。

fd = open("/dev/zero", O_RDWR);
if (fd == -1)
    errExit("open");
addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
    errExit("mmap");
addr = mmap(NULL, length, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
    errExit("mmap");


例子

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>

#include <sys/mman.h>


void errExit(char *msg){
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[]){
    int *addr;                  /* Pointer to shared memory region */

#ifdef USE_MAP_ANON             /* Use MAP_ANONYMOUS */
    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

#else                           /* Map /dev/zero */
    int fd;

    fd = open("/dev/zero", O_RDWR);
    if (fd == -1)
        errExit("open");

    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    if (close(fd) == -1)        /* No longer needed */
        errExit("close");
#endif

    //初始化1
    *addr = 1;

    switch (fork()) {
    case -1:
        errExit("fork");

    case 0:
        printf("Child started, value = %d\n", *addr);
        (*addr)++;
        if (munmap(addr, sizeof(int)) == -1)
            errExit("munmap");
        exit(EXIT_SUCCESS);

    default:
        if (wait(NULL) == -1)
            errExit("wait");
        printf("In parent, value = %d\n", *addr);
        if (munmap(addr, sizeof(int)) == -1)
            errExit("munmap");
        exit(EXIT_SUCCESS);
    }
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
Child started, value = 1
In parent, value = 2


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/5d  


©著作权归作者所有
收藏
推荐阅读
  • err
    linux c管道和FIFO

    每个 shell 用户都对命令中使用管道比价熟悉,如下面这个统计一个目录中文件的数目的命令所示。ls | wc -l为了执行上面的命令,shell 创建了2个进程分别执行...

  • linux c共享库(动态库)高级特性(2)

    动态加载库当一个可执行文件开始运行之后,动态链接器会加载程序的动态依赖列表中的所有共享库,但有些时候延迟加载库是比较有用的,如只在需要的时候再加载一个插件。动态链接器的这项功能是通过一组 API 来实...

  • err
    linux c共享库(动态库)基础(1)

    在很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是将这些源代码文件只编译一次,然后在需要的时候将它们链接进不同的可执行文件中。虽然这项技术能够竹省...

  • linux c进程资源

    每个进程都会消耗诸如内存和CPU时间之类的系统资源,本文将介绍与资源相关的系统调用。#include &lt;sys/resource.h&gt; int getrusage(int who, st...

  • err
    linux c使用syslog记录消息

    syslog 工具提供了一个集中式日志工具,系统中的所有应用程序都可以使用这个记录日志消息。如下图syslogd 从两个不同的源接收日志消息:一个是unix domain...

  • nginx模块 ngx_http_headers_module

    ngx_http_headers_module 模块是用来增加 Expires 和 Cache-control,或者是任意的响应头。Syntax: add_header name value [alw...

  • nginx模块 ngx_http_gunzip_module、ngx_http_gzip_module、ngx_http_gzip_static_module

    ngx_http_gunzip_module 模块将文件解压缩后并在响应头加上 "Content-Encoding: gzip" 返回给客户端。为了解决客户端不支持gzip压缩。编译的时候带上 --w...

  • nginx模块 ngx_http_flv_module、ngx_http_mp4_module

    ngx_http_flv_module模块提供了对 flv 视频的伪流支持。编译的时候带上 --with-http_flv_module。它会根据指定的 start 参数来指定跳过多少字节,并在返回数...

  • nginx模块 ngx_http_fastcgi_module

    ngx_http_fastcgi_module 模块使得nginx可以与 fastcgi 服务器通信。比如目前要使得 nginx 支持 php 就得使用 fastcgi技术,在服务器上装上 nginx...

  • nginx模块 ngx_http_autoindex_module

    ngx_http_autoindex_module 模块可以将uri以 / 结尾时,列出里面的文件和目录。Syntax: autoindex on | off; Default: autoindex ...

简介
天降大任于斯人也,必先苦其心志。