在《你真的理解内存分配》一文中,我们介绍了 malloc 申请内存的原理,但其在内核怎么实现的呢?所以,本文主要分析在 Linux 内核中对堆内存分配的实现过程。

本文使用 Linux 2.6.32 版本代码

内存分区对象

在《你真的理解内存分配》一文中介绍过,Linux 会把进程虚拟内存空间划分为多个分区,在 Linux 内核中使用 vm_area_struct 对象来表示,其定义如下:

 1struct vm_area_struct {2   struct mm_struct *vm_mm;        // 分区所属的内存管理对象34   unsigned long vm_start;         // 分区的开始地址5   unsigned long vm_end;           // 分区的结束地址67   struct vm_area_struct *vm_next; // 通过这个指针把进程所有的内存分区连接成一个链表8  ...9   struct rb_node vm_rb;           // 红黑树的节点, 用于保存到内存分区红黑树中
10  ...
11};

我们对 vm_area_struct 对象进行了简化,只保留了本文需要的字段。

内核就是使用 vm_area_struct 对象来记录一个内存分区(如 代码段数据段堆空间 等),下面介绍一下 vm_area_struct 对象各个字段的作用:

  • vm_mm:指定了当前内存分区所属的内存管理对象。

  • vm_start:内存分区的开始地址。

  • vm_end:内存分区的结束地址。

  • vm_next:通过这个指针把进程中所有的内存分区连接成一个链表。

  • vm_rb:另外,为了快速查找内存分区,内核还把进程的所有内存分区保存到一棵红黑树中。vm_rb 就是红黑树的节点,用于把内存分区保存到红黑树中。

假如进程 A 现在有 4 个内存分区,它们的范围如下:

  • 代码段:00400000 ~ 00401000

  • 数据段:00600000 ~ 00601000

  • 堆空间:00983000 ~ 009a4000

  • 栈空间:7f37ce866000 ~ 7f3fce867000

那么这 4 个内存分区在内核中的结构如 图1 所示:

在 图1 中,我们可以看到有个 mm_struct 的对象,此对象每个进程都持有一个,是进程虚拟内存空间和物理内存空间的管理对象。我们简单介绍一下这个对象,其定义如下:

1struct mm_struct {
2   struct vm_area_struct *mmap;  // 指向由进程内存分区连接成的链表
3   struct rb_root mm_rb;         // 内核使用红黑树保存进程的所有内存分区, 这个是红黑树的根节点
4   unsigned long start_brk, brk; // 堆空间的开始地址和结束地址
5  ...
6};

我们来介绍下 mm_struct 对象各个字段的作用:

  • mmap:指向由进程所有内存分区连接成的链表。

  • mm_rb:内核为了加快查找内存分区的速度,使用了红黑树保存所有内存分区,这个就是红黑树的根节点。

  • start_brk:堆空间的开始内存地址。

  • brk:堆空间的顶部内存地址。

我们来回顾一下进程虚拟内存空间的布局图,如 图2 所示:

start_brkbrk 字段用来记录堆空间的范围, 如 图2 所示。一般来说,start_brk 是不会变的,而 brk 会随着分配内存和释放内存而变化。

虚拟内存分配

在《你真的理解内存分配》一文中说过,调用 malloc 申请内存时,最终会调用 brk 系统调用来从堆空间中分配内存。我们来分析一下 brk 系统调用的实现:

 1unsigned long sys_brk(unsigned long brk)2{3   unsigned long rlim, retval;4   unsigned long newbrk, oldbrk;5   struct mm_struct *mm = current->mm;6  ...7   down_write(&mm->mmap_sem);  // 对内存管理对象进行上锁8  ...9   // 判断堆空间的大小是否超出限制, 如果超出限制, 就不进行处理
10   rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
11   if (rlim < RLIM_INFINITY
12       && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim)
13       goto out;
14
15   newbrk = PAGE_ALIGN(brk);      // 新的brk值
16   oldbrk = PAGE_ALIGN(mm->brk);  // 旧的brk值
17   if (oldbrk == newbrk)          // 如果新旧的位置都一样, 就不需要进行处理
18       goto set_brk;
19  ...
20   // 调用 do_brk 函数进行下一步处理
21   if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
22       goto out;
23
24set_brk:
25   mm->brk = brk; // 设置堆空间的顶部位置(brk指针)
26out:
27   retval = mm->brk;
28   up_write(&mm->mmap_sem);
29   return retval;
30}

总结上面的代码,主要有以下几个步骤:

  • 1、判断堆空间的大小是否超出限制,如果超出限制,就不作任何处理,直接返回旧的 brk 值。

  • 2、如果新的 brk 值跟旧的 brk 值一致,那么也不用作任何处理。

  • 3、如果新的 brk 值发生变化,那么就调用 do_brk 函数进行下一步处理。

  • 4、设置进程的 brk 指针(堆空间顶部)为新的 brk 的值。

我们看到第 3 步调用了 do_brk 函数来处理,do_brk 函数的实现有点小复杂,所以这里介绍一下大概处理流程:

  • 通过堆空间的起始地址 start_brk 从进程内存分区红黑树中找到其对应的内存分区对象(也就是 vm_area_struct)。

  • 把堆空间的内存分区对象的 vm_end 字段设置为新的 brk 值。

至此,brk 系统调用的工作就完成了(上面没有分析释放内存的情况),总结来说,brk 系统调用的工作主要有两部分:

  1. 把进程的 brk 指针设置为新的 brk 值。

  2. 把堆空间的内存分区对象的 vm_end 字段设置为新的 brk 值。

物理内存分配

从上面的分析知道,brk 系统调用申请的是 虚拟内存,但存储数据只能使用 物理内存。所以,虚拟内存必须映射到物理内存才能被使用。

那么什么时候才进行内存映射呢?

在《你真的理解内存分配》一文中介绍过,当对没有映射的虚拟内存地址进行读写操作时,CPU 将会触发 缺页异常。内核接收到 缺页异常 后, 会调用 do_page_fault 函数进行修复。

我们来分析一下 do_page_fault 函数的实现(精简后):

 
 1void do_page_fault(struct pt_regs *regs, unsigned long error_code)2{3   struct vm_area_struct *vma;4   struct task_struct *tsk;5   unsigned long address;6   struct mm_struct *mm;7   int write;8   int fault;9
10   tsk = current;
11   mm = tsk->mm;
12
13   address = read_cr2(); // 获取导致页缺失异常的虚拟内存地址
14  ...
15   vma = find_vma(mm, address); // 通过虚拟内存地址从进程内存分区中查找对应的内存分区对象
16  ...
17   if (likely(vma->vm_start <= address)) // 如果找到内存分区对象
18       goto good_area;
19  ...
20
21good_area:
22   write = error_code & PF_WRITE;
23  ...
24   // 调用 handle_mm_fault 函数对虚拟内存地址进行映射操作
25   fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
26  ...
27}

do_page_fault 函数主要完成以下操作:

  1. 获取导致页缺失异常的虚拟内存地址,保存到 address 变量中。

  2. 调用 find_vma 函数从进程内存分区中查找异常的虚拟内存地址对应的内存分区对象。

  3. 如果找到内存分区对象,那么调用 handle_mm_fault 函数对虚拟内存地址进行映射操作。

从上面的分析可知,对虚拟内存进行映射操作是通过 handle_mm_fault 函数完成的,而 handle_mm_fault 函数的主要工作就是完成对进程 页表 的填充。

我们通过 图3 来理解内存映射的原理,可以参考文章《一文读懂 HugePages的原理》:

下面我们来分析一下 handle_mm_fault 的实现,代码如下:

 1int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,2                   unsigned long address, unsigned int flags)3{4   pgd_t *pgd;  // 页全局目录项5   pud_t *pud;  // 页上级目录项6   pmd_t *pmd;  // 页中间目录项7   pte_t *pte;  // 页表项8  ...9   pgd = pgd_offset(mm, address);         // 获取虚拟内存地址对应的页全局目录项
10   pud = pud_alloc(mm, pgd, address);     // 获取虚拟内存地址对应的页上级目录项
11  ...
12   pmd = pmd_alloc(mm, pud, address);     // 获取虚拟内存地址对应的页中间目录项
13  ...
14   pte = pte_alloc_map(mm, pmd, address); // 获取虚拟内存地址对应的页表项
15  ...
16   // 对页表项进行映射
17   return handle_pte_fault(mm, vma, address, pte, pmd, flags);
18}

handle_mm_fault 函数主要对每一级的页表进行映射(对照 图3 就容易理解),最终调用 handle_pte_fault 函数对 页表项 进行映射。

我们继续来分析 handle_pte_fault 函数的实现,代码如下:

 1static inline int2handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma,3                unsigned long address, pte_t *pte, pmd_t *pmd,4                unsigned int flags)5{6   pte_t entry;78   entry = *pte;9
10   if (!pte_present(entry)) { // 还没有映射到物理内存
11       if (pte_none(entry)) {
12          ...
13           // 调用 do_anonymous_page 函数进行匿名页映射(堆空间就是使用匿名页)
14           return do_anonymous_page(mm, vma, address, pte, pmd, flags);
15      }
16      ...
17  }
18  ...
19}

上面代码简化了很多与本文无关的逻辑。从上面代码可以看出,handle_pte_fault 函数最终会调用 do_anonymous_page 来完成内存映射操作,我们接着来分析下 do_anonymous_page 函数的实现:

 1static int2do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,3                 unsigned long address, pte_t *page_table, pmd_t *pmd,4                 unsigned int flags)5{6   struct page *page;7   spinlock_t *ptl;8   pte_t entry;9
10   if (!(flags & FAULT_FLAG_WRITE)) { // 如果是读操作导致的异常
11       // 使用 `零页` 进行映射
12       entry = pte_mkspecial(pfn_pte(my_zero_pfn(address), vma->vm_page_prot));
13      ...
14       goto setpte;
15  }
16  ...
17   // 如果是写操作导致的异常
18   // 申请一块新的物理内存页
19   page = alloc_zeroed_user_highpage_movable(vma, address);
20  ...
21   // 根据物理内存页的地址生成映射关系
22   entry = mk_pte(page, vma->vm_page_prot);
23   if (vma->vm_flags & VM_WRITE)
24       entry = pte_mkwrite(pte_mkdirty(entry));
25  ...
26setpte:
27   set_pte_at(mm, address, page_table, entry); // 设置页表项为新的映射关系
28  ...
29   return 0;
30}

do_anonymous_page 函数的实现比较有趣,它会根据 缺页异常 是由读操作还是写操作导致的,分为两个不同的处理逻辑,如下:

  • 如果是读操作导致的,那么将会使用 零页 进行映射(零页 是 Linux 内核中一个比较特殊的内存页,所有读操作引起的 缺页异常 都会指向此页,从而可以减少物理内存的消耗),并且设置其为只读(因为 零页 是不能进行写操作)。如果下次对此页进行写操作,将会触发写操作的 缺页异常,从而进入下面步骤。

  • 如果是写操作导致的,就申请一块新的物理内存页,然后根据物理内存页的地址生成映射关系,再对页表项进行填充(映射)。

总结

本文主要介绍了 Linux 内存分配的整个过程,当然只是介绍从堆空间分配的内存的过程。Linux 分配内存的方式还有很多,比如 mmapHugePages 等,有兴趣的可以查阅相关的资料和书籍。


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

一文读懂 Linux 内存分配全过程相关推荐

  1. 一文搞懂Linux 内存管理原理

    导语 linux 内存是后台开发人员,需要深入了解的计算机资源.合理的使用内存,有助于提升机器的性能和稳定性.本文主要介绍 linux 内存组织结构和页面布局,内存碎片产生原因和优化算法,linux ...

  2. Linux如何访问mmio空间,一文读懂Linux下如何访问I/O端口和I/O内存

    虽然访问I/O端口非常简单,但是检测哪些I/O端口已经分配给I/O设备可能就不这么简单了,对基于ISA总线的系统来说更是如此.通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数 ...

  3. 一文读懂 | Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    点击蓝字 关注我们 因公众号更改推送规则,请点"在看"并加"星标"第一时间获取精彩技术分享 来源于网络,侵删 栈是什么?栈有什么作用? 首先,栈 (stack) ...

  4. 一文读懂linux 下zram

    本文转自https://blog.csdn.net/longwang155069/article/details/51900031 zram 技术的由来: zram(也称为 zRAM,先前称为 com ...

  5. 一文读懂Linux进程、进程组、会话、僵尸

    作者简介 herongwei,北交硕士毕业,现就职于搜狗公司,后端开发工程师.从事 C++,Golang ,Linux 后端开发. 追求技术,热爱编程与分享,希望能和大家多多交流学习~ 座右铭:    ...

  6. 一文读懂Java内存模型(JMM)及volatile关键字

    点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...

  7. 一文读懂linux操作系统

    ☞☞☞点击查看更多优秀嵌入式博客☜☜☜ 三相电机故障排除 初识linux 发展历史 linux操作指令 常用 Linux 命令的基本使用 常用快捷键 linux 与window系统的区别 linux版 ...

  8. 一文读懂 Linux 下单机实现百万并发的内核黑科技:连接跟踪(Conntrack)

    公众号关注 「奇妙的 Linux 世界」 设为「星标」,每天带你玩转 Linux ! 本文介绍连接跟踪(connection tracking,conntrack,CT)的原理,应用,及其在 Linu ...

  9. ac3165 linux驱动_一文读懂Linux系统启动流程

    Linux启动管理 11.1 CentOS 6.x系统启动过程详解 CentOS 6.x系统启动过程发生了较大的变化,使用Upstart启动服务取代了原先的System V init启动服务.Upst ...

最新文章

  1. loadrunner中的c函数----从参数列表中取参数并与特定字符进行字符串比较。
  2. 打包解决方案后,安装时提示只能在IIS5.1以上运行解决方法
  3. Closure--1
  4. Unity2018新功能抢鲜 | 粒子系统改进
  5. rs232读取智能电表_供电局智能费控电表功能和通讯方式介绍
  6. java 线程安全集合
  7. 《大话数据结构(C#实现)》(Yanlz+VR云游戏+Unity+SteamVR+云技术+5G+AI+软件架构设计+框架编程+数组+栈+链表+图+队列+树+堆+二叉树+哈希表+立钻哥哥+==)
  8. BCB 第三方组件/控件 ZZ
  9. MySQL相关知识整理
  10. 接触mybatisplus单页500条限制_单页网站制作教程,单页网站设计五大技巧
  11. 一些互联网标准化组织
  12. 我爱天文 - 月亮从哪边升出来?
  13. 半角和全角的区别 java_全角和半角区别
  14. 容器三把斧之 | OverlayFS原理与实现
  15. C语言初阶第二篇:换我心,为你心,始知C语言 从程序调试到全网最详细字符串教学来喽
  16. JS下载文件|无刷新下载文件
  17. 1646. Prime Path
  18. sql语句常见错误:Unknown column 'xxx' in 'where clause该如何解决?
  19. k8s在华为openeuler搭建
  20. 【矩阵论笔记】零化多项式

热门文章

  1. 驱动开发之 设备读写方式:缓冲区方式
  2. 宋体配置JAVA j2ee (一) 轻松入门
  3. ubuntu 修改host,以便在本地调试
  4. 成功数据恢复一例LINUX EXT3 下误删除ORACLE数据库
  5. HashMap30连问,彻底搞懂HashMap
  6. mse均方误差计算公式_PCA的两种解读:方差最大与均方误差最小的推导
  7. 项目宝提供的服务器,开源WebSocket服务器项目宝贝鱼CshBBrain V4.0.1 和 V2.0.2发布
  8. Downloader Middlewares反反爬虫【学习笔记04】
  9. SecureCRT常用的使用技巧
  10. 如何访问另一台电脑的共享文件夹_如何远程控制另一台电脑