Cu 大彻大悟内存管理 mm (update 0410)
66 | 2012-4-10 11:23:53 一口气读完,畅快。。原来站在设计者的角度是这样的
|
来源于cu 讨论 |
创意
ARM在内存管理上,和x86的区别主要是页表构造不同。
除了用层的思想,用映射思想,还可以使用 函数.说 保护模式,实模式。
当进程的页表发生变化的时候,会修改TLB的内容;当CPU上发生进程切换的时候,会将本地的TLB清空(不考虑惰性TLB的情况)。
反复 内存 与寄存器。。
反复读 分页
反复读:为什么 不在实模式设计 任务切换,反而在 保护模式设计。设计的基本工作
基础,寻址空间大小
基础cr3 ,cr2
任务切换的基本工作就是将原任务“环境”存入TSS数据段,更新TR寄存器,系统将自动查GDT表获得并载入新任务的“环境”,然后转到新任务执行
如果只有一个LDTR寄存器显然不能满足多个LDT的要求。
上面 虚拟专物理地址,反复读
实模式,保护模式的共性:都用 映射。映射规则不同而已。
我的理解:保护模式,应用层的思想,加了一层。
悟: 如果不理解内核的cache系统,mmap的作用很难理解。
下一次 反复读:(代码overview)版主 作者精炼的 虚拟内存的认识!!
这里说到gdt,idt设计思想,前者只有一个,后者多个,一个任务一个
待办事项
当发生缺页异常的时候,内核根据缺页地址(cr2寄存器中)查看页表项的内容,
66使用新的页面 “浅析高端内存”
补上我的那个图,task_struct 2 pgd
反复读:为什么 不在实模式设计 任务切换,反而在 保护模式设计。设计的基本工作
反复读linux on arm特征
开了眼界了。。adv
内核映射了低端896M物理内存 ,但这并不是说这些内存就属于内核了,用户态不能用了。
浅。 Cache …mmu..下一次
下一次 反复读:(代码overview)版主 作者精炼的 虚拟内存的认识!!
下一次把 三个门 in idt区分开来。
选择子的设计思想,反复读
这是论坛作者的观点:比如一个page即链接在inactive_list中,又链接在buffer+cache中,待证实
定义
任务切换的基本工作就是将原任务“环境”存入TSS数据段,更新TR寄存器,系统将自动查GDT表获得并载入新任务的“环境”,然后转到新任务执行
问题
当发生缺页异常的时候,内核根据缺页地址(cr2寄存器中)查看页表项的内容,
(slab也有释放操作?)Slab 之前向底层要的物理内快完了,它会负责再要一点;之前从底层拿的内存多了用不完,它会择时还回去
66想到:内核运行程序,访问一个用户空间的地址
Adv slab..碎片
内核映射了低端896M物理内存 ,但这并不是说这些内存就属于内核了,用户态不能用了。
所以,896M用于与物理内存建立一一对应关系,剩余的虚拟地址空间,分时映射没能被896M覆盖到的物理内存,这部分物理内存就是高端内存
有时间思考 ,物理地址已经被硬件固化 ,如何配置到linux 的内存中
Cu mm开篇
大家可以分享自己对linux内存管理的理解和建议
也可以分享linux内存管理的代码分析
也可以分享与linux内存相关的代码
源文档 <http://bbs.chinaunix.net/thread-3691966-1-1.html>
回复 1# 瀚海书香
我先抛个砖了,希望能引玉
首先关于虚拟地址、物理地址、逻辑地址的概念,可以参考独孤九贱的这个帖子 http://bbs.chinaunix.net/thread-2083672-1-1.html,写的非常好。
这里先贴一下当时看内存页表的一点笔记。
(1)每个进程只有一个页表,0-3G映射的是用户空间的内存,也就是应用程序可以直接操作的,3-4G映射的是内核空间,这部分页表所有进程共享。页表保存在内核态,由内核帮助进程来维护。
66 | 3:1概念扩展开来,就是五轮64位上,用户空间只能用3g!! 看最后的回复。 |
| 所有进程共享页表,包含内核。 |
(2)内核给进程分配物理内存的时候,通过按需分配的方法。也就说进程申请一个页表,这个时候进程获得的只 是一个地址空间,而这个地址空间并没有真正的物理页面对应,只有当进程试图访问这个页面的时候,才会给进程分配真实的物理页面。这个过程通过缺页异常来实现。
(3)当发生缺页异常的时候,内核根据缺页地址(cr2寄存器中)查看页表项的内容,如果为空,表明还没有映射,内核根据虚拟地址判断出它所在的vm_area_struct,这个结构体描述了进程地址空间中相同属性的一块区域,比 如映射到一个文件。
66 | 当发生缺页异常的时候,内核根据缺页地址(cr2寄存器中)查看页表项的内容, |
(4)对于已经交换到交换分区的步伐,这个时候页表项非空,而是保存了它在交换分区中的地址信息,根据这个信息可以从交换分区中取出相应的物理页面。
源文档 <http://bbs.chinaunix.net/thread-3691966-1-1.html>
回复 1# 瀚海书香
这里说一个关于内存的实际的应用,用户态进程的虚拟地址如何转换成物理地址?
66 | 作者给出了代码 |
区分一个进程,我们都知道最简单就是进程的pid。我们就从(pid,virtualaddress)来看看如何将一个进程的虚拟地址转换为物理地址phyaddress。
首先根据pid我们可以得到这个进程的task_struct,进而通过task_struct得到mm,通过mm得到pgd。
好了,现在我们有pgd和virtualaddress.
66 | 补上我的那个图,task_struct 2 pgd |
通过pgd和virtualaddress我们可以得到页表pte.
有了pte和virtualaddress,我们就可以计算物理地址了
phyaddress=(pte_val(pte)&PAGE_MASK)|(virtualladdress&~PAGE_MASK)
物理地址既然出来了,访问这个地址的值就比较简单了,只需要将物理地址转换为内核线性地址就行。
*phyaddress=*((char *)phyaddress+PAGE_OFFSET)
66 | 上面 虚拟专物理地址,反复读 |
源文档 <http://bbs.chinaunix.net/thread-3691966-2-1.html>
如果把内存管理的范围扩大点,如何访问和管理设备的地址空间呢, 这个领域有没有深入学习的同学来分享知识?比如说,我现在开发了一块基于PowerPC 的平台, 其物理地址已经被硬件固化。 如何在Linux 系统里,使用什么机制来访问这些物理地址,
66 | 有时间思考 ,物理地址已经被硬件固化 ,如何配置到linux 的内存中 |
Unit computer memory configuration
Resource Address range Size Port width Chip Select
Main memory 0x0000_0000 – 0x1FFF_FFFF 512 MB 64 M1_MCS0
Application FLASH (NAND) 0x4000_0000 – 0x4000_1FFF 8KB 8 HP_LCS2#
PCI Express Memory Window 0x5000_0000 – 0x5FFF_FFFF 256 MB n/a n/a
SRIO Window 0x6000_0000 – 0x9FFF_FFFF 1 GB n/a n/a
Memory mapped registers 0xC000_0000 – 0xC000_000F 16 B 16 HP_LCS3#
CCSRBAR 0xE000_0000 – 0xE00F_FFFF 1MB
Redundant boot FLASH 0xF000_0000 – 0xF7FF_FFFF 128 MB 16 Flash1_CS#
Boot FLASH 0xF800_0000 – 0xFFFF_FFFF 128 MB 16 Flash0_CS#
源文档 <http://bbs.chinaunix.net/thread-3691966-2-1.html>
发表于 2012-03-28 21:24:22 |只看该作者
我讲一讲自己在学习过程中遇到的不解之处,及各个概念的高度总结。好吧,其实是前几天发现包破了。。。。
1.内核是否有独立的地址空间。
X86上的Linux内核没有独立的地址空间。作为对比,DEC-11上的Unix V6内核有独立的地址空间,一旦进入内核态,页表自动切换。这样操纵用户态时就可跳出三界外。现在几乎没有操作系统这样做。根据某部FreeBSD著作所讲,内核态常做的一件事就是从、向用户空间搬、传数据。内核与用户态进程不在一个地址空间,则需要额外的指令在两个地址空间里传数据。
但这确实是一种设计,有其历史渊源。
66 | 读后感,就是我想 用户空间相互独立(除共享),核心共享的,当然不独立 |
2.内核态内存的映射。
X86上Linux内核之内核态虚拟地址,大部分固定地与低端物理内存一一映射。这样做的理由,一来是想更容易地访问物理内存,二来方便使用动态申请到的内存。
DEC-11上的Unix V6内核就算有自己独立的地址空间,其大部分映射也是与物理内存一一对应的---在内核眼中,大部分虚拟地址与物理地址都是一样。
Linux上则仅仅有个差值。
3.896M与高端内存
X86上Linux内核与进程共用地址空间,另外,内核所有的地址空间被设计成与内存固定映射。结果是:内核拥有的地址空间很小(因为用户态占了大部分);能灵活使用的虚拟地址不多(因为大部分要与物理内存做一一对应的关联)。于是,内核能使用物理内存的大小被其狭小的虚拟地址空间所限制了。即便有再多的物理内存,内核已没有多余的虚拟地址与之映射,从而无法访问。
所以,896M用于与物理内存建立一一对应关系,剩余的虚拟地址空间,分时映射没能被896M覆盖到的物理内存,这部分物理内存就是高端内存。内核自己申请动态内存时,通常只在这896M以内申请,免得为了使用还得建立映射关系。其内部大量cache都在这一区域内,所以这块内存比较宝贵。用户虚拟地址空间可以随意设置,给用户态分配内存时,优先从高端内存取(如果有的话)。
66 | 所以,896M用于与物理内存建立一一对应关系,剩余的虚拟地址空间,分时映射没能被896M覆盖到的物理内存,这部分物理内存就是高端内存 |
4.映射与使用
内核映射了低端896M物理内存,大部分动态内存都从这里获得。但这并不是说这些内存就属于内核了,用户态不能用了。动态内存的使用要由mm子系统来协调,基本原理就是:分出去的内存不能再被分出去;收回来的内存可以被再分出去。动态内存的归属,最终由mm子系统来调度,申请到了再使用,这样才能保证秩序。分给用户态了,先建立映射,用户程序就可安心用了,即使内核可以直接访问这块内存,只要没有BUG,它都是很安全的。分给内核态了,直接使用即可--映射早已建立。
66 | 66想到:内核运行程序,访问一个用户空间的地址 |
品味 | 内核映射了低端896M物理内存 ,但这并不是说这些内存就属于内核了,用户态不能用了。 |
如果896M内的物理内存分给用户态,内核自然不能使用。而且与之关联的虚拟地址段也不能另作他用。可惜了。
5.虚拟地址空间
虚拟地址空间也是资源。于用户态而言,典型的例子是在扩展地址空间之时:需要找到一块没用的地址空间。必需得记录哪些地址空间被用了,哪些没有被用。分配虚拟地址空间与分配物理内存其实是一样的道理,只不过物理内存是全局的,需要用更精妙的算法来分配。Linux虚拟地址空间似使用first fit----沿途看看有没有合适的地址段,第一个找到的就是。其实Unix V6就是这么分配物理内存的。
有数据结构描述用户态地址空间段,内核怎么管理自己的虚拟地址空间呢?是什么决定其动态申请内存时获得内存的位置?由于大部分虚拟地址空间都与物理内存一一对应,两者完全可以合二为一:管理虚拟内存段就是管理物理内存。申请的虚拟地址的情况取决于申请到物理内存的情况。
高端分时复用的那一块虚拟地址空间,面临的需求和解决方案,与用户态一样。
6.Buddy System
管理物理内存最基本的要求:判断一块内存单位是否被使。只记录这一个信息的话,一个bit足矣。于是可以用一个bitmap描述当前物理内存的使用情况。实际上,一块内存单位的属性远不止是否被使用,其复杂程序需要用一个结构体来描述。下一步是考虑分配内存的策略。Buddy System就是策略的一种。其原理,至少从表面上看起来,并不复杂。它主要为了解决外部碎片:系统中有足够量的内存满足需要,但是这些内存散乱在各地,无法作为整体来使用。
66 | 收获:伙伴算法,解决外部碎片。内部碎片没有办法 |
7. Slab
现代操作系统通常以页作为物理内存单位,这是分配与回收的最小单位。分配一页给一页你,分配几byte还是得给一页你。如果分出去的内存大部分没有被使用,那就太可惜了。一个频繁分配小内存的代码段,可能会先分配几页,然后自己内部管理这些内存,通过某种方法一点点满足小内存分配的需要。slab就是提取出来,专门做这种事的代码。它并不在乎底层用的是何种内存分配方式,它只在乎地层提供的分配与释放的接口,然后向上层提供分配小块内存的接口。之前向底层要的物理内快完了,它会负责再要一点;之前从底层拿的内存多了用不完,它会择时还回去。它默默地做着这些脏活,留给上层一个功能简单明了的接口。
66 | 只申请,不释放。66理解。小粒度。 |
66 | (slab也有释放操作?)Slab 之前向底层要的物理内快完了,它会负责再要一点;之前从底层拿的内存多了用不完,它会择时还回去 |
8.mmap与虚拟地址空间
以前看APUE,以为mmap是个trick十足的系统调用。后来才明白,如果不理解内核的cache系统,mmap的作用很难理解。在内核里,带mmap字样的函数更让人觉得trick:它们用来分配用户态的虚拟地址空间。这似乎偏离了mmap的初始意图。
66 | 悟: 如果不理解内核的cache系统,mmap的作用很难理解。 |
66 | Mmap...扩展了内存空间。。作者意思 |
初始的时候,虚拟地址空间的样子由可执行文件,比如ELF,来决定。如果地址空间与文件映射,就叫有名映射(好吧,好像没有这种叫法);如果纯粹扩展一段地址空间,比如从用户态上去理解malloc的语义,就叫匿名映射,虽然看不出来整个过程哪里跟映射有关了。于是,用户态的地址空间就这么一点一点被有名映射,无名映射筑起来了。
源文档 <http://bbs.chinaunix.net/thread-3691966-2-1.html>
本帖最后由 塑料袋于 2012-03-28 23:48 编辑
占位,计划谈page compaction及migration的提出目的,实现手段。
目的:
1) NUMA结构,进程迁移,则进程的内存页面若也迁移至目标CPU,则对这个进程来说,访存最快。
2) 内存热插拔
3) huge page
4) buddy造成的的内存碎片化:buddy有求必应,一律从满足要求大小的,最小的连续内存片段分配,
66 | 开了眼界了。。adv |
但是无视分配的性质。考虑场景如下,1234共4个页:
a) slab临时性的分走[1],随时可能shrink cache收回,此时剩余[2],[34]
b) 某driver长期性的分走[2],这页不可能收回,此时剩余[34]
c) slab临时性的分走[3],随时可能shrink cache收回,此时剩余[4]
d) 某driver长期性的分走[4],这页不可能收回,此时无剩余
e) slab释放了[1],此时剩余[1]
f) slab释放了[3],此时剩余[1],[3]
由此导致碎片化。但若将内存按照分配的性质分类,[12]归于movable,临时性的分配从这里分;[34]归于unmovable,长期性占用从这里分,则不会产生这种情况。
66 | Adv slab..碎片 |
66 | Free 内存分配的性质 |
将内存按照分配的性质来来划分成几部分,需要一个划分的依据。以前的版本中分了三部分:
MOVABLE,RECLAIMABLE ,UNMOVABLE 。movable最可能被随时释放,unmovable最不可能被释放。buddy中的每个order,都被分成这3部分。
这三部分中,那一部分到底应涵盖多少内存,不应设置成死的,而应根据分配的情况来动态调整。即:
unmovable不够,则先自reclaimable借,后自movable借。
movable不够,则先自reclaimable借,后自unmovable借。
原则为尽量少改动内存的易释放性,从最接近自己的那类中来借。
66 | 品:原则为尽量少改动内存的易释放性,从最接近自己的那类中来借。 |
当本order管理的内存中借也借不着时,那只好在斥逐于更高的order,整个拿来一块,将这一块分成小块,除了分配出去的那小块外,其他的尽量不更改易释放性。
MOVABLE中的内存是重头戏,除了只分配给那些临时性的请求外,对这些内存,还可以进行compaction及migration。
compaction发生于内存不足时,基本上是个和page reclaim并列的逻辑。自头至尾释放zone内的页,这些页都在lru中,且页与页之间可能有缝隙,即碎片化;自尾至头分配zone内的页,这些页都属于movable,且分配的原则为见缝插针,中间不产生缝隙。将被释放页的内容复制到新分配的页,同时依据被释放页的反向映射,修改所有引用被释放页的pte。
占位待续
源文档 <http://bbs.chinaunix.net/thread-3691966-2-1.html>
回复 16# dooros
由于不同的操作系统实现不同,而且我手头也没有这本书,这能根据你的描述以x86 linux为例,简单回答几个问题:
一、Page 116 4.3.2 地址变换机构
2.具有快表的地址变换机构
快表用以存放当前访问的那些页表项。
问题:在单处理机系统中,是不是每当进程阻塞、挂起、结束都要清除一次快表,即:快表只保留当前进程的页表项?
当前访问的理解?比如现在进程使用50个页,则快表中只保存50个页的信息,一段时间后,进程使用20个页,则快表立即清除其余30个页的信息,只保留20个页的信息。这样理解对否?
如果在多处理机系统中,进程并发执行,则快表同时保存多个进程的页表项(页号与物理块号的对应信息),如果快表中多个进程的页号相同,岂不乱套?是不是多处理机系统中有多个快表?
所谓的快表,在linux下就是TLB。
当进程的页表发生变化的时候,会修改TLB的内容;当CPU上发生进程切换的时候,会将本地的TLB清空(不考虑惰性TLB的情况)。
在SMP下,每个CPU都有一个本地TLB。但是仍然会出现多个CPU使用同一个页表集的情况,这样每当某一个CPU的TLB发生变化,都会通过IPI通知其他CPU修改各自的TLB项。
二、Page 114 4.3.1 页面与页表
问题:在分页存储管理中是所有的进程的页面大小都相等,还是不同进程有不同的页面大小?
分页存储管理中是不是把所有内存都划分为大小相等的物理块?页表是不是也存储在这样的物理块中?还是页表独立分配空间?
所有进程的页大小是相同的。
分页存储将所有的物理内存划分为大小相等的物理块。页表也是存储在这些物理块中。
三、Page 119 4.3.3 两级和多级页表
2 多级页表
倒数第二段最后一句:
这样的结果显然是不能令人接受的,因此必须采用多级页表,将外层页表再进行分页,也是将各分页离散地装入到不相邻的物理块中,再利用第二级的外层页表来映射它们之间的关系。
问题:这里是不是只能使用将部分页表调入内存这种方法?
不是的。多级页表的使用会减少因为页表管理所占用的物理地址。
四、Page 159 5.3.4缓冲池
第一段的最后一句话:为了提高缓冲区的利用率,目前广泛流行公用缓冲池;在池中设置了多个可供若干个进程共享的缓冲区。
问题:是不是整个缓冲池有一个空缓冲队列emq,而对应每个进程均需要一个输入队列inq和一个输出队列outq?如果整个缓冲区仅有一个输入队列inq 和一个输出队列outq,那么,在进程共享使用时,岂不是无法确定摘取的是否是自己需要的缓冲区?另外,队列的数据结构也不允许随机摘取。
这个不知道描述的什么?难道是linux下的slab
源文档 <http://bbs.chinaunix.net/thread-3691966-3-1.html>
ARM在内存管理上,和x86的区别主要是页表构造不同。
kernel里的注释已经说的比较清楚了,解释arch/arm/include/asm/pgtable.h里的注释如下:
从硬件上说,ARM期望使用二级页表结构:
1) 第一级有4096个目录项
2) 第二级的每个表中,有256个目录项
每个目录项均为32bit。
第二级中,每个目录项指向了4096大小的page。这样,系统共:pgd的4096项 * pte的256项 * 每页的4096大小 = 4G地址空间。
ARM的问题是,第二级每个目录项的32bit中,绝大部分已经为硬件所使用,并且没有accessed与dirty位。
accessed位为内存回收的依据,确定每个页在LRU链中位置;
dirty位作为内存回写的依据,脏页面应回写至文件或交换区。
因此,linux在构造页表时,制造了一个假象:
1) 硬件PGD还是4096项,每项4字节;但是linux按照PGD共2048项,每项8字节来计算,计算得来的每项中实际都含有两个pgd项。
2) 硬件PTE还是256项,每项4字节;但是linux每次分配pte表时,都分配4K大小的页,页的前2048字节折合512个pte项,即折合两个硬件的pte表;页的后2048字节留作它用,折合为虚拟的512个pte项,即折合两个虚拟的pte表,与前半页对应。
这样,2)中前半页折合出来的两个硬件pte表,正好填入1)中的2个pgd项中。
而且:
2)中前半页折合出来的两个硬件pte表中,每项都包含一些页的属性bit,如是否present,是否可写....这些属性均为ARM硬件支持的属性,命名为PTE_xxx
2)中后半页折合出来的两个虚拟pte表中,每项都包含一些页的属性bit,如是否accessed,是否dirty....这些属性均为ARM硬件不能支持的属性,命名为L_PTE_xxx
这样,就能模拟出来诸如accessed,dirty.....这样硬件还不支持的属性。
66 | 反复读linux on arm特征 |
比如说模拟dirty:我们可以另页属性为不可写,一个写操作将引发页保护异常,进入到页异常的处理程序handle_pte_fault()后,linux将把虚拟pte项中置上L_PTE_dirty标志,同时将硬件pte项修改为可写;kswapd在操作LRU的时候,将清除掉虚拟pte项中的L_PTE_dirty位,同时也清除掉硬件pte项中的可写属性
比如说模拟access:我们可以清掉硬件pte中的present位,另映射不存在,一个读写操作将引发缺页异常,进入到页异常的处理程序handle_pte_fault()后,linux将把虚拟pte项中置上L_PTE_accessed标志,同时将置位硬件pte项的present位;kswapd在操作LRU的时候,将清除掉虚拟pte项中的L_PTE_access位,同时也清除掉硬件pte项中的present属性
注释中最后还提到,这么搞的话,应注意刷TLB,以另对硬件pte项的修改可见。
源文档 <http://bbs.chinaunix.net/thread-3691966-3-1.html>
浅析高端内存,不对的请指点
http://blog.csdn.net/woshixingaaa/article/details/6823194
http://blog.csdn.net/woshixingaaa/article/details/6826485
66使用新的页面 “浅析高端内存”
源文档 <http://bbs.chinaunix.net/thread-3691966-4-1.html>
博客文章地址:
[url]http://blog.chinaunix.net/space.php?uid=15007890&do=blog&id=3153898[/url]
[align=center] 我的内存管理理解 [/align]
Ø 80386中的寄存器
• 4个32位通用寄存器:EAX、EBX、ECX、EDX
• 4个32位地址寄存器:ESP、EBP、EDI、ESI
• 32位指令指针寄存器:EIP
• 32位标志寄存器:EFLAGS
• 6个16位段寄存器:CS(代码段)、DS(数据段)、ES(附加段)、SS(堆栈段)、FS、GS
• 4个32位控制寄存器:CR0、CR1、CR2、CR3
• 4个段表基地址寄存器:GDTR(48)、IDTR(48)、LDTR(16)、TR(16)
• 排错和测试寄存器
66 | 反复 内存 与寄存器。。 |
Ø 实模式与保护模式
• 实模式:寻址采用和8086相同的16位段和偏移量,最大寻址空间1MB,最大段64KB。可以使用32位指令。32位的x86 CPU做高速的8086.
• 保护模式:寻址采用32位段和偏移量,最大寻址空间4GB,最大分段4GB(pentium pre及以后为64GB)。在保护模式下CPU可以进入虚拟8086方式,这是在保护模式下的实模式运行环境。
66 | 基础,寻址空间大小 |
第一:实模式下程序运行回顾。
程序运行的实质是什么? 其实很简单,就是指令的执行,显然cpu是指令的一致性的硬件保障。
那么cpu如何知道指令在什么地方呢?对了,80x86系列是使用CS寄存器配合IP寄存器来通知CPU指令在内存中的位置。程序可能需要调用系统的服务子程序,80x86系列使用中断机制来实现系统服务。
总得来说,这些就是实模式下一个程序运行的主要内容(其他如跳转、返回、端口操作等相对来说比较次要。)
第二:保护模式----从道行徐运行说起
无论是实模式还是 保护模式,根本问题还是程序如何在其中运行。因此我们在学习保护模式的时候应该时刻围绕这个问题来思考。
和实模式下一样,保护模式下程序运行的实质仍是“cpu执行指令,操作相关数据”,因此是模式下的各种代码段、数据段、堆栈段、中断服务程序仍然存在,且功能和作用不变。
那么和保护模式下最大的变化是什么呢? 地址转换方式
第三: 地址转换方式的比较
先看一下实模式下地址转换方式,假设我们在ES中存入0x1000,DI中存入0xFFFF,那么ES:DI=0x1000=0x1000*0x10 + 0xFFFF,这就是众所周知的“左移4位加偏移量”。
那么如果在保护模式下呢? 假设上面的数据不变ES=0x1000,DI=0xFFFF,现在ES:DI等于什么呢?
公式如下:(注:0x1000=1000000000000b=10 0000 0000 0 00)
ES:DI=全局描述符表中第0x1000项描述符给出的段基地址+0xFFFF
现在比较一下,好像是不一样,再仔细看,又好像没什么区别!
为什么说没什么区别,因为既然ES中内容不是真正的段地址,凭什么实模式下称ES为“段寄存器”,而到了保护模式就说是“选择子”?
66 | 我的理解:保护模式,应用层的思想,加了一层。 |
其实它们都是一种映射关系,只是映射规则不同而已: 在实模式下这个地址转换方式是“左移4位”,在保护模式下是“查全局/局部描述符表”。前者是系统定义的映射方式,后者是用户自定义的转换方式,而它影响的都是“shadow register”。
66 | 实模式,保护模式的共性:都用 映射。映射规则不同而已。 |
保护模式地址转换:
从函数观点来看,前者是表达式,后者是列举式函数:
实模式: F(esàsegment)= {segment|segment = es*0x10}
保护模式: F(esàsegment)= {segment|(es,segment) ∈GDT/LDT}
66 | 除了用层的思想,用映射思想,还可以使用 函数.说 保护模式,实模式。 |
其中GDT、LDT分别表示全局描述符表和局部描述符表。
描述符表有三种,分别为GDT、LDT和IDT(中断描述符表)
1. 全局描述符表GDT:
全局描述符表在系统中只能有一个,且可以被每个任务所共享,任何描述符都可以放在GDT中,但中断门和陷阱门放在GDT中是不会起作用的。能被多个任务共享的内存区就是通过GDT完成的。
2. 局部描述符表:
局部描述符表在系统中可以有多个,通常情况下是与任务的数量保持对等,但任务可以没有局部描述符表,任务间不相干的部分也是通过LDT实现的,这里涉及到地址映射的问题,和GDT一样,中断们和陷阱门不会起作用。
66 | 基础,gdt ldt中,中断们和陷阱门不会起作用。 |
3. 中断描述符表IDT:
和GDT一样,中断描述符表在系统最多只能有一个,中断描述符表内可以存放256个描述符,分别对应256个中断,因为每个描述符占用8个字节,所以IDT的长度可达2k,中断描述符中可以有任务门、中断门、陷阱门三个描述符,其他描述符在中断描述符表中无意义。
66 | 下一次把 三个门 in idt区分开来。 |
4. 段选择子
在保护模式下,段寄存器的内容已不是段值,而成其为选择子。该选择子只是描述符在上面这三个表中的位置,所以说选择子即是索引值。
当我们把选择子装入寄存器时不仅使用该寄存器值,同时CPU将该选择子所对应的GDT或LDT中的描述符装入了不可见的部分。这样只要我们不进行代码切换(不重装入新的选择子)CPU就不会对不可见部分存储的描述符进行更新,可以直接进行访问,加快了访问速度。一旦寄存器被重新赋值,不可见部分也将被重新赋值。
66:这里说到cpu的工作,behind选择子寄存器。
为什么LDT要放在GDT中:
LDT中的描述符和GDT中的描述符除了选择子的bit3一个为0一个为1用于区分该描述符是在GDT中还是在LDT中外,描述符本身的结构完全一样。开始我考虑既然是这样,为什么要将LDT放在GDT中而不是像GDT那样找一个GDTR寄存器呢?
后来终于明白了原因——很简单,GDT表只有一个,是固定的;而LDT表每个任务就可以有一个,因此有多个,并且由于任务的个数在不断变化其数量也在不断变化。如果只有一个LDTR寄存器显然不能满足多个LDT的要求。因此INTEL的做法是把它放在放在GDT中。
GDTR全局描述符寄存器:48位,高32位存放GDT基址,低16为存放GDT限长。
LDTR局部描述符寄存器:16位,高13为存放LDT在GET中的索引值。
66: | 这里说到gdt,idt设计思想,前者只有一个,后者多个,一个任务一个
。一个固定,一个扩展的思想重现。 |
|
悟: | 如果只有一个LDTR寄存器显然不能满足多个LDT的要求。 66:干脆放到gdt.一个唯一的gdt寄存器可以满足扩展。用gdt索引到ldt |
|
66 | 小注明:下面三段pass |
IA-32处理器仍然使用xxxx:yyyyyyyy(段选择器:偏移量)逻辑方式表示一个线性地址,那么是怎么得到段的基址呢?在上面说明中我们知道,要得到段的基址首先通过段选择器xxxx中TI位指定的段描述符所在位置:
当TI=0时表示段描述符在GDT中,如下图所示:① 先从GDTR寄存器中获得GDT基址。② 然后再GDT中以段选择器高13位位置索引值得到段描述符。③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
当TI=1时表示段描述符在LDT中,如下图所示:① 还是先从GDTR寄存器中获得GDT基址。② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。④ 用段选择器高13位位置索引值从LDT段中得到段描述符。⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
第四:保护模式的基本组成
保护模式最近本的组成部分是围绕着“地址转换方式”的变化增设了相应的机构。
1. 数据段
前面说过,实模式下的各种代码段、数据段、堆栈段、中断服务程序仍然存在,我将它们统称为“数据段”,本文从此乡下提到数据段都是使用这个定义。
2. 保护模式下引入描述符来描述各种数据段,所有的描述符均为8个字节(0-7),由第5个字节说明描述符的类型,类型不同,描述符的结构也有所不同。
若干个描述符集中在一起组成描述符表,而描述符表也是一种数据段,也使用描述 符进行描述。
从现在起,“地址转换”由描述表来完成,从这个意义上说,描述符表也是一张地址转换的函数表。
3. 选择子
选折子是一个2字节的数,共16位,最低2位表示RPL,第3位表示查表是利用GDT(全局描述符表)还是LDT(局部描述符表)进行,最高13位给出了所需的描述符在描述符表中的地址。(注:13位正好足够地址8k项)
有了以上三个概念之后可以进一步工作了,现在程序的运行与实模式下完全一样!!!各段寄存器仍然给出一个“段值”,只是这个“假段值”到真正的段地址不再是“左移4位”,而是利用描述符来完成。但出现一个新的问题是:
系统如何知道GDT/LDT在内存中的位置呢?
为了解决这个问题,显然需要引入新的寄存器用于指示GDT/LDT在内存中的位置。在80x86系列中引入了两个新寄存器GDR和LDR,其中GDR用于表示GDT在内存中的段地址和段限(就是表的大小),因此GDR是一个48位的寄存器,其中32位表示段地址,16位表示段限(最大64K,每个描述符8字节,故最多有64k/8=8k个描述符)。LDR用于在内存中的位置,但是因为LDT本身也是一种数据段,它必须有一个描述符,且该描述符必须放在GDT中,因此LDR使用了DS、ES、CS等相同的机制,其中只存放一个“选择子”,通过查GDT表获得LDT的真正内存地址。
对了,还有中断要考虑,在80x86系列中为中断服务提供中断/陷阱描述符,这些描述符构成中断描述符(IDT),并引入一个48位的全地址寄存器存放IDT的内存地址,理论上IDT表同样可以有8K项,可是因为80x86只支持256个中断,因此IDT实际上最大只能有256项(2k大小)。
66 | 选择子的设计思想,反复读 |
第五:新要求---任务篇
前面介绍了保护模式的基本问题,也是核心问题,解决了上面的问题,程序就可以在保护模式下运行了。
但众所周知80286以后在保护模式下实现了对多任务的硬件支持。我的第一反应是:为什么不在实模式下支持多任务,是不能还是不愿?
思考之后,我的答案是:实模式下能实现多任务。因为多任务的关键是有了描述符,可以给出关于数据段的额外描述,如权限等,进而在这些附加信息的基础上进行相应的控制,而实模式下缺乏描述符,但假设我们规定各段的前2个字节或若干字节用于描述段的附加属性,我觉得和使用描述符这样的机制没有本质区别,如果再附加其他机制…
基于上述,我更倾向于任务任务是独立于保护模式之外的功能。下面我们来分析一下任务,任务的实质是什么呢?很简单,就是程序。所谓任务切换其实就是程序的切换!!
现在问题明朗了。实模式下程序一个接一个的运行,因此程序运行的环境不必保存;保护模式下可能一个程序在运行过程中被暂停,转而执行下一个程序,我们要做什么?很容易想到保存程序运行的环境就行了(想想游戏程序中的保存进度功能),不如各寄存器值等。
显然这些环境数据构成了一类新的数据段(即TSS)。沿用前面的思路,这些类段数据设置描述符(TSS描述符),将该类描述符放在GDT中(不能放在LDT中,因为80x86不允许),最后再加一个TR寄存器用于查表。TR是一个起“选择子”作用的寄存器,16位。
好了,任务切换的基本工作就是将原任务“环境”存入TSS数据段,更新TR寄存器,系统将自动查GDT表获得并载入新任务的“环境”,然后转到新任务执行。
66 | 反复读:为什么 不在实模式设计 任务切换,反而在 保护模式设计。设计的基本工作 |
| 任务切换的基本工作就是将原任务“环境”存入TSS数据段,更新TR寄存器,系统将自动查GDT表获得并载入新任务的“环境”,然后转到新任务执行 |
第六:附加要求---分页篇
为什么叫附加要求,因为现在任务还不能很好地工作。前面说过,任务实质上是程序,不同的程序是由不同的用户写的,所有这些程序完全可能 使用相同的地址空间,而任务的切换过程一般不会包括内存数据的刷新, 不是不可能,而是如果那样做太浪费了。因此必须引入分页机制才可能有效 地完成对多任务的支持。
分页引入的主要目标就是解决不同任务相互之间发生地址冲突的问题。 分页的实质就是实现程序内地址到物理地址的映射,这也是一个“地址转换” 机制,同样可以使用前面的方案(即类似GDT的做法):
首先建立页表这样一种数据段,在80x86中使用二级页表方案,增设一个CR3寄存器用于存放 一级页表(又称为页目录)在内存中的地址,CR3共32位,其低12位总是为 零,高20位指示页目录的内存地址,因此页目录总是按页对齐的。
66 | 鉴于内存局部性原理,我认为一级页表在长时期值是不变的,求证。 |
CR3作为 任务“环境”的一部分在任务切换时被存入TSS数据段中。 当然还得有相应的缺页中断机制及其相关寄存器CR2(页故障线性地址寄存器)。
66 | 反复读 分页 |
第七:总结
保护模式下增加了什么?
1、寄存器 GDR LDR IDR TR CR3
2、数据段 描述符表(GDT LDT) 任务数据段(TSS) 页表(页目录 二级页表)
3、机制权限检测(利用选择子/描述符/页表项的属性位)
线性地址到物理地址的映射
第八:保护模式常用的名词解释
前面内容中出现过的不再解释。
1、RPL 选择子当中的权限位确定的权限
2、CPL 特指CS中的选择子当中的权限位确定的权限
3、EPL EPL=Max(RPL,CPL),即RPL和CPL中数值较大的,或说权限等级较小的
4、DPL 描述符中的权限位确定的权限
5、PL 泛指以上4种特权级
6、任务特权 =CPL
7、I/O特权 由EFLAGS寄存器的位13、14确定的权限
8、一致代码段一种特殊的代码段,它在CPL>=DPL时允许访问
正常的代码段在CPL=DPL RPL<=DPL时才允许访问
第九:x86的控制寄存器
CR0,CR1,CR2,CR3
状态和控制寄存器组除了EFLAGS、EIP ,还有四个32位的控制寄存器,它们是CR0,CR1,CR2和CR3。
这几个寄存器中保存全局性和任务无关的机器状态。
CR0 中包含了6个预定义标志,
0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
1位是监控协处理位MP(Moniter coprocessor),它与第3位一起决定:当TS=1时操作码WAIT是否产生一个“协处理器不能使用”的出错信号。
第3位是任务转换位(Task Switch),当一个任务转换完成之后,自动将它置1。随着TS=1,就不能使用协处理器。
CR0的第2位是模拟协处理器位 EM (Emulate coprocessor),如果EM=1,则不能使用协处理器,如果EM=0,则允许使用协处理器。
第4位是微处理器的扩展类型位 ET(Processor Extension Type),其内保存着处理器扩展类型的信息,如果ET=0,则标识系统使用的是287协处理器,如果 ET=1,则表示系统使用的是387浮点协处理器。CR0的第31位是分页允许位(Paging Enable),它表示芯片上的分页部件是否允许工作。
CR1是未定义的控制寄存器,供将来的处理器使用。
CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
CR3是页目录基址寄存器,保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会。
这几个寄存器是与分页机制密切相关的,因此,在进程管理及虚拟内存管理中会涉及到这几个寄存器,读者要记住CR0、CR2及CR3这三个寄存器的内容.
66 | 基础cr3 ,cr2 保存页目录表的物理地址 最后一次出现页故障的全32位线性地址 .. |
源文档 <http://bbs.chinaunix.net/thread-3691966-4-1.html>
发表于 2012-04-01 16:53:42 |只看该作者
大家都是从内核级别,设计级别讲的,比较高深。
我讲点实际应用中:
1.32 vs 64.
如果内存超过4G,基本上还是用64位系统+64位程序了,
就算用了pae,或者64系统跑32位程序,程序占用的内存数也是有限的
2.要保证一定的内存空闲度。
linux默认会把内存用在cache和buffer上,如果有较高的io,最好保证系统有30%甚至是40%的内存空闲
3.不要依赖swap
swap虽好,但是如果有一点占用,对系统的性能也会影响很大
4.oom经常会干掉sshd
oom在实际应用中不是想想的那么好用,该杀的不杀,不该杀的杀掉了....当然这个貌似可以配置优先级,不过具体没设置过.
源文档 <http://bbs.chinaunix.net/thread-3691966-6-1.html>
回复 51# ak47mig
脏页是可控的。前些时面了个试,看了一下这方面的代码,可能有点冷门,我正好简单概括一下。
基本上,用户通过proc目录设定两个参数来决定系统对脏页的容忍程度。每个一个CPU弄脏一定数量的disk cache里的页时,就要检查一下当前脏页情况如何。检查的频率要设置成,在最极端的情况下也可以保证,两次检查脏页情况期间,被弄脏的页数量不超过系统总内存的3%。最极端的情况是,两次检查期间,所有CPU并发地弄脏页。
每一次检查并不一定会引发页回写的操作。判断是否要采取行为取决于用户设置的容忍度,但并非简单将这个值与系统脏页值做对比。
下班了,下次有机会再说吧。
66 | Adv 脏页是可控的 |
源文档 <http://bbs.chinaunix.net/thread-3691966-6-1.html>
发表于 2012-04-02 09:47:51 |只看该作者
抛开cache,每条指令执行前都要从CPU 外部SDRAM取指令(Prefetch),如果是LDR/STR/LDM/STM 等指令,这些指令执行的时候涉及到外部存储器数据的装载和保存,所有这些都需要把相关的地址送到系统总线, 再有总线控制器解码然后送到相关的外设控制器。不同的外设控制器对总线来的地址或者数据就按照自己的协议和规范来操作
MMU打开的时候:指令预取和 Load/Store指令执行的时候,地址都会送到MMU. MMU 和ARM核一样是硬件的,当然MMU事先必须配置,包括转换类型,页表,转换表基地址。把这些信息告诉MMU.MMU就可以把虚拟地址转换成物理地址,最后送到总线上,对于ARM来讲,控制外设,一定要把外设的物理地址送到总线才可以。
66 | 浅。 Cache …mmu..下一次 |
源文档 <http://bbs.chinaunix.net/thread-3691966-6-1.html>
发表于 2012-04-04 21:09:03 |只看该作者
本帖最后由 ljzbq123 于 2012-04-04 21:12 编辑
在arm系统中,modules是放置在3G-16M~3G之间,如下linux 2.6.35的定义。
Memory.h (kernel\arch\arm\include\asm)
#define TASK_SIZE (UL(CONFIG_PAGE_OFFSET) - UL(0x01000000))
#define MODULES_VADDR (PAGE_OFFSET - 16*1024*1024)
另外本人研究linux内存管理也有一段时间了,对active_list和inactive_list还有些问题没有完全弄清楚,哪位大侠可以讲讲active_list和inactive_list具体是怎么产生的?目前对 active_list和inactive_list的一些理解如下:
1. 这2个list只针对可以回收的内存。
1)文件读写操作过程中用于缓冲数据的页面
2)用户地址空间中用于文件内存映射的页面
3)匿名页面:进程用户模式下的堆栈或者是使用 mmap 匿名映射的内存区
4)特殊的用于 slab 分配器的缓存,比如用于缓存文件目录结构 dentry 的 cache,以及用于缓存索引节点 inode 的 cache)
2. 只针对应用进程,内核内存不涉及到这两个list。
3. active_list和inactive_list和buffer、cache的关系。buffer+cache作为最大可使用“准空闲”内存,而inactive_list也具有这种可转化为空闲内存的能力,2者之间肯定有一定的联系,比如一个page即链接在inactive_list中,又链接在buffer+cache中,从下面的meminfo接口中,我大概可以得出如下的一种关系:
66 | 这是论坛作者的观点:比如一个page即链接在inactive_list中,又链接在buffer+cache中,待证实 |
Inactive( 107076 kB)< buffer+cache(23864 +126840 )<Inactive( 107076 kB)< +Active(203992 kB).
上面3点是本人的一些初步理解,希望哪位大侠能够把理解不够或错误的地方指出来,同时抛砖引玉,继续深入阐释active_list和inactive_list和buffer、cache的关系。
MemTotal: 446684 kB
MemFree: 30748 kB
Buffers: 23864 kB
Cached: 126840 kB
SwapCached: 0 kB
Active: 203992 kB
Inactive: 107076 kB
Active(anon): 160556 kB
Inactive(anon): 416 kB
Active(file): 43436 kB
Inactive(file): 106660 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 4 kB
Writeback: 0 kB
AnonPages: 160360 kB
Mapped: 40612 kB
Shmem: 624 kB
Slab: 13504 kB
SReclaimable: 3432 kB
SUnreclaim: 10072 kB
KernelStack: 3728 kB
PageTables: 11380 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 223340 kB
Committed_AS: 1755684 kB
VmallocTotal: 491520 kB
VmallocUsed: 107460 kB
VmallocChunk: 367620 kB
#
源文档 <http://bbs.chinaunix.net/thread-3691966-7-1.html>
66 | 编辑好自己的责任田,别人的贴 昨天都还在更新 |
发表于 2012-04-06 22:06:52 |只看该作者
虽然说现在的内存价格越来越便宜了,但是在任何一个操作系统中,内存管理都是OS的一个主要任务,不仅仅是Linux。
因为受操作系统限制,比如32位的OS就只能支持3G左右的内存,所以就算你有N多的钱,买N多的内存,但受系统支持的限制,现在你也只能使用其中的3G内存。 所以,内存管理很重要。综合如下::
1. 内核初始化:
* 内核建立好内核页目录页表数据库,假设物理内存大小为len,则建立了[3G--3G+len]::[0--len]这样的虚地址vaddr和物理地址paddr的线性对应关系; * 内核建立一个page数组,page数组和物理页面系列完全是线性对应,page用来管理该物理页面状态,每个物理页面的虚地址保存在page->virtual中; * 内核建立好一个free_list,将没有使用的物理页面对应的page放入其中,已经使用的就不用放入了;
2. 内核模块申请内存vaddr = get_free_pages(mask,order):
* 内存管理模块从free_list找到一个page,将page->virtual作为返回值,该返回值就是对应物理页面的虚地址; * 将page从free_list中脱离; * 模块使用该虚拟地址操作对应的物理内存;
3. 内核模块使用vaddr,例如执行指令mov(eax, vaddr):
* CPU获得vaddr这个虚地址,利用建立好的页目录页表数据库,找到其对应的物理内存地址; * 将eax的内容写入vaddr对应的物理内存地址内;
4. 内核模块释放内存free_pages(vaddr,order):
* 依据vaddr找到对应的page; * 将该page加入到free_list中;
5. 用户进程申请内存vaddr = malloc(size):
* 内存管理模块从用户进程内存空间(0--3G)中找到一块还没使用的空间vm_area_struct(start--end); * 随后将其插入到task->mm->mmap链表中;
6. 用户进程写入vaddr(0-3G),例如执行指令mov(eax, vaddr):
* CPU获得vaddr这个虚地址,该虚地址应该已经由glibc库设置好了,一定在3G一下的某个区域,根据CR3寄存器指向的current->pgd查当前进程的页目录页表数据库,发现该vaddr对应的页目录表项为0,故产生异常; * 在异常处理中,发现该vaddr对应的vm_area_struct已经存在,为vaddr对应的页目录表项分配一个页表; * 随后从free_list找到一个page,将该page对应的物理页面物理首地址赋给vaddr对应的页表表项,很明显,此时的vaddr和paddr不是线性对应关系了; * 将page从free_list中脱离; * 异常处理返回; * CPU重新执行刚刚发生异常的指令mov(eax, vaddr); * CPU获得vaddr这个虚地址,根据CR3寄存器指向的current->pgd,利用建立好的页目录页表数据库,找到其对应的物理内存地址; * 将eax的内容写入vaddr对应的物理内存地址内;
7. 用户进程释放内存vaddr,free(vaddr):
* 找到该vaddr所在的vm_area_struct; * 找到vm_area_struct:start--end对应的所有页目录页表项,清空对应的所有页表项; * 释放这些页表项指向物理页面所对应的page,并将这些page加入到free_list队列中; * 有必要还会清空一些页目录表项,并释放这些页目录表项指向的页表; * 从task->mm->mmap链中删除该vm_area_struct并释放掉;
|
66 | 下一次 反复读:(代码overview)版主 作者精炼的 虚拟内存的认识!! |