mmap详解

想写一篇文章,详细的介绍一下mmap,主要是原理、用法、mmap泄露来进行介绍。说到mmap,首先得从堆空间说起。

申请堆空间

其实,不管是 32 位系统还是 64 位系统,内核都会维护一个变量 brk,指向堆的顶部,所以,brk 的位置实际上就决定了堆的大小。Linux 系统为我们提供了两个重要的系统调用来修改堆的大小,分别是 sbrk 和 mmap。接下来,我们来学习这两个系统调用是如何使用的。我们先来看 sbrk。

sbrk

sbrk 函数的头文件和原型定义如下:

#include 

void* sbrk(intptr_t incr);

sbrk 通过给内核的 brk 变量增加 incr,来改变堆的大小,incr 可以为负数。当 incr 为正数时,堆增大,当 incr 为负数时,堆减小。如果 sbrk 函数执行成功,那返回值就是 brk的旧值;如果失败,就会返回 -1,同时会把 errno 设置为 ENOMEM。

在实际应用中,我们很少直接使用 sbrk 来申请堆内存,而是使用 C 语言提供的 malloc 函数进行堆内存的分配,然后用 free 进行内存释放。这里你要注意的是,malloc 和 free 函数不是系统调用,而是 C 语言的运行时库。Linux 上的主流运行时库是 glibc,其他影响力比较大的运行时库还有 musl 等。C 语言的运行时库多是以动态链接库的方式实现的。

在 C 语言的运行时库里,malloc 向程序提供分配一小块内存的功能,当运行时库的内存分配完之后,它会使用 sbrk 方法向操作系统再申请一块大的内存。我们可以将 C 语言的运行时库类比为零售商,它从操作系统那里批发一块比较大的内存,然后再通过零售的方式一点点地提供给程序员使用。

mmap

另一个可以申请堆内存的系统调用是 mmap,它是最重要的内存管理接口。mmap 的头文件和原型如下所示:

#include 
#include 

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize); 

我来解释一下上述代码中的各个变量的意义:

start 代表该区域的起始地址;

length 代表该区域长度;

prot 描述了这块新的内存区域的访问权限;

flags 描述了该区域的类型;

fd 代表文件描述符;

offset 代表文件内的偏移值。

mmap 的功能非常强大,根据参数的不同,它可以用于创建共享内存,也可以创建文件映射区域用于提升 IO 效率,还可以用来申请堆内存。决定它的功能的,主要是 prot, flags和 fd 这三个参数,我们分别来看看。

prot 的值可以是以下四个常量的组合:

PROT_EXEC,表示这块内存区域有可执行权限,意味着这部分内存可以看成是代码段,它里面存储的往往是 CPU 可以执行的机器码。

PROT_READ,表示这块内存区域可读。

PROT_WRITE,表示这块内存区域可写。

PROT_NONE,表示这块内存区域的页面不能被访问。

而 flags 的值可取的常量比较多,你可以通过 man mmap 查看,这里我只列举一下最重要的四种可取值常量:

MAP_SHARED:创建一个共享映射的区域,多个进程可以通过共享映射的方式,来共享同一个文件。这样一来,一个进程对该文件的修改,其他进程也可以观察到,这就实现了数据的通讯。

MAP_PRIVATE:创建一个私有的映射区域,多个进程可以使用私有映射的方式,来映射同一个文件。但是,当一个进程对文件进行修改时,操作系统就会为它创建一个独立的副本,这样它对文件的修改,其他进程就看不到了,从而达到映射区域私有的目的。

MAP_ANONYMOUS:创建一个匿名映射,也就是没有关联文件。使用这个选项时,fd 参数必须为空。

MAP_FIXED:一般来说,addr 参数只是建议操作系统尽量以 addr 为起始地址进行内存映射,但如果操作系统判断 addr 作为起始地址不能满足长度或者权限要求时,就会另外再找其他适合的区域进行映射。如果 flags 的值取是 MAP_FIXED 的话,就不再把addr 看成是建议了,而是将其视为强制要求。如果不能成功映射,就会返回空指针。

通常,我们使用私有匿名映射来进行堆内存的分配。

我们再来看参数 fd。当参数 fd 不为 0 时,mmap 映射的内存区域将会和文件关联,如果fd 为 0,就没有对应的相关文件,此时就是匿名映射,flags 的取值必须为MAP_ANONYMOUS。

明白了 mmap 及其各参数的含义后,你肯定想知道什么场景下才会使用 mmap,我们又该怎么使用它。

mmap 的其他应用场景

mmap 这个系统调用的能力非常强大,我们在后面还会经常遇到它。在这节课里,我们先来了解一下它最常见的用法。
根据映射的类型,mmap 有四种最常用的组合:

mmap详解_第1张图片

其中,私有匿名映射常用于分配内存,也就是我们上文讲的申请堆内存,而私有文件映射常用于加载动态库。

这里我们重点看看共享匿名映射。我们通过一个例子,来了解一下 mmap 是如何用于父子进程之间的通信的,它的用法示例代码如下:

共享匿名映射

#include 
#include 
#include 
#include 


int main(int argc,char* argv[])
{
    pid_t pid;

    char* shm = (char*)mmap(0, 4096, PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(!(pid = fork()))
    {
        sleep(1);
        printf("child got a message: %s\n",shm);
        sprintf(shm, "%s", "hello, father.");
        exit(0);
    }

    sprintf(shm, "%s", "hello, my child");
    sleep(2);
    printf("parent got a message: %s\n", shm);

    return 0;
}

wj@wj:~/WORK/Learning/learning/cpp_learning$ gcc test.c -o test.out 
wj@wj:~/WORK/Learning/learning/cpp_learning$ ./test.out 
child got a message: hello, my child
parent got a message: hello, father.

在这个过程中,我们先是用 mmap 方法创建了一块共享内存区域,命名为 shm,接着,又通过 fork 这个系统调用创建了子进程:具体来讲,子进程休眠一秒后,从 shm 中取出一行字符并打印出来,然后又向共享内存中写入了一行消息。

在子进程的执行逻辑之后,是父进程的执行逻辑:父进程先写入一行消息,然后休眠两秒,等待子进程完成读取消息和发消息的过程并退出后,父进程再从共享内存中取出子进程发过来的消息。

我想请你结合我刚才的讲解,来分析一下这个程序运行的结果,这样你就理解的更透彻
了。

关于共享匿名映射,我们就讲到这里,至于 mmap 的另一个组合共享文件映射。它的作用其实和共享匿名映射相似,也可以用于进程间通讯。不同的是,共享文件映射是通过文件名来创建共享内存区域的,这就让没有父子关系的进程,也可以通过相同的文件创建共享内存区域,从而可以使用共享内存进行进程间通讯。

小结

你可能感兴趣的:(#,功耗/性能/内存管理,mmap)