# 程序运行背后的内存管理机制 :::success contributed by <[Felizs](https://hackmd.io/@Felizs)> ::: ## 参考资料 [你所不知道的C Jserv](https://hackmd.io/@sysprog/c-memory?type=view) 看到50min \ [What a C programmer should know about memory](https://marek.vavrusa.com/memory/ )([札记](http://wen00072.github.io/blog/2015/08/08/notes-what-a-c-programmer-should-know-about-memory/))(非常大的主题g libc的维护者 Drepper的文章) \ [Anatomy of a Program in Memory](https://manybutfinite.com/post/anatomy-of-a-program-in-memory/) \ [What Every Programmer Should Know A bout Memory](https://akkadia.org/drepper/cpumemory.pdf) --- 可能很多人都知道[每个程序员都应该了解的内存知识](https://akkadia.org/drepper/cpumemory.pdf)这篇文章,我也大概看过,写的太深入(底层过于详细)了,只能囫囵吞枣,读完也不清楚重点。但搞内存,脑子里总是要有个框图的,所以梳理了一下与日常工作更相关的部分(主要是进程的虚拟内存管理)。 行文主要由以下五个部分组成: - 虚拟内存(内存空间布局介绍) - 栈分配 - 堆分配 - 内存映射 - 内存消耗 引申出的主题会非常多,也在此处做一个记录,日后可以整理成博文。 - [ ] 指针与内存分配 - [ ] 函数与内存分配 - [ ] Cache原理 - [ ] Linux中的内存管理 --- ## 虚拟内存 我们一般在保护模式下工作,程序拥有各自的**虚拟**地址空间。**虚拟**意味着我们并不受可用内存的束缚,同时也无权拥有它们。如果需要使用虚拟地址空间,我们需要os的提供真实的物理支持,这就叫做mapping。支持可以是物理内存(不一定是RAM) 或者持久性存储。 前者称为'匿名映射'。 :::info :warning:VMA可能会分配它并不拥有的空间,给出过度承诺(overcommiting)。每次分配后进行NULL检查是由必要的,但不能确保不出错。 因为过度承诺的存在,os可能给了你足够多的指向内存的有效指针,但当你试图访问的时候,就Out of memory了。和银行有些像。 ::: ### 虚拟内存-整体视图 多任务操作系统的每个进程都在它自己的**虚拟地址空间**中运行,在32位的模式下,它就是4GB的内存地址块。这些虚拟地址通过页表映射到物理内存,这些页表是由操作系统维护,由处理器进行查询的。 每个一个进程都有他自己的一组页表. 一旦虚拟空间被启用了,它们会应用于及其中运行的所有软件,包括内核本身。所以虚拟空间地址的一部分必须保留给内核。  This does **not** mean the kernel uses that much physical memory, only that it has that portion of address space available to map whatever physical memory it wishes. 内核空间在页表中被标记为特权代码专有,所以当用户模式的程序想访问它的时候,会出发页错误。在Linux中,内核空间一直存在,并在所有进程中映射相同的物理内存。内核代码和数据一直是可寻址的,随时可以处理中断和系统调用。相反,每当进程切换发生时,地址空间的用户模式部分的映射都会改变。  above, Firefox has used far more of its virtual address space due to its legendary memory hunger 下图是Linux进程的标准段布局:  当计算是安全友好的时候,上图显示的段的起始地址堆几乎堆计算机中的每个进程都是一样的。而漏洞常常引用绝对的内存地址:堆栈上的地址,库函数的地址等,攻击者可以闭着眼睛找到这些位置。因此地址空间随机化已经变得非常流行。Linux会通过加入偏移量的方法随机化栈,内存映射段和堆(不幸的是32的地址空间已经非常紧凑了) ### 虚拟内存-栈 进程最上边的段称作栈(向下增长),存储的是本地变量和函数参数(在大部分语言中)。调用函数的时候会在栈中push一个栈帧,栈帧在函数返回的时候被摧毁。这种简单的设计是因为数据遵循严格的LIFO(Last In First Out)顺序。所以跟踪栈内容只需要简单地改变栈指针,push和pop因此非常快速且拥有确定性。此外,对栈的重用会倾向于在cpu缓存里保留活跃的栈内存。 push超过其适合范围的数据可能会耗尽栈内存,这会触发页错误。该错误在Linux中[expand_stack()](http://lxr.linux.no/linux+v2.6.28/mm/mmap.c#L1716)处理,该错误又调用[acct_stack_growth()](http://lxr.linux.no/linux+v2.6.28/mm/mmap.c#L1544)来检查是否适合增加堆栈。栈大小会根据需求进行调整:当栈小于RLIMIT_STACK(通常是8M),那么它会在程序没有感知的情况下正产高增长。但是如果达到了栈的最大限制,会出现stack overflow,程序会受到段错误。 >当栈变小时,它不会收缩。// 和联邦预算一样 动态栈增长是访问未映射内存区域的唯一有效的情况。其他的访问会触发页错误->段错误。一些只读的内存区域遇到写入操作,也会发生段错误。 ### 虚拟内存-堆 堆提供了程序运行时的内存分配,和栈一样。不同的是,它存的数据活的比函数分配的时候更长。 >原文:meant for data that must outlive the function doing the allocation 大部分语言为程序提供了堆管理。因此,满足内存请求是内核和程序的公共事务。 In C, the interface to heap allocation is [malloc()](http://www.kernel.org/doc/man-pages/online/pages/man3/malloc.3.html) and friends, whereas in a garbage-collected language like C# the interface is the `new` keyword. 如果堆有足够的空间去满足分配内存的请求时,它可以直接由语言运行时处理,而不需要内核介入。否则堆就通过brk系统调来生成空间。堆的管理是很复杂的,需要复杂的算法来应对应用程序混乱的分配模式,以提高速度和有效的内存访问。实时系统拥有专门的分配器去处理这个问题。 堆同时也是碎片化的,如下所示:  ### 虚拟内存-内存映射段 在这里,内核将文件的内容直接映射到内存。 任何Linux的程序都可以通过mmap()系统调用来请求(创建?)此类映射。内存映射是一种执行文件IO的便捷且高效能的方式,因此它用来加载动态库。也可以在这个区域创建不应用于任何文件的匿名内存映射,将其用于程序数据。 在Linux中,如果你通过malloc()请求大块内存,则C库会创建此类匿名映射,而不使用堆内存。 ### BSS data Program text BSS和数据段都是用来存静态(全局)变量的内容。区别在于BSS存的是未初始化的静态变量的内容。 BSS的内存区域是匿名的,它不映射任何文件。 比如声明了static int a , a就在BSS段。  据段存储了指针,已被初始化的变量。这个存储区域不是匿名的,映射了程序二进制图像的一部分。即使数据段映射了一个文件,它也是一个私有的内存映射,意味着不会反应全局变量的更新。 指针指向的实际字符串,二进制文件存储在text段。 --- ## 堆分配 ### slab 如何最佳利用cache memory ### Demand paging :::info Linux系統提供一系列的内存管理API - 分配和释放 - 内存管理API - mlock 禁止换出 - madvise - give advice about use of memory 他不是标准。 ::: ### copy-on-write --- ## 栈分配 - alloca() -> automatically freed 用的是栈空间 - VLA是指viable-length array,的实现和alloca也是在栈空间。存在安全隐患 --- ## 内存映射 ## 内存 ## 内存消耗 --- --- # 素材 ## Linux内存管理 > ... 写了一半 发现自己基础知识忘光了 捡起来先看看。 > 以下笔记主要来源: 《深入Linux内核架构》 / 《现代操作系统》 ### 概况 Linux程序设计 内存管理 整体分以下几块: 用户空间 内核空间 和 硬件层 用户空间主要是进程那块。 :::warning 进程的内存管理主要也是这篇文章想要探讨的。 但既然学 就把内存前后一起搞定了吧 ::: 内核空间有非常多的课题: ```bash= sys_brk sys_mmap sys_madvise ... VMA管理 缺页中断 匿名页面 page cache 页面回收 反向映射 KSM 页迁移 伙伴系统/页面分配器 页表管理(内核与用户进程) ``` 硬件层主要是 MMU/TLV,Cache,物理内存DDR。 ### 分页机制与内存寻址 **Physicla Address** > 访问物理内存的地址 **Virtual Address** > 操作系统引入的一个内存管理的抽象,进程 代码里用的都是虚拟地址。 需要经过MMU转换后才能访问物理地址 **Logical Address** \ > 分段机制中的概念 是段选择符和虚拟地址的组合。 **Linear Address** > 分段机制中的概念 逻辑地址经过分段映射之后得到的地址。 (如果没有分页,线性地址就是物理地址。) ps: 分段分页 忘光了 有点像cache的映射?  在Linux中 所有的段基址都为0 逻辑地址 = 虚拟地址 Linux采用分页的基址对内存进行寻址: 页基址 + 页内偏移 - 虚拟地址被分为两个部分 页面索引+页内偏移 - 通过页表将虚拟的页号转换成物理页框号 当然modern os走的是多级页表了,Linux中表现如下:  **TLB**: 缓存最近访问的虚拟页号到物理页框号的映射。 **大页机制** 没搞懂是啥 ### 虚拟地址空间管理 ...没啥好说的  内核地址应该还分内核逻辑地址和***? **VMA** > 一块连续的虚拟地址空间的抽象 拥有自身的权限和属性 > mm_struct的成员mmap指向存有VMA的链表 > 现在一般用红黑树保障特性 ### 物理内存管理 层级管理 : node zone page **Node** - NUMA架构 - 每个CPU都有自己的内存 - 访问其他的CPU内存需要通过QPI总线 - 远端内存访问延迟是本地内存的1.7倍 - Struct pglist_data - 描述了一个内存node下物理内存的信息(其实页框号 总页框数 zone链表等) **zone** 内存分区的概念。方便内存的分配和管理 以下三项标志大小? - DMA - NORMAL - HIGHMEM **Page Frame** 页框是物理内存的最小管理单元 描述信息存储来struct page中 ### 内存分配 ### 内存回收
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.