“事实胜于雄辩”,我们用一个小例子(原形取自《User-Level Memory Management》)来展示上面所讲的各种内存区的差别与位置。
#includestdio.h>
#includemalloc.h>
#includeunistd.h>
int bss_var;
int data_var0=1;
int main(int
argc,char **argv)
{
printf("below
are addresses of types of process's mem\n");
printf("Text
location:\n");
printf("\tAddress of main(Code Segment):%p\n",main);
printf("____________________________\n");
int stack_var0=2;
printf("Stack
Location:\n");
printf("\tInitial end of stack:%p\n",&stack_var0);
int stack_var1=3;
printf("\tnew end of stack:%p\n",&stack_var1);
printf("____________________________\n");
printf("Data
Location:\n");
printf("\tAddress of data_var(Data
Segment):%p\n",&data_var0);
static int data_var1=4;
printf("\tNew end of data_var(Data
Segment):%p\n",&data_var1);
printf("____________________________\n");
printf("BSS
Location:\n");
printf("\tAddress of bss_var:%p\n",&bss_var);
printf("____________________________\n");
char *b = sbrk((ptrdiff_t)0);
printf("Heap Location:\n");
printf("\tInitial end of heap:%p\n",b);
brk(b+4);
b=sbrk((ptrdiff_t)0);
printf("\tNew end of heap:%p\n",b);
return 0;
}
它的结果如下
below are addresses of types of process's mem
Text location:
Address of main(Code
Segment):0x8048388
____________________________
Stack Location:
Initial end of stack:0xbffffab4
new end of
stack:0xbffffab0
____________________________
Data Location:
Address of data_var(Data Segment):0x8049758
New end of data_var(Data Segment):0x804975c
____________________________
BSS Location:
Address of bss_var:0x8049864
____________________________
Heap Location:
Initial end of heap:0x8049868
New end of heap:0x804986c
利用size命令也可以看到程序的各段大小,比如执行size example会得到
text data bss dec
hex filename
1654 280
8 1942 796 example
进程内存空间
Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。
在讨论进程空间细节前,这里先要澄清下面几个问题:
l
第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
l
第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。
l
第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。
我们这里的实例希望利用内存映射,将系统内核中的一部分虚拟内存映射到用户空间,以供应用程序读取——你可利用它进行内核空间到用户空间的大规模信息传输。因此我们将试图写一个虚拟字符设备驱动程序,通过它将系统内核空间映射到用户空间——将内核虚拟内存映射到用户虚拟地址。从上一节已经看到Linux内核空间中包含两种虚拟地址:一种是物理和逻辑都连续的物理内存映射虚拟地址;另一种是逻辑连续但非物理连续的vmalloc分配的内存虚拟地址。我们的例子程序将演示把vmalloc分配的内核虚拟地址映射到用户地址空间的全过程。
程序里主要应解决两个问题:
第一是如何将vmalloc分配的内核虚拟内存正确地转化成物理地址?
因为内存映射先要获得被映射的物理地址,然后才能将其映射到要求的用户虚拟地址上。我们已经看到内核物理内存映射区域中的地址可以被内核函数virt_to_phys转换成实际的物理内存地址,但对于vmalloc分配的内核虚拟地址无法直接转化成物理地址,所以我们必须对这部分虚拟内存格外“照顾”——先将其转化成内核物理内存映射区域中的地址,然后在用virt_to_phys变为物理地址。
转化工作需要进行如下步骤:
a)
找到vmalloc虚拟内存对应的页表,并寻找到对应的页表项。
b)
获取页表项对应的页面指针
c)
通过页面得到对应的内核物理内存映射区域地址。
如下图所示:
SHAPE \* MERGEFORMAT
[2]
术语"BSS"已经有些年头了,它是block started by symbol的缩写。因为未初始化的变量没有对应的值,所以并不需要存储在可执行对象中。但是因为C标准强制规定未初始化的全局变量要被赋予特殊的默认值(基本上是0值),所以内核要从可执行代码装入变量(未赋值的)到内存中,然后将零页映射到该片内存上,于是这些未初始化变量就被赋予了0值。这样做避免了在目标文件中进行显式地初始化,减少空间浪费(来自《Linux内核开发》) [3]
还有些情况必须要求内存连续,比如DMA传输中使用的内存,由于不涉及页机制所以必须连续分配。 [4]
这种存储池的思想在计算机科学里广泛应用,比如数据库连接池、内存访问池等等。