一:Linux虚拟地址空间布局
在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space
),在32
位模式下它是一个4GB
的内存地址块。
在Linux
系统中, 内核进程和用户进程所占的虚拟内存比例是1:3
,而Windows系统为2:2
(通过设置Large-Address-Aware Executables
标志也可为1:3
)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。
虚拟地址通过页表(Page Table
)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault
)。
在Linux
中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。
Linux
进程在虚拟内存中的标准内存段布局如下图所示:
其中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel
处理器的段没有关系。
上图中Random stack offset
和Random mmap offset
等随机值意在防止恶意程序。
Linux
通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。
execve(2)
负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)
还会将BSS
段清零。
用户进程部分分段存储内容如下表所示(按地址递减顺序):
在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS
段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。
BSS
段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。
以下详细介绍各个分段的含义。
1 内核空间
内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
2 栈(stack)
栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。堆栈主要有三个用途:
为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame)或过程活动记录(Procedure Activation Record)。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。
持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。
Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。
堆栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述堆栈向下增长。
堆栈的大小在运行时由内核动态调整。
3 内存映射段(mmap)
此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux
的mmap()
系统调用或Windows
的CreateFileMapping()/MapViewOfFile()
请求这种映射。内存映射是一种方便高效的文件I/O
方式, 因而被用于装载动态共享库。用户也可创建匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。在Linux
中,若通过malloc()
请求一大块内存,C
运行库将创建一个匿名内存映射,而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD
还大,缺省为128KB
,可通过mallopt()
调整。
该区域用于映射可执行文件用到的动态链接库。在Linux 2.4
版本中,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000
开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 2.6
内核中,共享库的起始地址被往上移动至更靠近栈区的位置。
从进程地址空间的布局可以看到,在有共享库的情况下,留给堆的可用空间还有两处:一处是从.bss
段到0x40000000
,约不到1GB
的空间;另一处是从共享库到栈之间的空间,约不到2GB
。这两块空间大小取决于栈、共享库的大小和数量。这样来看,是否应用程序可申请的最大堆空间只有2GB
?事实上,这与Linux
内核版本有关。在上面给出的进程地址空间经典布局图中,共享库的装载地址为0x40000000
,这实际上是Linux kernel 2.6
版本之前的情况了,在2.6
版本里,共享库的装载地址已经被挪到靠近栈的位置,即位于0xBFxxxxxx
附近,因此,此时的堆范围就不会被共享库分割成2
个“碎片”,故kernel 2.6
的32
位Linux
系统中,malloc
申请的最大内存理论值在2.9GB
左右。
4 堆(heap)
堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc©/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
使用堆时经常出现两种问题:1) 释放或改写仍在使用的内存(“内存破坏”);2)未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据