剖析应用程序内存

原文:《Anatomy of a Program in Memory


   内存管理是操作系统的核心;对于程序员和系统管理员来说,内存管理都是非常重要的。文中涉及到的是常见的概念,例子也大多来源于Linux和 Windows 32位操作系统。本文主要描述了应用程序中的内存布局。

   在多任务操作系统中,每个进程都运行在自己的内存沙箱中。沙箱即是进程的虚拟地址空间(virtual address space),32位系统中进程的虚拟地址空间是4GB,虚拟地址空间通过页表(page table)被映射到实际的物理内存,页表由操作系统内核维护,且被处理器访问。每个进程有属于它自己的页表(page table)集合,但这里有个陷阱需要注意。一旦虚拟地址空间被使用,它将应用到机器上所以运行的程序,包括操作系统内核本身。因此,一部分虚拟地址空间必须被保留给内核:

这里写图片描述

   这并不意味着内核真的使用了这么多的物理内存,仅仅是保留这部分地址空间用来映射到程序期望的物理空间而已。页表中的内核空间被标记独占式的特权代码,一旦用户模式下的程序试图去访问它,就会触发一个page fault。在linux中,所有进程的内核空间是持续存在的、且映射到相同的物理空间。内核代码和数据总是通过寻址可找到的,且准备好在任何时间来处理中断或系统调用。相比之下,当进程切换发送时,虚拟地址空间中用户模式所占的部分总是发生改变。

这里写图片描述

   上图中,蓝色部分代表被映射到实际物理内存的虚拟地址,而白色代表没有被映射到的虚拟地址。虚拟地址空间中不同颜色的地带对应于不同的内存段(memory segment),例如堆、栈等。下图是Linux程序的标准内存段布局:

这里写图片描述

   对于机器中的所有进程来说,每个内存段采用相同的起始地址,将导致远程安全漏洞,因为远程攻击者知道进程的栈地址,库函数地址,将能够轻易的修改内存段中的数据。因此,地址空间随机化变的流行起来。Linux在栈、内存映射段、堆的起始地址上加上随机的偏移量作为它们新的起始地址。32位系统由于地址空间非常紧凑、导致留个随机化的空间很小、影响了效率。

栈(Stack)

   在进程地址空间中,最高层的段是栈,栈在多数编程语言中用来存储局部变量和函数参数。调用一个方法或者函数会向栈中入栈一个新的栈帧(stack frame)。栈帧在函数返回的时候被销毁。可能因为栈中的数据遵循严格的先进后出顺序,所以这个简单的设计意味着我们不需要复杂的数据结构来追踪栈中的内容 —— 一个指向栈顶的简单的指针就可以满足需求。入栈和出栈操作因此是非常快速和确定的。此外,栈区域的不断重用会趋向于使栈内存在CPU缓存中保持活跃,加速CPU对栈中数据的访问。进程中的每个线程都有自己的栈。

   入栈大量的数据可能会耗尽栈的内存空间,这时将触发一个页错误(page fault),在Linux中页错误将由expand_stack()函数处理,expand_stack()函数反过来会调用acct_stack_growth()函数来检测是否需要增加栈的内存空间。如果当前栈的内存空间小于RLIMIT_STACK(通常为8MB),那么一般来说,栈的内存空间将会增长,应用程序继续运行。这也是栈内存空间增长到需求大小时的正常机制。然而,如果当前栈的内存空间已经达到了最大栈内存空间(即RLIMIT_STACK),那么栈的内存空间将不能够继续增长,这时我们将遇到栈溢出(stack overflow)的问题,程序将因为段错误(Segmentation Fault)而异常中止。虽然栈的内存空间会增长以满足需求,但当栈中的数据出栈后,栈的内存空间不会退缩。也就是说栈的内存空间只会增长,不会收缩。

   访问未映射到实际物理内存的内存区域(简称为未映射内存空间,上图中白色区域)的唯一方式就是动态栈增长。其他任何访问未映射内存区域的方式将会触发页错误(page fault),继而导致程序出现段错误(Segmentation Fault)。一些已经映射到实际物理内存的内存区域可能是只读的,一旦尝试去写该只读内存区域,也将导致程序出现段错误(Segmentation Fault)。

内存映射段(Memory Mapping Segment)

   栈的下方即是内存映射段(Memory Mapping Segment),内核直接映射文件内容到该内存区域。任何程序都可以要求这样的映射,Linux中通过mmap()系统调用来映射文件内容到该区域,Windows中通过CreateFileMapping() / MapViewOfFile()来映射文件内容到该区域。内存映射是一种非常方便和高性能的处理文件I/O的方式,因此该区域也被用来载入程序需要链接的动态库。也可以创建一个不跟任何文件关联的匿名内存映射(anonymous memory mapping),而不仅仅被用于程序数据(动态库、文件)。在Linux中,如果你通过malloc()请求分配大块的内存,C语言库将创建一个匿名内存空间映射,而不是在堆上分配。那多大的内存算是大内存呢?一般指大于 MMAP_THRESHOLD字节的内存为大内存, MMAP_THRESHOLD默认是128kB,该值可通过mallopt()函数修改。

堆(Heap)

   内存映射段下方就是堆(heap),堆提供了程序运行时的内存分配,跟栈相同的是,数据必须比内存分配函数活的更长,跟栈不同的是,大多数语言向程序提供了堆内存管理。因此,满足堆内存需求需要语言运行时和内核之间共同作用。C语言中,堆内存分配接口是malloc()及相关函数,而在像C#这种有垃圾回收机制的语言中,堆内存分配接口是new关键字。

   如果堆中有足够的空间来满足内存需求,那么分配内存的操作将在语言运行时被处理,且不需要内核的参与。除此之外,堆内存空间可以通过brk()系统调用来增长,以便有足够的空间来分配。堆管理是复杂的,在面对我们程序混乱的内存分配模式时,需要负责的算法来争取速度和高效的内存使用率。处理堆分配请求操作的时间是大幅度变化的。实时系统中采用 special-purpose allocators来解决这个问题。随着在堆上不断的分配和释放内存空间,堆会变得支离破碎,如下所示:

这里写图片描述

BSS Segment、Data Segment、Text Segment

   最后,我们将认识图中最下方的几个段:BSS、Data,和Text。C语言中,BSS和Data都用来存储静态(全局)变量的内容。不同之处是,BSS存储的是在源码中没有被初始化的静态变量的内容。BSS内存区域是匿名的:它不会映射到任何文件。如果你定义了static int cntActiveUsers,那么cntActiveUsers 的内容存在于BSS中。

   Data段存储着在源码中被初始化后的静态变量的内容。这是个具名内存空间。它映射了程序的部分二进制镜像,其包含了在源码中给出的已初始化静态变量的值。因此,如果你定义了static int cntWorkerBees = 10cntWorkerBees 的内容存在于Data段中,且内容为10。尽管Data段映射了文件,但它是一个私有内存映射(private memory mapping),这意味着更新该部分内存空间的内容不会造成和底层映射文件的冲突。否则的话,更新全局变量的内容导致硬盘上的二进制镜像改变,这将是不可思议的。

   在下图所示的例子中,指针gonzo的内容(4-byte地址,0x080484f0)存储在Data段中,但指针指向的字符串内容(”God’s own prototype”)并不存储在该区域中,而是在Text段。Text段是只读内存空间,它将二进制可执行文件映射到内存中,对该区域的写操作将触发一个段错误( Segmentation Fault)。下图展示了这三个段及示例变量:

这里写图片描述

   通过读取文件/proc/pid_of_process/maps可以查看Linux进程的各个内存区域。一个段可能包含多个区域,举例说明,在内存映射段中,每个内存映射文件通常有自己的内存区域,动态库有跟BSS和Data段相似的额外内存区域。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值