内存寻址:
内存访问分为三个部分
逻辑地址(logical address):每个逻辑地址由一个段(segment)和偏移量(offset)组成;偏移量指明了从断开始的地方到实际地址之间的距离。
线性地址:也称为虚拟地址,是一个32位unsinged int 数,可以用来表示高达4GB的地址;
物理地址:用于内存芯片级内存单元寻址。
内存管理单元MMU通过硬件电路把一个逻辑地址装换成线性地址;第二个分页单元把线性地址转换成物理地址;
分段:
保护模式下的地址装换;
逻辑地址由两部分组成,段选择符和段内相对地址偏移量
段标识符(一个16bits的)称为段选择符
段寄存器仅存放段选择符,任意段选择符包含3个字段:
index:指定放在GDT或LDT中相应段描述符的入口。
TI:(Table Indicator) 指明段描述符是在GDT(TI=0)还是LDT(TI=1)中。
RPL:请求者特权级
段描述符地址=GDT地址+(2*index),GDT的第一项为0,能够保存在GDT中的段描述符 最大数目为:2^13 -1= 8191。gdt保存在gdtr寄存中
偏移量 为一个32bits长的字段,
段寄存器是为了快速方便的找到段选择符而存在;其目的存放段选择符;
分段单元执行将逻辑地址转换为相应的线性地址示意图如下:
段寄存器有 cs.ss.ds.es.fs和gs;
cs 代码段寄存器 指向包含程序指令的段
ss 栈段寄存器 指向包含当前程序栈的段。
ds 数据段寄存器 指向包含静态数据或者全局数据段
其他三个段寄存器可以指向任意的数据段;
cs段寄存器中含有一个2bits的字段;用以指明CPU当前的特权级(current privilege level ,CPL);
段描述符
每个段是由8字节的短描述符表示,他描述了段的特征。
段描述符放在全局描述符表中(global description Table ,GDT)或局部描述符表(local description table LDT)
每个进程除了放在GDT中的段外如果还需要创建附加的段,就可以有自己的LDT。
GDT在主存中的地址和大小放在gdtr控制寄存中;
当前正在使用的LDT地址放在ldtr寄存器中。
段描述符中各字段如下:
Base 包含首字节的线性地址
G 粒度标志,段大小以字节(0)或4096字节倍数计(1)
Limit 段中最后一个内存单元的偏移量,段大小位于1字节–1MB(G=0)或4KB–4GB
S 系统标志,0表示系统段
Type 段的类型和存取权限
DPL 描述符特权级(Descriptor Privilege Level),为0只有CPL=0才可访问,为3则无限制。
P Segment-Present标志,为0表示段当前不在主存中,Linux总是将其置1。
D或B 代码段 or 数据段
AVL 操作系统使用,被Linux忽略。
Linux中被广泛使用的段描述符有:
代码段描述符。[S=1]
数据段描述符。[S=1]
任务状态段描述符 (TSSD),用于保存处理器寄存器的内容,只能出现在GDT中。
[Type=11 or 9,S=0]
局部描述符表描述符(LDTD),代表一个包含LDT的段,只出现在GDT中。[Type=2,S=0]
linux中的分段
运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址:用户代码段和用户数据段;相应的,运行在内核态的进程使用内核代码段和内核数据段,列表如下:
Base G Limit S Type DPL D/B P 宏
用户代码段 0×00000000 1 0xfffff 1 10 3 1 1 __USER_CS
用户数据段 0×00000000 1 0xfffff 1 2 3 1 1 __USER_DS
内核代码段 0×00000000 1 0xfffff 1 10 0 1 1 __KERNEL_CS
内核数据段 0×00000000 1 0xfffff 1 2 0 1 1 __KERNEL_DS
所有的段都是从0×00000000开始的,Linux下逻辑地址与线性地址是一致的。
GDT;
每个CPU对应一个GDT,所有的GDT都存放在一个cpu_gdt_table数组中;而所有的GDT的地址和他们的大小都被存放在cpu_gdt_descr数组中。
每个GDT包含18个段描述符和14个空的未使用保留的项,插入未使用的项是为了使经常一起访问的描述符能够处于同一个32字节的硬件高速缓存行中。
用户态和内核态下的代码段和数据段共4个;
任务状态段(TSS),每个处理器1个,所有TSS均存放在init_tss数组中。
1个包括缺省局部段描述符表的段。
3个局部线程储存(Thread-Local Stroage,TLS)段。
高级电源管理(AMP)相关的3个段。
支持即插即用(PnP)功能的BIOS服务程序相关的5个段。
内核用于处理双重错误异常的特殊TSS段。
linux LDT:
大多数用户态下的linux程序不使用局部描述符表;即内核定义了一个缺省的LDT供大多数进程共享。LDT存放在default_ldt数组中;包含5个项,内核仅定义了其中的2个:用于iBCS执行文件的调用门和Solaris/x86可执行文件的调用门,modfiy_ldt()系统调用允许进程创建自己的局部描述符表。
硬件分页
存放在主存中的页表(Page Table)将线性地址映射到物理地址。
常规分页
32位线性地址被划分为如下3部分:
——Directory
最高10位
——Table
中间10位
——Offset
最后12位
页目录项和页表项的结构相同,包含如下字段:
Present:页(页表)在主存中(1)还是不在主存中。
页框物理地址最高20位:每个页框4KB,地址为4096倍数,故低12位均为0。
Accessed:分页单元对相应页框寻址时设置。
Dirty:仅存于页表项中,当对页框进行写操作时设置。
Read/Write:页(页表)的存取权限(Read/Write or Read)。
User/Supervisor:访问页(页表)所需的特权级。
PCD和PWD:硬件高速缓存处理页(页表)的方式。
Page Size:仅用于页目录项,为1则目录项所指为2MB或4MB页框。
Global:仅用于页表项,防止常用页从TLB刷新出去,仅在cr4的PGE标志置位时才给力。
扩展分页:允许页框大小为4MB;可以把大连续的线性的地址转换成相应的物理地址;在这情况下内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。此时32位线性地址分为:Directory(10位)+Offset(22位)。
通过cr4处理寄存器的pse标志扩展分页与常规分页共存。
物理地址扩展(Physical Address Extension,PAE)
通过增加处理器管脚数至36使寻址能力达到2^36=64GB,64GB的RAM被分为2^24个页框,因此页表项物理地址字段扩展到24位,页表项大小从32位变为64位,从而使4KB的页表包含512个页表项。
引入页目录指针表(Page Derictory Pointer Table,PDPT)的页表新级别,由4个64位表项组成。
cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段。
线性地址被映射到4KB的页时,32位线性地址被解释成如下形式:
——cr3
指向一个PDPT
——31~30
指向PDPT中4个项中的一个
——29~21
指向页目录中512项中的一个
——20~12
指向页表中512项中的一个
——11~0
4KB页中偏移量
线性地址被映射到2MB的页时(页目录项中PS=1),32位线性地址被解释成如下形式:
——cr3
指向一个PDPT
——31~30
指向PDPT中4个项中的一个
——29~21
指向页目录中512项中的一个
——20~0
2MB页中偏移量
64位Linux系统分页级别:
平台 页大小 寻址位数 分页级别 线性地址级别
alpha 8KB 43 3 10+10+10+13
ia64 4KB 39 3 9+9+9+12
ppc64 4KB 41 3 10+10+9+12
sh64 4KB 41 3 10+10+9+12
x86_64 4KB 48 4 9+9+9+9+12
高速缓存
局部性原理:由于程序的循环结构及相关数组可以组织成线性数组,最近常用的相邻地址在最近的将来又被用到的可能性极大。
命中高速缓存时,对于高速缓存控制器的不同操作:
写操作分为通写(write-through):即写RAM也写高速缓存行;回写(write-back):只更新高速缓存行。由于Linux清除了所有页目录项和页表项中的PCD和PWT标志,使得对所有的页框都启用高速缓存,对写操作都采用回写策略。
转换后援缓冲器(Translation Lookaside Buffer,TLB)
用于加速线性地址的转换,当地址首次被使用时,通过慢速访问RAM中的页表计算出相应物理地址,被存放到一个TLB表项(TLB entry)中,以便以后对同一个线性地址的引用可以快速得到转换
高速缓存单元存在分页单元和主内存之间包含一个硬件高速缓存内存和一个高速缓存控制器
TLB
转换后援缓冲器或TLB(translation lookaside buffer)用于加快线性地址的转换。当一个线性地址被使用过一次时,通过慢速访问RAM中的页表计算相应的物理地址。同时物理地址存放在一个TLB表项中以便后面快速的访问。
linux中的分页:
linux2.6.11以采用四级分页模型:
——页全局目录(Page Global Directory)
——页上级目录(Page Upper Directory)
——页中间目录(Page Middle Directory)
——页表(Page Table)
其中页全局目录包括若干页上级目录的地址,页上级目录依次包含若干页中间目录的地址,页中间目录包含若干页表的地址,每个页表指向一个页框。线性地址的映射关系如图示:
线性地址字段:
|<------------------------------ BITS_PER_LONG --------------------------------->|
+---------------+---------------+---------------+---------------+----------------+
| PGD | PUD | PMD | PTE | Offset |
+---------------+---------------+---------------+---------------+----------------+
| | | |<- PAGE_SHIFT -->|
| | |<----------- PMD_SHIFT ---------->|
| |<---------------- PUD_SHIFT ---------------------->|
|<-------------------------- PGDIR_SHIFT -------------------------->|
The linear address space of a process is divided into two parts:
0x00000000 ~ 0xbfffffff:
内核空间和用户空间的进程都可访问。
0xc0000000 ~ 0xffffffff:
仅限于内核态进程访问。
PAGE_SHIFT:Offset字段位数。
PMD_SHIFT:Offset和Table字段总位数。
PMD_MASK:用于屏蔽Offset和Table字段。
PMD_SIZE:页中间目录的一个单独表项所映射区域大小(PMD_SIZE=2^PMD_SHIFT)。
PUD_SHIFT:页上级目录所能映射的区域大小的对数。
PUD_MASK:用于屏蔽Offset,Table,Middle Air,Upper Air字段。
PUD_SIZE:页上级目录所能映射的区域大小(PUD_SIZE=2^PUD_SHIFT)
PGDIR_SHIFT:页全局目录项能映射的区域大小的对数。
PGDIR_SIZE:页全局目录项能映射的区域大小。
PGDIR_MASK:用于屏蔽Offset,Table,Middle Air,Upper Air字段。
PTRS_PER_PTE、PTRS_PER_PMD、PTRS_PER_PUD、PTRS_PER_PGD:
页表、页中间目录、页上级目录、页全局目录中表项的个数:
PAE禁止时为1024、1、1、1024;PAE启用时为512、512、1、4。
页表处理:
对页表操作的宏
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) /* * pgd_offset() returns a (pgd_t *) * pgd_index() is used get the offset into the pgd page's array of pgd_t's; */*这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符mm内的一个指针可以找到页全局目录</span></span>
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))
* a shortcut which implies the use of the kernel's pgd, instead * of a process's */
产生主内核页全局目录中的某个线性地址</span>
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
pmd_offset</span><br style="color:rgb(51,51,51); 根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)
/* Find an entry in the second-level page table.. */ #define pmd_offset(dir, addr) ((pmd_t *)(dir)) //即为pgd项的值
内核页表:
内核维持着一组使用的页表,驻留在主内核页全局目录中,
内核如何初始化自己的页表:
第一个阶段:
内核创建一个有限的地址空间,包括内核代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间。
第二阶段:
内核充分利用剩余的RAM并适当建立分页页表;
临时内和页表
临时页全局目录放在是在内核编译过程中静态的初始化的,而临时页表是由startup_32()汇编语言(位于arch/i386/kernel/head.s)函数初始化;临时页全局,目录放在swapper_pg_dir变量中,临时页表在pg0变量处开始存放,紧接着是内核未初始化的数据;
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函数继续初始化.
分页第一个目的是为了允许在实时模式下和保护模式下都能够很容易的对RAM前8M寻址;因此内核必须创建一个映射,把从0x00000000到0x007ffffff线性地址和从0xc000000000到0xc07fffff的线性地址映射到从0x00000000到0x007fffffff的物理地址。即在内核初始化的第一阶段可以通过和物理地址相同的线性地址或者通过0xc0000000开始的
8M线性地址对RAM的前8M进行寻址。
当RAM小于896M时最终内核页表
有内核提供的映射必须把从0xc0000000开始的线性地址转化为从0开始的物理地址;
在start_kernel()-->setup_arch()-->pageing_init()函数;
* * paging_init() sets up the page tables - note that the first 8MB are * already mapped by head.S. * * This routines also unmaps the page at virtual kernel address 0, so * that we can trap those pesky NULL-reference errors in the kernel. */ void __init paging_init(void) { #ifdef CONFIG_X86_PAE set_nx(); if (nx_enabled) printk("NX (Execute Disable) protection: active\n"); #endif /* 建立页表项*/ pagetable_init(); /* 把swapper_pg_dir 的物理地址写入cr3控制寄存器中 */
load_cr3(swapper_pg_dir); #ifdef CONFIG_X86_PAE /* * We will bail out later - printk doesn't work right now so * the user would just see a hanging kernel. 如果CPU支持PAE并且如果内核编译时支持PAE,则将cr4控制寄存器的PAE的标志置位
*/ if (cpu_has_pae) set_in_cr4(X86_CR4_PAE); #endif
__flush_tlb_all();//使TLB所有的项无效; kmap_init(); zone_sizes_init(); }
static void __init pagetable_init (void) { unsigned long vaddr; pgd_t *pgd_base = swapper_pg_dir;//#define swapper_pg_dir ((pgd_t *) 0) #ifdef CONFIG_X86_PAE int i; /* Init entries of the first-level page table to the zero page */</span></strong> 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); remap_numa_kva(); /* * Fixed mappings, only the page table structure has to be * created - mappings will be set by set_fixmap(): */ vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; page_table_range_init(vaddr, 0, pgd_base); permanent_kmaps_init(pgd_base); #ifdef CONFIG_X86_PAE /* * starting up on an AP from real-mode. In the non-PAE * case we already have these mappings through head.S. * All user-space mappings are explicitly cleared after * SMP startup. */ pgd_base[0] = pgd_base[USER_PTRS_PER_PGD]; #endif }
/* * This maps the physical memory to kernel virtual address space, a total * of max_low_pfn pages, by creating page tables starting from address * PAGE_OFFSET. */ 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; /*#ifdef __ASSEMBLY__ #define __PAGE_OFFSET (0xC0000000) #else #define __PAGE_OFFSET (0xC0000000UL) #endif
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#ifndef _I386_PGTABLE_3LEVEL_DEFS_H #define _I386_PGTABLE_3LEVEL_DEFS_H /* * PGDIR_SHIFT determines what a top-level page table entry can map */ #define PGDIR_SHIFT 30 #define PTRS_PER_PGD 4 /* * PMD_SHIFT determines the size of the area a middle-level * page table can map */ #define PMD_SHIFT 21 #define PTRS_PER_PMD 512 /* * entries per page directory level */ #define PTRS_PER_PTE 512 #endif /* _I386_PGTABLE_3LEVEL_DEFS_H */
#ifndef _I386_PGTABLE_2LEVEL_DEFS_H #define _I386_PGTABLE_2LEVEL_DEFS_H /* * traditional i386 two-level paging structure: */ #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 /* * the i386 is two-level, so we don't really have any * PMD directory physically. */ #define PTRS_PER_PTE 1024 #endif /* _I386_PGTABLE_2LEVEL_DEFS_H */
*/ pgd_idx = pgd_index(PAGE_OFFSET)//值为0xc0000000左移22位后值为0x300
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)) et_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); else set_pte(pte, pfn_pte(pfn, PAGE_KERNEL)); } } } } }
/* * Creates a middle page table and puts a pointer to it in the * given global directory entry. This only returns the gd entry * in non-PAE compilation mode, since the middle layer is folded. */ static pmd_t * __init one_md_table_init(pgd_t *pgd) { pud_t *pud; 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)); pud = pud_offset(pgd, 0); if (pmd_table != pmd_offset(pud, 0)) BUG(); #else pud = pud_offset(pgd, 0); pmd_table = pmd_offset(pud, 0); #endif return pmd_table; }
/* * Create a page table and place a pointer to it in a middle page * directory entry. */ 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); }
//分配一个page
void * __init __alloc_bootmem_node (pg_data_t *pgdat, unsigned long size, unsigned long align, unsigned long goal) { void *ptr; ptr = __alloc_bootmem_core(pgdat->bdata, size, align, goal); if (ptr) return (ptr); return __alloc_bootmem(size, align, goal); }
</pre><pre name="code" class="cpp">#define pte_offset_kernel(dir, address) \ ((pte_t *) pmd_page_kernel(*(dir)) + pte_index(address))
</pre><pre name="code" class="cpp">
# define __flush_tlb_all() \ do {\ if (cpu_has_pge)\ _flush_tlb_global();\ else \ __flush_tlb();\ } while (0)
#define __flush_tlb() \ do { \ nsigned int tmpreg;\ \ __asm__ __volatile__(\ movl %%cr3, %0; \ movl %0, %%cr3; # flush TLB \ "=r" (tmpreg)\ :: "memory");\ } while (0) /* * Global pages have to be flushed a bit differently. Not a real * performance problem because this does not happen often. */ #define __flush_tlb_global() \ do {\ unsigned int tmpreg;\ <\ __asm__ __volatile__(\ "movl %%cr3, %0; \n"\ "movl %0, %%cr3; # flush TLB \n"\ "movl %2, %%cr4; # turn PGE back on \n"\ : "=&r" (tmpreg)\ : "r" (mmu_cr4_features & ~X86_CR4_PGE),\ "r" (mmu_cr4_features)\ : "memory");>\ } while (0)