在《[你真的理解内存分配]》一文中,我们介绍了 malloc
申请内存的原理,但其在内核怎么实现的呢?所以,本文主要分析在 Linux 内核中对堆内存分配的实现过程。
本文使用 Linux 2.6.32 版本代码
在《[你真的理解内存分配] 》一文中介绍过,Linux 会把进程虚拟内存空间划分为多个分区,在 Linux 内核中使用 vm_area_struct
对象来表示,其定义如下:
1struct vm_area_struct {
2 struct mm_struct *vm_mm; // 分区所属的内存管理对象
3
4 unsigned long vm_start; // 分区的开始地址
5 unsigned long vm_end; // 分区的结束地址
6
7 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_brk
和 brk
字段用来记录堆空间的范围, 如 图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}
总结上面的代码,主要有以下几个步骤:
brk
值。brk
值跟旧的 brk
值一致,那么也不用作任何处理。brk
值发生变化,那么就调用 do_brk
函数进行下一步处理。brk
指针(堆空间顶部)为新的 brk
的值。我们看到第 3 步调用了 do_brk
函数来处理,do_brk
函数的实现有点小复杂,所以这里介绍一下大概处理流程:
start_brk
从进程内存分区红黑树中找到其对应的内存分区对象(也就是 vm_area_struct
)。vm_end
字段设置为新的 brk
值。至此,brk
系统调用的工作就完成了(上面没有分析释放内存的情况),总结来说,brk
系统调用的工作主要有两部分:
brk
指针设置为新的 brk
值。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
函数主要完成以下操作:
address
变量中。find_vma
函数从进程内存分区中查找异常的虚拟内存地址对应的内存分区对象。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 int
2handle_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;
7
8 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 int
2do_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 分配内存的方式还有很多,比如 mmap
、HugePages
等,有兴趣的可以查阅相关的资料和书籍。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/mphzuIqqBYecry0psTsbxQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。