Try   HackMD

程序运行背后的内存管理机制

contributed by <Felizs>

参考资料

你所不知道的C Jserv 看到50min
What a C programmer should know about memory札记)(非常大的主题g libc的维护者 Drepper的文章)
Anatomy of a Program in Memory
What Every Programmer Should Know A bout Memory


可能很多人都知道每个程序员都应该了解的内存知识这篇文章,我也大概看过,写的太深入(底层过于详细)了,只能囫囵吞枣,读完也不清楚重点。但搞内存,脑子里总是要有个框图的,所以梳理了一下与日常工作更相关的部分(主要是进程的虚拟内存管理)。

行文主要由以下五个部分组成:

  • 虚拟内存(内存空间布局介绍)
  • 栈分配
  • 堆分配
  • 内存映射
  • 内存消耗

引申出的主题会非常多,也在此处做一个记录,日后可以整理成博文。

  • 指针与内存分配
  • 函数与内存分配
  • Cache原理
  • Linux中的内存管理

虚拟内存

我们一般在保护模式下工作,程序拥有各自的虚拟地址空间。虚拟意味着我们并不受可用内存的束缚,同时也无权拥有它们。如果需要使用虚拟地址空间,我们需要os的提供真实的物理支持,这就叫做mapping。支持可以是物理内存(不一定是RAM) 或者持久性存储。 前者称为'匿名映射'。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
VMA可能会分配它并不拥有的空间,给出过度承诺(overcommiting)。每次分配后进行NULL检查是由必要的,但不能确保不出错。 因为过度承诺的存在,os可能给了你足够多的指向内存的有效指针,但当你试图访问的时候,就Out of memory了。和银行有些像。

虚拟内存-整体视图

多任务操作系统的每个进程都在它自己的虚拟地址空间中运行,在32位的模式下,它就是4GB的内存地址块。这些虚拟地址通过页表映射到物理内存,这些页表是由操作系统维护,由处理器进行查询的。 每个一个进程都有他自己的一组页表.

一旦虚拟空间被启用了,它们会应用于及其中运行的所有软件,包括内核本身。所以虚拟空间地址的一部分必须保留给内核。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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中,内核空间一直存在,并在所有进程中映射相同的物理内存。内核代码和数据一直是可寻址的,随时可以处理中断和系统调用。相反,每当进程切换发生时,地址空间的用户模式部分的映射都会改变。
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
above, Firefox has used far more of its virtual address space due to its legendary memory hunger

下图是Linux进程的标准段布局:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
当计算是安全友好的时候,上图显示的段的起始地址堆几乎堆计算机中的每个进程都是一样的。而漏洞常常引用绝对的内存地址:堆栈上的地址,库函数的地址等,攻击者可以闭着眼睛找到这些位置。因此地址空间随机化已经变得非常流行。Linux会通过加入偏移量的方法随机化栈,内存映射段和堆(不幸的是32的地址空间已经非常紧凑了)

虚拟内存-栈

进程最上边的段称作栈(向下增长),存储的是本地变量和函数参数(在大部分语言中)。调用函数的时候会在栈中push一个栈帧,栈帧在函数返回的时候被摧毁。这种简单的设计是因为数据遵循严格的LIFO(Last In First Out)顺序。所以跟踪栈内容只需要简单地改变栈指针,push和pop因此非常快速且拥有确定性。此外,对栈的重用会倾向于在cpu缓存里保留活跃的栈内存。

push超过其适合范围的数据可能会耗尽栈内存,这会触发页错误。该错误在Linux中expand_stack()处理,该错误又调用acct_stack_growth()来检查是否适合增加堆栈。栈大小会根据需求进行调整:当栈小于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() and friends, whereas in a garbage-collected language like C# the interface is the new keyword.

如果堆有足够的空间去满足分配内存的请求时,它可以直接由语言运行时处理,而不需要内核介入。否则堆就通过brk系统调来生成空间。堆的管理是很复杂的,需要复杂的算法来应对应用程序混乱的分配模式,以提高速度和有效的内存访问。实时系统拥有专门的分配器去处理这个问题。 堆同时也是碎片化的,如下所示:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

虚拟内存-内存映射段

在这里,内核将文件的内容直接映射到内存。 任何Linux的程序都可以通过mmap()系统调用来请求(创建?)此类映射。内存映射是一种执行文件IO的便捷且高效能的方式,因此它用来加载动态库。也可以在这个区域创建不应用于任何文件的匿名内存映射,将其用于程序数据。 在Linux中,如果你通过malloc()请求大块内存,则C库会创建此类匿名映射,而不使用堆内存。

BSS data Program text

BSS和数据段都是用来存静态(全局)变量的内容。区别在于BSS存的是未初始化的静态变量的内容。 BSS的内存区域是匿名的,它不映射任何文件。 比如声明了static int a , a就在BSS段。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

据段存储了指针,已被初始化的变量。这个存储区域不是匿名的,映射了程序二进制图像的一部分。即使数据段映射了一个文件,它也是一个私有的内存映射,意味着不会反应全局变量的更新。

指针指向的实际字符串,二进制文件存储在text段。


堆分配

slab

如何最佳利用cache memory

Demand paging

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程序设计 内存管理 整体分以下几块: 用户空间 内核空间 和 硬件层

用户空间主要是进程那块。

进程的内存管理主要也是这篇文章想要探讨的。 但既然学 就把内存前后一起搞定了吧

内核空间有非常多的课题:

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的映射?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

在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中

内存分配

内存回收