[Pthread] Linux中的内存管理(二)--Layout
上次我们讲到了硬件平台和操作系统对物理内存的管理机制,经过这两层处理,应用程序中只要好好利用自己独占的4G虚拟内存就行了。这次我们来讨论一下这应用程序是如何分配这4G内存的,当然这都离不开编译器,操作系统的帮助。
PS: 在这之前,再强调一下,这里所说的虚拟内存是逻辑概念。和Windows平台上设置的那个虚拟内存不一样,那个相当于交换分区。
3. 用户进程的虚存布局
首先Linux把每个用户进程可访问的4G虚存空间分成两个大的部分。其中0x0~0xBFFFFFFF的3GB空间为用户态空间,用户态进程可以直接访问。从0xC0000000~0xFFFFFFFF的1GB空间为内核态空间,存放内核访问的代码和数据,用户态进程不能直接访问(Windows下是2G/2G)。进程只有通过中断/系统调用,从用户态切换到内核态,执行特权指令。
1GB的内核态空间映射到Linux内核,所以是所有用户进程共享的。当进程切换的时候,只需要切换用户态空间,内核态空间不变。这也是为什么内核很多地方需要加锁的原因,在较早的Linux实现中,使用的是大锁。如果有一个进程已经陷入内核,则其他进程如果也要陷入内核,则只能等待前一个进程从内核中退出。当然现在已经被很多细粒度的锁给取代了。
3GB的用户空间是每个用户进程真正可以独立访问和使用的,用户进程常常把它换分成代码段,数据段,BSS段,堆,栈等区域。其中,代码段主要用来存放操作指令,一般是只读的,不允许修改。数据段用来存放已初始化的全局变量(包括申明为static的变量),一般是可读可写的。BBS段(Block Started by Symbol)用来存放未初始化的全局变量,一般会初始化为0。堆一般用来存放在程序中动态分配的数据,可以动态的扩大缩小。栈一般用来存放函数的局部变量,包括函数调用时的参数,以及调用返回值。
通常我们在开发程序的时候并不会去考虑3G用户空间的某个具体的逻辑地址,都是使用的变量,或者变量地址。在经过编译,在产生的目标文件中生成了相应的逻辑地址。然后在链接的时候把某些地址重定位。如下的test.c程序:
int main( )
{
static int uninit_var; //未初始化的static变量
static int init_var = 1; //初始化的static变量
int stack_var = 2; //局部变量
int* heap_var = malloc(4*sizeof(int)); //堆变量
printf("Address of main (Text):%p/n",main);
printf("Address of init_var (Data):%p/n",&init_var);
printf("Address of uninit_var (BSS):%p/n",&uninit_var);
printf("Address of stack_var (Stack):%p/n",&stack_var);
printf("Address of heap_var (Heap):%p/n",heap_var);
return 0;
}
我们用gcc -c test.c -o test.o生成目标文件。这个ELF文件是可重定位的,用readelf -S ./test.o可以看到类似如下的输出:
[ 1] .text PROGBITS 00000000 000034 000097 00 AX 0 0 4
[ 2] .rel.text REL 00000000 0004a0 000070 08 9 1 4
[ 3] .data PROGBITS 00000000 0000cc 000004 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 0000d0 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 0000d0 00009f 00 A 0 0 4
...
我们可以看到所有目标文件的各种基地址都是从0x0开始,只是偏移不一样。.rel.text中记录了哪些是需要重定位的,如上面的printf,malloc。这些函数定义都需要链接的时候从相应的库中找到,并重定位。
然后我们用gcc test.o -o test生成可执行的ELF文件。用readelf -S ./test 可以看到类似如下的输出:
[ 9] .rel.dyn REL 080482b8 0002b8 000008 08 A 5 0 4
[10] .rel.plt REL 080482c0 0002c0 000020 08 A 5 12 4
[11] .init PROGBITS 080482e0 0002e0 000017 00 AX 0 0 4
[12] .plt PROGBITS 080482f8 0002f8 000050 04 AX 0 0 4
[13] .text PROGBITS 08048350 000350 0001dc 00 AX 0 0 16
[14] .fini PROGBITS 0804852c 00052c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048548 000548 0000a7 00 A 0 0 4
...
[23] .data PROGBITS 080496f0 0006f0 000010 00 WA 0 0 4
[24] .bss NOBITS 08049700 000700 000008 00 WA 0 0 4
...
我们看到经过链接后,目标文件的的基地址都发生了变化,都是唯一的逻辑地址了。其中.rel.dyn,.rel.plt等段中记录了哪些是需要运行时,由动态链接器加载重新定位的, 链接只是做了个标记,这些函数应该从哪个动态链接库中去找。如printf,malloc。
我们运行./test程序,可以看到输出:
Address of main (Text):0x80483f4
Address of init_var (Data):0x80496fc
Address of uninit_var (BSS):0x8049704
Address of stack_var (Stack):0xbfcc6a4c
Address of heap_var (Heap):0x804a008
即main函数的逻辑地址是0x80483f4,比对上面readelf -S ./test的输出,可以看到它位于.text段中
初始化的static变量init_var的逻辑地址是0x80496fc, 位于.data段中
未初始化的static变量uninit_var的逻辑地址是0x8049704,位于.bss段中
栈上的变量stack_var的逻辑地址是0xbfcc6a4c,从用户态空间的高地址(0xBFFFFFFF)向低地址增加
堆上的变量heap_var的逻辑地址是0x804a008,从用户态空间的低地址向高地址增加
所以用户程序进程在虚拟内存中的大致分布是:
0xFFFFFFFF
内核空间(1G)
0xBFFFFFFF
栈
.
.
. 用户空间(3G)
堆
BSS
数据段
代码段
0x00000000
当然这只是一个示意简图,只画出了主要的区段,其实虚存中还有其他的区段,包括内核空间也有自己的堆栈,还有ELF文件格式中区/段的区别,这里我就不详述了。感兴趣的同学可以去查看ELF文件格式说明等资料。
这一次,我们分析出了用户进程的虚存布局,下一次,我们将来研究,这些虚存是如何使用,如何被分配回收的。以及相应的物理内存又是怎么被分配回收的。
Pthread 08/01/21
PS: 在这之前,再强调一下,这里所说的虚拟内存是逻辑概念。和Windows平台上设置的那个虚拟内存不一样,那个相当于交换分区。
3. 用户进程的虚存布局
首先Linux把每个用户进程可访问的4G虚存空间分成两个大的部分。其中0x0~0xBFFFFFFF的3GB空间为用户态空间,用户态进程可以直接访问。从0xC0000000~0xFFFFFFFF的1GB空间为内核态空间,存放内核访问的代码和数据,用户态进程不能直接访问(Windows下是2G/2G)。进程只有通过中断/系统调用,从用户态切换到内核态,执行特权指令。
1GB的内核态空间映射到Linux内核,所以是所有用户进程共享的。当进程切换的时候,只需要切换用户态空间,内核态空间不变。这也是为什么内核很多地方需要加锁的原因,在较早的Linux实现中,使用的是大锁。如果有一个进程已经陷入内核,则其他进程如果也要陷入内核,则只能等待前一个进程从内核中退出。当然现在已经被很多细粒度的锁给取代了。
3GB的用户空间是每个用户进程真正可以独立访问和使用的,用户进程常常把它换分成代码段,数据段,BSS段,堆,栈等区域。其中,代码段主要用来存放操作指令,一般是只读的,不允许修改。数据段用来存放已初始化的全局变量(包括申明为static的变量),一般是可读可写的。BBS段(Block Started by Symbol)用来存放未初始化的全局变量,一般会初始化为0。堆一般用来存放在程序中动态分配的数据,可以动态的扩大缩小。栈一般用来存放函数的局部变量,包括函数调用时的参数,以及调用返回值。
通常我们在开发程序的时候并不会去考虑3G用户空间的某个具体的逻辑地址,都是使用的变量,或者变量地址。在经过编译,在产生的目标文件中生成了相应的逻辑地址。然后在链接的时候把某些地址重定位。如下的test.c程序:
int main( )
{
static int uninit_var; //未初始化的static变量
static int init_var = 1; //初始化的static变量
int stack_var = 2; //局部变量
int* heap_var = malloc(4*sizeof(int)); //堆变量
printf("Address of main (Text):%p/n",main);
printf("Address of init_var (Data):%p/n",&init_var);
printf("Address of uninit_var (BSS):%p/n",&uninit_var);
printf("Address of stack_var (Stack):%p/n",&stack_var);
printf("Address of heap_var (Heap):%p/n",heap_var);
return 0;
}
我们用gcc -c test.c -o test.o生成目标文件。这个ELF文件是可重定位的,用readelf -S ./test.o可以看到类似如下的输出:
[ 1] .text PROGBITS 00000000 000034 000097 00 AX 0 0 4
[ 2] .rel.text REL 00000000 0004a0 000070 08 9 1 4
[ 3] .data PROGBITS 00000000 0000cc 000004 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 0000d0 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 0000d0 00009f 00 A 0 0 4
...
我们可以看到所有目标文件的各种基地址都是从0x0开始,只是偏移不一样。.rel.text中记录了哪些是需要重定位的,如上面的printf,malloc。这些函数定义都需要链接的时候从相应的库中找到,并重定位。
然后我们用gcc test.o -o test生成可执行的ELF文件。用readelf -S ./test 可以看到类似如下的输出:
[ 9] .rel.dyn REL 080482b8 0002b8 000008 08 A 5 0 4
[10] .rel.plt REL 080482c0 0002c0 000020 08 A 5 12 4
[11] .init PROGBITS 080482e0 0002e0 000017 00 AX 0 0 4
[12] .plt PROGBITS 080482f8 0002f8 000050 04 AX 0 0 4
[13] .text PROGBITS 08048350 000350 0001dc 00 AX 0 0 16
[14] .fini PROGBITS 0804852c 00052c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048548 000548 0000a7 00 A 0 0 4
...
[23] .data PROGBITS 080496f0 0006f0 000010 00 WA 0 0 4
[24] .bss NOBITS 08049700 000700 000008 00 WA 0 0 4
...
我们看到经过链接后,目标文件的的基地址都发生了变化,都是唯一的逻辑地址了。其中.rel.dyn,.rel.plt等段中记录了哪些是需要运行时,由动态链接器加载重新定位的, 链接只是做了个标记,这些函数应该从哪个动态链接库中去找。如printf,malloc。
我们运行./test程序,可以看到输出:
Address of main (Text):0x80483f4
Address of init_var (Data):0x80496fc
Address of uninit_var (BSS):0x8049704
Address of stack_var (Stack):0xbfcc6a4c
Address of heap_var (Heap):0x804a008
即main函数的逻辑地址是0x80483f4,比对上面readelf -S ./test的输出,可以看到它位于.text段中
初始化的static变量init_var的逻辑地址是0x80496fc, 位于.data段中
未初始化的static变量uninit_var的逻辑地址是0x8049704,位于.bss段中
栈上的变量stack_var的逻辑地址是0xbfcc6a4c,从用户态空间的高地址(0xBFFFFFFF)向低地址增加
堆上的变量heap_var的逻辑地址是0x804a008,从用户态空间的低地址向高地址增加
所以用户程序进程在虚拟内存中的大致分布是:
0xFFFFFFFF
内核空间(1G)
0xBFFFFFFF
栈
.
.
. 用户空间(3G)
堆
BSS
数据段
代码段
0x00000000
当然这只是一个示意简图,只画出了主要的区段,其实虚存中还有其他的区段,包括内核空间也有自己的堆栈,还有ELF文件格式中区/段的区别,这里我就不详述了。感兴趣的同学可以去查看ELF文件格式说明等资料。
这一次,我们分析出了用户进程的虚存布局,下一次,我们将来研究,这些虚存是如何使用,如何被分配回收的。以及相应的物理内存又是怎么被分配回收的。
Pthread 08/01/21