linux中的分段分页机制分三层,页目录(PGD),中间目录(PMD),页表(PT)。PT中的表项称为页表项(PTE)。注意英文缩写,在linux程序中函数变量的名字等都会和英文缩写相关。
LINUX中的三级映射流程如图:
但是arm结构的MMU在硬件只有2级映射,所以在软件上会跳过PMD表。即:在PGD中直接放的是PT的base address。在linux软件上就是:
新的2.6内核和内核源码情景分析上的差别挺大的,在2.6.11版本以后,linux将软件上的3级映射变成了4级映射,在PMD后面增加了一个PUD(page upper directory). 在arm的两级映射中,跳过PMD和PUD
在2.6.39内核arch/arm/include/asm/pgtable.h中有下代码:
#define PTRS_PER_PTE 512
//PTE的个数#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)
/*
/*linux将PGD为2k,每项为8个byte。(MMU中取值为前高12bit,为4k,每项4个byte,但linux为什么要这样做呢?) ,另外linux在定义pte时,定义了两个pte,一个供MMU使用,一个供linux使用,来用描述这个页。
*根据注释中的表图,我看到有 #define PTRS_PER_PTE 512 #define PTRS_PER_PGD 2048 , linux将PDG定义为2K,8byte,每个pte项为512,4byte。 他将 两个pte项进行了一下合并。为什么?为什么?
**/
在进程中,传说是可以看到4G的空间,按照linux的用户空间和内核空间划分。 其实进程可以看到3G的自己进程的空间,3G-4G的空间是内核空间,进程仅能通过系统调用进入。
将x86的段地址,略。。。
PGD,PTE的值定义:
这几个结构体定义PTE等他们的结构, pgprot_t 是page protect的意思,最上面的图可知,在具体映射时候,需要将PTE表中的高20bit的值 + 线性地址的低 12bit的值 才是具体的物理地址。所以PTE只有高20bit是在地址映射映射时有效的,那么他的低12bit用来放一些protect的数据,如writeable,rdonly......下面是pgprot_t的一些值:
所以当我们想做一个PTE时(即生成一个page的pte),调用下列函数:
这个函数即是 page_to_pfn(page)得到高20bit的值 + prot的值。 prot即为pgprot_t的结构型值,低12bit。表示页的属性。
生成这一个pte后,我们要让这个pte生效,就要在将这个值 赋值到对应的地方去。
set_pte(pteptr,pteval): 但是在2.6.39没有找到这个代码,过程应该差不多了,算了·····
另外,我们可以通过对PTE的低12bit设置,来让mmu判断,是否建立了映射?还是建立了映射但已经被swap出去了?
还有其他很多的函数,和宏定义,具体可以看《深入linux内核》的内存寻址这章。
这里都是说的PTE中的低12bit的, 但是在2.6.39中明显多了一个linux pt, 具体做什么用还不太清楚。但是linux在arch/arm/include/asm/pgtable.h 中由个注释
应该可以看出,linux pt和PTE的低12bit是相呼应的,用来记录page的一些信息。
linux中,每个物理页都有一个对应的page结构体,组成一个数组,放在mem_map中。
页的page结构放在mem_map中。 要想找到一个物理地址对应的page结构很简单, 就是 mem_map[pfn]即可。
由于硬件的关系,会将整个内存分成不同的zone,:ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM. 这样分的原因是 :有些芯片的DMA设置只能设置到固定区域的地址,所以将这些区域作为ZONE_DMA以免其他操作占用。NORMAL就是正常的内存。高位内存内核空间因为只有1G不能全部映射,所以也要做特殊的处理,进行映射,才能使用。
这三个ZONE使用的描述符是:
在这个描述符中,有一个struct free_area free_area[MAX_ORDER]; 这样的成员变量,这个数组的每个元素都包含一个page的list,那么,一共有MAX_ORDER个list。他是将 所有的free page按照连续是否进行分组。 有2个连续的page的,有4个连续的page的,有8个连续的page的,....2^MAX_ORDER。 这样将page进行分类。当需要alloc一些page时,可以方便的从这些list中找连续的page。(slub中也会像这样来分类,但是slub中分类是按byte为单位,如2byte,4byte....)
另外还有一个
enum lru_list {
这样的成员变量,这也是一个list数组, 其中NR_LRU_LISTS表示在enum 中定义的成员的个数(新的linux中好像有不少这样的用法),这个list数组中,每个list也是很多page结构体。LRU是一中算法,会将长时间不同的page给swap out到flash中。这个数组是LRU算法用的。 其中有 inactive的page, active的page, inactive 的file,等//// 在后面对LRU介绍。
UMA和NUMA
在linux中,如果CPU访问所有的memory所需的时间都是一样的,那么我们人为这个系统是UMA(uniform memory architecture),但是如果访问memory所需的时间是不一样的(在SMP(多核系统)是很常见的),那么我们人为这个系统是NUMA(Non-uniform memory architecture)。在linux中,系统会将内存分成几个node,每个node中的memory,CPU进入的时间的相等的。这个我们分配内存的时候就会从一个node进行分配。
在pglist_data结构中,一个成员变量node_zones[], 他代表了这个node下的所有zone。组成一个数组。
另一个成员变量node_zonelist[]. 这是一个list数组,每一个list中将不同node下的所有的zone都link到一起。数组中不同元素list中,zone的排列顺序不一样。这样当内核需要malloc一些page的时候,可能当前的这个node中并没有足够的page,那么就会按照这个数组list中的顺序依次去申请空间。内核在不同的情况下,需要按照不同的顺序申请空间。所以需要好几个不同的list,这些list就组成了这个数组。
每一个pglist_data结构对应一个node。这样,在每个zone结构上又多了一个node结构。这样内存页管理的结构应该是
node:同过UMA和NUMA将内存分成几个node。(在arm系统中,如果不是smp的一般都是一个node)
zone:在每个node中,再将内存分配成几个zone。
page:在每个zone中,对page进行管理,添加都各种list中。
for example: 可以分析一下 for_each_zone这个宏定义,会发现就是先查找每个node,在每个node下进行zone的扫描。
从物理空间来看,物理空间主要是“供”,他是实实在在存在的,主要目的就是向OS提供空间。
从虚拟空间来看,虚拟空间是“需”,他是虚拟的,主要是就发送需求。
*****当虚拟空间提出了 “需求”,但物理空间无法满足时候,就会进行swap out操作了。
vm_area_struct结构:这个结构描述 进程的的内存空间的情况。
这个数据结构在程序的变量名字常常是vma。
这个结构是成员变量, vm_start 和vm_end表示一段连续的虚拟区间。 但是并不是一个连续的虚拟区间就可以用一个vm_area_struct结构来表示。而是要 虚拟地址连续,并且这段空间的属性/访问权限等也相同才可以。
所以这段空间的属于和权限用
这两个成员变量来表示。看到这两个成员变量可算看到亲人了。 还记得那个大明湖畔的容嬷嬷妈?-------pgprot_t vm_page_prot;
每一个进程的所有vm_erea_struct结构通过
这个指针link起来。
当然,有上面这种链表的链接方式,查找起来是很麻烦的,而且一个进程中可能会有好多个这样的结构,所以 除了上面的链表,还要有一中更有效的查找方式:
有两种情况虚拟空间会与磁盘flash发生关系。
1. 当内存分配失败,没有足够的内存时候,会发生swap。
2. linux进行mmap系统时候,会将磁盘上的内存map到用户空间,像访问memory一样,直接访问文件内容。
为了迎合1. vm_area_struct 的mapping,vm_next_share,vm_pprev_share,vm_file等。但是在2.6.39中没找到这些变量。仅有:(在page结构体中,也有swap相关的信息)
在后面再分析这些结构。
为了迎合2. vm_area_struct结构提供了
这样的变量,进行操作。
在vm_area_struct结构中,有个mm_struct结构,注释说他属于这个结构,由此看来,mm_struct结构应该是vm_area_struct结构的上层。
这个结构在代码中,经常是mm的名字。
他比vm_area_struct结构更上层,每个进程只有一个mm_struct结构在task_struct中由指针。因此mm_struct结构会更具总结型。
所以虚拟空间的联系图为:
总结一下:
讲解了,
1.PGD,PTE,
2.node--->zone---->page
3. mm_struct------>vm_area_struct
虚拟地址和物理地址 通过 PGD,PTE联系起来
//include/asm-generic/memory_model.h
#define PHYS_PFN_OFFSET (PHYS_OFFSET >> PAGE_SHIFT)
#define ARCH_PFN_OFFSET PHYS_PFN_OFFSET
#define pfn_to_page __pfn_to_page
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET)) //根据页号转换为表述它的page的指针 mem_map是page* (前进多少个page*呢?当前页号相对于第一页的offset) 显然pfn是phy的而不是vir的,pfn比较好计算 pte(val)>>PAGE_SHIFT 而PAGE_SHIFT是12, 一页是4k
找这个宏费了不少时间,鄙视自己,接下来该描述arm页表和linux页表了,网上很少有人介绍这一部分!
先来个内核里的图
* This leads to the page tables having the following layout:
*
* pgd pte
* | |
* +--------+ +0
* | |-----> +------------+ +0
* +- - - - + +4 | h/w pt 0 |
* | |-----> +------------+ +1024
* +--------+ +8 | h/w pt 1 |
* | | +------------+ +2048
* +- - - - + | Linux pt 0 |
* | | +------------+ +3072
* +--------+ | Linux pt 1 |
* | | +------------+ +4096
typedef unsigned long pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;
#define pte_val(x) (x)
#define pmd_val(x) (x)
#define pgd_val(x) ((x)[0])
#define pgprot_val(x) (x)
#define __pte(x) (x)
#define __pmd(x) (x)
#define __pgprot(x) (x)
arm是两级页表pgd pte, linux是三级的pgd pmd pte, arm把pgd和pmd搞成一级了
arm是2048-512-4096
vir--->phy
根据cp15协处理器的c2找到pgd的基地址,利用虚拟地址的高11位找到pgd的入口 //pgd_offset
把*pgd加上虚拟地址次9位,找到pte的入口//pte_offset_map不过这个宏不太好理解,
把*pte加上虚拟地址最后的12位 就得到存有物理地址的这个地址了
/* Find an entry in the second-level page table.. */
#define pmd_offset(dir, addr) ((pmd_t *)(dir)) //这个宏就能看出是两极页表
举个例子
if (!mm)
mm = &init_mm; //mm是mm_struct, 是进程task_struct里的虚拟内存空间表述符,init_mm是内核进程idle的,就是进程号为0的那个!
printk(KERN_ALERT "pgd = %p\n", mm->pgd);
pgd = pgd_offset(mm, addr); //mm->pgd是swap_pg_dir 汇编里定义的 一般就是0xc0004000
printk(KERN_ALERT "[%08lx] *pgd=%08lx", addr, pgd_val(*pgd));
do {
pmd_t *pmd;
pte_t *pte;
if (pgd_none(*pgd))
break;
if (pgd_bad(*pgd)) {
printk("(bad)");
break;
}
pmd = pmd_offset(pgd, addr); //pmd_offset只是把pgd强制转换为了pmd而已
if (PTRS_PER_PMD != 1)
printk(", *pmd=%08lx", pmd_val(*pmd));
if (pmd_none(*pmd))
break;
if (pmd_bad(*pmd)) {
printk("(bad)");
break;
}
/* We must not map this if we have highmem enabled */
if (PageHighMem(pfn_to_page(pmd_val(*pmd) >> PAGE_SHIFT)))
break;
pte = pte_offset_map(pmd, addr);//这个结合上面的图应该可以理解,*pte+512*sizeof(void*)就找到pte的入口了 注意看图
printk(", *pte=%08lx", pte_val(*pte));
printk(", *ppte=%08lx", pte_val(pte[-PTRS_PER_PTE]));
pte_unmap(pte);
} while(0);
h/w pt 这个还有待研究
启动涉及到一个解压与定位的过程,对于x86体系结构而言,系统被加载到0x100000的地方,那么swapper_pg_dir的值是什么呢?我们知道swapper_pg_dir是一个很重要的东西,它是所有进程内核空间的页表的模板,而且在涉及到896M以上的内存分配时,swapper_pg_dir也是一个同步的根,这些内存分配包括vmalloc区,高端永久区,高端临时区等。这里需要说明的是,swapper_pg_dir这个东西其实就是一个页目录的指针,页目录指针在x86中是要被加载到cr3寄存器的,每个进程都有一个页目录指针,这个指针指示这个进程的内存映射信息,每当切换到一个进程时,该进程的页目录指针就被加载到了cr3,然后直到切换到别的进程的时候才更改,既然swapper_pg_dir是一个页目录指针,那么这个指针是被哪个进程用的呢?现代操作系统的含义指示了进程间内存隔离,那么一个页目录指针只能被一个进程使用,那么到底是哪个特定的进程使用了swapper_pg_dir指针呢?遗憾的是,答案是没有任何用户进程使用swapper_pg_dir作为页目录指针,swapper_pg_dir只是在内核初始化的时候被载入到cr3指示内存映射信息,之后在init进程启动后就成了idle内核线程的页目录指针了,/sbin/init由一个叫做init的内核线程exec而成,而init内核线程是原始的内核也就是后来的idle线程do_fork而成的,而在do_fork中会为新生的进程重启分配一个页目录指针,由此可见swapper_pg_dir只是在idle和内核线程中被使用,可是它的作用却不只是为idle进程指示内存映射信息,更多的,它作为一个内核空间的内存映射模板而存在,在linux中,任何进程在内核空间就不分彼此了,所有的进程都会公用一份内核空间的内存映射,因此,内核空间是所有进程共享的,每当一个新的进程建立的时候,都会将swapper_pg_dir的768项以后的信息全部复制到新进程页目录的768项以后,代表内核空间。另外在操作3G+896M以上的虚拟内存时,只会更改swapper_pg_dir的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。
了解到swapper_pg_dir的意义与实际作用,我们来看一下它的初始化吧,首先要看它的定义,在arch/i386/kernel/head.S中:
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007 //第一个页目录项,指示前4M的页面映射信息,0x00102000是前4M页表所在的物理页,而0x00000007是访问控制权限,二者相加构成一个页表,以下同义
.long 0x00103007 //第二个页目录项,指示4-8M的页面映射信息
.fill BOOT_USER_PGD_PTRS-2,4,0 //填充到内核边界
.long 0x00102007 //第768个页目录项,指示前4M的页面映射信息
.long 0x00103007 //第769个页目录项,指示第4-8M的页面映射信息
...
org 0x1000说明了让系统将swapper_pg_dir加载到地址0x1000处,可是内核最终搬到了0x100000处,那么swapper_pg_dir也就到了0x101000处,现在物理地址已经搞定了,那么最终进入保护模式并且启动分页时,swapper_pg_dir的虚拟地址会在哪里呢?我们看一下内核加载到了哪里然后加上0x1000就是swapper_pg_dir加载到的虚拟地址了,在vmlinux.lds中可以看出内核被加载到了0xc0100000处,于是swapper_pg_dir加载的虚拟地址就是0xc0101000,在初始化的时候,内核将0到8M的物理内存分别映射到了虚拟地址的0到8M和3G到3G+8M两个地方,而且过了初始化阶段到了最终的稳定页表,也同样有前XM的物理内存一一映射到虚拟内存的3G+XM的地方,于是swapper_pg_dir的虚拟地址就是0xc0101000,我们可以打印出swapper_pg_dir看看到底是多少,不幸的是,swapper_pg_dir并不从内核导出,那么怎么办呢?难道非要将打印信息加入内核启动函数然后从新编译一遍内核吗?其实不用,虽然swapper_pg_dir没有被导出,可是init_mm被导出了啊,我们知道init_mm就是内核启动时也就是idle进程的mm_struct,其中一个字段是pgd,就是swapper_pg_dir,我们可以打印init_mm->pgd的值,看看是多少。
事情到此还没有结束,如果你真的写了一个模块,并且打印init_mm.pdg的话,发现可能它并不是期望的0xc0101000,怎么回事呢?不要急, 遇到这种情况比遇到内核莫名其妙的down掉要好处理的多,再说这并不影响我们的生活,即使你最终没有弄清楚这是怎么一回事,那么也不会有什么损失的。这 种情况好解决的原因还有就是事情发生在内核初始化的阶段,也就是说swapper_pg_dir的初始化在内核初始化阶段,并且以后也不会变化,可能它的 内容会变,但是其本身的位置是不会变化的,因此,几乎不用调试,光看代码就可以解决问题,我们搜索一下近来的Changelog,发现在2.6.6中将 swapper_pg_dir从原来的固定的.org 0x1000的位置移到了BSS段当中了,因为bss段仅仅拥有占位符而不占用映像静态空间,它的真正数据并没有初始化,因此交给操作系统初始化就可以 了,既然没有初始化的数据,那么就没有必要在静态的映像中占据空间,而是让操作系统将其载入内存时将bss清零即可,linux内核本身就是操作系统内 核,因此它自己负责在启动的时候将bss段清零,因为初始化的时候,临时页表也就需要两个页目录用来映射物理内存的0到8M,这两个页目录很简单,一点不 复杂,没有必要写死到内核映像从而占据着3个页面的空间,因此放到bss段中就可以节省3页面的空间,然后在内核启动过程中再手动初始化那两个页目录的 值,这样做十分有意义。那么bss被加载到哪里就决定了swapper_pg_dir被加载到了哪里,那么bss到底在哪呢?从arch/i386 /kernel/vmlinux.lds.S中可以大致知道答案,如果想知道更加确切的,可以参考/boot/System.map文件,然后可以再打印 一下init_mm.pgd的值,看看是不是在bss里面,其实都不用打印,在System.map里面就有swapper_pg_dir的值,该值在 2.6.6之后的内核肯定在bss中,之前的肯定是0xc0101000。最后我们看一下2.6.6以后的swapper_pg_dir的定义:
.section ".bss.page_aligned","w"
ENTRY(swapper_pg_dir)
.fill 1024,4,0
ARM MMU只支持两级页表地址转换,也就是采用三级分页映射,能够满足32bitCPU的存储管理需求
ARM支持的页大小有几种 - 1M, 64K, 4K, 1K。在linux kernel中,ARM采用了4K大小的页,4K大小的页决定了虚拟地址的低12bit留作偏移地址。从上图可以看出,页全局目录索引有效位数是12bit,二级索引有效位数是8bit,页内偏移量为12bit。
根据ARM的硬件分页机制,我们得出第一级全局页目录有4096项,第二级为256项,这样第二级可以有很多位可以被硬件使用。
在arm linux实现上,针对ARM的硬件分页机制做了些微小的调整。第一级目录保留了2048项,每项占用8 bytes(换句话说,是两个硬件指针指向二级页表);第二级则把两个硬件PTE表连续放在一起,在这两个PTE表后面则保存相应的Linux状态信息,因此二级表项实际上有512项(每个表256项,两个则为512项)。这样每个逻辑PTE表刚好占用一个page。
ARM linux页表layout如下:
在arch/arm/include/asm/pgtable.h中,可以看到PTRS_PER_PTE和PTRS_PER_PTE的定义
以一个例子的形式讲解逻辑地址到物理地址的转换:
某虚拟存储器的用户编程空间共32个页面,每页为1KB,内存为16KB。假定某时刻一用户页表中已调入内存的页面的页号和物理块号的对照表如下:
页号 |
物理块号 |
0 |
3 |
1 |
7 |
2 |
11 |
3 |
8 |
则逻辑地址0A5C(H)所对应的物理地址是什么?要求:写出主要计算过程。
解题过程:
首先要知道页式存储管理的逻辑地址分为两部分:页号和页内地址。物理地址分为两部分:
关系为:逻辑地址= 页号+页内地址
物理地址= 块号+页内地址;
分析题:已知:用户编程空间共32个页面,2ˆ5 = 32 得知页号部分占5位,由“每页为1KB”,1K=210,可知内页地址占10位。
由“内存为16KB”,2^4=16得知块号占4位。
逻辑地址0A5C(H)所对应的二进制表示形式是:0000101001011100,后十位1001011100是页内地址,
00010为为页号,页号化为十进制是2,在对照表中找到2对应的物理块号是11,11转换二进制是1011,即可求出物理地址为10111001011100,化成十六进制为2E5C;
即则逻辑地址0A5C(H)所对应的物理地址是2E5C;