深入理解Linux内存映射机制[节选]

来源:互联网 发布:java类库大全有哪些 编辑:程序博客网 时间:2024/05/02 06:09

三. 内核对页表的设置
CPU做出映射的前提是操作系统要为其准备好内核页表,而对于页表的设置,内核在系统启动的初期和系统初始化完成后都分别进行了设置。
3.1 与内存映射相关的几个宏
这几个宏把无符号整数转换成对应的类型
#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )
根据x把它转换成对应的无符号整数
#define pte_val(x) ((x).pte_low)
#define pmd_val(x) ((x).pmd)
#define pgd_val(x) ((x).pgd)
#define pgprot_val(x) ((x).pgprot)
把内核空间的线性地址转换为物理地址
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
把物理地址转化为线性地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

x是页表项值, 通过pte_pfn得到其对应的物理页框号, 最后通过pfn_to_page得到对应的物理页描述符
#define pte_page(x) pfn_to_page(pte_pfn(x))
如果对应的表项值为0, 返回1
#define pte_none(x) (!(x).pte_low)
x是页表项值, 右移12位后得到其对应的物理页框号
#define pte_pfn(x) ((unsigned long)(((x).pte_low >> PAGE_SHIFT)))
根据页框号和页表项的属性值合并成一个页表项值
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
根据页框号和页表项的属性值合并成一个中间表项值
#define pfn_pmd(pfn, prot) __pmd(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
向一个表项中写入指定的值
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)
#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)
根据线性地址得到高10位值, 也就是在目录表中的索引
#define pgd_index(address) (((address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1))
根据页描述符和属性得到一个页表项值
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
3.2内核页表的初始化
内核在进入保护模式前, 还没有启用分页功能, 在这之前内核要先建立一个临时内核页表,因为在进入保护模式后, 内核继续初始化直到建立完整的内存映射机制之前, 仍然需要用到页表来映射相应的内存地址。 临时页表的初始化是在arch/i386/kernel/head.S中进行的:
swapper_pg_dir是临时页全局目录表, 它是在内核编译过程中静态初始化的.
pg0是第一个页表开始的地方, 它也是内核编译过程中静态初始化的.
内核通过以下代码建立临时页表:
ENTRY(startup_32)
…………
/* 得到开始目录项的索引,从这可以看出内核是在swapper_pg_dir的768个表项开始进行建立的, 其对应的线性地址就是0xc0000000以上的地址, 也就是内核在初始化它自己的页表 */
page_pde_offset = (__PAGE_OFFSET >> 20);
/* pg0地址在内核编译的时候, 已经是加上0xc0000000了, 减去0xc00000000得到对应的物理地址 */
movl $(pg0 - __PAGE_OFFSET), %edi
/* 将目录表的地址传给edx, 表明内核也要从0x00000000开始建立页表, 这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡, 下面会详细解释 */
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
movl $0x007, %eax
leal 0x007(%edi),%ecx
Movl %ecx,(%edx)
movl %ecx,page_pde_offset(%edx)
addl $4,%edx
movl $1024, %ecx
11:
stosl addl $0x1000,%eax
loop 11b
/* 内核到底要建立多少页表, 也就是要映射多少内存空间, 取决于这个判断条件。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段, 初始页表和用于存放动态数据结构的128k大小的空间就行 */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
在上述代码中, 内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢,虽然在head.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令, 如果代码中遇到了符号地址,只能减去0xc0000000才行, 当开启了映射机制后就不用了现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射, 那么当内核开启映射机制后, 低区中的地址就没办法寻址了,应为没有对应的页表, 除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制.
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* cr3控制寄存器保存的是目录表地址 */
movl %cr0,%eax /* 向cr0的最高位置1来开启映射机制 */
orl $0x80000000,%eax
movl %eax,%cr0
ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
1:
lss stack_start,%esp
通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行 因为__BOOT_CS是个符号地址,地址在0xc0000000以上。
在head.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel函数继续初始化.
3.3内核页表的完整建立内核在start_kernel()中继续做第二阶段的初始化,因为在这个阶段中, 内核已经处于保护模式下,前面只是简单的设置了内核页表, 内核必须首先要建立一个完整的页表才能继续运行,因为内存寻址是内核继续运行的前提。
pagetable_init()的代码在mm/init.c中:
[start_kernel()>setup_arch()>paging_init()>pagetable_init()]
为了简单起见, 我忽略了对PAE选项的支持。
static void __init pagetable_init (void)
{
……
pgd_t *pgd_base = swapper_pg_dir;
……
kernel_physical_mapping_init(pgd_base);
……
}
在这个函数中pgd_base变量指向了swapper_pg_dir, 这正是内核目录表的开始地址,pagetable_init()函数在通过kernel_physical_mapping_init()函数完成内核页表的完整建立。
kernel_physical_mapping_init函数同样在mm/init.c中, 我略去了与PAE模式相关的代码:
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
unsigned long pfn;
pgd_t *pgd;
pmd_t *pmd;
pte_t *pte;
int pgd_idx, pmd_idx, pte_ofs;
pgd_idx = pgd_index(PAGE_OFFSET);
pgd = pgd_base + pgd_idx;
pfn = 0;
for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
pmd = one_md_table_init(pgd);
if (pfn >= max_low_pfn)
continue;
for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {
unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
……
pte = one_page_table_init(pmd);
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
if (is_kernel_text(address))
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
else
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
……
}
}
通过作者的注释, 可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从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要进行寻址的时候,无论在内核空间,还是在用户空间, 都会通过这个页表来进行映射。对于这个函数, 内核把整个物理内存空间都映射完了, 当用户空间的进程要使用物理内存时, 岂不是不能做相应的映射了? 其实不会的, 内核只是做了映射, 映射不代表使用, 这样做是内核为了方便管理内存而已。

原创粉丝点击