先转一张图,图出自chinaunix论坛,but具体找不到是哪个帖子。
写在最前面:linux每个进程都有4G虚拟内存空间,而虚拟内存可以这么理解,就相当于我用3公里的铁轨(物理内存),让火车(程序当前需要的物理内存数)从北京跑到上海(虚拟内存), 只要铁轨铺的够及时,火车始终能在铁轨上跑。
在应用层看到的内存地址全部都是虚拟地址。
一:
在图上面,观察到物理内存是分区的,分别分成DMA,NORMAL,HIGHMEM。这个分区过程是在start_kernel的时候,setup_arch ->arch_mem_init(cmdline_p) ->paging_init() ->free_area_init(zones_size);->free_area_init_core 这一系列的流程完成了物理内存的分区。but一般嵌入式系统中,只有ZONE_DMA一个区。
分区完毕后,需将这些区通过链表连接起来,这时候就出现了zonelist,同样是在start_kernel中 build_all_zonelists ->__build_all_zonelists->build_zonelists,通过这三个不同的zone,链接在zonelist->zone上面。
start_kernel接下来关于内存部分的,就该轮到mem_init了,mem_init其实也没什么花头,就是统计一下有多少空闲页,然后将空闲的页放入zone->free_area[order]->free_list(page->lru在这个链表里面),zone->free_area[order]->nr_free是记录空闲页数量的。这个函数可以这么理解,对物理内存进行了以及比较系统的初始化,一页一页的比较清晰了。这时候已经精确到页了。
salb接下来就是这个蛋疼的伙伴系统了,其实也很容易理解,这个系统就是用来分配物理内存的,比如我申请一个32字节的内存,它就会给我去32字节的那个链表的物理页里寻找空间,然后分配给我。 kmem_cache_init这个start_kernel函数就是slab功能的初始化。而这个slab也就先搞几个大小不等的(32,64,128…………)的空间页表出来,再者有三链结构,slabs_partial; slabs_full;slabs_free;这个三链是用来管理分配物理内存的,当其中某个特定大小的三链被填满后,就会重新分配出新的一页,page_alloc。start_kernel经过这一步骤后,就可以使用各种kmalloc类似的函数了。到这里结束,物理内存已经被有效的管理起来了。
内核部分暂时告一段落。
二:
接下来就是蛋疼的task->mm,这个东西怎么出来的呢,揭露过程有点羞涩,中间可能存在一些错误观点。
一个程序第一件事情就是do_execve(只有第一次载入镜像的时候才会有此过程),这时候就是去调用mm_alloc,初始化current->mm。
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
……
bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
bprm->mm = mm_alloc(); //这里malloc出mm
……
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
retval = search_binary_handler(bprm,regs); //这个
……
}
上面的代码做大的简化,就留了几段对我们理解有用的。其中mm_alloc最最重要的是生成了pgd这个目录项,这个目录项最后可用于物理地址的查找,由于pgd分配的是物理地址,所以每个进程的pgd是不同的。
因为我们的执行文件是elf,所以search_binary_handle中最为关键的load_elf_binary。在load_elf_binary这个函数里,实现了执行文件code的载入,数据段的载入,以及栈顶位置的分配。
比较有用的载入信息是:1、函数里的静态变量存放在数据段里2、全局变量在BSS里面,由程序编译完链接的时候定义3、函数里的参数,存放在栈里,栈顶set_arg_pages(bprm,randomize_stack_top(STACK_TOP),executable_table)是随机分配的
下面通过一个例子来简单说明这载入信息。
#include <stdio.h>
#include <malloc.h>
int z=0;
static int y=0;
int main()
{
unsigned int i;
i=0;
static int j=1;
char *c;
c = (char *)malloc(1024*1024);
printf("i:%lx\n",&i);
printf("global_address:%lx\n",&z);
printf("static g_address:%lx\n",&j);
printf("static:%lx\n",&y);
printf("malloc:%lx\n",c);
free(c);
return 0;
}
运行结果:
star: 0x440a74 end:0x440b30 stack:0x7fad4e80//这个是在内核打印的,跟程序无关,start和end都是data数据段的
i:7fad4d9c
g_address:440b50
staticg_address:440b54
static:440ac0
malloc:2abe1008
这还比较云里雾里,but有一点是确定的,非全局变量的静态数据存放在data数据段里。接下来用readelf -a来看编译完后的信息。
ELF Header:
……
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
……
[21] .bss NOBITS 00440b30 000b30 000030 00 WA 0 0 16
……
There are no sectiongroups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
……
LOAD 0x000a74 0x00440a74 0x00440a740x000bc 0x000ec RW 0x1000
……
There are norelocations in this file.
There are no unwindsections in this file
Symbol table'.dynsym' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
……
18: 00440b50 4 OBJECT GLOBAL DEFAULT 21 z
……
Symbol table'.symtab' contains 80 entries:
Num: Value Size Type Bind Vis Ndx Name
……
50: 00440b54 4 OBJECT LOCAL DEFAULT 21 y
……
62: 00440b50 4 OBJECT GLOBAL DEFAULT 21 z
……
这时候是不是已经发觉了,全部变量的虚拟地址也是在编译的时候给定义好了。这时候其实很容易想到了这些编译完后就存在的信息,都存放在mm->vma里面,一个个vma链表。数据段,bss段这些vma,是程序一开启就挂载上去的,malloc出来的vma,只要不去释放,也是挂在那里的,而栈的vma就随时在各种删除添加。至于VMA,它有一个红黑树和单向链表,这个查找起来和插入进去都很方便。
三:
那么程序跑起来后是怎么调用物理地址的呢?在do_page_fault(缺页中断)中将虚拟内存转换成物理内存,首先要找到虚拟内存所在的vma,接下来就是需找物理内存pgd是一个4k(4096)表,虚拟内存的bit 31-22是pgd的索引,得到pmd,因为采用二级页表,所以pmd也就是pgd,再根据pmd,结合虚拟地址的bit 21-12 得到pte,再感觉pte得到物理内存的前20 bit,虚拟地址的后12bit结合,得到相应的物理地址。其实pgd,pte可以这么理解,都是1024字节的表,1024*1024这是这两张表结合起来的大小,再加上偏移量是4k。也就是4k*1024*1024=4G,刚好进程虚拟地址的大小。
本人水平有限,只能大致分析至此。这部分东西很多很羞涩难懂。