主要议题:

1分页,分段模式及实模式

2Linux分页 

3linux内存线性地址空间布局及物理内存空间布局

4linux页表初始化及代码解析


1.1.1内存寻址和保护模式
在X86平台上,内存控制单元通过分段单元电路把逻辑地址转换为线性地址,又通过分页单元把线性地址转换为物理地址。
 




    一个逻辑地址由段标识符和段内偏移地址组成。段标示符是一个16位长度的字段,称为段选择符,而偏移地址是32位的字段。


 
     一般用段寄存器来保存段选择符,如CS,DS,ES,SS等,CS段选择符中用RPL来表示CPU当前的特权级别,0表示工作在内核态,3标示工作在用户态。每个段由一个8个字节的描述符进行管理,段描述符表放在GDT或者LDT中,通常只定义一个GDT,而每个进程除了GDT中的段外还需要创建附加的段,就可以有自己的LDT段,通常GDT段存放在GDTR控制寄存器中。


每当一个段选择符被加入到段寄存器时,段描述符就被自动加载到非编程寄存器中.
 


实模式由于是由8086/8088发展而来因此他更像是一个运行单片机的简单模式,计算机启动后首先进入的就是实模式,通过8086/8088只有20根 地址线所以它的寻址范围只有2的20次幂,即1M。内存的访问方式就是我们熟悉的seg:offset逻辑地址方式,例如我们给出地址逻辑地址它将在 cpu内转换为20的物理地址,即将seg左移4位再加上offset值。例如地址1000h:5678h,则物理地址为 10000h+5678h=15678h。实模式在后续的cpu中被保留了下来,但实模式的局限性是很明显的,由于使用seg:offset逻辑地址只能 访问1M多一点的内存空间,在拥有32根地址线的cpu中访问1M以上的空间则变得很困难。而且随着计算机的不断发展实模式的工作方式越来越不能满足计算机对资源(存储资源和cpu资源等等)的管理,由此产生了新的管理方式——保护模式。
存储方式主要体现在内存访问方式上,由于兼容和IA32框架的限制,保护模式在内存访问上延用了实模式下的seg:offset的形式(即:逻辑地址), 其实seg:offset的形式在保护模式下只是一个躯壳,内部的存储方式与实模式截然不同。在保护模式下逻辑地址并不是直接转换为物理地址,而是将逻辑 地址首先转换为线性地址,再将线性地址转换为物理地址。


1.1.2linux分段:
运行在用户态的所有linux进程都使用同一对相同的段对指令和数据寻址,这两个段就是所谓的用户代码段和用户数据段,类似的,运行在内核态的所有linux进程都使用一对相同的段进行指令和数据的寻址:分别叫做内核代码段和内核数据段。从下图中可以看出linux下逻辑地址和线性地址其实是一致的。


 
  每个处理器都有一个gdtr的寄存器,所有的gdt都存放在cpu_gdt_table数组里面,而所有GDT的地址和他们的大小都被存放在cpu_gdt_descr数组中。


1.1.3linux分页:
  在cpu中通过cr3寄存器来切换对应的页表。
  下面是线性地址和页表之间的关系,反应了如何从一个线性地址找到一个物理页面,并定位到相关字节。这个表反应的是32位86x86的映射机制:


 
   对于64位cpu的页表管理,一般使用三级或者四级页表,X86_64使用的是四级页表,几级页表主要是根据CPU硬件规格来制定的。
   在linux内核中,统一使用四级页表的数据结构来描述cpu的页表结构,以达到代码的统一。请注意,这里仅仅是用了四级页表来进行描述cpu的页表结构,不代表硬件上就是四级页表,这里是逻辑上的四级。比如,32位的X86是两级页表,它要用四级页表来表示的话,页上级和页中间目录的位数就是为0,在实际的代码中对应的页上级目录和页中间目录都只有一项,其地址和其所属的页全局目录的项是一样的.......


 
1.1.4linux物理内存布局


 
其中,不可用页框(页框0)主要是用来存放bios加电自检期间检测到的硬件配置,0x9f~0x100页框即(640K~1M)留给bios例程,用来映射ISA图形卡上的部分内存,_text表示地址0x100000,即1M用来存放内核的代码段,_etext和_edata之间存放的是内核的已初始化的数据,_edata到_eend之间存放的是内核未初始化的数据,从_end到第768个页框会之间映射到对应的内核空间使用,至于768个页框以后的物理页框,要在内核中直接使用的话,必须进行高端内存映射,或使用vmalloc()将他们映射到内核空间的3G+896~~4G的内核线性地址空间。这部分可以配合1.1.4中的linux虚拟内存布局来看。


1.1.5linux虚拟内存布局
 


内核通过内核页全局目录来管理所有的物理内存,由于线形地址前3G空间为用户使用,内核页全局目录前768项(刚好3G)除0、1两项外全部为0,后256项(1G)属于linux内核的地址空间,用来管理所有的物理内存。内核页全局目录在编译时静态地定义为swapper_pg_dir数组,该数组从物理内存地址0x101000处开始存放。


由图可见:
(1) 内核线形地址空间部分从PAGE_OFFSET(通常定义为3G)开始,为了将内核装入内存,从PAGE_OFFSET开始8M线形地址用来映射内核所在的物理内存地址;(此处映射的物理地址是否包含了物理存储布局中的内存中最开始的1M?)
(2)接下来是mem_map数组,mem_map的起始线形地址与体系结构相关,比如对于UMA结构,由于从PAGE_SIZE开始16M线形地址空间对应的16M物理地址空间是DMA区,mem_map数组通常开始于PAGE_SIZE+16M的线形地址;
(3)从PAGE_SIZE开始到VMALLOC_START – VMALLOC_OFFSET的线形地址空间直接映射到物理内存空间(一一对应映射,物理地址=线形地址-PAGE_OFFSET),这段区域的大小和机器实际拥有的物理内存大小有关,这儿VMALLOC_OFFSET在x86上为8M,主要用来防止越界错误;(这一段其实就是对DMA_ZONE和DMA_NORMAL区的物理内存进行直接映射)
(4)在内存比较小的系统上,余下的线形地址空间(还要再减去空白区即VMALLOC_OFFSET)被vmalloc()函数用来把不连续的物理地址空间映射到连续的线形地址空间上,在内存比较大的系统上,vmalloc()使用从VMALLOC_START到VMALLOC_END(也即PKMAP_BASE减去2页的空白页大小PAGE_SIZE)的线形地址空间
(5)此时余下的线形地址空间(还要再减去2页的空白区即VMALLOC_OFFSET)又可以分成2部分:
第一部分从PKMAP_BASE到FIXADDR_START用来由kmap()函数映射高端内存;
第二部分,从FIXADDR_START到FIXADDR_TOP,这是一个固定大小的线形地址空间,(引用:Fixed virtual addresses are needed for subsystems that need to know the virtual address at compile time such as the APIC),在x86体系结构上,FIXADDR_TOP被静态定义为0xFFFFE000,此时这个固定大小空间结束于整个线形地址空间最后4K前面,该固定大小空间大小是在编译时计算出来并存储在__FIXADDR_SIZE变量中。


正是由于vmalloc()使用区、kmap()使用区及固定大小区的存在才使ZONE_NORMAL区大小受到限制,由于内核在运行时需要这些函数,因此在线形地址空间中至少要VMALLOC_RESERVE大小的空间。VMALLOC_RESERVE的大小与体系结构相关,在x86上,VMALLOC_RESERVE定义为128M,这就是为什么我们看到ZONE_NORMAL大小通常是16M到896M的原因。


1.1.6内核页表的初始化过程
主要分为两个阶段:
    1第一个阶段,内核需要创建一个有限的地址空间,用来存放内核的代码段,数据段,初始页表,和一些动态数据,这个最小限度地址空间的目的是仅仅能将内核加载进去以及让内核做一些初始化的操作。一般可以认为这个最小限度地址空间大小为8MB。临时业全局目录在swap_pg_dir数组中,临时页表在pg0中存放。
当处于第一个阶段时,cpu尚处于实模式的寻址模式,第一个阶段的目标是让实模式和保护模式下都能对着8MB的内存进行寻址。为此,需要把0x00000000~0x007fffff和0x0c000000~0xc7fffff的线性地址空间映射到0~0x7fffff的物理地址空间。在内核中,用swap_pg_dir来存放临时页全局目录,可以将所有的页全局目录表项清0,然后把0,1,768,769这四项来进行设置,来达到我们的目的。(将0x00000000~0x007fffff线性地址也需要对应的在页表里面进行设置,应该是为了兼容当前运行实模式的代码,这样在开启了页寻址模式后,通过分段+分页寻址,寻到的物理地址会仍是运行在实模式时操作的物理地址)
临时页表由startup_32()来进行初始化,临时的页全局目录是在编译时初始化的。在startup_32()中建立临时页表:
    //页表初始化
page_pde_offset = (__PAGE_OFFSET >> 20);
movl $pa(__brk_base), %edi //第一张页表的物理地址
movl $pa(swapper_pg_dir), %edx //页目录的物理地址
movl $PTE_IDENT_ATTR, %eax //页目录中项的标识位
10:
leal PDE_IDENT_ATTR(%edi),%ecx //PDE_IDENT_ATTR其实是0x007,这里是为了算出页全//局目录目录项里应该被放入什么值
movl %ecx,(%edx) //存入对应的页全局目录项里面0,1
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry *///存入对应的页全局目录项、、里面768,769
addl $4,%edx //下一个页表项的地址
movl $1024, %ecx //每个页表有1024项需要初始化
11:
stosl //存到页表里,edi指向的地方
loop 11b //这个循环对每张页表都会循环1024次, edi会自增。
movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
cmpl %ebp,%eax
jb 10b


    建立完页表,启用保护模式:
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 */
ljmp $__BOOT_CS,$1f/* Clear prefetch and normalize %eip */


2第二个阶段,内核充分利用物理内存并适当的建立页表。
内核在启动后需要对内核页表进行初始化(即对应上面的第二阶段),对应代码主要在函数kernel_physical_mappin g_init()中。以下是32位x86内核对于页表进行的初始化代码。
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;


//计算linux内核态空间起始地址(3G) 在页全局表中的索引
pgd_idx = pgd_index(PAGE_OFFSET);
pgd = pgd_base + pgd_idx;
pfn = 0;


//每个pgd对应有1024个表项,每个表项指向一个页表
for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
//在二级页表的情形中,pmd和pgd的值是相等的
pmd = one_md_table_init(pgd);
if (pfn >= max_low_pfn)
continue;
//在二级页表中,该PTRS_PER_PMD值为1
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 {
//该pmd指向该page table
pte = one_page_table_init(pmd);
//每个页表有1024个页表项,指向1024个物理页
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
//地址为kernel代码区,设置对应页表项,填入
//对应的物理页的地址
if (is_kernel_text(address))
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
else
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
}
}
}
}
}


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);
}

 

Copyright © 2024 冰天雪域
Powered by .NET 8.0 on Kubernetes