一文读懂 Linux 内存分配全过程

在《你真的理解内存分配》一文中,我们介绍了 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」获取学习资料网盘链接。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465996.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CodeForces - 540D Bad Luck Island —— 求概率

题目链接&#xff1a;https://vjudge.net/contest/226823#problem/D The Bad Luck Island is inhabited by three kinds of species: r rocks, s scissors and p papers. At some moments of time two random individuals meet (all pairs of individuals can meet equiprobabl…

你真的理解内存分配吗?

内存是计算机中必不可少的资源&#xff0c;因为 CPU 只能直接读取内存中的数据&#xff0c;所以当 CPU 需要读取外部设备&#xff08;如硬盘&#xff09;的数据时&#xff0c;必须先把数据加载到内存中。我们来看看可爱的内存长什么样子的吧&#xff0c;如图1所示&#xff1a;一…

每日一练(8)—— 野指针

int *p&#xff1b; int a 20; *p a; printf("%d",*p);运行结果是什么&#xff1f;A.10 B.a 的 地址值 C.编译错误 D.运行异常分析&#xff1a; 一、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针&#xff0c;它的缺省值是随机的&#xff0…

深信服2021秋招笔试题

来源于读者投稿&#xff0c;作者Angel。笔试时间&#xff1a;2020.08.25&#xff0c;19&#xff1a;00---21&#xff1a;00。岗位&#xff1a;嵌入式软件工程师。题型&#xff1a;5个不定项选择题&#xff0c;16分5个填空题&#xff0c;19分2道编程题&#xff0c;65分不定项选择…

每日一题(9)—— 写一个标准宏MIN,这个宏输入两个参数并返回较小的一个

写一个标准宏MIN&#xff0c;这个宏输入两个参数并返回较小的一个。 分析&#xff1a; 宏定义的注意两点&#xff1a; 1、数据类型是否有溢出的风险&#xff08;如一年有多少个秒的宏&#xff09;&#xff1b; 2、带参宏的每个参数都要用括号括起来。 #define MIN(x,y) ((x)…

用ISA阻挡用户向论坛发贴子

右击访问网站这条策略&#xff0c;点击“配置HTTP”<?XML:NAMESPACE PREFIX V /><?XML:NAMESPACE PREFIX O />找到方法选项&#xff0c;并添加一条阻止的HTTP命令POST应用ISA的配置转载于:https://blog.51cto.com/freemanluo/186829

看我解决Linux下的OTG切换问题

1.硬件原理图看下面的原理图VCC_OTG_EN 引脚&#xff0c;这个脚主要是用来控制给外部OTG设备提供电源控制的。如果设备作为DEVICE设备&#xff0c;这时候VBUS的电是由外部提供的&#xff0c;比如通过USB线和电脑连接&#xff0c;这个时候&#xff0c;VBUS的电压是由电脑提供的。…

每日一题(10)—— 数组与指针

分析下面的代码&#xff0c;求输出结果。 int a[5] {1,2,3,4,5};int *p (int *)(&a 1);printf("%d %d",*(a 1),*(p - 1)); 分析&#xff1a; a —— 数组首元素的地址 等价于 &a[0] &a —— 数组的首地址 int —— 4字节 *(a 1) a[1] 2&…

美图赏析:拆解USB无线网卡,电路方案非常经典

很多台式机没有无线网卡&#xff0c;只能插网线。想要使用WiFi&#xff0c;插个USB无线网卡就行&#xff0c;简单方便&#xff1a;USB无线网卡非常小巧&#xff0c;以至于会好奇&#xff0c;电路板是怎么塞进去的&#xff1a;下面拆解其中某个厂家的一款&#xff1a;另一个角度…

程序员经常说的「设计模式」到底是什么?

当程序员说去「设计模式」时&#xff0c;你是否会一脸懵逼&#xff0c;到底什么是设计模式呢&#xff1f; 很多人应该听说过设计模式&#xff08;Design pattern&#xff09;&#xff0c;又或多或少的看过或用过设计模式&#xff0c;但是实际用在开发过程中总有点心有余而力不足…

每日一题(11)—— 结构体大小

分析下面的代码&#xff0c;求运行结果&#xff08;64位&#xff09;。 #include <stdio.h>struct {int id;unsigned char arg;char *p;void (*func)(void); } test;int main(void) {printf("sizeof(test.id):%d\n", sizeof(test.id));printf("sizeof(tes…

Linux 内存管理之vmalloc

走进vmalloc 根据前面的系列文章&#xff0c;我们知道了buddy system是基于页框分配器&#xff0c;kmalloc是基于slab分配器&#xff0c;而且这些分配的地址都是物理内存连续的。但是随着碎片化的积累&#xff0c;连续物理内存的分配就会变得困难&#xff0c;对于那些非DMA访问…

《观止》书评

收到《观止》一书已经一周了&#xff0c;因为工作很忙的原因&#xff0c;前几天完全没有看。到了周末才稍有点空闲&#xff0c;便拿起手边的这本《观止》一起。谁知一“观”而无法“止”。硬是活生生的占用了我整个本来打算用来补觉的周末。 严格说来《观止》并不算是技术书籍…

进程是如何使用内存的?

程序运行概述程序&#xff08;我们这里只讨论单进程情况&#xff0c;存在多进程的程序如淘宝微信等不展开讨论&#xff09;镜像存在磁盘中&#xff0c;运行时将镜像加载至内存RAM中&#xff0c;然后开始执行。先来看一下CPU的多级存储结构&#xff0c;CPU通用寄存器访问速度最快…

如何用SQLDMO在ASP.NET页面下实现数据库的备份与恢复

我们知道&#xff0c;用SQLDMO可以实现对数据库的备份与恢复&#xff0c;下面给出简单的实现方法。首先需要添加对SQLDMO引用1.实现数据库的备份&#xff1a;1/**//// <summary> 2 /// 数据库备份 3 /// </summary> 4 /// <returns>备份…

TP4056 实现可编程锂电充电器+电量计

本文作者&#xff1a;t3486784401链接&#xff1a;https://www.mydigit.cn/forum.php?modviewthread&tid250916&extra手头有些容量非常小的软包锂电&#xff0c;直接使用市面上的大功率充电板&#xff08;500mA/1A&#xff09;倍率太大&#xff0c;容易损坏电池。索性…

电车防盗报警器电路原来是这样的!

前几天电路菌从电车上拆开了一个防盗报警器&#xff0c;今天来看看防盗报警器这内部的电路到底是怎样的&#xff01;上图就是防盗报警器的主机&#xff0c;引出来的这根黑线是天线。在防盗报警主机的侧面&#xff0c;可以看到印刷有文字“RP-48V-64V”&#xff0c;应该是指这主…

每日一题(14)—— 交换a,b的值(不使用中间变量)

如何将a,b的值进行交换&#xff0c;并且不使用任何中间变量&#xff1f; 推荐答案&#xff1a; a a ^ b; b a ^ b; a a ^ b; 下面的答案不好&#xff0c;可能会导致越界的问题 a a b; // 可能越界 b a - b; a a - b;

买到假芯片,血的教训!

关注星标公众号&#xff0c;不错过精彩内容作者 | 写代码的篮球球痴素材来源 | 云汉芯城2021年太难了&#xff0c;芯片涨价不说&#xff0c;涨价之后还买不到芯片&#xff0c;芯片交货周期已经超过一年了。嵌入式Linux1涨价、缺货 ——> 翻新、假货在芯片涨价、缺货的背景下…

Windows 7 :微软目前最好的操作系统

好消息&#xff0c;各位&#xff01;如果你是 PC 用户&#xff0c;如果你仍然在使用有 8 年历史的 Windows XP&#xff0c;或者令人讨厌的 Windows Vista &#xff0c;现在你终于可以解脱了&#xff1a;Windows 7 就在眼前。微软交付了一套设计巧妙、改进巨大的操作系统&#x…