物理内存布局
在初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可用而哪些不可用。
内核将下列页框记为保留:
在不可用的物理地址范围内的页框
含有内核代码和已初始化数据结构的页框
一般来说,Linux内核安装在RAM中从物理地址0x00100000开始的地方,也就是说,从第二个MB开始。所需页框总数依赖于内核的配置方案,典型的配置所得到的内核可以被安装在小于3MB的RAM中。
在启动阶段,内核询问BIOS并了解物理内存的大小,随后内核执行machine_specific_memory_setup()函数,该函数建立物理地址映射。内核可能不会见到BIOS报告的所有物理内存,内核也只能寻址4GB大小的RAM。setup_memory()函数在machine_specific_memory_setup()执行后被调用,它分析物理内存区域表并初始化一些变量来描述内核的物理内存布局,以下是这些变量的说明。
变量名称 | 说明 |
---|---|
num_physpages | 最高可用页框的页框号 |
totalram_pages | 可用页框的总数量 |
min_low_pfn | RAM 中在内核映像后第一个可用页框的页框号 |
max_pfn | 最后一个可用页框的页框号 |
max_low_pfn | 被内核直接映射的最后一个页框的页框号(低地址内存) |
totalhigh_pages | 内核非直接映射的页框的总数(高地址内存) |
highstart_pfn | 内核非直接映射的第一个页框的页框号 |
highend_pfn | 内核非直接映射的最后一个页框的页框号 |
为了避免把内核装入一组不连续的页框里,Linux更愿意跳过RAM的第一个MB
进程页表
进程的线性地址分成两部分:
- 从0x00000000到0xbfffffff的线性地址,无论进程运行在用户态还是内核态都可以寻址。
- 从0xc0000000到0xffffffff的线性地址,只有内核态的进程才能寻址。
当进程运行在用户态时,他产生的线性地址小于0xc0000000;当进程运行在内核态时,它执行内核代码,所产生的地址大于等于0xc0000000。但是,在某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。
页全局目录的第一部分表项映射的线性地址小于0xc0000000(在PAE未启用时是前768项,PAE启用时是前3项),具体大小依赖于特定进程。
内核页表
内核维持着一组自己是用的页表,驻留在所谓的主内核全局目录(master kernel Page Global Directory)中。系统初始化后,这组页表还从未被任何进程直接使用,也就是说,主内核全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录提供参考模型。
内核初始化自己的页表分为两个阶段:
第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段,初始页表和用于存放动态数据结构的供128KB大小的空间。
第二个阶段,内核充分利用剩余的RAM并适当的建立分页表。
临时内核页表
临时页全局目录是在内核编译过程中静态的初始化,而临时页表是有startup_32()汇编语言函数初始化的,在这个阶段PAE支持并未激活。
当内核从16位的实模式进入到保护模式(通过在汇编代码中的setup函数中设置linux的cr0寄存器的PE位),内核要创建一个有限的地址空 间,容纳内核的代码段、数据段、初始页表和用于存放动态数据结构的共128KB大小的空间,此时通常的程序设计者假定,内核使用的段、临时页表和 128KB的内存范围可以全部放在RAM前8MB的空间内。于是我们需要做的工作是建立一个页表可以对内存的前8MB的物理地址进行寻址。
为了映射8MB空间需要两个页表,一个是pg0处,另一个存放在pg0后边。为了在实模式和保护模式下都能对这8MB寻址,因此内核必须创建一个映射,把从0x00000000到0x007fffff的线性地址和从0xc0000000到0xc07fffff的线性地址映射到从0x00000000到0x007fffff。所以要设置临时页全局目录的第0,1,768,769项,其他项为0。
前两项是给用户线性地址映射,后两项给内核线性地址映射。内核会将swapper_gp_dir的0项和768项字段设 置为pg0的物理地址(pg0中存放第一张页表的地址),而1项和769项设置为紧随pg0后的页框的物理地址(一般是pg0+4k)。
下面是有关的代码片段:
page_pde_offset = (__PAGE_OFFSET >> 20);
movl $(pg0 - __PAGE_OFFSET), %edi
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
leal 0x007(%edi),%ecx /* Create PDE entry */
movl %ecx,(%edx) /* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx
movl $1024, %ecx
11:
stosl
addl $0x1000,%eax
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
- 首先page_pde_offset计算__PAGE_OFFSET在页目录中的偏移,将pg0的物理地址与标志位合到一起,创建一个页目录项,用%edi和%edx分别存放pg0和swapper_pg_dir的物理地址,将置为Present,Read/Write和User/Supervisor标志的值保存在%eax中。第一次循环填充0和768项,第二次循环填充1和769项。
- movl $1024, %ecx 设置计数,即要设置临时页全局目录表1024个表项,stosl对应eax赋值给[edi];edi = edi + 4,填充pg0时,一个表项对应一个4KB page即0x1000。
- 将INIT_MAP_BEYOND_END+0x007+%edi与%ebp比较,如果小于就进行下一轮循环,下一次循环,填充pg0后面页表的1024个表项,此时是填充pg0后的第二个页表。
汇编函数startup_32()也启用分页单元,通过项cr3控制寄存器装入swapper_pg_dir的地址及设置cr0控制寄存器的PG标志来达到这一目的。
/*
* Enable paging
*/
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
最终内核页表
在Linux2.6.11内核代码中,最终内核页表的建立是在paging_init()函数中完成的。该函数首先调用pagetable_init()适当的建立页表项,然后将在swapper_pg_dir内保存的主内核页全局目录的物理地址写入cr3控制寄存器中,如果CONFIG_X86_PAE宏被定义,即CPU支持PAE分页模式,则将cr4寄存器的PAE标志位置位,最后调用__flush_tlb_all()刷新TLB。
pagetable_init()函数执行的操作既依赖于现有RAM容量,也依赖于CPU模型。
static void __init pagetable_init (void)
{
unsigned long vaddr;
pgd_t *pgd_base = swapper_pg_dir;
#ifdef CONFIG_X86_PAE
int i;
/* Init entries of the first-level page table to the zero page */
for (i = 0; i < PTRS_PER_PGD; i++)
set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif
/* Enable PSE if available */
if (cpu_has_pse) {
set_in_cr4(X86_CR4_PSE);
}
/* Enable PGE if available */
if (cpu_has_pge) {
set_in_cr4(X86_CR4_PGE);
__PAGE_KERNEL |= _PAGE_GLOBAL;
__PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;
}
kernel_physical_mapping_init(pgd_base);
- 该代码片段首先根据CONFIG_X86_PAE宏判断CPU是否开启PAE分页机制,如果是那么使用页目录指针表(PDPT),此时pgd只有4项,将其填充为零页的地址
- 设置相应的PSE与PSG位
- 随后进入kernel_physical_mapping_init函数设置直接映射区的页表结构
直接映射区的页表建立由kernel_physical_mapping_init函数完成。
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;
/* Map with big pages if possible, otherwise create normal page tables. */
if (cpu_has_pse) {
unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;
if (is_kernel_text(address) || is_kernel_text(address2))
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
else
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));
pfn += PTRS_PER_PTE;
} else {
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));
}
}
}
}
}
- 通过pgd_index计算PAGE_OFFSET在临时页全局目录中的索引值。
- one_md_table_init函数和one_page_table_init函数会调用alloc_bootmem_low_pages分配pmd,pte占用的page,并填充pgd,pmd相应的相应的表项。
- 建立直接映射区物理页与内核线性空间的映射,如果是内核代码段设置为可读,写,执行。
如果开启了PAE,则设置512项的pmd,否则1项的pmd;当开启PAE,则pte为512项,否则1024项。
固定映射的线性地址
内核线性地址的第四个GB的初始部分映射系统的物理内存,其中内核至少使用128MB的线性地址用来实现非连续内存分配和固定映射的线性地址。
固定映射的线性地址(fix-mapped linear address)基本上是一种类似于0xffffc000这样的常量线性地址,其对应的物理地址不必等于线性地址减去0xc0000000,而是可以以任意方式建立。因此,每个固定映射的线性地址都映射一个物理内存的页框。
每个固定映射的线性地址都定义于enmu fixed_addresses数据结构中的整形索引来表示:
enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
#ifdef CONFIG_X86_LOCAL_APIC
FIX_APIC_BASE, /* local (CPU) APIC) -- required for SMP or not */
#endif
#ifdef CONFIG_X86_IO_APIC
FIX_IO_APIC_BASE_0,
FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS-1,
#endif
#ifdef CONFIG_X86_VISWS_APIC
FIX_CO_CPU, /* Cobalt timer */
FIX_CO_APIC, /* Cobalt APIC Redirection Table */
FIX_LI_PCIA, /* Lithium PCI Bridge A */
FIX_LI_PCIB, /* Lithium PCI Bridge B */
#endif
#ifdef CONFIG_X86_F00F_BUG
FIX_F00F_IDT, /* Virtual mapping for IDT */
#endif
#ifdef CONFIG_X86_CYCLONE_TIMER
FIX_CYCLONE_TIMER, /*cyclone timer register*/
#endif
#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif
#ifdef CONFIG_ACPI_BOOT
FIX_ACPI_BEGIN,
FIX_ACPI_END = FIX_ACPI_BEGIN + FIX_ACPI_PAGES - 1,
#endif
#ifdef CONFIG_PCI_MMCONFIG
FIX_PCIE_MCFG,
#endif
__end_of_permanent_fixed_addresses,
/* temporary boot-time mappings, used before ioremap() is functional */
#define NR_FIX_BTMAPS 16
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1,
FIX_WP_TEST,
__end_of_fixed_addresses
};
每个固定映射的线性地址都存放在线性地址第四个GB的末端。