linux内存管理解析1----linux物理,线性内存布局及页表的初始化
主要议题:
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);
}
linux内存管理解析1----linux物理,线性内存布局及页表的初始化的更多相关文章
- Linux内存管理--虚拟地址、逻辑地址、线性地址和物理地址的区别(二)【转】
本文转载自:http://blog.csdn.net/yusiguyuan/article/details/9668363 这篇文章中介绍了四个名词的概念,下面针对四个地址的转换进行分析 CPU将一个 ...
- arm-linux内存管理学习笔记(1)-内存页表的硬件原理
linux kernel集中了世界顶尖程序猿们的编程智慧,犹记操作系统课上老师讲操作系统的四大功能:进程调度 内存管理 设备驱动 网络.从事嵌入式软件开发工作,对设备驱动和网络接触的比較多. 而进程调 ...
- JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏
摘要: 作者将自己常用的JavaScript模块分享给大家. 原文:JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏 作者:前端小智 Fundebug经授权转载,版权归原作者所有. ...
- C语言堆内存管理上出现的问题,内存泄露,野指针使用,非法释放指针
C语言堆内存管理上出现的问题,内存泄露,野指针使用,非法释放指针 (1)开辟的内存没有释放,造成内存泄露 (2)野指针被使用或释放 (3)非法释放指针 (1)开辟的内存没有释放.造成内存泄露,以下的样 ...
- java虚拟机学习-JVM内存管理:深入垃圾收集器与内存分配策略(4)
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项 ...
- Linux内存管理解析(一) : 分段与分页机制
背景 : 在此文章里会从分页分段机制去解析Linux内存管理系统如何工作的,由于Linux内存管理过于复杂而本人能力有限.会尽量将自己总结归纳的部分写清晰. 从实模式到保护模式的寻址方式的不同 : 1 ...
- linux内存管理---虚拟地址、逻辑地址、线性地址、物理地址的区别(一)
分析linux内存管理机制,离不了上述几个概念,在介绍上述几个概念之前,先从<深入理解linux内核>这本书中摘抄几段关于上述名词的解释: 一.<深入理解linux内核>的解释 ...
- Linux内存管理解析(二) : 关于Linux内存管理的大体框架
什么是内存管理 ? 首先内存管理管理的主要对象是虚拟内存,但是虚拟内存对应的映射主要为物理内存,其次也可能通过交换空间把虚拟内存与硬盘映射起来,既然如此,那我们先了解物理内存的管理. 对于物理内存而言 ...
- Linux内核入门到放弃-内存管理-《深入Linux内核架构》笔记
概述 内存管理的实现涵盖了许多领域: 内存中的物理内存页管理 分配大块内存的伙伴系统 分配较小内存块的slab.slub和slob分配器 分配非连续内存块的vmalloc机制 进程的地址空间 在IA- ...
随机推荐
- 匿名函数和Lambda表达式
这个题目有点牵强,真不知道如何取一个比较中意的名称,写技术博客,我很少拘泥小节,但是注重细节,如果细节都出现问题了,那么这个博文也就失去了价值. 其实应该从委托说起,委托是C#中的一个重要的内容,记得 ...
- 关于linq
其实从08年的时候,我就已经知道了linq,开始的时候也并没有注意,我说过很多次,我不是一个有心人,只是在新建立一个工程的时候,程序会自动引入linq这个玩意,怀着好奇的心去找了点资料,有的时候,看一 ...
- GetActiveView 返回 NULL 为 MDI 框架窗口
blog 在 MDI 应用程序中,MDI 主框架窗口(CMDIFrameWnd) 不具有与其相关联的视图.相反,每个单独的子窗口(CMDIChildWnd)具有与之关联的一个或多个视图.因此,对 MD ...
- WCF - Architecture
WCF - Architecture WCF has a layered architecture that offers ample support for developing various d ...
- Discuz! x3.1的插件/utility/convert/index.php代码执行漏洞
漏洞版本: Discuz! x3.1及以下版本 漏洞描述: Discuz! x3.1的插件/utility/convert/index.php存在代码执行漏洞,如果用户在使用完之后不删除,会导致网站容 ...
- 《C#并行编程高级教程》第7章 VS2010任务调试 笔记
没有什么好说的,主要是将调试模式下的Parallel Tasks窗体和Parallel Stacks窗体.折腾一下应该比看书效果好.(表示自己没有折腾过) 另外值得注意的是,主线程不是一个任务.所以主 ...
- 超大型 LED 显示屏
http://acm.hunnu.edu.cn/online/?action=problem&type=show&id=11574&courseid=0 题目 E. 超大型 L ...
- java中字符串切割的方法总结
StringTokenizer最快 ,基本已经不用了,除非在某些需要效率的场合.Scanner最慢. String和Pattern速度差不多.Pattern稍快些. String和Pattern的sp ...
- POJ1006 - Biorhythms(中国剩余定理)
题目大意 略...有中文... 题解 就是解同余方程组 x≡(p-d)(mod 23) x≡(e-d)(mod 28) x≡(i-d)(mod 33) 最简单的中国剩余定理应用.... 代码: #in ...
- linux select 与 阻塞( blocking ) 及非阻塞 (non blocking)实现io多路复用的示例
除了自己实现之外,还有个c语言写的基于事件的开源网络库:libevent http://www.cnblogs.com/Anker/p/3265058.html 最简单的select示例: #incl ...