zoukankan      html  css  js  c++  java
  • malloc 和mmap

    从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap(不考虑共享内存)。

    1. brk 的实现方式是将 Data Segment 的最高地址指针 _edata 往高地址推(分配的内存小于 128k )。
    2. mmap 的实现方式是在 Memory Mapping Segment 找一块空闲的虚拟内存(分配的内存大于 128k )。

    (Data segment 和 Memory Mapping Segment 的相关内容查看这里。)

    这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

    在标准 C 库中,提供了 malloc / free 函数分配释放内存,这两个函数底层是由 brk,mmap,munmap 这些系统调用实现的。

    example 1

    1、进程调用 A = malloc ( 30k ) 以后,内存空间如下图所示。malloc 函数会调用 brk 系统调用,将 _edata 指针往高地址推 30K,就完成虚拟内存分配。

    你可能会问:只要把_edata + 30K 就完成内存分配了?

    事实是这样的,_edata + 30K 只是完成虚拟地址的分配,A 这块内存现在还是没有物理页与之对应的,等到进程第一次读写 A 这块内存的时候,发生缺页中断,这个时候,内核才分配 A 这块内存对应的物理页。也就是说,如果用 malloc 分配了 A 这块内容,然后从来不访问它,那么,A 对应的物理页是不会被分配的。 

    example2

    进程调用 B = malloc(40K) 以后,内存空间如下图所示。

    example 3

    3、当 malloc 分配大于 128k 的内存时,使用 mmap 分配内存。在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为 0 )。

    这么做的原因是 brk 分配的内存需要等到高地址内存释放以后才能释放(例如,在 B 释放之前,A 是不可能释放的,这就是内存碎片产生的原因,什么时候收缩看下面),而 mmap 分配的内存可以单独释放。,如下图所示,这里分配 200k 。

    example 4

     4、进程调用 D = malloc(100k) 以后,内存空间如下图所示。

    example 5

     5、进程调用 free(C) 以后,C 对应的虚拟内存和物理内存一起释放

    example 6

    6、进程调用 free(B) 以后,如下图所示,B 对应的虚拟内存和物理内存都没有释放,因为只有一个 _edata 指针,如果往回推,那么 D 这块内存怎么办呢?当然,B 这块内存是可以重用的,如果这个时候再来一个 40K 的请求,那么 malloc 很可能就将 B 这块内存返回的。 

     

    example 7

    7、进程调用 free(D) 以后,如下图所示,B 和 D 连接起来变成一块 140K 的空闲内存。当最高地址空间的空闲内存超过128K(可由 M_TRIM_THRESHOLD 选项调节)时,执行内存紧缩操作(trim)。在上一个步骤 free 的时候,发现最高地址空闲内存超过 128 K,于是内存紧缩,如下图所示。

    2 mmap
    了解完 虚拟内存 ,再回过头来讲一下 mmap ,也就是内存映射 。内存映射是将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容的过程。

    2.1 基础概念
    先讲下内存映射里的一些概念。

    映射对象类型

    虚拟内存区域可以映射以下两种类型的对象:

    普通文件:即磁盘文件中的一块 连续 的区域。
    匿名文件:一个由内核创建的全为 二进制零 的文件。当CPU首次引用此区域时,将以二进制零填充到页表中。
    共享对象

    在上一节 虚拟内存 可得知,系统为每个进程提供了单独的页表,从而也实现了进程间数据访问权限的管理以及数据的保护。但同时,通过内存映射的机制,将对象作为 共享对象 映射到两个进程的虚拟内存亦可实现数据的共享。

     

    2.2 使用方式
    然后先讲下如果我们应该如何通过内存映射的方式来访问文件。 mmap() 的函数定义如下:

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



    其中参数的含义分别是:

    start: 期望的进程虚拟内存起始位置,填 NULL 时由内核来决定起始位置
    length: 需要映射的对象字节大小
    fd: 文件句柄
    offset: 距离文件开始处的偏移量
    prot: 映射对象的访问权限,用于可指定是否可读写、执行。
    flags: 映射对象的类型,例如指定是映射普通文件还是请求二进制零、映射共享对象还是私有的写时复制对象等。
    前4项地含义可通过下图更直观地了解:

     

    而在 iOS 开发中,当我们需要的数据类型是 NSData 时,可以更简便地通过调用以下方法

     
     

    2.3 读取过程
    当我们通过 mmap 读取文件时,将经历以下步骤:

    在当前用户虚拟内存空间中分配一片 指定映射大小 的虚拟内存区域。
    将磁盘中的文件映射到这片内存区域,等待后续 按需 进行页面调度。
    当CPU真正访问数据时,触发 缺页异常 将所需的数据页从磁盘拷贝到物理内存,并将物理页地址记录到页表。
    进程通过页表得到的物理页地址访问文件数据。


    而作为对比,当通过 标准IO 读取一个文件时,步骤为:

    将 完整 的文件从磁盘拷贝到物理内存(内核空间)。
    将完整文件数据从 内核空间 拷贝到 用户空间 以供进程访问。


    2.4 优劣
    通过上面 mmap 与 标准IO 的对比,不难发现调用mmap具有以下的优势:

    物理内存占用延后:数据直到真正被使用时才会发生拷贝。
    物理内存占用减少:对于同一份文件无需在物理内存中存放两份,且文件区被划分成片,缺页异常时只将所需的页拷贝到物理内存。
    方便实现跨进程数据交互、共享:当映射到虚拟内存的对象被设置为共享对象,则不同进程对映射对象的写操作相互可见。
    然而也能发现 mmap 存在以下 劣势 :

    无法映射变长文件:调用mmap()时需指定要映射的文件位置和需要映射的大小范围。
    如果需要映射的文件过大,会导致过度占用虚拟内存:在调用mmap()后,虚拟内存空间就创建了,此时虽然不会占用物理内存,但依然会占用虚拟内存。此时可考虑只映射文件中自己需要的部分。
    由此,当我们需要访问一个比较大的文件,尤其是当我们只需要访问其中的一小部分数据的时候,我们可以尝试通过 mmap 的方式来进行访问,减少由于该文件过大而对物理内存的过度占用。
     

  • 相关阅读:
    yum只下载不安装
    知乎的 Flink 数据集成平台建设实践
    饿了么EMonitor演进史
    手机淘宝轻店业务 Serverless 研发模式升级实践
    独家对话阿里云函数计算负责人不瞋:你所不知道的 Serverless
    一文详解物化视图改写
    业务团队如何统一架构设计风格?
    Fluid 给数据弹性一双隐形的翅膀 -- 自定义弹性伸缩
    开源 1 年半 star 破 1.2 万的 Dapr 是如何在阿里落地的?
    Service Mesh 从“趋势”走向“无聊”
  • 原文地址:https://www.cnblogs.com/dream397/p/14629276.html
Copyright © 2011-2022 走看看