首页 新闻 会员 周边 捐助

Linux应用程序的4G内存分布

0
[待解决问题]

什么是虚拟内存?
IBM在解释虚拟内存的概念时用了四句话:
如果它存在,而且你能看到它,它是真实的;
如果它不存在,但是你能看到它,它是虚拟的;
如果它存在,但是你看不到它,它是透明的;
如果它不存在,而且你也看不到它,那肯定是你把它擦掉了。

在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,0~3G为用户空间,3G~4G为内核空间。而Windows系统为2:2,0~2G为用户空间,2G~4G为内核空间(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。

虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化,操作系统会为每个进程分配4G的虚拟地址空间。

Linux虚拟地址空间的分布如下图:
这里写图片描述
现在来分别介绍各段:

.text段

代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

.data段

数据段通常用于存放程序中已初始化且初值不为0的全局变量、静态全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。其中有一个.rodata段,一般用于存放常量字符串和只读变量。

.bss段

BSS(Block Started by Symbol)段中通常存放程序中以下符号:
1.未初始化的全局变量和静态局部变量
2.初始值为0的全局变量和静态局部变量(依赖于编译器实现)
bss可以理解为better save space,请思考:节省的这个空间是什么空间?

C语言中,未显式初始化的静态变量被初始化为0(算术类型)或空指针(指针类型)。由于程序加载时,bss会被操作系统清零,所以未赋初值或初值为0的全局变量都在bss段中。bss段仅为这些变量预留位置,它们在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的变量大小总和)。当加载器(loader)加载程序时,将为bss段分配的内存初始化为0。

注意:尽管未初始化和初始化为0的全局变量和静态局部变量均在bss段,但初始值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他文件已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(弱符号会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。
某些编译器将未初始化的全局变量保存在common段,链接时,等符号确定后,再放入.bss段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

下面看一个例子:

main.c

include <stdio.h>

int x;

int func()
{
x = 20;
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
test.c

include <stdio.h>

int func();
short x = 10;
short y = 20;
int main()
{
func();
printf("x=%d, y=%d\n", x, y);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
答案是:x=20, y=0。你答对了吗?
编译时,各文件是分开编译的,此时main.c里面的x是弱符号,func函数里x=20被编译成: mov dword ptr[x], 14h。往x的内存上写14h,x写4字节。在链接时,发现test.c里面有一个同名的强符号x,在调用func函数的时候取了short x的地址。因为指令已经在编译阶段编译,所以,这个14h就写在了short x的地址上。但是short x只有两个字节,其他的部分就写在了内存紧挨着的short y的内存上,覆盖了y原有的数据。小端模式,14h在内存上的保存方式是:00010100 00000000 00000000 00000000。因此,x=20,y=0。

bss段和data段有什么区别?
1.bss段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。
对于大型数组如int arr1[10000] = {1, 2, 3, …}和int arr2[10000],arr1要记录每个数据1,2,3…,位于.data段。arr2位于.bss段,只需要记录共有10000*4个字节需要初始化为0。此时bss为目标文件所节省的磁盘空间相当可观。
2.当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取.bss段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

堆用于存放进程运行时动态分配的内存,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针解引用间接访问。当进程调用malloc(C)/new(C++)等函数分配内存时,新分配的内存动态添加到堆上;当调用free(C)/delete(C++)等函数释放内存时,被释放的内存从堆中剔除。
分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
使用堆时经常出现两种问题:
1.释放或改写仍在使用的内存(“内存破坏”);
2.未释放不再使用的内存(“内存泄漏”)。
当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为实际分配的内存通常会为下个大于申请数量的2的幂次(如申12B,实际申请256B)。
堆向高地址方向增长

栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。
堆栈主要有三个用途:
1.为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
2.记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame))。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于bss段。
3.临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。
Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。
栈向低地址方向增长

共享库

程序运行时,编译器会自动链接libc.so/libc++.so动态链接库,程序中用到的库函数就被加载到共享库这部分内存上。共享库位于栈和堆之间。

内核空间

内核空间分为3部分,ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEME。
ZONE_DMA,0-16M,直接内存访问。该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。这部分的数据可以直接访问,目的在于加快磁盘和内存之间交换数据,数据不需要经过总线流向CPU的PC寄存器,再流向物理内存,直接通过总线就可到达物理内存。
ZONR_NORMAL,16M-896M,内核最重要的部分,该区域的物理页面是内核能够直接使用的。
ZONE_HIGHMEM,896M-结束,共128M,高端内存。主要用于32位Linux系统中,映射高于1G的物理内存。64位不需要高端内存。
这部分内容作为了解知识,如果你对操作系统内核特别感兴趣,推荐《Linux内核源代码情景分析》,此处不做详细介绍。

坐着坟头上吃饺子的主页 坐着坟头上吃饺子 | 菜鸟二级 | 园豆:202
提问于:2018-09-11 21:40
< >
分享
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册