linux内核源代码情景分析(第二章 存储管理)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mouse1598189/article/details/87372918

第二章 存储管理

2.1 linux内存管理的基本框架

linux中的内存管理的设计
PGD:页面目录
PMD:中间目录
PT:页面表
PTE:页面表项 page table entry
在这里插入图片描述

2.2 地址映射的全过程

段式映射

在这里插入图片描述
所有的进程都共用一个GDT。
在这里插入图片描述

  1. 四个描述段下列内容是相同的在这里插入图片描述 每个段都是从0地址开始的整个4GB虚存空间,虚地址到线性地址的映射保持原值不变
  2. 有区别的地方主要在两个地方,一个是DPL,内核为最高的0级,用户为最低的3级。另一个是段的类型,代码/数据段。这两项都是CPU在映射过程中要加以检查核对的。实际上,这里所做的检查比对在页式映射的过程中还要进行,所以既然用了页式映射,这里的检查比对就是多余的。要不是i386CPU中的MMU要做这样的检查比对,也就只要一个段描述项就够了,进一步,要不是i386CPU中的MMU规定先做段式映射然后再做页式映射,那就根本不需要段描述项和段寄存器了。所以这里的linux内核只不过装模作样的糊弄i386CPU,对付其检查比对而已。

所以linux内核设计的段式映射直接把地址映射到其自身,现在作为先行地址出现了。

页式映射

每一个进程都有其自身的页面目录PGD,指向这个目录的指针保持在每个进程的mm_struct数据结构中。每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好寄存器CR3,而MMU的硬件则总是从CR3中取得指向当前页面目录的指针。
按照线性地址的格式,CPU先从地址最高10bit为下标从页面目录中找到目录项,找到页面表之后,CPU再看中间的10bit,以此为下标从页表中找到对应的表项,然后再加上线性地址的最低12位就得到了最终的物理内存地址。

2.3 几个重要的数据结构和函数

物理内存管理
在这里插入图片描述
在这里插入图片描述
虚拟内存管理
在这里插入图片描述
find_vma
给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及相应的vma_area_struct结构。这个是由find_vma实现。

insert_vm_struct
当find_vma没有找到时,表示该地址所属的区间还未建立。此时通常就得建立一个新的虚存区间结构,再调用insert_vm_struct将其插入到mm_struct中的线性队列或者AVL树中。
在这里插入图片描述

2.4 越界访问

内存映射异常

  1. 页面目录或者页面表项为空,表示线性地址和物理地址的映射关系尚未建立,或者已经撤销
  2. 相应的物理页面不在内存中
  3. 指令规定的访问方式和页面的权限不符

页错误处理函数(do_page_fault)

asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)

do_page_fault传递两个参数,一个是pt_regs结构指针regs,指向例外发生前CPU中各个寄存器的一份副本,这是由内核的中断服务响应机制保存下来的现场。第二个是error_code,指明映射失败的具体原因。
具体处理过程:

  • 当i386CPU产生页面错的异常时,CPU将导致映射失败的线性地址放在控制寄存器CR2中
  • 获取当前进程的task_struct结构 tsk = current;
  • 检测两种特殊情况,一个是in_interupt,说明失败发生在某个中断服务程序。另一个是当前进程的mm指针为空,也就是说该进程的映射尚未建立。这两种情况都和当前进程无关,直接goto到no_context标号去处理
  • 在知道了发生映射失败的地址以及所属的进程以后,接下来就是搞清楚的是这个地址是否落在某个已经建立起映射的区间,或者进一步具体指出在哪个区间。使用find_vma试图在一个虚存空间中找到结束地址大于给定地址的第一个区间。如果没有一个区间的结束地址高于给定地址,那就是说明这个地址在堆栈之上,属于系统空间,从用户空间访问系统空间,当然后越界了。转到bad_area进行核查。
  • 如果找到一个区间,其起始地址不高于给定的地址,那就说明给定的地址恰好落在这个区间。说明映射已经建立,转到good_area进行核查。
  • 除了上面两种情况,剩下的就是给定地址落在这个两个区间之间的空洞里,也就是该地址所在的页面尚未建立或者已经撤销。
  • 当error_code的bit2为1时,表示失败是当CPU处于用户模式时发生的。对当前进程的task_struct结构内的一些成分进行一些设置后,然后向该进程发出一个强制的SIGSEGV信号,本次例外服务也就结束了。
    在这里插入图片描述

2.5 用户堆栈的扩展

在这里插入图片描述
过程

  1. 首先判断是否是堆栈扩展的情况 +32字节内,如果属于正常的堆栈扩展要求,就应该从空洞的顶部开始分配若干页面建立映射,并将之并入堆栈区间,使其得以扩展。
  2. 下面调用expand_stack,expand_stack只是改变了堆栈区的vm_area_struct结构,而并未建立起新扩展的页面对物理内存的映射,下面的任务由good_area来完成。
  3. 在switch语句中对error_code进一步进行确定映射失败的原因并采取相应的对策。然调用虚存管理handle_mm_fault函数
  4. 由handle_mm_fault计算出来pgd、pmd、pte。如果页面目录中对应的目录项如果已经指向一个页面表,则直接返回,如果页面目录是空的,则需要先分配一个页面表。这些工作由pte_alloc来完成。
  5. 分配到一个页面表之后,通过set_pmd将其起始地址连同一些属性标志位一起写入到中间目录项pmd中,这样映射所需的基础设施已经齐全了,但是页面表项pte还是空的,剩下的就是物理内存页面本身了。由handle_pre_fault来完成。
  6. 由于pte_present pte_none测试都通过,一定会进入do_no_page。
  7. do_no_page中,如果vma->vm_ops->nopage不为空,就执行这个函数指针指向的函数,用来分配内存页面。如果不存在,就调用do_anonymous_page
  8. do_anonymous_page对于读页面使用pte_wrprotect修正,写页面首先使用alloc_page分配独立的内存,并将分配的内存连同所有状态和标志位一起通过set_pte设置进指针page_table所指的页面表项。至此从虚存页面到物理页面的映射终于建立起来了。
  9. 然后各个函数逐层返回,CPU把之前异常的执行重新执行即可。这些过程对于用户来说是透明的,像是什么都没有发生一样。

在这里插入图片描述

2.6 物理页面的使用和周转

页面的换出和内存页面的释放分为两步来做。当系统挑选出若干内存页面准备换出时,将这些页面的内容写入相应的磁盘页面,并且将相应的页面表项的内容改为指向盘上页面,但是所占据的内存页面却并不立即释放,而是将其page结构留在一个暂存队列中,只是使其从活跃状态转入了不活跃状态,就像军人从现役转入了预备役。至于内存页面的退役,即最后的释放,则推迟到以后有条件地进行。这样,如果在一个页面被换出以后立即又受到访问而发生缺页异常,就可以从物理页面的暂存队列中找回对应的页面,再次为之建立映射。由于此页面尚未释放,还保留着其原来的内容,就不需要从盘上读入了。反之,如果经过一段时间以后,一个不活跃的内存页面,即还留在暂存队列却已不再有映射的页面,还是没有受到访问,那就到了最后退役的时候了。如果留在暂存队中的页面又受到访问,那么只要恢复这个页面的映射并使其脱离暂存队列就可以了,此时该页面又回到了活跃状态。
这种策略显然可以减少抖动的可能,并且减少系统在页面交换的花费。但是还有改进。首先,在准备换出一个页面时并不一定要把它的内容写入到磁盘。分为脏页面和干净页面。

物理内存页面的换入换出的周转要点如下:

  1. 空闲。页面的page数据结构通过其队列头结构list链入某个页面管理区zone的空闲区队列free_area。页面的使用计数count为0.
  2. 分配。通过函数__alloc_page()或者__get_free_page()从某个空闲队列中分配内存页面,并将所分配页面的使用计数count置为1,其page数据结构的队列头list结构则变成空闲。
  3. 活跃状态。页面的page数据结构通过其队列头结构lru链入活跃页面队列active_list,并且至少有一个进程的页面表项指向该页面。每当为页面建立或者恢复映射时,都使页面的使用计数count加1.
  4. 不活跃状态(脏)。页面的page数据结构通过其队列头结构lru链入不活跃脏页面队列Inactive_dirty_list,但是原则上不再有任何进程的页面表项指向该页面。每当断开页面的映射时都使页面的使用计数count减1.
  5. 将不活跃脏页面的内容写入交换设备,并将页面的page数据结构爱从不活跃脏页面队列Inactive_dirty_list转移到某个不活跃干净页面队列中。
  6. 不活跃状态(干净)。页面的page数据结构通过其队列头结构lru链入某个不活跃ganjing页面队列,每个页面管理区都有一个不活跃干净页面队列inactive_clean_list.
  7. 如果在转入不活跃状态以后的一段时间内页面受到访问,则又转入活跃状态并恢复映射。
  8. 当有需要时,就从干净页面队列中回收页面,或者退回到空闲队列中,或者直接另行分配。

下面是内核怎样将一个内存页面链入到队列中的:
在这里插入图片描述
在这里插入图片描述

2.7 物理页面的分配

在这里插入图片描述
具体的页面分配由函数__alloc_page()完成。

2.8 页面的定期换出

在这里插入图片描述
linux内核设置了一个专司定期将页面换出的守护神 kswapd。
kswapd和其他进程一样,受内核的调度,这样就可以让它在系统相对空闲的时候来运行。kswapd也有自己的特殊性:

  1. 它没有独立的地址空间,它使用内核的地址空间
  2. 它的代码是静态地连接到内核中的可以直接调用内核的各种子程序。

kswapd至少每秒执行一次,主要工作可以分为两个部分,第一部分是在发现物理页面已经短缺的情况下才进行的,目的在于预先找出若干页面,且将这些页面的映射断开,是这些物理页面从活跃的状态转入不活跃状态,为页面的换出做好准备。第二部分是每次都要执行的,目的把已经处于不活跃状态的脏页面写入交换设备,使他们成为不活跃干净页面继续缓冲,或进一步回收一些这样的页面成为空闲页面。

kreclaimd和kswapd程序结构类似,kreclaimd通过reclaim_page扫描各个页面管理区中的不活跃干净的页面队列,从中回收页面加以释放。

2.9 页面的换入

在这里插入图片描述

2.10 内核缓冲区的管理

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.11 外部设备存储空间的地址映射

在这里插入图片描述

2.12 系统调用brk()

在这里插入图片描述

2.13 系统调用mmap()

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/mouse1598189/article/details/87372918