Linux 内存映射机制(2)
}
通过作者的注释, 可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,
直到物理内存映射完毕为止。这个函数比较长, 而且用到很多关于内存管理方面的宏定义,理解了这个函数, 就能大概理解内核是如何建立
页表的,将这个抽象的模型完全的理解。 下面将详细分析这个函数:
函数开始定义了4个变量pgd_t *pgd, pmd_t *pmd, pte_t *pte, pfn;
pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0. pgd_idx根据
pgd_index宏计算结果为768,也是内核要从目录表中第768个表项开始进行设置。 从768到1024这个256个表项被linux内核设置成内核目录项,
低768个目录项被用户空间使用. pgd = pgd_base + pgd_idx; pgd便指向了第768个表项。
然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。
one_md_table_init()函数根据pgd找到指向的pmd表。
它同样在mm/init.c中定义:
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{
pmd_t *pmd_table;
#ifdef CONFIG_X86_PAE
pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
if (pmd_table != pmd_offset(pgd, 0))
BUG();
#else
pmd_table = pmd_offset(pgd, 0);
#endif
return pmd_table;
}
可以看出, 如果内核不启用PAE选项, 函数将通过 pmd_offset返回pgd的地址。因为linux的二级映射模型,本来就是忽略pmd中间目录表的。
接着又个判断语句:
>> if (pfn >= max_low_pfn)
>> continue;
这个很关键, max_low_pfn代表着整个物理内存一共有多少页框。 当pfn大于max_low_pfn的时候,表明内核已经把整个物理内存都映射到了系
统空间中, 所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空间了, 没必要继续填充剩下的表项。
紧接着的第2个for循环,在linux的3级映射模型中,是要设置pmd表的, 但在2级映射中忽略, 只循环一次,直接进行页表pte的设置。
>> address = pfn * PAGE_SIZE + PAGE_OFFSET;
address是个线性地址, 根据上面的语句可以看出address是从0xc000000开始的,也就是从内核空间开始,后面在设置页表项属性的时候会用
到它.
>> pte = one_page_table_init(pmd);
根据pmd分配一个页表, 代码同样在mm/init.c中:
static pte_t * __init one_page_table_init(pmd_t *pmd)
{
if (pmd_none(*pmd)) {
pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
if (page_table != pte_offset_kernel(pmd, 0))
BUG();
return page_table;
}
return pte_offset_kernel(pmd, 0);
}
pmd_none宏判断pmd表是否为空, 如果为空则要利用alloc_bootmem_low_pages分配一个4k大小的物理页面。 然后通过set_pmd(pmd, __pmd
(__pa(page_table) | _PAGE_TABLE));来设置pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,在与上_PAGE_TABLE 宏,
此时它们还是无符号整数,在通过__pmd把无符号整数转化为pmd类型,经过这些转换, 就得到了一个具有属性的表项, 然后通过set_pmd宏设
置pmd表项.
接着又是一个循环,设置1024个页表项。
is_kernel_text函数根据前面提到的address来判断address线性地址是否属于内核代码段,它同样在mm/init.c中定义:
static inline int is_kernel_text(unsigned long addr)
{
if (addr >= (unsigned long)_stext && addr <= (unsigned long)__init_end)
return 1;
return 0;
}
_stext, __init_end是个内核符号, 在内核链接的时候生成的, 分别表示内核代码段的开始和终止地址.
如果address属于内核代码段, 那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性.
#define _PAGE_KERNEL_EXEC \
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED)
#define _PAGE_KERNEL \
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)
最后通过set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));来设置页表项, 先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,
然户在用set_pte宏把页表项值写到页表项里。
当pagetable_init()函数返回后,内核已经设置好了内核页表,紧着调用load_cr3(swapper_pg_dir);
#define load_cr3(pgdir) \
asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir)))
将控制swapper_pg_dir送入控制寄存器cr3. 每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分. 现
在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的, 所以从这条指令以后就扩大
了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存)除外. 实际上此时swapper_pg_dir中已经改变的目录项很可能还
在高速缓存中, 所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
3.4 对如何构建页表的总结
通过上述对pagetable_init()的剖析, 我们可以清晰的看到, 构建内核页表, 无非就是向相应的表项写入下一级地址和属性。 在内核空间
保留着一部分内存专门用来存放内核页表.当cpu要进行寻址的时候,无论在内核空间,还是在用户空间, 都会通过这个页表来进行映射。对于
这个函数, 内核把整个物理内存空间都映射完了, 当用户空间的进程要使用物理内存时, 岂不是不能做相应的映射了? 其实不会的, 内核
只是做了映射, 映射不代表使用, 这样做是内核为了方便管理内存而已。
四. 实例分析映射机制
4.1示例代码
通过前面的理论分析,我们通过编写一个简单的程序, 来分析内核是如何把线性地址映射到物理地址的。
[root@localhost temp]# cat test.c
#include <stdio.h>
void test(void)
{
printf("hello, world.\n");
}
int main(void)
{
test();
}
这段代码很简单, 我们故意要main调用test函数, 就是想看下test函数的虚拟地址是如何映射成物理地址的。
4.2 段式映射分析
我们先编译, 在反汇编下test文件
[root@localhost temp]# gcc -o test test.c
[root@localhost temp]# objdump -d test
08048368 <test>:
8048368: 55 push %ebp
8048369: 89 e5 mov %esp,%ebp
804836b: 83 ec 08 sub $0x8,%esp
804836e: 83 ec 0c sub $0xc,%esp
8048371: 68 84 84 04 08 push $0x8048484
8048376: e8 35 ff ff ff call 80482b0 <printf@plt>
804837b: 83 c4 10 add $0x10,%esp
804837e: c9 leave
804837f: c3 ret
08048380 <main>:
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 83 ec 08 sub $0x8,%esp
8048386: 83 e4 f0 and $0xfffffff0,%esp
8048389: b8 00 00 00 00 mov $0x0,%eax
804838e: 83 c0 0f add $0xf,%eax
8048391: 83 c0 0f add $0xf,%eax
8048394: c1 e8 04 shr $0x4,%eax
8048397: c1 e0 04 shl $0x4,%eax
804839a: 29 c4 sub %eax,%esp
804839c: e8 c7 ff ff ff call 8048368 <test>
80483a1: c9 leave
80483a2: c3 ret
80483a3: 90 nop
从上述结果可以看到, ld给test()函数分配的地址为0x08048368.在elf格式的可执行文件代码中,ld的实际位置总是从0x8000000开始安排程序
的代码段, 对每个程序都是这样。至于程序在执行时在物理内存中的实际位置就要由内核在为其建立内存映射时临时做出安排, 具体地址则
取决于当时所分配到的物理内存页面。假设该程序已经运行, 整个映射机制都已经建立好, 并且CPU正在执行main()中的call 8048368这条指
令, 要转移到虚拟地址0x08048368去运行. 下面将详细介绍这个虚拟地址转换为物理地址的映射过程.
首先是段式映射阶段。由于0x08048368是一个程序的入口,更重要的是在执行的过程中是由CPU中的指令计数器EIP所指向的, 所以在代码段中
。 因此, i386CPU使用代码段寄存器CS的当前值作为段式映射的选择子, 也就是用它作为在段描述表的下标.那么CS的值是多少呢?
用GDB调试下test:
(gdb) info reg
eax 0x10 16
ecx 0x1 1
edx 0x9d915c 10326364
ebx 0x9d6ff4 10317812
esp 0xbfedb480 0xbfedb480
ebp 0xbfedb488 0xbfedb488
esi 0xbfedb534 -1074940620
edi 0xbfedb4c0 -1074940736
eip 0x804836e 0x804836e
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
可以看到CS的值为0x73, 我们把它分解成二进制:
0000 0000 0111 0011
最低2位为3, 说明RPL的值为3, 应为我们这个程序本省就是在用户空间,RPL的值自然为3.
第3位为0表示这个下标在GDT中。
高13位为14, 所以段描述符在GDT表的第14个表项中, 我们可以到内核代码中去验证下:
在i386/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
可以看到段描述符的确就是GDT表的第14个表项中。
我们去GDT表看看具体的表项值是什么, GDT的内容在arch/i386/kernel/head.S中定义:
ENTRY(cpu_gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* 0x0b reserved */
顶(0)
踩(0)
下一篇:linux编译和安装硬件的驱动
- 最新评论