[Pthread] Linux中的内存管理(二)--Layout

 

分类: Linux 711人阅读 评论(0) 收藏 举报
上次我们讲到了硬件平台和操作系统对物理内存的管理机制,经过这两层处理,应用程序中只要好好利用自己独占的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
posted @ 2013-05-27 10:57  tangr206  阅读(310)  评论(0编辑  收藏  举报