Linux 进程虚拟地址空间布局

1.简介

虚拟地址空间(Virtual Address Space)是每一个程序被加载运行,操作系统为进程分配的虚拟内存,它为每个进程提供了一个独占使用主存的假象。

每个进程所能访问的最大的虚拟地址空间由计算机的硬件平台决定,具体地说是由 CPU 的位数决定的。比如 32 位的 CPU 决定了虚拟地址空间的大小为 0 - 2 32 2^{32} 232-1,即 0x00000000 - 0xFFFFFFFF,也就是我们常说的 4 GB 虚拟内存空间。如果是 64 位的CPU,那么寻址范围是 0 - 2 64 2^{64} 264-1,即 0x0000000000000000 - 0xFFFFFFFFFFFFFFFF,共有 17 179 864 184 GB。

假设我们使用的是 32 位的硬件平台,4GB 的虚拟内存空间可以被用户程序完全占用吗?很显然,不行。因为除了用户进程,操作系统会独占一部分虚拟内存空间,用户进程只能使用操作系统分配给进程的地址空间,如果用户进程访问未经允许的地址空间,则会被操作系统判为非法请求,结果就是程序被操作系统强制结束。比如 Windows 下的“进程因非法操作需要关闭” 和 Linux 下的 “Segmentation fault”,一般都是由于进程访问了非法的内存地址。

对于 Linux,4GB 的虚拟地址空间的默认分配状态如下:
在这里插入图片描述

2.布局

C/C++ 程序为编译链接后生成可执行的二进制文件,由多个段组成,一般包含代码段、数据段和 BSS 段等。由于可执行文件段的数量较多,映射到虚拟地址空间时,由于段的大小往往并不是系统页大小的整数倍,多余部分也会占用一个页,这就会造成内存空间的浪费。当操作系统装载程序时,会进行优化,将多个相同属性的段合并成一个段进行装入,比如将相同权限的段合并成一个段进行映射。

段的权限一般分为如下三种:
(1)以代码段 .text 为代表的可读可执行的段。
(2)以数据段 .data 和未初始化数据段 .bss 为代表的可读可写的段。
(3)以只读数据段 .rodata 为代表的只读的段。

比如 .text 和 .init 段,分别包含程序的可执行代码和初始化代码,操作系统在装载程序时可以将这两个段合并成一个段(Segment)进行映射,以节省内存空间。

说到合并后的段(Segment)和合并之前的段(Section),虽然中文叫法相同,但对于英文称谓不同。合并后的段是 Segment,是程序装载时的概念,合并之前的段是 Section,是程序链接时的概念,需要加以区分。系统按照 Segment 进行装载映射可执行文件而不是 Section。

可执行文件载入内存运行时,在 Linux 环境下的虚拟地址空间一般由代码段、初始化数据段、未初始化数据段、堆和栈构成,如果程序使用了内存映射文件(比如共享库、共享文件),那么包含映射段。

Linux 环境进程典型的内存布局如下图所示:
这里写图片描述
有时候,把 BSS 段与 Data 段看做成一个可读写的数据段也是可以的,这里做了区分。下面简要说明程序装载时相关的段。

  • 代码段(Text Segment)

用户存放 CPU 执行的机器指令,为防止指令被其它程序修改,代码段一般只读不可更改。比如,源码中的字符串常量存储于代码段,不可修改。

  • 初始化数据段(Data Segment)

又称为数据段,用于存储初始化的全局变量和 Static 变量,段大小在编译时确定,所以内存分配属于静态分配。

  • 未初始化数据段(BSS Segment,Block Started by Symbol)

又称为 BSS 段,通常用来存放程序中未初始化的全局变量和 Static 变量。虽未显示初始化,但在程序载入内存执行时,由内核清 0,所以未显示初始化则默认为 0。BSS 段的大小也是在编译时确定,内存分配属于静态分配。

  • 堆(Heap)

用于保存程序运行时动态申请的内存空间,由开发人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间,比如使用malloc()或new申请的内存空间。堆的地址空间“向上增加”,即当堆上保存的数据越多,堆的地址就越高。堆的内存分配属于动态分配,一般运行时才知道分配的内存大小,并且堆可分配存活于函数之外的内存,在未显示调用free()或delete释放时,其生命周期为进程的生命周期。

  • 映射段(Memory Mapping Segment)

该区域内核将文件内容直接映射到内存。任何应用程序都可以请求该区域。Linux 中通过mmap()系统调用,Windows 通过creatFileMapping()/MapViewOfFile()创建。文件 I/O 时内存映射方便并且高效,所以,它常用来加载动态库,还可以创建一种匿名映射,并不对应于文件,而用于程序数据。在 Linux 中,如果使用 malloc() 申请一块过大的内存,C 库函数便会创建这种内存映射段,而不是使用堆内存。过大的内存指超过 M_MMAP_THRESHOLD 字节,默认128KB,可以通过 mallopt() 函数调整。映射段也属于动态分配。

  • 栈(Stack)

用于保存函数的局部变量(但不包括 static 声明的静态变量,静态变量存放在数据段或 BSS 段)、参数、返回值、函数返回地址以及调用者环境信息(比如寄存器值)等信息,由系统进行内存的管理,在函数完成执行后,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以由用户自行设定,Windows默认的栈区大小为1M,可通过Visual Studio更改编译参数手动更改栈的大小。64bits的Linux默认栈大小为10MB,可通过命令ulimit -s临时修改。栈是一种“后进先出”(Last In First Out,LIFO)的数据结构,这意味着最后入栈的数据,将会是第一个出栈的数据。对于那些暂时存贮无需长期保存的信息来说,LIFO这种数据结构非常理想。在调用函数后,系统通常会清除栈上保存的信息。栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。

  • 内核空间(Kernel Space)

用于存储操作系统和驱动程序,用户空间用于存储用户的应用程序,二者不能简单地使用指针传递数据。当一个进程执行系统调用而陷入内核空间执行内核代码时,我们称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态),即此时处理器在执行最低特权级(3级)用户代码。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈,这与处于内核态进程的状态有些类似。

内存段的特点和区别如下。

段名存储内容分配方式生长方向读写特点运行态
代码段程序指令、字符串常量、虚函数表静态分配由低到高只读用户态
数据段初始化的全局变量和静态变量静态分配由低到高可读可写用户态
BSS段未初始化的全局变量和静态变量静态分配由低到高可读可写用户态
动态申请的数据动态分配由低到高可读可写用户态
映射段动态链接库、共享文件、匿名映射对象动态分配由低到高可读可写用户态
局部变量、函数参数与返回值、函数返回地址、调用者环境信息静态+动态分配由高到低可读可写用户态
内核空间操作系统、驱动程序静态+动态分配由低到高+由高到低不能直接访问内核态

由于内核空间包含内核栈和内核的数据段,所以内存地址生长方向既有由低到高(内核数据段),也有由高到低(内核栈)。关于读写的特点,由内核进行读写,用户程序不可直接访问。

下面以 C++ 为例,看一下常见变量所属的内存段。

#include <string.h>

int a = 0;                 		// a在数据段,0为文字常量,在代码段
char *p1;                  		// BSS段,系统默认初始化为NULL
void main() {
    int b;                 		// 栈
    char *p2 = "123456";  		// 字符串"123456"在代码段,p2在栈上
    static int c = 0;      		// c 在数据段
    const int d = 0; 			// 栈
    static const int d;			// 数据段
    p1 = (char*)malloc(10);		// 分配的10字节在堆
    strcpy(p1,"123456"); 		// "123456"放在代码段,编译器可能会将它与p2所指向的"123456"优化成一个地方
}

以上所有代码,编译成二进制机器指令存放于代码段,不可修改。


参考文献

linux内核空间和用户空间详解
程序或-内存区域分配(五个段)–终于搞明白了
进程内存分布剖析
深入理解计算机系统中文版[M].C1.7.3虚拟内存.P12-P14
深入理解计算机系统中文版[M].C9.7.2.Linux虚拟内存系统.P580-P581
俞甲子,石凡,等.程序员的自我修养——链接、装载与库[M].北京:电子工业出版社,2009-04.C6.4 进程虚存空间分布.P161-173
认真分析mmap:是什么为什么怎么用- 胡潇

  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值