页表及内存映射

来源:互联网 发布:淘宝怎么看别人日销量 编辑:程序博客网 时间:2024/06/04 19:26

原文地址找不到了

用户空间:
0x00000000~0xbfffffff
内核空间:
0xc0000000~0xffffffff
内核前896M线性空间:
0xc0000000~0xc0037fff
 
页目录表
swapper_pg_dir
系统启动时会先用这个目录表的前两项初始化前8M的物理空间,分别映射到用户空间和内核空间的开始部分。
页目录表共有0~1023共1024项。从实模式转换到保护模式之后会把0~767这768项映射成用户空间,1个页目录
对应一个页表,一个页表共1024项,每一项对应一个4K的页,所以一个页表映射的空间为4K*1024=4M,
那768*4M*1024=3G,正好为用户空间大小。最后,768和769个目录项中又存放pg0和pg1这两个页表的地址和属性。
剩下的770~1023这254项总大小为:254*4M*1024=1G,正好映射内核空间。
pg0/pg1是映射最初8M空间的页表的物理地址。
 
 
我们知道编译好的可执行程序都保存在硬盘上,那么它是怎么load到内存以及被用户访问的呢?
大概讲一下过程:当某个程序开始执行时,可执行映象必须装入到进程的虚拟地址空间。当程序执行时,被引用的部分会由操作系统装入到物理内存。映射到进程虚拟地址空间这一过程由do_mmap()函数完成。它扫描当前进程地址空间的vm_area_struct结构所形成的红黑树,找到一个合适的区间,找到之后为新的虚拟区分配一个vm_area_struct结构,这是通过调用Slab分配函数kmem_cache_alloc()来实现的,然后就对这个结构的各个域进行了初始化。页面的映射到底在何时建立?实际上,generic_file_mmap( )就是真正进行映射的函数。因为这个函数的实现涉及很多文件系统的内容,了解了文件系统的有关内容后,再分析。注意,这里要说明的是,文件到虚存的映射仅仅是建立了一种映射关系,也就是说,虚存页面到物理页面之间的映射还没有建立。当某个可执行映象映射到进程虚拟内存中并开始执行时,因为只有很少一部分虚拟内存区间装入到了物理内存,可能会遇到所访问的数据不在物理内存。这时,处理器将向 Linux 报告一个页故障及其对应的故障原因,于是就用到了请页机制(即缺页异常)。虚拟地址映射到物理地址,只有在请页时才完成,这时要建立页表和更新页表(页表是动态建立的)。页表不可被换出,不记年龄,它们被内核中保留,只有在exit时清除。
------------------------------------------------------------------------
从线性地址找到物理地址的过程:
Linux采用页式映射管理。虚拟地址空间划分成固定大小的“页面”,由MMU在运行时将虚拟地址“映射”成某个物理内存页面中的地址。由于i386系列的历史演变过程,它对页式存储管理的支持是在其段式存储管理已经存在了相当长的时间以后才发展起来的。所以,不管程序怎样写,i386一律对程序中使用的地址先进行段式映射,然后才能进行页式映射。不过Linux在映射的过程中使段式映射不起什么作用。你有政策,我有对策。
./arch/x86/include/asm/segment.h文件中有写:
#define __KERNEL_CS     (GDT_ENTRY_KERNEL_CS * 8)
#define __KERNEL_DS     (GDT_ENTRY_KERNEL_DS * 8)
#define __USER_DS     (GDT_ENTRY_DEFAULT_USER_DS* 8 + 3)
#define __USER_CS     (GDT_ENTRY_DEFAULT_USER_CS* 8 + 3)
也就是说Linux中只使用四种段寄存器,两个用于内核空间,两个用于用户空间。Intel是将一个进程的映象分成代码段、数据段、堆栈段,而在Linux内核中堆栈段和数据段是不分的(这里是指程序经过编译、链接生成的映像文件的组织结构不分堆栈段和数据段)。

举个地址映射的例子:
----------------------------
foo()
{
    printf("hello\n");
}

main()
{
    foo();
}
----------------------------
用objdump -d xxx反汇编一下,可以看到:
 80483f8:    55                       push   %ebp
 80483f9:    89 e5                    mov    %esp,%ebp
 80483fb:    83 e4 f0                 and    $0xfffffff0,%esp
 80483fe:    e8 e1 ff ff ff           call   8048368 <foo>
ld(链接器)为foo分配的地址是0x8048368,Linux内核设计的段式映射机制把地址0x8048368映射到了自身,现在作为线性地址出现,真正做的工作是页式映射。我们看下页式映射的过程:
每个进程都有其自身的页面目录PGD,指向这个目录的指针保存在每个进程的mm_struct数据结构中。每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件则总是从CR3中取得指向当前页面目录的指针。不过,CPU在执行程序时使用的是虚拟地址,而MMU硬件在进行映射时所用的是物理地址。
arch/x86/include/asm/page.h
#define __pa(x)         __phys_addr((unsigned long)(x))
arch/x86/include/asm/page_32.h
#define __phys_addr(x)          __phys_addr_nodebug(x)
#define __phys_addr_nodebug(x)  ((x) - PAGE_OFFSET)
#define PAGE_OFFSET             0xfffffc0000000000UL
通过__pa()把页面目录PGD的物理地址装入寄存器CR3中。当我们在程序中要转移到地址0x8048368去的时候,进程正在运行中,CR3早已设置好,指向我们这个进程的页面目录了。将0x8048368展开:

0000 1000 0000 0100 1000 0011 0110 1000

最高10位为二进制0000 1000 00,即十进制32。i386 CPU中的MMU会以32为下标去页面目录中找到其目录项。这个目录项中的高20位指向一个页面表。CPU在这20位后边添上12个0就得到该页面表的指针(每个页面表占一个页面,自然就是4K字节边界对齐的,其起始地址的低12位一定是0.正因为如此,才可以把32位目录项中的低12位挪作它用,其中最低位为P标志位,为1时表示该页面表在内存中)。找到页面表以后,CPU再来看线性地址的中间10位,00 0100 1000。即十进制72。于是CPU就以此为下标在已经找到的页面表中找到相应的表项。与目录项相似,当页面表的P标志位,为1时表示该页面表在内存中。32位页面表项中的高20位指向一个物理内存页面,在后边添上12个0就得到这个物理内存页面的起始地址。在其起始地址上加上线性地址中的最低12位,就得到了foo()在内存中最终的物理地址。这个线性地址的最低12位是0x368,所以,如果目标页面的起始地址为0x740000的话(具体取决于内核中的动态分配),那么foo()入口的物理地址就是0x740368,foo()的执行代码就存储在这里。
在页面映射的过程中,i386要访问内存三次。第一次是页面目录,第二次是页面表,第三次是访问真正的物理地址。

i386页式内存管理的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射。这种映射模式在大多数情况下可以节省页面表所占用的空间。因为大多数进程不会用到整个虚拟存储空间,在虚拟存储空间中通常都留有很很大的“空洞”。采用两层的方式,只要一个目录项所对应的那部分空间是个空洞,就可以把该目录项设置成“空”,从而省下了与之对应的页面表(1024个页面描述项)。当地址的宽度为32位时,两层映射机制比较有效也比较合理。但是,当地址的宽度大于32位时,两层映射就显得不尽合理,不够有效了。
Linux内核的设计要考虑到在各种不同CPU上的实现,还要考虑在64位CPU上的实现。所以不能仅仅针对i386结构来设计它的映射机制,而要以一种假想的、虚拟的CPU和MMU为基础,设计出一种通用的模型,再把它分别落实到各种具体的CPU上。因此,Linux内核的映射机制设计成三层,在页面目录和页面表中间增加了一层“中间目录”。在代码中,页面目录称为PGD,中间目录称为PMD,面页面表则称为PT。PT中的表项称为PTE。PGD、PMD、PT三者均为数组。相应的,在逻辑上也把线性地址从高位到低位划分成4个位段,分别用作在目录PGD中的下标、中间目录PMD中的下标、页面表中的下标、物理页面的位移。
i386是二层映射,Linux在逻辑上把PMD抽去了,以满足i386的需要。
可以查看
./arch/x86/include/asm/pgtable-2level_types.h
./arch/x86/include/asm/pgtable-3level_types.h
文件中对PTRS_PER_PMD的定义。

摘自《深入理解Linux内核源码》、《源代码情景分析》相关章节。

一点札记:
--------------------------------------------
高端内存    ZONE_HIGHMEM
低端内存    ZONE_NORMAL/ZONE_DMA

kmalloc    分配的内存在物理上是连续的 对应低端内存区域

vmalloc    分配的虚拟内存是连续的,物理上不一定连续 如果有__GFP_HIGHMEM那么对应ZONE_HIGHMEM,否则对应低端内存,
    由于ZONE_HIGHMEM区域没有建立映射,所以要在内核空间的vmalloc区分配虚拟页面,修改内核页表,映射到ZONE_HIGHMEM
    不过一般vmalloc都会从高端内存分配页面,因为低端内存是一一映射到内核空间,非常宝贵,一般供kmalloc分配连续物理页面使用

ioremap    和vmalloc一样,对应的内核空间是vmalloc区,唯一不同的是对应的物理地址空间不再是高端或低端内存,而是设备I/O内存

alloc_page    同vmalloc一样都在高端区域分配

kmalloc是建立在slab分配器之上的,slab是建立在伙伴系统之上的.
slab用于小块内存的分配

原创粉丝点击