导图社区 linux内核-内存管理
基于内核源码剖析内存管理,深入理解linux内核,了解这些概念和机制有助于更好地理解Linux内核的内存管理系统以及如何有效地管理和优化Linux系统的内存使用。
编辑于2024-05-27 12:07:07这是一篇关于程序和库信息的思维导图,主要内容包括:(查看基础信息),获取ELF节的长度信息,显示可执行文件或库需要静态加载的动态库完整列表--显示加载时的依赖项,列出二进制文件的节信息,查看动态节,列出并查看段,查看重定位节,反汇编,列出库中未定义的符号,列出动态符号,列出二进制文件或库的符号表,查看节中的数据,符号的类型。
这是一篇关于设备驱动的思维导图,主要内容包括:主要功能,设备驱动模型。阐述了设备驱动的主要功能、信号定义、设备驱动模型等内容。
这是一篇关于算法的思维导图,主要内容包括:云计算,内存管理算法,分布式同步算法,避免死锁算法,进程调度算法,磁盘调度算法。
社区模板帮助中心,点此进入>>
这是一篇关于程序和库信息的思维导图,主要内容包括:(查看基础信息),获取ELF节的长度信息,显示可执行文件或库需要静态加载的动态库完整列表--显示加载时的依赖项,列出二进制文件的节信息,查看动态节,列出并查看段,查看重定位节,反汇编,列出库中未定义的符号,列出动态符号,列出二进制文件或库的符号表,查看节中的数据,符号的类型。
这是一篇关于设备驱动的思维导图,主要内容包括:主要功能,设备驱动模型。阐述了设备驱动的主要功能、信号定义、设备驱动模型等内容。
这是一篇关于算法的思维导图,主要内容包括:云计算,内存管理算法,分布式同步算法,避免死锁算法,进程调度算法,磁盘调度算法。
机制
缓存机制
磁盘高速缓存
内核在读写磁盘时都引用页高速缓存 新页被追加到页高速缓存以满足用户态进程的读请求 如果页不在高速缓存中 新页就被加到高速缓存 然后用从磁盘读出的数据填充它 如果内存有足够的空闲空间 就让该页在高速缓存中长期保留 使其他进程再使用该页时不再访问磁盘
页高速缓存-address_space
含有普通文件数据的页
含有目录的页
含有直接从块设备文件读出的数据的页
含有用户态进程数据的页(但页中的数据已经被交换到磁盘)
属于特殊文件系统文件的页
缓冲区高速缓存-buffer_head
内存回收机制
特权级保护机制
描述符特权级DPL
当前特权级CPL
代码段选择子的RPL一经装入CS 就是CPL
请求特权级RPL
页加载&分配机制
(缺页)需求加载-execve
在execve执行过程中 系统会清掉fork复制的原程序的页目录和页表项 并释放对应页面 系统仅为新加载的程序代码重新设置进程数据结构中的信息 申请和映射了命令行参数和环境参数块所占的内存页面 以及设置了执行代码的执行点(此时内核并不从执行文件所在块设备上加载程序的代码和数据) 当该过程返回时即开始执行新程序 开始执行就一定会引起缺页异常的中断发生(代码和数据还未被从设备读入内存) 此时缺页异常处理程序会根据引起异常的线性地址在主内存区为新程序申请内存页面(内存帧) 并从块设备上读入引起异常的指定页面 同时还为该线性地址设置对应的页目录项和页表项 这种加载执行文件的方法叫做需求加载 根据程序局部性原理 进程虚拟地址空间的对应一部分物理内存由其他程序暂用 将内存页面的分配一直推迟到它要访问的页不在物理内存位置 由此引起一个缺页错误
(写保护)写时拷贝-fork
父进程和子进程共享页面而不是复制页面 只要页面被共享,它们就不能被修改 无论父进程和子进程何时试图写一个共享的页面,就产生一个错误 这时内核就把这个页复制到一个新的页面中并标记为可写 原来的页面仍然是写保护的:当其他进程试图写入时 内核检查写进程是否是这个页面的唯一属主 如果是,它把这个页面标记为对这个进程是可写的 进行如下复制: 为子进程的页表分配页面 为子进程的页分配页面 初始化子进程的页表 把父进程的页复制到子进程相应的页中
页交换机制
策略
需要时才交换
每当缺页异常发生时,就给它分配一个物理页面 如果发现没有空闲的页面可供分配 就设法将一个或多个内存页面换出到磁盘上 从而腾出一些内存页面来 缺点 这是一种被动的交换策略,需要时才交换 系统势必要付出相当多的时间进行换入换出
系统空闲时交换
在系统空闲时 预先换出一些页面而腾出一些内存页面 从而在内存中维持一定的空闲页面供应量 使得在缺页中断发生时总有空闲页面可供使用 换出页面的选择一般都采用 LRU(最近最少使用)算法 缺点: 没有哪种方法能准确地预测对页面的访问 所以,完全可能发生这样的情况 即一个好久没有受到访问的页面刚刚被换出去,却又要访问它了 于是又把它换进来 在最坏的情况下,有可能整个系统的处理能力都被这样的换入/换出所影响 而根本不能进行有效的计算和操作(页面的抖动)
换出但并不立即释放
当系统挑选出若干页面进行换出时,将相应的页面写入磁盘交换区中 并修改相应页表中页表项的内容(把 present 标志位置为 0) 但是并不立即释放,而是将其 page 结构留在一个缓冲(Cache)队列中 使其从活跃(Active)状态转为不活跃(Inactive)状态 如果一个页面在释放后又立即受到访问 就可以从物理页面的缓冲队列中找到相应的页面 再次为之建立映射 由于此页面尚未释放 还保留着原来的内容 就不需要磁盘读入了
把页面换出推迟到不能再推迟为止
首先在换出页面时不一定要把它的内容写入磁盘 如果一个页面自从最近一次换入后并没有被写过 那么这个页面是干净的就没有必要把它写入磁盘 其次,即使脏页面,也没有必要立即写出去 可以采用策略三 至于干净页面 可以一直缓冲到必要时才加以回收 因为回收一个干净页面花费的代价很小
守护进程 kswapd
页管理机制
页面管理
释放页面
如果一个页面变为空闲可用 就把该页面的 page 结构链入某个页面管理区(Zone)的空闲队列 free_area 同时页面的使用计数 count 减 1
分配页面
调用__alloc_pages()或__get_free_page()从某个空闲队列分配内存页面 并将其页面的使用计数 count 置为 1
页面状态
活跃状态
已分配的页面处于活跃状态 该页面的数据结构 page 通过其队列头结构 lru 链入活跃页面队列 active_list 并且在进程地址空间中至少有一个页与该页面之间建立了映射关系
不活跃-脏状态
处于该状态的页面其 page 结构通过其队列头结构 lru 链入不活跃脏页面队列 inactive_dirty_list 并且原则是任何进程的页面表项不再指向该页面(断开页面的映射 同时把页面的使用计数 count 减 1) 将不活跃脏页面的内容写入交换区 并将该页面的 page 结构从不活跃脏页面队列 inactive_dirty_list 转移到不活跃干净页面队列 准备被回收
不活跃-干净状态
页面 page 结构通过其队列头结构 lru 链入某个不活跃干净页面队列 每个页面管理区都有个不活跃干净页面队列 inactive_clean_list 如果在转入不活跃状态以后的一段时间内,页面又受到访问 则又转入活跃状态并恢复映射 当需要时 就从干净页面队列中回收页面(或者把页面链入到空闲队列 或者直接进行分配)
刷新机制
保持 TLB 和其他缓存中的内容的同步性
作用
保证在任何时刻内存管理硬件所看到的进程的内核映射和内核页表一致
如果负责内存管理的内核代码对用户进程页面进行了修改, 那么用户的进程在被允许继续执行前, 要求必须在缓存中看到正确的数据
接口
flush_cache_xxx
在地址空间改变前必须刷新MMU的缓存,防止缓存中存在非法的空映射 flush_cache_xxx把MMU缓存中的映射变成无效
flush_cache_all
通知相应机制,内核地址空间的映射已被改变,它意味着所有的进程都被改变了
flush_cache_mm
通知系统被 mm_struct 结构所描述的地址空间正在改变 仅发生在用户空间的地址改变时
flush_cache_range
刷新用户空间中的指定范围
flush_cache_page
刷新一页面
flush_tlb_xxx
刷新 TBL以便硬件可以把新的页表信息装入 TLB 通过对 CR3 寄存的重新装入,完成对 TLB 的刷新
flush_tlb_all
通知相应机制,内核地址空间的映射已被改变,它意味着所有的进程都被改变了
flush_tlb_mm
通知系统被 mm_struct 结构所描述的地址空间正在改变 仅发生在用户空间的地址改变时
flush_tlb_range
刷新用户空间中的指定范围
flush_tlb_page
刷新一页面
flush_page_to_ram
一般用在写时复制,它会使虚拟缓存中的对应项无效 这是因为如果虚拟缓存不可以自动地回写,于是会造成虚拟缓存中页面和主存中的内容不一致
映射机制
vmalloc的页面映射
4种映射长度
段-1MB
大页-64KB
小页-4KB
极小页-1KB
接口
vmap
用于相对长时间的映射,可同时映射多个pages
vunmap
kmap/kmap_atomic
用于相对短时间的映射,只能映射单个page
ioremap
将给定的物理地址映射为虚拟地址 注意此处的物理地址并不是真正内存的物理地址,而是cpu上的io memory
高端内存映射
固定映射空间
固定映射空间是内核线性地址空间中的一组保留虚拟页面空间 位于内核线性地址的末尾(最高地址部分) 其地址编译时确定用于特定用途 由枚举类型fixed_address决定 固定映射空间位于FIXADDR_START~FIXADDR_TOP之间 有一部分用于高端内存的临时映射
临时映射空间
【特点】: 每个CPU占用一块空间 可以用在中断处理函数和可延迟函数的内部 从不阻塞 禁止内核抢占 再每个CPU占用的那块空间中 又分为多个小空间 每个小空间大小1页 每个小空间用于一个目的(其目的定义在km_type中) 【接口函数】: kmap_atomic kunmap_atomic 使用从FIX_KMAP_BEGIN~FIX_KMAP_END之间的物理页
长久映射空间
长久映射空间是预留的线性地址空间 【使用方式】 先通过alloc_page获得高端内存对应的page 然后内核从专门为此留出的线性地址空间分配虚拟地址(从PKMAP_BASE~FIXADDR_START) 【接口】: kmap kunmap 在高低内存都能使用 可以睡眠
非连续映射地址空间
[适用情景]: 非连续映射地址空间适用于不频繁申请释放内存的情况(不会频繁的修改内核页表) 1.映射设备的IO空间 2.为内核模块分配空间 3.为交换分区分配空间 【接口】: vmalloc 主要目的为将零散的 不连续的页框拼凑成连续的内核逻辑地址空间
设备内存映射
ioremap
用于分配IO映射空间 将IO的寄存器和ram空间映射到内核空间 使CPU可以直接访问对应的线性地址
remap_page_range
在使用ioremap之后 还可以通过remap_page_range将设备内存ram或rom直接映射到用户空间中 这样用户端就可以直接操作设备寄存器
内存分配机制
分配算法
资源图分配算法(allocator)
2的幂次方空闲链表算法
McKusick-Karels分配算法
伙伴系统(Buddy)
Mach的区域Zone分配算法
Dynix分配算法
Solaris的Slab分配算法
8086保护模式
系统寄存器
标志寄存器 EFALGS
控制着 I/O、可屏蔽中断、调试、任务切换以及保护模式和多任务环境下虚拟 8086 程序的执行
VM
虚拟 8086 模式标志 置位时就开启了虚拟8086模式 复位时 就回到保护模式
RF
恢复标志 用于控制处理器对断点指令的响应 当设置时 RF的主要功能是允许在调试异常之后重新执行一条指令 当调试软件使用IRETD指令返回被中断程序之前 需要设置堆栈上EFLAGS的RF标志 以防止指令断点造成另一个异常 处理器会在指令返回之后自动清除该标志 从而再次允许指令断点异常
NT
嵌套任务标志 控制着被中断任务和调用任务之间的链接关系 在使用CALL指令 中断或异常执行任务调用时 处理器会设置该标志 通过使用IRET指令返回时 处理器会检查并修改这个NT标志 使用POPF/POPFD指令也可以修改这个标志 注意: 运行于任何特权级上的程序都可以修改NT标志(任何程序都可以设置NT标志并执行IRET指令) 这会让处理器去执行当前任务TSS的前一任务链接字段指定的任务
IO PL
I/O 特权级标志 指明了当前运行程序或任务的IO特权级IOPL 当前任务或程序的CPL必须小于或等于这个IOPL才能访问IO地址空间 只有当CPL为0时 程序才可以使用POPF或IRET指令修改这个字段 (IOPL也是控制对IF修改的机制之一) 当进程在用户态下执行in/out指令时 控制单元执行检查流程: 1.检查eflags寄存器中的IOPL字段 如果该字段值为3 控制单元就执行IO指令 否则执行下一个检查 2.访问tr寄存器以确定当前的TSS和相应的IO许可权位图 3.检查IO指令中指定的IO端口在IO许可权位图中对应的位 如果该位为0 就执行这条IO指令 否则控制单元就产生一个一般保护异常
OF
溢出标志
DF
方向标志
IF
中断允许标志
TF
跟踪标志 当设置该位时 可为调试操作启动单步执行方式 复位时 禁止单步执行 单步执行下 处理器会在每个指令执行之后产生一个调试异常 这样就可以观察执行程序在执行每条指令之后的状态
SF
负号
ZF
零标志
AF
辅助进位
PF
奇偶标志
CF
进位标志
内存管理寄存器
用于分段内存管理
GDTR
全局描述符表寄存器 指向段描述符表 GDT 这个寄存器以线性地址的方式保存了描述符表的基地址和表的长度 指令 lgdt 和 sgdt 用于访问 GDTR 寄存器
LDTR
局部描述符表寄存器 指向段描述符表 LDT (包含LDT表的段必须在GDT表中有一个段描述符项) 当进行任务切换时 处理器会把新任务的LDT的段选择符和段描述符自动加载进LDTR中
IDTR
中断描述符表寄存器 指向中断向量表 这个寄存器以线性地址的方式保存了描述符表的基地址和表的长度 指令 lldt 和 sldt用于访问 LDTR 寄存器 lgdt 使用的是内存中一个 6 字节操作数来加载 GDTR 寄存器的 头两个字节代表描述符表的长度,后 4 个字节是描述符表的基地址 访问 LDTR 寄存器的指令 lldt 所使用的操作数却是一个 2 字节的操作数 表示全局描述符表 GDT 中一个描述符项的选择符 该选择符所对应的GDT 表中的描述符项应该对应一个局部描述符表
TR
任务寄存器 (引用GDT表中的一个TSS类型的描述符) 指向处理器所需的当前任务的信息TSS来确定当前执行的任务 它也有 16 位的可见部分和不可见部分 可见部分中的选择符用于在 GDT 表中选择一个 TSS 描述符 处理器使用不可见部分来存放描述符中的基地址和段限长值 指令 LTR 和 STR 用于修改和读取任务寄存器中的可见部分,指令所使用的操作数是一 16 位的选择符 在以下 4 种情况下, CPU 会切换执行的任务: 1. 当前任务执行了一条引用 TSS 描述符的 JMP 或 CALL 指令 2. 当前任务执行了一条引用任务门的 JMP 或 CALL 指令 3. 引用了中断描述符表( IDT)中任务门的中断或异常 4. 当嵌套任务标志 NT 置位时,当前任务执行了一个 IRET 指令 执行任务切换时 处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中
控制寄存器
CR0
PE
保护模式开启位 (仅开启段级保护) 如果设置了该比特位,就会使处理器开始在保护模式下运行 复位时即进入实地址模式
MP
协处理器存在标志 用于控制 WAIT 指令的功能,以配合协处理的运行 MP=1 TS=1那么执行WAIT指令将产生设备不存在异常 MP=0 则TS标志不影响WAIT的执行
EM
仿真控制 指示是否需要仿真协处理器的功能 置位时 表示没有协处理器 执行协处理器指令时会引起设备不存在异常 清零时 表示系统有协处理器 设置这个标志可以迫使所有浮点指令用软件来模拟
TS
任务切换标志 用于推迟保存任务切换时的协处理器内容直到新任务开始实际执行协处理器指令 每当任务切换时处理器就会设置该比特位,并且在解释协处理器指令之前测试该位 在任务切换时 处理器并不自动保存协处理器的上下文 而是会设置TS标志 这个标志会使得处理器在执行新任务指令流的任何时候遇到一条协处理器指令时产生设备不存在异常 设备不存在异常处理程序就可以使用CLTS指令清除TS标志 并且保存协处理器的上下文 如果任务没有使用过协处理器 则相应的协处理器上下文就不用保存 如果设置了TS标志并且EM=0 MP=0 则在执行任何协处理器指令之前会产生一个设备不存在异常 如果设置了TS标志且MP=1 EM=0 则在执行协处理器WAIT/FWAIT之前不会产生设备不存在异常 如果设置了EM标志 则TS标志对协处理器指令的执行无影响
ET
扩展类型 该位指出了系统中所含有的协处理器类型 置位时 表明系统中有80387协处理器 并使用32位协处理器协议 复位时 指明使用80287协处理器 注意:如果仿真位EN=1 则该标志将被忽略
PG
分页操作 该位指示出是否使用页表将线性地址变换成物理地址 设置该位时即开启了分页机制 当复位时则禁止分页机制 (此时所有线性地址等同于物理地址) 在开启这个标志之前必须已经开启PE标志 (若要启用分页机制 那么PE和PG标志都要置位)
CR1
CR2
CR2用于出现页异常时报告出错信息 在报告页异常时 处理器会把引起异常的线性地址存放在CR2中 操作系统中的页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常
CR3
含有存放页目录表页面的物理地址 被称为PDBR 当任务切换时 CR3的内容也会随之改变
调试寄存器
测试寄存器
相关系统指令
LLDT
加载局部描述符表寄存器LDTR 从内存加载LDT段选择符和段描述符到LDTR寄存器中
SLDT
保存局部描述符寄存器LDTR 把LDTR中LDT段选择符保存到内存或通用寄存器中
LGDT
加载全局描述表寄存器GDTR 把GDT表的基地址和长度从内存加载到GDTR中
SGDT
保存全局描述符表寄存器GDTR 把GDTR中IDT表的基地址和长度保存到内存中
LTR
加载任务寄存器TR 只能被特权级0的程序执行 把TSS段选择符加载到任务寄存器中 通常用于系统初始化期间给TR寄存器加载初值以及 随后系统运行期间 TR的内容会在任务切换时自动被改变
STR
保存任务寄存器TR 把TR中当前任务TSS段选择符保存到内存或通用寄存器中
LIDT
加载中断描述符表IDTR 把IDT表的基地址和长度从内存加载到IDTR中
SIDT
保存中断描述符表寄存IDTR 把IDTR中IDT表的基地址和长度保存到内存中
Mov CRn
加载和保存控制寄存器CR0 CR1 CR2或CR3
LMSW
加载机器状态字(对应CR0寄存器位15-0)
SMSW
保存机器状态字
CLTS
清除CR0中的任务已切换标志TS 用于协处理器不存在异常
LSL
加载段限长
HLT
停止处理器执行
内存寻址机制
地址概念
虚拟地址
指由程序产生的由段选择符和段内偏移地址2部分组成的地址 因为这2部分组成的地址并没有直接用来访问物理内存 而是需要通过分段地址变换机制处理或映射之后才对应到物理内存地址上 因此这种地址被称为虚拟地址 虚拟地址空间由GDT映射的全局地址空间和由LDT映射的局部地址空间组成
逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分 在保护模式下即程序执行代码段限长内的偏移地址 应用程序员仅与逻辑地址打交道 分段和分页机制对他们是完全透明的
线性地址
虚拟地址到物理地址变换之间的中间层 是处理器可寻址内存空间中的地址 程序代码会产生逻辑地址(段中的偏移地址) 加上相应段的基地址就生成了一个线性地址 若启用了分页机制 那么线性地址再经变换以产生一个物理地址 若没有启用分页机制 那么线性地址就是物理地址
物理地址
指出现在CPU外部地址总线上的寻址物理内存的地址信号 是地址变换的最终结果地址 若启用了分页机制 那么线性地址会使用页目录和页表中的项变换成物理地址 若没有启用分页机制 那么线性地址就是物理地址
段变换
将一个由段选择符和段内偏移构成的逻辑地址转换为一个线性地址
段描述符
段描述符向 CPU 提供了将逻辑地址映射为线性地址所必要的信息 描述符是由程序编译器、链接器、加载器或操作系统创建的
字段说明
基地址Base
定义段在 4GB 线性空间中的位置 处理器会将基地址的三个部分组合成一个 32 位的值
颗粒度G
指定了限长字段值代表的单元含义 当为 0 时,限长单元值为 1 字节 当该位为 1时,限长的单元值为 4KB 字节
段限长Limit
定义了段的最大长度 处理器将组合段限长的两个部分形成一个 20 位的值 处理器会依据颗粒度( Granularity)位字段的值来解释段限长域的实际含义: 1. 当以 1 字节为单元时,则定义了最高可为 1MB 字节的长度 2. 当以 4KB 字节为单元时,则定义了最高可为 4GB 字节的长度 在加载时限长值将左移 12 位
段存在位P
如果该位为零,则该描述符无效,不能用于地址变换过程 当指向该描述符的选择符被加载到段寄存器中时,处理器就会发出一个异常信号
描述符特权级DPL
用于保护机制。共有 4 级: 0–3 0 级是最高特权级 3级是最低特权级
系统标志S
如果它为0 则这是一个系统段(存储如LDT这种结构) 如果为1 则它是一个普通的代码段或数据段
用于程序代码段和数据段的描述符
用于特殊系统段的描述符
类型TYPE
用于区分各种不同类型的描述符
访问位A
当处理器访问过该段时就会设置该比特位
描述符表
描述符表是由 8 字节构成的描述符项的内存中的一个数组 描述符表的长度是可变的,最多可以含有 8192( 213)个描述符 但是对于 GDT 表,其第一个描述符(索引 0)是不用的
全局描述符表
局部描述符表
选择符
逻辑地址的选择符部分是用于指定一描述符的 它是通过指定一描述符表并且索引其中的一个描述符项完成的 由于 GDT 表的第一项(索引值为 0)没有被使用 因此一个具有索引值 0 和表指示器值也为 0 的选择符 (也即指向 GDT 的第一项的选择符)可以用作为一个空(null)选择符 当一个段寄存器(不能是 CS 或 SS)加载了一个空选择符时,处理器并不会产生一个异常 但是若使用这个段寄存器访问内存时就会产生一个异常 对于初始化还未使用的段寄存器以陷入意外的引用来说,这个特性是很有用的
索引值
用于选择指定描述符表中 8192 个描述符中的一个 处理器将该索引值乘上 8(描述符的字节长度) 并加上描述符表的基地址即可访问表中指定的段描述符
表指示器TI
指定选择符所引用的描述符表 值为 0 表示指定 GDT 表 值为 1 表示指定当前的 LDT 表
RPL
请求者的特权级 用于保护机制
段寄存器
处理器将描述符中的信息保存在段寄存器中,因而可以避免在每次访问内存时查询描述符表 每个段寄存器都有一个“可见”部分和一个“不可见”部分 这些段地址寄存器的可见部分是由程序来操作的,就好象它们只是简单的 16 位寄存器 不可见部分则是由处理器来处理的
直接加载指令
MOV, POP, LDS, LSS, LGS, LFS 这些指令显式地引用了指定的段寄存器
隐式加载指令
远调用 CALL 和远跳转 JMP 这些指令隐式地引用了 CS 段寄存器,并用新值加载到 CS 中 程序使用这些指令会把 16 位的选择符加载到段寄存器的可见部分 处理器则会自动地从描述符表中将一个描述符的基地址、 段限长、类型以及其它信息加载到段寄存器中的不可见部分中去
页变换
将线性地址转换为对应的物理地址 在分页机制开启时 段转换和页转换组合在一起,即实现了从逻辑地址到物理地址的两个转换阶段 地址变换的这个阶段实现了基于分页的虚拟内存系统和分页级保护的基本功能 页变换仅在设置了 CR0 的 PG 比特位后才起作用,该比特位是在软件初始化时由操作系统设置的 如果操作系统需要实现多个虚拟 8086 任务、基于分页的保护机制或基于分页的虚拟内存,那么就一定要设置该位
页框
页框是一个物理内存地址连续的 4K 字节单元 它以字节为边界,大小固定
线性地址
线性地址通过指定一个页表、 页表中的某一页以及该页中的偏移值, 从而间接地指向对应的物理地址
线性地址转物理地址
通过使用两级页表,处理器将一个线性地址的页目录字段(DIR)、 页字段(PAGE)和偏移字段(OFFSET)翻译成对应的物理地址 寻址机制使用线性地址的页目录字段作为页目录中的索引值、 使用页表字段作为页目录所指定页表中的索引值、 使用偏移字段作为页表所确定的内存页中的字节偏移值
页表
页表只是一个简单的 32 位页指示器的数组 它含有 4K 字节的内存,可容纳 1K 个 32 位的项 最高层是页目录,页目录可定位最多 1K 个第二级页表 而每个二级页表可以定位最多 1K 内存页 当前页目录的物理地址是存储在 CPU 控制寄存器 CR3 中的 因此该寄存器也被称为页目录基地址寄存器 内存管理软件可以选择对所有的任务只使用一个页目录 或每个任务使用一个页目录,也可以组合两个任务使用一个页目录
页表项
页框地址
指定了一页内存的物理起始地址 因为内存页是位于 4K 边界上的,所以其低 12 比特总是 0 在一个页目录中,页表项的页框地址是一个页表的起始地址 在第二级页表中,页表项的页框地址是包含期望内存操作的页框的地址
存在位P
确定了一个页表项是否可以用于地址转换过程P=1 表示该项可用 当目录表项或第二级表项的 P=0 时,则该表项时无效的,不能用于地址转换过程 此时该表项的其它所有比特位都可供程序使用 处理器不对这些位进行测试 当 CPU 试图使用一个页表项进行地址转换时,如果此时任意一级页表项的 P=0 则处理器就会发出页异常信号 对于支持分页虚拟内存的软件系统中 页不存在异常处理程序就可以把所请求的页加入到物理内存中 此时导致异常的指令就可以被重新执行
已访问位A
在对一页内存进行读或写操作之前,处理器将设置相关的目录和二级页表项的已访问位 除了页目录项中的已修改位,这些比特位将由硬件置位,但不复位
已修改位D
在向一个二级页表项所涵盖的地址进行写操作之前,处理器将设置该二级页表项的已修改位 而页目录项中的已修改位是不用的 除了页目录项中的已修改位,这些比特位将由硬件置位,但不复位
读/写位R/W
用于分页级的保护机制,是由处理器在地址转换过程中同时操作的
用户/超级用户位U/S
用于分页级的保护机制,是由处理器在地址转换过程中同时操作的
页转换高速缓冲
为了最大地提高地址转换的效率,处理器将最近所使用的页表数据存放在芯片上的高速缓冲中 操作系统设计人员必须在当前页表改变时刷新高速缓冲,可使用以下两种方式之一: 1. 通过使用 MOV 指令重新加载 CR3 页目录基址寄存器 2. 通过执行一个任务切换
保护模式VS实模式
任务
任务的状态和结构
一个任务由{任务执行空间和TSS}组成 任务执行空间包括{代码段 堆栈段 一个或多个数据段} 如果操作系统使用了处理器的特权级保护机制 那么任务执行空间就需要为每个特权级提供一个独立的堆栈空间 TSS指定了构成任务执行空间的各个段 并且为任务状态信息提供存储空间 在多任务环境中 TSS也为任务之间的链接提供处理方法
任务状态段TSS
处理器管理一个任务的所有信息存储在一个特殊类型的段中,即任务状态段 TSS 包括: 处理器只读其中信息的静态字段集(图中灰色部分) 每次任务切换时处理器将会更新的动态字段集 TSS 与其它段一样,也是使用段描述符来定义的 访问 TSS 的描述符会导致任务切换 因此,在大多数系统中都将描述符的 DPL(描述符特权级)字段设置为最高特权级 0 这样就可以只允许可信任的软件执行任务的切换 TSS 的描述符只能放在全局描述符表 GDT中 一个任务的使用由指向其TSS的段选择符来指定 (当一个任务被加载进处理器中执行时 该任务的段选择符 基地址 段限长以及TSS段描述符属性就会被加载进任务寄存器TR中 如果使用了分页机制 那么任务使用的页目录表基地址就会被加载进控制寄存器CR3中)
动态字段
当任务切换而被挂起时 处理器会更新动态字段的内容
通用寄存器字段
用于保存EAX ECX EDX EBX ESP EBP ESI和EDI寄存器的内容
段选择符字段
用于保存ES CS SS DS FS和GS段寄存器的内容
标志寄存器EFLAGS字段
在切换之前保存EFLAGS
指令指针EIP字段
在切换之前保存EIP寄存器的内容
先前任务链接字段
含有前一个任务的TSS段选择符 该字段允许任务使用IRET指令切换到前一个任务
静态字段
处理器会读取静态字段的内容 但通常不会改变它们 这些字段的内容是在任务被创建时设置的
LDT段选择符字段
含有LDT段的选择符 对于一个给定的任务 通过把与任务相关的所有段描述符放入LDT中 任务的地址空间就可以与其他任务隔绝开来 几个任务也可以使用同一个LDT 这是一种简单而有效的允许某些任务互相通信或控制的方法
CR3控制寄存器字段
含有任务使用的页目录物理基地址 (控制寄存器CR3通常被称为页目录基地址寄存器) 如果开启了分页机制 则TSS中的CR3寄存器字段可以让每个任务有它自己的页表 或者几个任务能够共享相同页表集
特权级{0 1 2}的堆栈指针字段
堆栈指针由堆栈段选择符{SS0 SS1 SS2}和栈中偏移量指针{ESP0 ESP1 ESP2}组成 对于指定的一个任务 这些字段的值是不变的 只是任务发生堆栈切换时 寄存器SS和ESP的内容将会改变
调试陷阱T标志字段
该字段位于字节0x64位[0]处 当设置了该位时 处理器切换到该任务的操作将产生一个调试异常
IO位图基地址字段
该字段含有从TSS段开始处到IO许可位图处的16位偏移值
当前执行任务的状态包括
所有通用寄存器和段寄存器信息
标志寄存器EFLAGS
程序指针EIP
控制寄存器CR3
任务寄存器TR
LDTR寄存器
段寄存器指定的任务当前执行空间
IO映射位图基地址和IO位图信息(在TSS中)
特权级{0 1 2}的堆栈指针(在TSS中)
链接至前一个任务的链指针(在TSS中)
任务的调度执行
当调度一个任务执行时 当前正在运行任务和调度任务之间会自动的发生任务切换操作 在任务切换期间 当前运行任务的执行环境(任务的状态上下文)会被保存在它的TSS中并且暂停该任务的执行 此后新调度任务的上下文会被加载进处理器中 并且从加载的EIP指向的指令处开始执行新任务 如果当前执行任务(调用者)调用了被调度的新任务(被调用者) 那么调用者的TSS段选择符会被保存在被调用者TSS中 从而提供了一个返回调用者的链接 中断或异常可以通过切换到一个任务来进行处理 这种情况下 处理器不仅能够执行任务切换来处理中断或异常 而且会在中断或异常处理任务返回时自动地切换回被中断的任务中去 这种操作方式可以处理在中断任务执行时发生的中断 作为任务切换操作的一部分 处理器也会切换到另一个LDT 从而允许每个任务对基于LDT的段具有不同逻辑到物理地址的映射 同时 页目录寄存CR3也会在切换时被重新加载 因此每个任务可以有自己的一套页表
调度执行方式
所有调度任务执行的方法都会使用一个指向任务门或者任务TSS段的选择符来确定一个任务 当使用CALL或JMP指令调度一个任务时 指令中的选择符既可以直接选择任务的TSS 也可以选择存放有TSS选择符的任务门 当调度一个任务来处理一个中断或异常时 IDT中该中断或异常表项必须是一个任务门 并且其中含有中断或异常处理任务的TSS选择符
使用CALL指令明确的调用一个任务
使用JMP指令明确的跳转到一个任务
由处理器隐含的调用一个中断句柄处理任务
隐含的调用一个异常句柄处理任务
任务切换
中断发生时 CPU在中断向量表中找到相应的表项 如果此表项是一个任务门 并且通过了优先级别的检查 CPU就会将当前任务的运行现场保存在相应的TSS中 并将任务门所指向的TSS作为当前任务 将其内容装入CPU中的各个寄存器 从而完成一次任务的切换 1.切换页全局目录以安装一个新的地址空间 2.切换内核态堆栈和硬件上下文(因为硬件上下文提供了内核执行新进程所需的所有信息)
任务切换示意
处理器执行任务切换操作的方式
当前任务对GDT中的TSS描述符执行JMP或CALL指令
当前任务对GDT或LDT中的任务门描述符执行JMP或CALL指令
中断或异常向量指向IDT表中的任务门描述符
当中断或异常的向量索引是IDT中的一个任务门时 一个中断或异常就会造成任务切换 如果向量索引是IDT中的一个中断或陷阱门 则不会造成任务切换
当EFLAGS中的NT标志置位时当前任务执行IRET指令
当执行IRET指令时NT标志的状态确定了是否会发生任务切换 如果NT标志处于复位状态 则执行一般返回处理 如果NT标志处于置位状态 则返回操作会产生任务切换(切换到的新任务由中断服务过程TSS中的TSS选择符指定)
处理器执行流程
1.从{作为JMP或CALL指令操作数中 或任务门中 或当前TSS的前一任务链接字段中(对于由IRET引起的任务切换)} 取得新任务的TSS段选择符
2.检查当前任务是否允许切换到新任务(把数据访问特权级规则应用到JMP和CALL上) *当前任务的CPL和新任务段选择符的RPL必须<=TSS段描述符的DPL或者引用任务门 *无论目标任务门或TSS段描述符的DPL是何值 异常 中断和IRET指令都允许执行任务切换 但对于INT n指令产生的中断将检查DPL
3.检查新任务的TSS描述符是标注为存在(P=1)且TSS段长度有效(>0x67)
当试图执行会产生错误的指令时 都会恢复对处理器状态的任何改变 这使得异常处理过程的返回地址指向出错指令而非出错指令随后的一条指令 (因此异常处理过程可以处理出错条件并且重新执行)
4.如果任务切换产生自JMP或IRET指令 处理器就会把当前任务(老任务)TSS描述符中的忙标志B复位 如果任务切换时由{CALL指令 异常或中断}产生 则忙标志B不动
5.如果任务切换由IRET产生 则处理器会把临时保存的EFLAGS映像中的NT标志复位 如果任务切换由{CALL JMP 异常或中断}产生 则不会改动NT标志
6.把当前任务的状态保存到当前任务的TSS中
处理器会从任务寄存器取得当前任务TSS的基地址 并且把以下寄存器的内容复制到当前TSS中: {所有通用寄存 段寄存器中的段选择符 标志寄存器EFLAGS 指令指针EIP}
7.如果任务切换是由{CALL指令 异常或中断}产生 则处理器就会把从新任务中加载的EFLAGS中的NT标志置位 如果任务切换产生自JMP或IRET指令 则不改动新加载EFLAGS中的标志
8.如果任务切换是由{CALL JMP 异常或中断}产生 处理器就会设置新任务TSS描述符中的忙标志B 如果任务切换由IRET产生 则不改动B标志
9.使用新任务TSS的段选择符和描述符加载任务寄存器TR(包括隐藏部分) 设置CR0寄存器的TS标志
10.把新任务的TSS状态加载进处理器 在此期间检测到的任何错误都将出现在新任务的上下文中
TSS状态包括{LDTR寄存器 PDBR(CR3)寄存器 EFLAGS寄存器 EIP寄存器 通用寄存器和段选择符}
11.开始执行新任务
任务链
TSS的前一任务链接字段以及EFLAGS中的NT标志用于返回到前一个任务操作中 NT标志指出了当前执行的任务是否嵌套在另一个任务中执行 并且当前任务的前一任务链接字段中存放着嵌套层中更高层任务的TSS选择符 当CALL指令 中断或异常造成任务切换 处理器把当前TSS段的选择符复制到新任务TSS段的前一任务链接字段中 然后在EFLAGS中设置NT标志 NT标志指明TSS前一任务链接字段中存放有保存的TSS段选择符 如果软件使用IRET指令挂起新任务 处理器就会使用前一任务链接字段中值和NT标志返回到前一任务 (如果NT置位的话 处理器就会切换到前一任务链接字段指定的任务去执行) 如果任务切换是由JMP指令造成的 那么新任务就不会是嵌套的(NT标志会被设置为0 并且不使用前一任务链接字段) JMP指令用于不希望出现嵌套的任务切换中
任务地址空间
包含的相关段
代码段
数据段
堆栈段
TSS中引用的系统段
任务代码能够访问的任何其他段
把任务映射到线性地址空间和物理地址空间的2种方法
对于映射任务线性地址空间的这2种方法 所有任务TSS都必须存放在共享的物理地址空间区域中 并且所有任务都能访问这个区域
1.所有任务共享一个线性到物理地址空间的映射
当没有开启分页机制时 只能使用这种方法把任务映射到线性和物理地址空间(不开启分页时 所有线性地址映射到相同的物理地址上) 开启了分页机制 那么通过让所有任务使用一个页目录就可以使用这种从线性到物理地址空间的映射形式 (如果支持需求页虚拟存储技术 则线性地址空间可以超过现有物理地址空间的大小) GDT所映射的线性地址空间应该映射到共享的物理地址空间中 否则就丧失了GDT的作用
2.每个任务有自己的线性地址空间 并映射到物理地址空间
通过让每个任务使用不同的页目录就可以使用这种映射形式(因为每次任务切换都会加载PDBR(控制寄存器CR3)所以每个任务可以有不同的页目录) 不同任务的线性地址空间可以映射到完全不同的物理地址上 如果不同页目录的表项指向不同的页表而且页表也指向物理地址中不同的页面上 那么各个任务就不会共享任何物理地址 为了让处理器执行任务切换而读取或更新TSS时 TSS地址的映射不会改变 就需要使用这种映射方式
任务间数据共享 为数据段建立共享的逻辑到物理地址空间映射的方法
1.通过使用GDT中的段描述符
所有任务必须能够访问GDT中的段描述符 如果GDT中的某些段描述符指向线性地址空间中的一些段 并且这些段被映射到所有任务共享的物理地址空间中 那么所有任务都可以共享这些段中的代码和数据
2.通过共享的LDT
两个或多个任务可以使用相同的LDT (如果它们的TSS中LDT字段指向同一个LDT) 如果一个共享的LDT中某些段描述符指向映射到的物理地址空间公共区域的字段 那么共享LDT的所有任务可以共享这些段中的所有代码和数据 这样可以把共享局限于指定的一些任务中(系统中有于此不同LDT的其他任务没有访问这些共享段的权利)
3.通过映射到线性地址空间公共地址区域的不同LDT中的段描述符
如果线性地址空间中的这个公共区域对每个任务都映射到物理地址空间的相同区域 那么这些段描述符就允许任务共享这些段(这样的段描述符通常称为别名段) LDT中的其他段描述符可以指向独立的未共享线性地址区域
中断和异常
中断和异常是一种特殊类型的控制转换 中断与异常的主要区别在于中断常用于处理 CPU 外部的异步事件 而异常则是处理 CPU 在执行过程中本身检测到的问题 外部中断源有两种:由 CPU 的 INTR 引脚输入的可屏蔽中断和 NMI 引脚输入的不可屏蔽中断 异常也有两类:由 CPU 检测到的出错、陷阱或放弃事件以及编程设置的“软中断” 处理器使用标识号(中断号)来识别每种类型的中断或异常 处理器所能识别的不可屏蔽中断 NMI和异常的标识号是预先确定的,范围是 0 到 31( 0x00-0x1f)
中断描述符表IDT
中断描述符表将每个中断或异常标识号与处理相应事件程序指令的一个描述符相关联 每一个向量在表中有相应的中断或异常处理程序的入口地址 IDT 是一个 8 字节描述符数组,但其第 1 项可以含有一个描述符 处理器通过将中断号异常号乘上 8 即可索引 IDT 中对应的描述符 IDT 可以位于物理内存的任何地方 处理器是使用 IDT 寄存器( IDTR)来定位 IDT 的 修改和复制 IDT 的指令是 LIDT 和 SIDT IDT 也是使用 6 字节数据的内存地址作为操作数的 前两个字节表示表的限长,后 4 个字节是表的线性基地址
中断任务和中断过程
一个中断或异常也能“调用”中断处理程序,该程序是一个过程或一个任务 当响应一个中断或异常时, CPU 使用中断或异常的标识号来索引 IDT 表中的描述符 如果 CPU 索引到一个中断门或陷阱门时,它就调用处理过程;如果是一个任务门,它就引起任务切换 中断门或陷阱门间接地指向一个过程,该过程将在当前执行任务上下文中执行 门描述符中的段选择符指向 GDT 或当前 LDT 中的一个可执行段的描述符 门描述符中的偏移字段值指向中断或异常处理过程的开始处 中断会在把原指令指针压入堆栈之前,把原标志寄存器 EFLAGS 的内容也推入堆栈中 对于与段有关的异常, CPU 还会将一个错误码压入异常处理程序的堆栈上 对于中断过程处理结束的返回操作,中断返回指令 IRET 与 RET 相似 但是 IRET 为了去除压入堆栈的 EFLAGS 值, ESP 会多递增 4 个字节 中断门与陷阱门的区别在于对中断允许标志 IF 的影响 由中断门向量引起的中断会复位 IF,因为可以避免其它中断干扰当前中断的处理 随后的 IRET 指令会从堆栈上恢复 IF 的原值;而通过陷阱门产生的中断不会改变 IF
1.确定中断或异常关联的向量i
2.读由idtr寄存器指向的IDT表中的第i项
3.从gdtr寄存器获得GDT的基地址 在GDT中查找以读取IDT表项中断选择符所标识的段描述符 (这个段描述符指定中断或异常处理程序所在段的基地址)
4.检查中断是由授权的中断发生源发出的 将当前特权级CPL与段描述符的描述符特权级DPL比较 (CPL<DPL =>就产生一个一般保护异常 因为中断处理程序的特权不能低于引起中断的程序的特权) 接着比较CPL与处于IDT中的门描述符DPL (DPL<CPL =>就产生一个一般保护异常 以避免用户应用程序访问特殊的陷阱门或中断门 )
5.检查是否发生了特权级变化 CPL是否不同于所选择的段描述符的DPL 如果是 控制单元必须开始使用与新的特权级相关的栈=>执行子步骤
5.1读tr寄存器=>访问运行程序的TSS段
5.2用于新特权级相关的栈段和栈指针装载ss和esp寄存器 (值从TSS中获取)
5.3在新栈中保存ss和esp以前的值 (这些值定义了与旧特权级相关的栈的逻辑地址)
6.如果故障已发生 用引起异常的指令地址装载cs和eip寄存器 (从而使得这条指令再次被执行)
7.在栈中保存eflags cs和eip的内容
8.如果异常产生了一个硬件出错码 则将它保存在栈中
9.跳转到中断或异常处理程序 装载cs和eip 其值分别是IDT表中第i项门描述符的段选择符和偏移量字段 (这些值给出了中断或异常处理程序的第一条指令的逻辑地址)
10.中断处理程序
11.通过iret返回
11.1用保存在栈中的值装载cs eip和eflags寄存器 如果一个硬件出错码曾被压入栈中 并且在eip内容上面 那么执行iret指令前必须先弹出这个硬件出错码
11.2检查处理程序CPL是否等于cs中最低2位的值 如果是 iret终止执行
11.3从栈中装载ss和esp寄存器(返回到与旧特权级相关的栈)
11.4检查ds es fs和gs段寄存器的内容 如果其中一个寄存器包含的选择符是一个段描述符 且DPL<CPL 那么清相应的段寄存器 (为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)来访问内核地址空间)
系统描述符类型
段描述符
GDT或LDT的表项
调用门描述符
段选择符=>指定要访问的代码段 偏移值=>指定代码段中入口点(通常是指定过程的第一条指令) DPL=>指定访问过程的调用者需具备的特权级 P=>指明调用门描述符是否有效 参数个数=>若会发生堆栈切换 它会指定在堆栈之间需要复制的可选参数个数
当处理器访问调用门时 它会使用调用门中的段选择符来定位目的代码段的段描述符 然后CPU会把代码段描述符的基地址和调用门中的偏移值进行组合 形成代码段中指定程序入口点的线性地址
中断门描述符
利用段选择符:偏移地址=>把程序执行权转移到代码段中异常或中断的处理过程 当控制权转移到一个适当的段时 处理器清IF标志 从而关闭将来会发生的可屏蔽中断(以避免中断嵌套)
陷阱门描述符
利用段选择符:偏移地址=>把程序执行权转移到代码段中异常或中断的处理过程 当控制权转移到一个适当的段时 处理器清不修改IF标志
任务门描述符
用于任务切换=>(会导致任务切换) IDT 表中的任务门描述符间接地指向一个任务状态段 TSS 任务门描述符中的段选择符指向 GDT 表中的一个 TSS 描述符 当产生的中断或异常指向 IDT 中的一个任务门描述符,就会导致任务切换,从而会在独立的任务中处理中断
(使用单独的任务来处理中断)
优点
被中断程序或任务的完整上下文会被自动保存
在处理中断或异常时 新的TSS可以运行处理过程使用新特权级0的堆栈 在当前特权级0的堆栈已毁坏时 如果发生了一个异常或中断 为中断过程提供一个新特权级0的堆栈通过任务门访问中断处理过程能够防止系统崩溃
通过使用单独的LDT给中断或异常处理任务独立的地址空间 可以把它与其他任务隔离开来
缺点
在任务切换时必须对大量机器状态进行保存 使得它比使用中断门的响应速度要慢 导致中断延时增加
TSS段描述符
类型字段TYPE
忙标志B用于指明任务是否处于忙状态 忙状态的任务时当前正在执行的任务或等待执行(被挂起)的任务 处理器使用忙标志B来检测任何企图对被中断执行任务的调用 值=1001=>表明任务处于非活动状态 值=1011=>表明任务正忙
描述符特权级DPL
可以访问TSS描述符的程序其CPL数值必须<=TSS描述符的DPL (大多数系统中TSS描述符的DPL字段值应该设置成小于3 =>只有具有特权级的软件可以执行任务切换操作 而多任务应用中 某些TSS描述符的DPL字段值可以设置成3=>在用户特权级上也能进行任务切换操作)
颗粒度G
G=0时 限长字段必须具有>=103的值 (TSS段的最小长度不得小于104字节)
存在位P
保护模式实现原理
影子寄存器
影子寄存器含有描述符信息的一个副本 操作系统必须确保对描述符表的改动反映在影子寄存器中 (在对描述符表中描述符作过任何改动之后就立刻重新加载6个段寄存器 这将把描述符表中的相应段信息重新加载到影子寄存器中)
段级保护
段限长limit检查
段类型检查
CS寄存器中只能被加载进一个可执行段的选择符
不可读可执行段的选择符不能被加载进数据段寄存器中
只有可写数据段的选择符才能被加载进SS寄存器中
特权级检查
特权级
CPL
CPL是当前正在执行程序或任务的特权级(存放在CS和SS段寄存器的位0和位1中) CPL=当前代码段的特权级 程序把控制转移到另一个具有不同特权级的代码段时 处理器就会改变CPL 当访问一个一致性代码段时 则处理器对CPL的设置有些不同 特权级值高于或等于一致代码段DPL的任何段都可以访问一致性代码段 并且当处理器访问一个特权级不同于CPL的一致性代码段时 CPL并不会被修改成一致性代码段的DPL
DPL
DPL是一个段或门的特权级 存放在段或门描述符的DPL字段中 在当前执行代码段试图访问一个段或门时 段或门的DPL会用来与CPL以及段或门选择符中的RPL作比较
在数据段中的意义
其DPL指出允许访问本数据段的程序或任务应具有的最大特权级数值 (如果数据段的特权级DPL是1 则只有运行在CPL为0或1的程序可以访问这个段)
在非一致性代码段中的意义
其DPL指出程序或任务访问该段必须具有的特权级 (某一非一致性代码段的DPL是0 则只有运行在CPL为0的程序能够访问这个段)
在调用门中的意义
其DPL指出访问调用门的当前执行程序或任务可处于的最大特权级数值
在一致代码段中和通过调用门访问非一致性代码段时的意义
其DPL指出允许访问本代码段的程序或任务应具有的最小特权级数值 (若一致性代码段的DPL为2 则运行在CPL为0的程序就不能访问这个代码段)
在任务状态段TSS中的意义
其DPL指出访问TSS的当前执行程序或任务可处于的最大特权级数值
RPL
RPL是一种赋予段选择符的超越特权级 它存放在选择符的位[1-0]中 处理器会同时检查RPL和CPL 以确认是否允许访问一个段 (即使程序或任务具有足够的特权级CPL来访问一个段 但是如果RPL特权级不足则访问也将被拒绝 如果段选择符的RPL数值大于CPL 那么RPL将覆盖CPL 而使用RPL作为检查比较的特权级,反之亦然 --CPU始终会取RPL和CPL中数值较大的特权级作为访问段时的比较对象) RPL用来确保高特权级的代码不会代表应用程序取访问一个段 除非应用程序自己具有访问这个段的权限
访问数据段时的特权级检查
只有当段的DPL数值大于或等于CPL和RPL两者时 处理器才会把选择符加载进段寄存器中 否则 就会产生一个保护异常 并且不加载段选择符 (当使用堆栈段选择符加载SS段寄存器时也会执行特权级检查 与堆栈段相关的所有特权级必须与CPL匹配 即CPL 堆栈段选择符的RPL以及堆栈段描述符的DPL都必须相同 如果RPL或DPL与CPL不同 则处理器就会产生一个保护性异常)
代码段之间转移控制时的特权级检查
对于将程序控制权从一个代码段转移到另一个代码段 目标代码段的段选择符必须加载进代码段寄存器CS中 作为这个加载过程的一部分 处理器会检测目标代码段的段描述符并执行各种限长 类型和特权级检查 如果这些检查都通过了 则目标代码段选择符就会加载进CS寄存器 于是程序的控制器就被转移到新代码段中 程序将从EIP寄存器指向的指令处开始执行
通过JMP CALL和RET指令来引用另外一个代码段的实现方法
目标操作数含有目标代码段的段选择符
近转移形式
近转移形式只是在当前代码段中执行程序控制转移 因此不会执行特权级检查
远转移形式
JMP CALL或RET的远转移形式会把控制转移到另一个代码段中 因此处理器会验证: 1.当前特权级CPL (含有执行调用或跳转程序的代码段的CPL) 2.含有被调用过程的目的代码段描述符中的描述符特权级DPL 3.目的代码段的段选择符中的请求特权级RPL 4.目的代码段描述符中的一致性标志C (用于确定一个代码段是非一致性代码段还是一致性代码段)
非一致性代码段C=0
当访问非一致性代码段时 调用者的CPL必须等于目的代码段的DPL 否则将会产生一般保护异常 指向非一致性代码段的段选择符的RPL在数值上必须小于等于调用者的CPL才能使得控制转移成功完成 当非一致性代码段的段选择符被加载进CS寄存器中时 特权级字段不会改变(仍然是调用者的CPL)
一致性代码段C=1
当访问一致性代码段时 调用者的CPL可以在数值上大于等于目的代码段的DPL 仅当CPL<DPL时 处理器才会产生一般保护异常 对于访问一致性代码段 处理器忽略对RPL的检查 对于一致性代码段 DPL表示调用者对代码段进行成功调用可以处于的最低数值特权级 当程序控制被转移到一个一致代码段中 CPL并不改变 即使目的代码段的DPL在数值上小于CPL (这是CPL与当前代码段DPL可能不相同的唯一一种情况) 由于CPL没有改变 因此堆栈也不会切换
目标操作数指向一个调用门描述符 而该描述符中含有目标代码段的选择符
通过调用门进行程序控制转移时 CPU会对{当前特权级CPL 调用门选择符中的请求特权级RPL 调用门描述符中的描述符特权级DPL 目的代码段描述符中的RPL 和 目的代码段描述符中的一致性标志C}进行检查 以确定控制转移的有效性 调用门描述符的DPL字段指明了调用程序能够访问调用门的数值最大的特权级 为了访问调用门 调用者程序的特权级CPL必须小于等于调用门的DPL 调用门选择符的RPL必须小于等于调用门的DPL
检查规则
JMP指令特权级检查规则
如果调用者与调用门之间的特权级检查成功通过 CPU会接着把调用者的CPL与代码段描述符的DPL进行比较检查 1.可以把控制转移到更高特权级的一致性代码段中(转移到DPL<=CPL的一致性代码段中) 2.JMP指令只能通过调用门把控制转移到DPL=CPL的非一致性代码段中
CALL指令特权级检查规则
如果调用者与调用门之间的特权级检查成功通过 CPU会接着把调用者的CPL与代码段描述符的DPL进行比较检查 1.可以把控制转移到更高特权级的一致性代码段中(转移到DPL<=CPL的一致性代码段中) 2.CALL指令可以通过调用门把程序控制转移到特权级更高的非一致性代码段中 (转移到DPL<CPL的非一致性代码段中去执行)
目标操作数指向一个TSS 而该TSS中含有目标代码段的选择符
目标操作数指向一个任务门 该任务门指向一个TSS 该TSS中含有目标代码段的选择符
堆栈切换
每当调用门用于把程序控制转移到一个更高级别的非一致性代码段时 CPL就会被设置为目的代码段的DPL值 并且会引起堆栈切换 CPU会自动切换到目的代码段的堆栈去 堆栈切换操作的目的是为了防止高特权级程序由于栈空间不足而引起奔溃 同时也是为了防止低特权级程序通过共享的堆栈有意或无意的干扰高特权级程序
堆栈信息
操作系统需要负责为所有用到的特权级建立堆栈和堆栈段描述符 并且在任务的TSS中设置初始指针值 每个栈必须可读可写 并且具有足够的空间来存放以下信息: 1.调用过程的SS ESP CS和EIP寄存器内容 2.被调用过程的参数和临时变量所需要使用的空间 3.当隐含调用一个异常或中断过程时 标志寄存EFLAGS和出错码使用的空间
特权级3的堆栈
当特权级3的程序在执行时 特权级3的堆栈的段选择符和栈指针会被分别存放在SS和ESP中 并且在发送堆栈切换时被保存在被调用过程的堆栈上
特权级{0,1,2}的堆栈
特权级{012}的堆栈的初始指针都存放在当前运行任务的TSS段中 TSS段中这些指针都是只读的 在任务运行时CPU并不会修改它们 当调用更高特权级程序时 CPU才用它们来建立堆栈 当从调用过程返回时 相应栈就不存在了 下一次在调用该过程时 就会再次使用TSS中的初始指针值建立一个新栈
堆栈切换的步骤
1.使用目的代码段的DPL(新的CPL)从TSS中选择新栈的指针 从当前TSS中读取新栈的段选择符和栈指针 在读取栈段选择符 栈指针或栈段描述符过程中 任何违反段界限的错误都将产生一个无效TSS异常
2.检查栈段描述符特权级和类型是否有效 若无效则同样产生一个无效TSS异常
3.临时保存SS和ESP寄存器的当前值 把新栈的段选择符和栈指针加载到SS和ESP中 然后把临时保存的SS和ESP内容压入新栈中
4.把调用门描述符中指定参数个数的参数从调用过程栈复制到新栈中 调用门中参数个数最大为31 如果个数为0(无参数)则不需要复制
5.把返回指令指针(当前CS和EIP内容)压入新栈 把新的目的代码段选择符加载到CS中 同时把调用门中的偏移值(新指令指针)加载到EIP中 最后开始执行被调用过程
从被调用过程返回
RET指令
指令RET用于执行{近返回 同特权级远返回和不同特权级的远返回} 该指令用于从使用CALL指令调用的过程中返回 1.近返回仅在当前代码段中转移程序控制权(CPU仅进行界限检查) 2.对于同特权级远返回 CPU同时从堆栈中弹出返回代码段的选择符和返回指令指针 (CS和EIP通常都由CALL指令压入堆栈 CPU会执行特权级检查以应付当前过程可能修改指针值或者堆栈出现问题的情况) 3.会发生特权级改变的远返回仅允许返回到低特权级程序中(返回到的代码段DPL在数值上>CPL) CPU会使用CS寄存器中选择符RPL字段来确定是否要求返回到低特权级 如果RPL数值>CPL 就会执行特权级之间的返回操作
CPU执行步骤
1.检查保存的CS寄存器中RPL字段值 以确定在返回时特权级是否需要改变
2.弹出并使用被调用过程堆栈上的值 加载CS和EIP寄存器 在此过程中会对代码段描述符和代码段选择符的RPL进行特权级与类型检查
3.如果RET指令包含一个参数个数操作数并且返回操作会改变特权级 那么就在弹出栈中CS和EIP之后把参数个数值加到ESP寄存器中 (以跳过被调用者栈上的参数 此时ESP寄存器指向原来保存的调用者堆栈的指针SS和ESP)
4.把保存的SS和ESP值加载到SS和ESP寄存器中 从而切换回调用者的堆栈(此时被调用者堆栈的SS和ESP值被抛弃)
5.如果RET指令包含一个参数个数操作数 则把参数个数值加载到ESP寄存器中 (以跳过调用者栈上的参数)
6.检查段寄存器{DS ES FS GS}的内容 如果其中有指向DPL<新CPL的段 那么CPU就会用NULL选择符加载这个段寄存器
页级保护
如果通过所有段级保护 就会再进行页级保护检查 如果CPU在任何一级检测到一个保护违规错误 则会放弃内存访问并产生一个异常 如果是段机制产生的异常 就不会再产生一个页异常(页级保护不能替代或忽略段级保护) 页表项中的U/S标志和R/W标志应用于该表项映射的单个页面 页目录项中的U/S标志和R/W标志则对该目录项所映射的所有页面起作用 (页目录和页表的组合保护属性由2者属性的与操作构成)
内存管理单元MMU
TLB
为计算机装备一个不需要经过页表就能把虚拟地址映射成物理地址的小的硬件设备 这个设备叫做 TLB(相联存储器) 以提高虚拟内存系统效率 缓存少量的虚拟地址与物理地址的转换关系 是转换表的Cache 也就是快表
TLB寄存器条目
每一个 TLB 寄存器的每个条目包含一个页面的信息:有效位、虚页面号、修改位 保护码和页面所在的物理页面号 它们和页面表中的表项一一对应
工作原理
当一个虚地址被送到 MMU 翻译时,硬件首先把它和 TLB 中的所有条目同时(并行地)进行比较 如果它的虚页面号在 TLB 中,并且访问没有违反保护位 它的页面会直接从 TLB 中取出而不去访问页表 如果虚页面号在 TLB 中,但当前指令试图写一个只读的页面,这时将产生一个缺页异常,与直接访问页表时相同 如 MMU 发现在 TLB 中没有命中,它将随即进行一次常规的页表查找 然后从 TLB 中淘汰一个条目并把它替换为刚刚找到的页表项 在一个 TLB 条目被淘汰时,被修改的位被复制回在内存中的页表项,其他的值则已经在那里了 当 TLB 从页表装入时,所有的域都从内存中取得
DTLB
数据TLB
ITLB
指令TLB
TTW
转换表漫游 当TLB中没有缓存对应的地址转换关系时 需要通过对内存中转换表的访问来获得虚拟地址和物理地址的对应关系 TTW成功后 结果写入TLB
模式切换
切换到保护模式
在切换到保护模式之前 必须首先加载一些系统数据结构和代码模块 一旦建立了这些系统表 软件初始化代码就可以切换到保护模式中 通过执行在CR0寄存器中设置PE标志的MOV CR0指令 就可以进入保护模式(同一指令中 CR0的PG标志用于开启分页机制) 刚进入保护模式时 特权级是0 如果开启了分页机制 那么MOV CR0指令到JMP或CALL指令之间的代码必须来自对等映射的同一个页面上 (跳转之前的线性地址与开启分页后的物理地址相同)而JMP或CALL指令跳转到的目标指令不需要处于对等映射页面上
1.禁止中断(使用CLI指令可以禁止可屏蔽的硬件中断 NMI会由硬件电路来禁止) 同时软件应该确保在模式切换操作期间不产生异常和中断
2.执行LGDT指令把GDT表的基地址加载进GDTR寄存器
3.执行在控制寄存器CR0中设置PE标志的MOV CR0指令
4.在MOV CR0指令之后立刻执行一个远跳转JMP或远调用CALL指令 这个操作通常是远跳转或远调用指令流中的下一条指令
5.若要使用局部描述符表 则执行LLDT指令把LDT段的选择符加载到LDTR寄存器
6.执行LTR指令 用初始保护模式任务的段选择符或者可写内存区域的段描述符加载任务寄存器TR (这个可写内存区域用于在任务切换时存放任务的TSS信息)
7.进入保护模式后 段寄存器仍然含有在实地址模式时的内容 步骤4的JMP或CALL指令会重置CS寄存器 其余段寄存器内容通过重新加载或切换到一个新任务来更新
8.执行LIDT指令把保护模式IDT表的基地址和长度加载到IDTR寄存器中
9.执行STI指令开启可屏蔽硬件中断 并且执行必要的硬件操作开启NMI中断
切换到实模式
使用MOV CR0指令把控制寄存器CR0中的PE标志清0 重新进入实地址模式
1.禁止中断(使用CLI指令可以禁止可屏蔽的硬件中断 NMI会由硬件电路来禁止) 同时软件应该确保在模式切换操作期间不产生异常和中断
2.如果已开启分页机制 执行子步骤
2.1把程序的控制转移到对等映射的线性地址处
2.2确保GDT和IDT在对等映射的页面上
2.3清除CR0中的PG标志
2.4CR3寄存器设置为0x00 用于刷新TLB缓冲
3.把程序的控制转移到长度为64KB的可读段中 (使用实模式要求的段长度加载CS寄存器)
4.使用指向含有以下设置值的描述符来加载SS DS ES FS和GS段寄存器 段限长Limit=64KB 字节颗粒度G=0 向上扩展E=0 可写W=1 存在P=1
5.执行LIDT指令来指向在1MB实模式地址范围内的实地址模式中断表
6.清除CR0中的PE标志来切换到实地址模式
7.执行一个远跳转指令跳转到一个实模式程序中 (会刷新指令队列并且为CS寄存器加载合适的基地址和访问权限值)
8.加载实地址模式程序代码会使用的SS DS ES FS和GS寄存器
9.执行STI指令开启可屏蔽硬件中断 并且执行必要的硬件操作开启NMI中断
相关硬件
RAM
SDRAM
芯片初始化
操作参数
读写操作时序图
读取命令与列地址一块发出(当 WE#为低电平是即为写命令)
行有效时序图
初始化完成后,要想对一个 L-Bank 中的阵列进行寻址,首先就要确定行(Row) 使之处于活动状态(Active),然后再确定列 在 CS#、 L-Bank 定址的同时, RAS(Row Address Strobe,行地址选通脉冲)也处于有效状态 此时 An 地址线则发送具体的行地址(A0-A11,共有 12 个地址线)
数据输出-(读)
非突发连续读取模式
不采用突发传输而是依次单独寻址,此时可等效于 BL=1。虽然可以让数据是连续的传输, 但每次都要发送列地址与命令信息,控制资源占用极大
突发连续读取模式
突发(Burst)是指在同一行中相邻的存储单元连续进行数据传输的方式 连续传输所涉及到存储单元(列)的数量就是突发长度 突发传输技术是只要指定起始列地址与突发长度 内存就会依次地自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址 只要指定起始列地址与突发长度,寻址与数据的读取自动进行, 而只要控制好两段突发读取命令的间隔周期(与 BL 相同)即可做到连续的突发传输
读取时预充电
在进行完读写操作后,如果要对同一 L-Bank 的另一行进行寻址 就要将原来有效(工作)的行关闭,重新发送行/列地址 L-Bank 关闭现有工作行,准备打开新行的操作就是预充电(Precharge) 预充电是对工作行中所有存储体进行数据重写,并对行地址进行复位,同时释放 S-AMP (将S-AMP 中的数据回写 即使是没有工作过的存储体也会因行选通而使存储电容受到干扰 所以也需要 S-AMP进行读后重写)
读取时数据掩码操作
屏蔽不需要的数据,采用了数据掩码技术DQM 通过 DQM,内存可以控制 I/O 端口取消哪些输出或输入的数据 读取时数据掩码操作, DQM 在两个周期后生效,突发周期的第二笔数据被取消
数据输入-(写)
写入时预充电
在进行完读写操作后,如果要对同一 L-Bank 的另一行进行寻址 就要将原来有效(工作)的行关闭,重新发送行/列地址 L-Bank 关闭现有工作行,准备打开新行的操作就是预充电(Precharge) 预充电是对工作行中所有存储体进行数据重写,并对行地址进行复位,同时释放 S-AMP (将S-AMP 中的数据回写 即使是没有工作过的存储体也会因行选通而使存储电容受到干扰 所以也需要 S-AMP进行读后重写)
写入时数据掩码操作
屏蔽不需要的数据,采用了数据掩码技术DQM 通过 DQM,内存可以控制 I/O 端口取消哪些输出或输入的数据 写入时数据掩码操作, DQM 立即生效,突发周期的第二笔数据被取消
突发读后写时的操作
突发(Burst)是指在同一行中相邻的存储单元连续进行数据传输的方式 连续传输所涉及到存储单元(列)的数量就是突发长度 突发传输技术是只要指定起始列地址与突发长度 内存就会依次地自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址 在最后一个所需数据(本例为第一笔数据)输出前一个周期使 DQM有效,屏蔽第二笔数据的输出; 发出写入命令,此时所读取的第二笔数据被屏蔽。 继续 DQM 以屏蔽第三笔数据的输出。 其中 tHZ 表示输出数据与外部电路的连接周期,tDS 表示数据输入准备时间
内部结构框图
命令
突发停止命令
专用的突发停止命令可用来中断突发读取,其生效潜伏期与 CL 相同。对于写入则立即有效
预充电命令
用预充电命令来中断突发读取,生效潜伏期与 CL 相同,要小于或等于 tRP。 写入时预充电在最后一个有效写入周期完成,并经过 tWR 之后发出,同时立即中断突发传输
刷新
刷新操作与预充电中重写的操作一样,都是用 S-AMP 先读再写 预充电是对一个或所有 L-Bank 中的工作行操作,并且是不定期的 而刷新则是有固定的周期,依次对所有行进行操作,以保留那些久久没经历重写的存储体中的数据 与所有 L-Bank 预充电不同的是, 刷新的行是指所有 L-Bank 中地址相同的行 而预充电中各 L-Bank 中的工作行地址并不是一定是相同的
自动刷新AR
SDRAM 内部有一个行地址生成器(也称刷新计数器)用来自动的依次生成行地址 由于刷新是针对一行中的所有存储体进行,所以无需列寻址,或者说 CAS 在 RAS 之前有效 由于刷新涉及到所有 L-Bank,因此在刷新过程中,所有 L-Bank 都停止工作 而每次刷新所占用的时间为 9 个时钟周期 之后就可进入正常的工作状态 (在这 9 个时钟期间内,所有工作指令只能等待而无法执行) 64ms 之后则再次对同一行进行刷新,如此周而复始进行循环刷新
自刷新SR
主要用于休眠模式低功耗状态下的数据保存 在发出 AR 命令时,将 CKE 置于无效状态,就进入了 SR 模式 此时不再依靠系统时钟工作,而是根据内部的时钟进行刷新操作 在 SR 期间除了 CKE 之外的所有外部信号都是无效的(无需外部提供刷新指令) 只有重新使 CKE 有效才能退出自刷新模式并进入正常操作状态
引脚定义
CK差分时钟信号
与 CK 反相的 CK#保证了触发时机的准确性
数据选取脉冲DQS
数据的同步信号 用来在一个时钟周期内准确的区分出每个传输周期 并便于接收方准确接收数据 在读取时, DQS 与数据信号同时生成 DQS 生成时,芯片内部的预取已经完毕了 在写入时,以 DQS 的高/低电平期中部为数据周期分割点
写入延迟
当 CL=2.5 时,读后写的延迟将为 tDQSS+0.5 个时钟周期
L-Bank
行寻址时就要先确定是哪个 L-Bank,然后再在这个选定的 L-Bank 中选择相应的行与列进行寻址 区号+子仓库号码+楼层分机(片选+L-Bank 寻址+行有效/选通) 一次只能是一个 L-Bank 工作,而每次与北桥交换的数据就是 L-Bank 存储阵列中一个“存储单元”的容量 电话对于编号相同的子仓库是并联的,所以其他子仓库相同楼层的搬运工也收到相同的命令 从相同编号的房间搬出货物,运向各自的生产车间 此时,同一批货物同时出现在各自的 16 条传送带上,并整齐地向调度厂房运去
存储阵列L-Bank
存储原理
行选各列选信号将使存储电容与外界的传输电路导通 从而可进行放电(读取) 与充电(写入)
概念
物理BANK
CPU 在一个传输周期能接受的数据容量就是 CPU 数据总线的位宽,单位是 bit(位)。 当时控制内存与 CPU之间数据交换的北桥芯片也将内存总线的数据位宽等同于 CPU 数据总线的位宽, 而这个位宽就称之为物理 Bank(Physical Bank,下文简称 P-Bank)的位宽
芯片位宽
每个内存芯片也有自己的位宽,即每个传输周期能提供的数据量
存储单元数量=行数*列数*L-Bank数
行数*列数 相当于得到一个L-Bank的存储单元数量
介绍文档表示方法=>M*W
比如 8M×8,这是一个 8bit 位宽芯片,有 8M个存储单元,总容量是 64Mbit(8MB)
M 是该芯片中存储单元的总数,单位是兆
W 代表每个存储单元的容量,也就是 SDRAM 芯片的位宽,单位是 bit
业界正规的内存芯片容量表示方法
全页
全页突发传输是指L-Bank里的一行中所有存储单元从头至尾进行连续传输 内存系统中的每次传输都是以一个P-Bank位宽为单位的多颗芯片的集体工作 在每次寻址时 P-BANK内每个芯片所得到的L-Bank地址与行地址都是相同的 等于对P-BANK所包含的每个芯片内同一L-Bank里同一行的所有存储单元读/写
引脚图
首先,我们知道内存控制器要先确定一个 P-Bank 的芯片集合 然后才对这集合中的芯片进行寻址操作 因此要有一个片选的信号CS#,它一次选择一个 P-Bank 的芯片集 被选中的芯片将同时接收或读取数据,所以要有一个片选信号 接下来是对所有被选中的芯片进行统一的L-Bank的寻址 目前SDRAM中L-Bank的数量最高为 4 个,所以需要两个L-Bank地址信号(2^2=4)BA0 BA1 最后就是对被选中的芯片进行统一的行/列(存储单元)寻址 An 找到了存储单元后,被选中的芯片就要进行统一的数据传输 那么肯定要有与位宽相同数量的数据 I/O 通道才行 所以肯定要有相应数量的数据线引脚DQn
说明
DRAM
SRAM 比传统的DRAM要快 但它需要更大的硅片面积 SRAM是静态的--不需要刷新 SRAM的存取时间比DRAM要短的多 因为SRAM在数据访问之间不需要暂停 由于它价格较高 通常用于容量小速度快的情况 如高速缓存和CACHE
Cache
TLB一般采用相连存储器或按内容访问存储器(CMA) 相连存储器使用虚拟地址进行搜索 直接返回对应的物理地址 相对于内存中的多级页表需要多次访问才能得到最终的物理地址 TLB查找大大减少了处理器的开销 如果需要的地址在TLB Cache中 相连存储器迅速返回结果 然后处理器用该物理地址访问内存 (称为TLB命中) 如果需要的地址 不再TLB Cache中(不命中) 处理器就需要到内存中访问多级页表 才能最终得到物理地址
分类
根据映射关系分类
全关联型Cache
全关联型Cache是指主存中的任何一块内存都可以映射到Cache中的任意一块位置上 在Cache中 需要建立一个目录表 目录表的每个表项都有三部分组成:(内存地址 Cache块号 有效位) 当处理器需要访问某个内存地址时 首先通过该目录表查询是否该内存缓存在Cache中(比较内存块地址)
直接关联型Cache
直接关联型Cache是指主存中的一块内存只能映射到Cache的一个特定的块中 假设一个Cache中总共存在N个Cache line那么内存被分成N等分 每一等分对应一个Cache line(比较区号)
组关联型Cache
把内存分成多组 一个组分成多个cache line 一个组映射到对应的多个连续的Cache line(Cache组) 并且该组内的任意一块可以映射到对应Cache组的任意一个 在组外 采用直接关联型Cache的映射方式 在组内 采用全关联型Cache的映射方式 目录表由三部分组成(区 块号, Cache块号, 有效位) 首先根据组号按地址查找到一组目录表项 然后根据区号和块号在该组表项中进行关联查找 如果匹配且有效位有效 则表明该数据块缓存在Cache中 得到Cache块号 加上块内地址 =>得到该内存地址在Cache中映射的地址 =>得到数据
写策略
写穿(write-through)
指在处理器对Cache写入的同时 将数据写入到内存中 保证了任何时刻 内存的数据和Cache中的数据都是同步的
回写(write-back)
回写系统通过将Cache line的标志位字段添加Dirty标志位 当处理器在改写了某个Cache line后 并不马上把其写回内存 而是将该Cache line的Dirty标志置1 当处理器再次修改该Cache line并且写回到Cache 中 查表发现该Dirty位为1 则将Cache line内容写回到内存中相应的位置 再将心数据写到Cache中
预取
预测数据并取入到Cache中 根据空间局部性 时间局部性 以及当前执行状态 历史执行过程 软件提示等信息 以一定合理方法 在数据/指令被使用前提前取入Cache 这样当数据/指令需要被使用时 就能快速从Cache中加载到处理器内部进行运算和执行
原理
时间局部性
指程序即将用到的指令/数据可能就是目前正在使用的指令/数据 当前用到的指令/数据在使用完毕之后可以暂时存放在Cache中 可以在将来的时候再被处理器用到
空间局部性
程序即将用到的指令/数据可能与目前正在使用的指令/数据空间上相邻或相近 在处理当前指令/数据时 可以从内存中把相邻区域的指令/数据读取到Cache中 这样 当处理器需要处理相邻内存区域的指令/数据时 可以直接从Cache中读取 节省访问内存的时间
硬件预取
数据Cache预取单元
基于流的预取单元 当程序以地址递增方式访问数据时 该单元会被激活 自动预取下一个Cache行的数据
激活条件
读取的数据是回写(write-back)的内存类型
预取的请求必须在一个4K的物理页内部 (预取是根据物理地址进行判断的)
处理器的流水线作业中没有fence或lock这样的指令
当前读取Load指令没有出现很多Cache不命中
前端总线不是很繁忙
没有连续的存储Store指令
基于指令寄存器的预取单元
该单元检测指令寄存器的读取指令(Load) 当该单元发现读取数据的大小总是相对固定的情况下 会自动预取下一块数据
软件预取
磁盘
内部结构
磁头
读线圈
写线圈
磁道
每个磁道上都有存放信息的特定格式 通过识别所读位数据流中的格式 磁盘电路就可以区分并读取磁道上各扇区中的数据
扇区
每个扇区地址场的地址字段存放着相关扇区的柱面号 磁头号和扇区号 因此通过读取地址场的地址信息就可以唯一的确定一个扇区
读写原理
在读操作过程中 磁头首先移动到旋转的磁盘的某个位置上 由于磁盘在旋转 磁介质相对磁头做匀速运动 因此磁头实际上子切割磁介质上的磁感线 从而在读线圈中产生感应电流 根据磁盘表面剩磁状态方向的不同 在线圈中感应产生的电流方向也不同 因而磁盘上记录着0和1的数据就被读出 从而可从磁盘上顺序读出数据流
控制器
磁盘控制器是CPU与驱动器之间的逻辑接口电路 它从CPU接收请求命令 向驱动器发送寻道 读写和控制信号 并且控制和转换数据流形式
优化
提高磁盘IO速度的路径
磁盘高速缓存
提前读
延迟写
优化物理块分布
虚拟盘
廉价磁盘冗余阵列
提高磁盘可靠性
容错技术
第一级容错技术
第二级容错技术
基于集群系统的容错技术
后备系统
管理
外存的组织方式
连续组织
[优点] 顺序访问容易且速度快 [缺点] 要求空间连续 事先知道文件大小 增删记录不灵活 动态增长困难
链接组织
[优点] 外存利用率高 增删改记录容易 方便文件动态增长 [缺点] 不支持高效的直接存取
隐式链接
每个盘块中含有下一个盘块的位置支持 [缺点] 可靠性差
显式链接
各物理块的位置放在内存的FAT中 [优点] 在内存中检索速度快 [缺点] FAT会占内存空间
索引组织
[优点] 支持直接访问 查找速度快 外存利用率高 [缺点] 索引块会占据磁盘块 索引块利用率低 启动磁盘的次数会随索引级数的增加而增加
单级索引
多级索引
增量式索引
文件存储空间的管理
空闲区表法
连续分配方式 为所有空闲区建立一张空闲表 每个空闲区对应一个表项
空闲链表法
将所有空闲盘区连接成一条空闲链
位示图法
利用二进制的一位来表示磁盘中一个盘块的使用情况
成组链接法
Linux系统的文件系统采用的文件空间管理方法 将空闲块分成若干组 各组之间进行链接
ROM
Flash
NAND FLASH
功能结构及部件
X-Buffers Latche & Decoders
用于行地址
Y-Buffers Latche & Decoders
用于列地址
Command Register
用于命令字 操作Nand Flash时 先传输命令 再传输地址 最后读写数据 期间还要检查Flash的状态
Read 1
命令字00h或01h 发出命令00h或01h后 就选定了读操作时从A区或B区开始 列地址A0~A7可寻址范围是256字节 命令00h和01h使得可以在512字节大小的页内任意寻址(相当于A8被00h设为0 被01h设为1) 发出命令字后 再发出4个地址序列 然后检测R/nB引脚以确定Flash是否准备好 如果准备好了 就可以发起读操作依次读入数据
Read2
命令字为50h 取的是C区数据 操作序列: 发出命令字50h ->发出4个地址序列 ->等待R/nB引脚为高 ->读数据 地址序列中A0~A3用于设定C区要读取的起始地址 A4~A7被忽略
Read ID
命令字90h 发出命令字90h ->发出4个地址序列(都设为0)->连续读入5个数据 (分别表示厂商代码 设备代码 保留的字节 多层操作代码)
Reset
命令字ffh 发出命令字ffh即可复位nand flash芯片 如果芯片正处于读 写 擦除状态 复位命令会终止这些命令
page Program(True)
命令字分2个阶段 80h和10h 写操作一般是以页为单位的(可以只写一页中的一部分) 发出命令字80h后 紧接着是4个地址序列 然后向flash发送数据(最大528字节) 然后发出命令字10h启动写操作 此时flash内部会自动完成写 校验操作 一旦发出命令字10h后 就可以通过读状态命令70h获知当前写操作是否完成 是否成功
page Program(Dummy)
命令字分2个阶段 80h和11h 发出命令字80h 后紧跟4个地址序列及最多528字节之后 发出命令字11h 紧接着对相邻层(plane)上的页进行同样的操作 仅在第4页的最后使用10h代替11h 这样就可以启动flash内部的写操作 此时可以通过命令71h获知这些写操作是否完成 是否成功
Copy-Back Program(True)
命令字分3个阶段 00h 8Ah 10h 此命令用于将一页复制到同一层(plane)内的另一页 省略了读出源数据 将源数据重新载入flash 使得效率大大提高 (源页 目的页必须在同一层 且源地址 目的地址A14 A15必须相同) 发出命令Read 1(00h)->4个源地址序列(源页的528字节数据被全部读入内部寄存器中) ->发出命令字8ah->4个目的地址序列->发出命令字10h启动对目的页的写操作 ->使用命令字70h查看此操作是否完成 是否成功
Copy-Back Program(Dummy)
命令字分3个阶段 03h 8ah 11h 可以同时启动对多达4个连续plane内的copy-back program操作 发出命令字00h和源页地址(使得528字节数据被读入所在plane的寄存器) ->对随后的其他plane的源页发出命令字03h和相应的源页地址(将数据读入该plane的寄存器) ->读出最多4页的数据到寄存器->发出命令字8ah和目的地址->发出命令字11h ->在发出最后一页地址后 用10h代替11h以启动写操作
Block Erase
命令字分3个阶段 60h d0h 用于擦除Nand flash块 发出命令字60h->发出3个地址序列(block地址 A9-A13被忽略)
Multi-Plane Block Erase
用于擦除不同plane中的块 发出命令字60h->发出block地址序列->发出4个block地址 ->发出命令字d0h启动擦除操作
读状态命令
Read Status
命令字70h
Read Muti-Plane Status
命令字71h
Control Logic &High Voltage Generator
控制逻辑及产生Flash所需的高电压 写入命令 地址或数据时 都需要将WE# CE#信号同时拉低 数据在WE#信号的上升沿被Nand Flash锁存 命令锁存信号CLE 地址锁存信号ALE用来分辨命令或地址
Nand Flash Array
存储部件
Page Register & S/A
页寄存器 当读写某页时 会将数据先读入/写入此寄存器
Y-Gating
IO Buffers & Latches
Global Buffers
Output Driver
硬件连接示意
8个IO引脚(IO0~IO7) 5个使能信号(nWE ALE CLE nCE nRE) 一个状态引脚(RDY/B) 一个写保护引脚(nWP) 通过8个IO引脚进行地址 命令或数据传输 写地址 数据 命令时 nCE nWE信号必须为低电平 在nWE信号的上升沿被锁存 命令锁存使能信号CLE和地址锁存信号ALE用来区分IO引脚上传输的是命令还是地址
地址序列
K9F1208U0M一页大小为528字节 列地址A0~A7可寻址范围是256字节 需要辅助手段才能完全寻址这528字节 将一页分为ABC三个区 A区为0~255字节 B区为256~511字节 C区为512~527字节 访问某页时 需要选定特定的区(使地址指针指向特定的区) 通过3个命令来实现: 命令00h让地址指针指向A区 命令01h让地址指针指向B区 命令50h让地址指针指向C区 命令00h和50h会使得访问flash的地址指针一直从A区或C区开始(除非发出了其他的修改地址指针命令) 命令01h的效果仅能维持一次 当前的读写 擦除 复位或者上电操作完成后 地址指针重新指向A区 写A区或C区的数据时 必须在发出80h之前发出命令00h或者50h 写B区的数据时 发出命令01h后必须紧接着就发出命令80h
地址指针操作
寄存器介绍
NFCONF
nand flash配置寄存器 用来设置nand flash的时序参数TACLS TWRPH0 TWRPH1 设置数据位宽和一些只读位 用来指示是否支持其他大小的页
NFCONT
nand flash控制寄存器 用来使能/禁止nand flash控制器 使能/禁止控制引脚信号nFCE 初始化ECC 锁存Nand flash
NFCMD
nand flash命令寄存器
NFADDR
nand flahs地址寄存器 写这个寄存器时 将对flash发出地址信号
NFDATA
nand flash数据寄存器 读/写此寄存器将启动对nand flash的读数据/写数据操作
NFSTAT
nand flash状态寄存器 位[0]-{0:busy 1:ready}
编程操作流程示例
内存管理
start_kernel
page_address_init
考虑支持高端内存业务: 初始化page_address_pool链表; 将page_address_maps数组元素按索 引降序插入page_address_pool链表; 初始化page_address_htable数组
setup_arch(&command_line)
paese_tags(tags)
parse_tag_mem32(tag)
arm_add_memory(tag->u.mem.start, tag->u.mem.size);
为meminfo添加内存信息 meminfo.bank[meminfo. nr_banks].start = start; meminfo.bank[meminfo. nr_banks].size = size; meminfo.bank[meminfo. nr_banks].node = 0; meminfo.nr_banks++;
init_mm.start_code = (unsigned long)_text;
init_mm.end_code = (unsigned long)_etext;
init_mm.end_data = (unsigned long)_edata;
init_mm.brk = (unsigned long)_end;
parse_early_param
注意,这里也会根据boot传入的command_line中信息来修正meminfo的内存信息 此处忽略(假定command_line不含内存信息)
early_initrd
randisk
phys_initrd_start = start;
phys_initrd_size = size;
paging_init(mdesc);
bootmem位图分配器初始化,I/O空间、中断向量空间映射, PKMAP空间映射初始化,"0"页面建立.
build_mem_type_table
此处没有深入查看ARM的页表项, ARM的页表项和unicore不同,我的疑问在于: ARM页表项中没有提供Dirty、Accessed位,那么kswap线程进行页面回收时, 它是怎样判定该操作哪些页?关于页表项就按unicore的理解,比较简单
sanity_check_meminfo
以一块2G DRAM为例,前期meminfo.nr_banks = 1; 开启高端内处支持,则需将meminfo分成两个bank, (为什么以bank作为变量名,DRAM的物理组成就有bank的概念, 此处需要作出区分)
struct membank *bank = &meminfo.bank[0];
memove(bank + 1, bank, sizef(*bank))
meminfo.nr_banks++;
bank[1].size -= VMALLOC_MIN - __va(bank->start);
bank[1].start = __pa(VMALLOC_MIN - 1) + 1;
bank[1].highmem = 1;
bank->size = VMALLOC_MIN - __va(bank->start);
prepare_page_table
将swapper_pg_dir处的页表清除(部分页表项已缓存在TLB中, 在bootmem_init中会间接调用create_mapping(&map),其中会再次建立)
bootmem_init
bootmem分配器初始化 完成位图分配器的建立 也使用了位图分配器进行内存分配
struct meminfo *mi = &meminfo;
sort(&mi->bank, mi->nr_banks, sizeof(mi->bank[0]),meminfo_cmp, NULL);
将meminfo中的bank数组元素按其start地址升序排序
int initrd_node = 0;
initrd_node = check_initrd(mi);
ramdisk在meminfo下的哪个bank
check_initrd(mi)
struct membank *bank = &mi->bank[i];
if (bank_phys_start(bank) <= phys_initrd_start && end <= bank_phys_end(bank)) initrd_node = bank->node;
return initrd_node
for_each_node(node)
UMA体系,只有一个node, 仅循环一次
find_node_limits(node, mi, &min, &node_low, &node_high);
此处两个bank(高、低) min:物理内存的最小页帧号(pfn) node_low:物理内存中低端内存的最大页帧号 node_high:物理内存中高端内存的最大页帧号 max_low:物理内存中低端内存的最大页帧号 max_high:物理内存中高端内存的最大页帧号
bootmem_init_node(node, mi, min, node_low);
业务在于:将低端内存部分与虚拟空间做固定偏移映射,而且采用一级页表完成; 采集位图分配器信息,并存放在contig_page_data.bdata内, 而且将位图分配器自身所占用的物理内存在位图分配器内标记为占用 此位图分配器暂时只管理低端内存(依据meminfo.bank[0],未使用meminfo.bank[1])
unsigned long boot_pfn;
unsigned long boot_pfn;
pg_data_t *pgdat;
int i;
for_each_nodebank(i, mi, node)
i依次取得meminfo中的bank索引
struct membank *bank = &mi->bank[i];
if(!bank->highmem) map_memory_bank(bank);
对于低端内存所在的bank,需执行map_memory_bank(bank);
struct map_desc map;
map.pfn = bank_pfn_start(bank);
map.virtual = __phys_to_virt(bank_phys_start(bank));
map.length = banks_phys_size(bank);
map.type = MT_MEMORY;
create_mapping(&map);
boot_pages = bootmem_bootmap_pages(end_pfn - start_pfn)
对于低端内存,先用位图进行管理,获取bit位所需的页数
boot_pfn = find_bootmap_pfn(node, mi, boot_pages);
获取内核结束地址的页号,作为寻找位图页的起始页
pg_data_t *pgdat = NODE_DATA(node);
init_bootmem_node(pgdat, boot_pfn, start_pfn, end_pfn);
init_bootmem_core(pgdat->bdata, boot_pfn, start_pfn, end_pfn);
bdata->node_bootmem_map = phys_to_virt(PFN_PHY(mapstart));
bdata->node_bootmem_map存放位图页的虚拟地址
bdata->node_min_pfn = start;
存放低端内存的起始物理页号
bdata->node_low_pfn = end;
存放低端内存的结束物理页号
link_bootmem(bdata);
将bdata按照node_min_pfn值的升序顺序插入到bdata_list链表中
unsigned long mapsize = bootmap_bytes(end - sart);
获取位图所需的字节数
memset(bdata->node_bootmem_map, 0XFF, mapsize);
将位图全部标记为已被占用(后期会再做修改, 注意文件系统位置)
for_each_nodebank(i, mi, node)
i依次取得meminfo中的bank索引
struct membank *bank = &mi->bank[i];
if(!bank->highmem) free_bootmem_node(pgdat, bank_phys_start(bank), bank_phys_size(bank));
对于低端内存所在的bank,需执行free_bootmem_node
mark_bootmem_node(pgdat->bdata, start, end, 0, 0)
start为低端内存起始物理页帧号, end为低端内存终止页帧号
__free(bdata, sidx, eidx);
sidx:低端内存起始页号(需减去bdata->node_min_pfn); eidx:低端内存终止页号(需减去bdata->node_min_pfn); 业务:将bdata中的页图标注为未被占用
reserve_bootmem_node(pgdat, boot_pfn << PAGE_SHIFT, boot_pages << PAGE_SHIFT, BOOTMEM_DEFAULT);
将位图分配器自身所占用的内存标记为被占用
mark_bootmem_node(pgdata->bdata, start, end, 1, 0);
start为低端内存起始物理页帧号, end为低端内存终止页帧号
sidx = start - bdata->node_min_pfn;
eidx = end - bdata->node_min_pfn;
__reserve(bdata, sidx, eidx, flags)
sidx:低端内存起始页号(需减去bdata->node_min_pfn); eidx:低端内存终止页号(需减去bdata->node_min_pfn); 业务:将bdata中的页图标注为被占用
reserve_node_zero(&contig_page_data)
reserve_bootmem_node(pgdat, __pa(_stext), _end - _stext, BOOTMEM_DEFAULT)
把内核中内核所占物理内存在位图分配器中标记为被占用
reserve_bootmem_node(pgdat, __pa(swapper_pg_dir), PTRS_PER_PGD * sizeof(pgd_t), BOOTMEM_DEFAULT)
把0进程的一级页表所占用的物理内存标记为被占用, 该一级页表是我们迄今为止惟一没有在内核编译时所占用的空间
bootem_reserve_initrd(node)
res = reserve_bootmem_node(pgdat, phys_initrd_start, phys_initrd_size, BOOTMEM_EXCLUSIVE);
initrd_start = __phys_to_virt(phys_initrd_start);
文件系统的虚拟起始地址
initrd_end = initrd_start + phys_initrd_size;
文件系统的虚拟结束地址
for_each_node(node)
UMA体系,只有一个node, 仅循环一次
find_node_limits(node, mi, &min, &max_low, &max_high);
此处两个bank(高、低) min:物理内存的最小页帧号(pfn) min:物理内存中低端内存的最大页帧号 max_low:物理内存中低端内存的最大页帧号 max_high:物理内存中高端内存的最大页帧号
unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
memset(zone_size, 0, sizeof(zone_size));
zone_size[0] = max_low - min;
ZONE_NORMAL区的页帧数
zone_size[ZONE_HIGHMEM] = max_high - max_low;
ZONE_HIGHMEM的页帧
memcpy(zhole_size, zone_size, sizeof(zhole_size));
从zhole_size的各个区中减去各个zone_size, 结果是zhole_size数组元素都为0
free_area_init_node(node, zone_size, min, zhole_size);
完善contig_page_data,并调用重量级函数free_area_init_core
pg_data_t *pgdat = &contig_page_data;
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
物理内存起始地址的页帧号
calculate_node_totalpages(pgdat, zones_size, zholes_size);
totalpages = 该pgdata下的各个区(zone)所含页的页数
pgdat->node_spanned_pages = totalpages;
realtotalpages = totalpages;
realtotalpages -= 该pgdata下各个区(zone)所含的洞的页数
对于连续型,实际上不存在“洞”
pgdat->node_present_pages = realtotalpages;
alloc_node_mem_map(pgdat);
为pglist_data建立mem_map(struct page数组)
start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
因为最后要迁移到伙伴系统,因此做了调整
end = pgdat->node_start_pfn + pgdat->node_spanned_pages;
end = ALIGN(end, MAX_ORDER_NR_PAGES);
size = (end - start) * sizeof(struct page);
为了管理pglist所跨越的总的页数目,首先获得需要申请的 struct page实例的内存大小
struct page *map =alloc_bootmem_node(pgdat, size);
依bootmem位图分配器申请内存
__alloc_bootmem_node(pgdat, size, SMP_CACHE_BYTES,_ _pa(MAX_DAM_ADDRESS))
ptr = alloc_bootmem_core(pgdat->bdata, size, align, goal, 0);
若位图中出现连续的未被占用的页数满足size的要求, 则将在位图中找到的相应bit位置1(标记被占用), 并将对应物理页清0,返回对应物理页的虚拟起始地址
return ptr;
pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
终于为pglist_data的node_mem_map域建立好了空间,所有的 struct page 实例均存于该空间内
mem_map = (&contig_page_data)->node_mem_map
free_area_init_core(pgdat, zones_size, zholes_size)
初始化pgdat下的各个zone及相关信息
init_waitqueue_head(&pgdat->kswapd_wait);
pgdat->kswapd_max_order = 0;
pgdat->nr_zones = 0;
for(j = 0; j < MAX_NR_ZONES; j++)
依次建立pglist_data下的每个zone
struct zone *zone = pgdat->node_zones + j;
unsigned long size, realsize, memmap_pages;
enum lru_list l;
size = zone_spanned_pages_in_node(nid, j, zones_size);
获取该区所跨越的页的总数
realsize = size - zone_absent_pages_in_node(nid, j,zholes_size);
获取该区实际可用的物理页的总数(除去“洞”)
memmap_pages = PAGE_ALIGN(size * sizeof(struct page))>> PAGE_SHIFT;
获取因管理该区所使用的struct page实例的内存大小
realisze -= memmap_pages;
获取该区实际可用的物理页的总数(除去管理结构所占用页数)
if(!is_highmem_idx(j)) nr_kernel_pages += realsize;
将非高端内存区中,还未被所占用的页数计入nr_kernel_pages
nr_all_pages += realsize;
将所有还未被占用的页数计入nr_all_pages
开始为pglist_data下的各个区建立信息
zone->spanned_pages = size;
将该区跨越的页数存入pglist_data下相应的 zone->spanned_pages
zone->present_pages = realsize;
将该区可以使用的实际页数存入pglist_data下相应的 zone->present_pages
zone->name = zones_names[j];
为pglist_data下相应的zone添加名称
spin_lock_init(&zone->lock);
spin_lock_init(&zone->lru_lock);
zone->zone_pgdat = pgdat;
记录zone所在的pglist_data
zone->pre_priority = DEF_PRIORITY
zone_pcp_init(zone);
for_each_lru(l) {INIT_LIST_HEAD(&zone->lru[l].list); zone->reclaim_stat.nr_saved_scan[l] = 0;}
zone->reclaim_stat.recent_rotated[0] = 0;
zone->reclaim_stat.recent_rotated[1] = 0;
zone->reclaim_stat.recent_scanned[0] = 0;
zone->reclaim_stat.recent_scanned[1] = 0;
memset(zone->vm_stat, 0, sizeof(zone->vm_stat);
zone->flags = 0;
setup_usemap(pgdat, zone, size);
将管理该zone中的pageblock的比特位图的起始地址存入zone->pageblock_flags
unsigned long usemapsize = usemap_size(zonesize);
每个zone中的页按pageblock被分成几个block, 一个pageblock所含页数为(1 << (MAX_ORDER - 1)), 每个pageblock需要几个bit位来存储信息(这几个bit位的作用,暂时不知道) usemap_size的作用就在于计算该zone中的pageblock数所对应的bit位数,并转化成字节数
zone->pageblock_flags = alloc_bootmem_node(pgdat, usemapsize);
将管理该zone中的pageblock的比特位图的起始地址存入zone->pageblock_flags
init_currently_empty_zone(zone, zone_start_pfn,size, MEMMAP_EARLY);
分配zone的hash资源(用于进程请求页时阻塞); 初始化zone的free_area,以及free_area元素下 的各类free_list
zone_wait_table_init(zone, size);
初始化zone下的hash表(用于进程等待页资源时使用, 我们可以将等待对列存放在各个struct page内, 但是这样会使struct page结构体空间太大,造成浪费, 因此放在了zone中,并用hash表实现)
zone->wait_table_hash_nr_entries = wait_table_hash_nr_entries(size);
获取所需的hash表的数组元素个数
zone->wait_table_bits = wait_table_bits(zone->wait_table_hash_nr_entries);
获取值wait_table_hash_nr_entries中首个bit位值为1的序号 (从最低位0开始记起,例如1,则获取值为0)
alloc_size = zone->wait_table_hash_nr_entries * sizeof(wait_queue_head_t);
获取所需的hash表的数组所需空间大小
zone->wait_table = (wait_queue_head_t *)alloc_bootmem_node(pgdat, alloc_size)
分配hash表数组空间
init_waitqueue_head(zone->wait_table[0...wait_table_hash_nr_entries]);
初始化各个队列头
pgdat->nr_zones = zone_idx(zone) + 1;
更新pgdat下的zone的数目
zone->zone_start_pfn = zone_start_pfn;
zone_init_free_lists(zone);
for(order = 0; order < MAX_ORDER; order++) for(type = 0; type < MIGRATE_TYPES; type++) {INIT_LIST_HEAD(&zone->free_area[order].free_list[type]); zone->free_area[order].nr_free = 0;}
每个zone除了被分为pageblock外,还被分为数个free_area, 每个free_area又被分为不同类型的free_list 各个free_area下的各自的free_list所含的页数是下同的
memmap_init_zone(size, nid, j, zone_start_pfn, MEMMAP_EARLY);
修正最高的页帧数highest_memap_pfn; 获取zone所管理的页对应的struct page实例, 在struct page中的flags中标注各种标志; 将页所隶属的pageblock的位图标记为MIGRATE_MOVABLE;
struct page *page = NULL;
unsigned long end_pfn = start_pfn + size;
unsigned long pfn = 0;
struct zone *z = NULL;
if(highest_memmap_pfn < end_pfn - 1) highest_memap_pfn = end_pfn - 1;
修正最高的页帧数
z = &NODE_DATA(nid)->node_zones[zone];
获取需要操作的zone
for(pfn = start_pfn; pfn < end_pfn; pfn++)
page = pfn_to_page(pfn);
获取页帧号所对应的struct page实例地址
set_page_links(page, zone, nid, pfn);
set_page_zone(page, zone);
在struct page->flags中记录该页是属于哪个zone
set_page_node(page, node);
set_page_section(page, pfn_to_section_nr(pfn);
对于单个node,实际上无需在page->flags中存储node,section信息
init_page_count(page)
atomic_set(&page->_count, 1);
page的访问计数,当为0时,说明page是空闲的,当大于0的时候 说明page被一个或多个进程正在使用该页或者有进程在等待该页
reset_page_mapcount(page)
atomic_set(&(page)->_mapcount, -1);
SetPageReserved(page);
INIT_LIST_HEAD(&page->lru)
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
实际上此处是先测试,若满足条件再执行,一般直接执行也没问题。 我们已经知道,内存中的一些页隶属于同一个pageblock, 而且内存所对应的zone中,已存储了管理pageblock的位图 pageblock_flags的起始地址。此函数的任务在于将每个page 所属于的pageblock标记为MIGRATE_MOVABLE (即:属于该pageblock 中的页均MIGRATE_MOVABLE)
zone_start_pfn += size;
high_memory = __va((max_low << PAGE_SHIFT) - 1) + 1;
获取高端内存的起始虚拟地址
max_low_pfn = max_low - PHYS_PFN_OFFSET;
低端内存所对应的页帧数
max_pfn = max_high - PHYS_PFN_OFFSET;
总共的物理内存页帧数
devicemaps_init(mdesc);
为中断向量和I/O空间的虚拟与物理地址建立映射关系
void *vectors =alloc_bootmem_low_pages(PAGE_SIZE);
为中断向量申请内存空间,实际上仍是通过alloc_bootmem_core函数完成内存分配
for(addr = VMALLOC_END; addr; addr += PGDIR_SIZE) pmd_clear(pmd_off_k(addr))
将VMALLOC_END ~ 4G的页表映射全部清除
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = CONFIG_VECTORS_BASE;
map.length = PAGE_SIZE;
map.type = MT_HIGH_VECTORS;
create_mapping(&map);
将物理地址和虚拟地址建立映射关系,此处即为: 为中断向量虚拟地址寻找一个物理页面,并且建立映射关系
>mdesc->map_io
为I/O空间建立映射,注意页表中cache的属性, 这部分完全和SOC设计相关, 将需要建立的映射关系存放于一个struct map_desc实例数组中, 调用create_mapping完成I/O空间映射
local_flush_tlb_all
flush_cache_all
同步硬件缓存与物理内存
kmap_init
永久映射区域保留,对于ARM,该区域位于3G-4M ~ 3G
pmd_t *pmd = pmd_off_k(PKMAP_BASE);
获取PKMAP_BASE虚拟地址所对应的一级页表项的的地址
pte_t *pte = alloc_bootmem_low_pages( PTRS_PER_PTR * sizeof(pte_t);
PKMAP空间需做二级页表映射,此处获得二级页表的起始地址
__pmd_populate(pmd, __pa(pte) | _PAGE_KERNEL_TABLE);
在相应的一级页表项中计入二级页表项的物理地址,并设置好一级页表项的属性
pkmap_page_table = pte + PTRS_PER_PTE
记录PKMAP虚拟空间的二级页表项的物理末尾地址
top_pmd = pmd_off_k(0xffff0000);
记录0xffff0000相应的一级页表项地址
zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
分配一个“0”页面
empty_zero_page = virt_to_page(zero_page);
管理"0"页面所对应的struct page虚拟地址
__flush_dache_page(NULL, empty_zero_page);
request_standart_resources(&meminfo, mdesc);
kernel_code.start = virt_to_phys(_text);
kernel_code.end = virt_to_phys(_etext - 1);
kernel_data.strt = virt_to_phys(_data);
kernel_data.end = virt_to_phys(_end - 1);
for(i = 0; i < mi->nr_banks; i++)
此处是将meminfo的资源放入iomem_resource树中, 同时将内核镜像资源也放入iomem_resource树中. 注意内核镜像资源如何放入
struct *res = NULL;
res = alloc_bootmem_low(sizeof(*res));
res->name = "System RAM"
res->start = mi->bank[i].start;
res->end = mi->bank[i].start + mi->bank[i].size - 1;
res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;
request_resource(&iomem_resource, res)
将内存资源放入iomem_resource树中
if(kernel_code.start >= res->start &&kernel_code.end <= res->end) request_resource(res, &kernel_code);
if(kernel_data.start >= res->start && kernel_data.end <= res->end) request_resource(res, &kernel_data);
将内核镜像资源放入iomem_resource树中
smp_init_cpus
cpu_init
为每个核的irq、abt、und状态设置栈 每个状态只有12字节栈空间(static struct stack stacks[NR_CPUS]) 因为 基本所有的事情都在svc状态即被处理
tcm_init
early_trap_init
memcpy(vectors, __vectors_start,__vectors_end - __vectors_start);
拷贝中断向量
memcpy(vectors + 0x200, __stubs_start,__stubs_end - __stubs_start);
拷贝中断向量
memcpy(vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
ARM的特殊之处,为用户态进行原子操作提供接口, 即用户态直接进入该部分(3G~4G), 中断处将做特别检查和相应的处理.
memcpy(KERN_SIGRETURN_CODE, sigreturn_codes, sizeof(sigreturn_codes));
memcpy(KERN_RESTART_CODE, syscall_restart_code, sizeof(syscall_restart_code));
flush_icache_range(vectors, vectors + PAGE_SIZE);
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
物理内存布局
linux 内核占用物理内存最前段的一部分,图中 end 标示出内核模块结束的位置 随后是高速缓冲区,它的最高内存地址为 4M 高速缓冲区被显示内存和 ROM BIOS 分成两段 剩余的内存部分 称为主内存区 若系统中还存在 RAM 虚拟盘时,则主内存区前段还要扣除虚拟盘所占的内存空间 当需要使用主内存区时就需要内存管理程序申请,所申请的基本单位是内存页
进程在线性地址空间中的分布
每个进程在线性地址中都是从 nr*64M 的地址位置开始( nr 是任务号) 占用线性地址空间的范围是64M 其中最后部的环境参数数据块最长为 128K,其左面起始堆栈指针 在进程创建时 bss 段的第一页被初始化为全0
高速缓冲
虚拟盘
主内存区
linux-0.11实现
接口
memory.c
用于内存的初始化操作、页目录和页表的管理和内核其它部分对内存的申请处理过程
do_no_page
是页异常中断过程中调用的缺页处理函数 把需要的页面从块设备中取到内存指定位置处 首先判断指定的线性地址在一个进程空间中相对于进程基址的偏移长度值 如果它大于代码加数据长度,或者进程刚开始创建 则立刻申请一页物理内存,并映射到进程线性地址中,然后返回 接着尝试进行页面共享操作,若成功,则立刻返回 否则申请一页内存并从设备中读入一页信息 若加入该页信息时,指定线性地址+1 页长度超过了进程代码加数据的长度,则将超过的部分清零 然后将该页映射到指定的线性地址处
do_wp_page
页异常中断过程中调用的页写保护处理函数 复制被写的页面也取消了对页面的共享 它首先判断地址是否在进程的代码区域 若是则终止程序(代码不能被改动);然后执行写时复制页面的操作
get_free_page
用于在主内存区中申请一页空闲内存页,并返回物理内存页的起始地址 它首先扫描内存页面字节图数组 mem_map[],寻找值是 0 的字节项(对应空闲页面) 若无则返回 0 结束,表示物理内存已使用完 若找到值为 0 的字节,则将其置 1,并换算出对应空闲页面的起始地址 然后对该内存页面作清零操作 最后返回该空闲页面的物理内存起始地址
free_page
用于释放指定地址处的一页物理内存 它首先判断指定的内存地址是否<1M,若是则返回 因为 1M 以内是内核专用的 若指定的物理内存地址大于或等于实际内存最高端地址,则显示出错信息 然后由指定的内存地址换算出页面号: (addr - 1M)/4K 接着判断页面号对应的 mem_map[]字节项是否为 0, 若不为 0,则减 1 返回 否则对该字节项清零,并显示“试图释放一空闲页面”的出错信息
free_page_tables
以一个页表对应的物理内存块为单位 释放指定线性地址和长度(页表个数)对应的物理内存页 它首先判断指定的线性地址是否在 4M 的边界上,若不是则显示出错信息,并死机 然后判断指定的地址值是否=0 若是,则显示出错信息“试图释放内核和缓冲区所占用的空间”,并死机 接着计算在页目录表中所占用的目录项数 size,也即页表个数,并计算对应的起始目录项号 然后从对应起始目录项开始,释放所占用的所有 size个目录项 同时释放对应目录项所指的页表中的所有页表项和相应的物理内存页 最后刷新页变换高速缓冲
copy_page_tables
以一个页表对应的物理内存块为单位 复制指定线性地址和长度(页表个数)内存对应的页目录项和页表 从而被复制的页目录和页表对应的原物理内存区被共享使用 该函数首先验证指定的源线性地址和目的线性地址是否都在 4Mb 的内存边界地址上 否则就显示出错信息,并死机 然后由指定线性地址换算出对应的起始页目录项( from_dir, to_dir) 并计算需复制的内存区占用的页表数(即页目录项数) 接着开始分别将原目录项和页表项复制到新的空闲目录项和页表项中 页目录表只有一个,而新进程的页表需要申请空闲内存页面来存放 此后再将原始和新的页目录和页表项都设置成只读的页面 当有写操作时就利用页异常中断调用,执行写时复制操作 最后对共享物理内存页对应的字节图数组 mem_map[]的标志进行增 1 操作
put_page
用于将一指定的物理内存页面映射到指定的线性地址处 它首先判断指定的内存页面地址的有效性 要在 1M 和系统最高端内存地址之外 否则发出警告 然后计算该指定线性地址在页目录表中对应的目录项 此时若该目录项有效( P=1),则取其对应页表的地址 否则申请空闲页给页表使用,并设置该页表中对应页表项的属性 最后仍返回指定的物理内存页面地址
get_empty_page
用于取得一页空闲物理内存并映射到指定线性地址处
page.s
包含内存页异常的中断处理过程( int 14) 主要实现了对缺页和页写保护的处理
malloc.c
malloc
1. 首先搜索目录,寻找适合请求内存块大小的目录项对应的描述符链表 当目录项的对象字节长度大于请求的字节长度,就算找到了相应的目录项 如果搜索完整个目录都没有找到合适的目录项,则说明用户请求的内存块太大 2. 在目录项对应的描述符链表中查找具有空闲空间的描述符 如果某个描述符的空闲内存指针freeptr 不为 NULL,则表示找到了相应的描述符 如果没有找到具有空闲空间的描述符,那么就需要新建一个描述符。 新建描述符的过程如下: a. 如果空闲描述符链表头指针还是 NULL 的话,说明是第一次调用 malloc()函数 此时需要init_bucket_desc()来创建空闲描述符链表 b. 然后从空闲描述符链表头处取一个描述符,初始化该描述符,令其对象引用计数为 0 对象大小等于对应目录项指定对象的长度值,并申请一内存页面 让描述符的页面指针 page 指向该内存页,描述符的空闲内存指针 freeptr 也指向页开始位置 c. 对该内存页面根据本目录项所用对象长度进行页面初始化,建立所有对象的一个链表 也即每个对象的头部都存放一个指向下一个对象的指针 最后一个对象的开始处存放一个 NULL指针值 d. 然后将该描述符插入到对应目录项的描述符链表开始处 3. 将该描述符的空闲内存指针 freeptr 复制为返回给用户的内存指针,然后调整该 freeptr 指向描述符 对应内存页面中下一个空闲对象位置,并使该描述符引用计数值增 1
使用存储桶原理进行内存分配管理
基本思想是对不同请求的内存块大小(长度),使用存储桶目录(下面简称目录)分别进行处理 比如对于请求内存块的长度在 32 字节或 32 字节 以下但大于 16 字节时,就使用存储桶目录第二项对应的存储桶描述符链表分配内存块
空闲存储桶描述符链表结构
在第一次调用 malloc()函数时,首先要建立一个页面的空闲存储桶描述符(下面简称描述符)链表 其中存放着还未使用或已经使用完毕而收回的描述符 其中 free_bucket_desc是链表头指针 从链表中取出或放入一个描述符都是从链表头开始操作 当取出一个描述符时,就将链表头指针所指向的头一个描述符取出 当释放一个空闲描述符时也是将其放在链表头处
free_s
用于回收用户释放的内存块 基本原理是首先根据该内存块的地址换算出该内存块对应页面的地址(用页面长度进行模运算) 然后搜索目录中的所有描述符,找到对应该页面的描述符 将该释放的内存块链入 freeptr 所指向的空闲对象链表中,并将描述符的对象引用计数值减 1 如果引用计数值此时等于零,则表示该描述符对应的页面已经完全空出 可以释放该内存页面并将该描述符收回到空闲描述符链表中
页目录和页表的管理
页目录表和页表结构示意图
内存分页管理是通过页目录表和内存页表所组成的二级表进行的 页目录表和页表的结构是一样的,表项结构也相同 页目录表中的每个表项用来寻址一个页表 而每个页表项用来指定一页物理内存页 当指定了一个页目录项和一个页表项,我们就可以唯一地确定所对应的物理内存页 所有进程都使用一个页目录表,而每个进程都有自己的页表
页目录和页表表项结构
每个页表项对应的物理内存页是由页表项中页框地址内容确定的 也即是由内存管理程序通过设置页表项确定的 每个表项由页框地址、访问标志位、脏(已改写)标志位和存在标志位等构成
页框地址
指定了一页内存的物理起始地址 因为内存页是位于 4K 边界上的,所以其低 12 比特总是 0 因此表项的低 12 比特可作它用 在一个页目录表中,表项的页框地址是一个页表的起始地址 在第二级页表中,页表项的页框地址则包含期望内存操作的物理内存页地址
已修改D
提供有关页使用的信息 除了页目录项中的已修改位,这些比特位将由硬件置位,但不复位 在向一个二级页表项所涵盖的地址进行写操作之前,处理器将设置该二级页表项的已修改位 而页目录项中的已修改位是不用的 当所需求的内存超出实际物理内存量时 内存管理程序就可以使用这些位来确定那些页可以从内存中取走,以腾出空间 内存管理程序还需负责检测和复位这些比特位
已访问A
提供有关页使用的信息 由硬件置位,但不复位 在对一页内存进行读或写操作之前, CPU 将设置相关的目录和二级页表项的已访问位 当所需求的内存超出实际物理内存量时 内存管理程序就可以使用这些位来确定那些页可以从内存中取走,以腾出空间 内存管理程序还需负责检测和复位这些比特位
用户/超级用户位U/S
用于分页级的保护机制,是由 CPU 在地址转换过程中同时操作的
读/写位R/W
用于分页级的保护机制,是由 CPU 在地址转换过程中同时操作的
存在位P
确定了一个页表项是否可以用于地址转换过程 P=1 表示该项可用 当目录表项或第二级表项的 P=0 时,则该表项时无效的,不能用于地址转换过程 当 CPU 试图使用一个页表项进行地址转换时,如果此时任意一级页表项的 P=0 则处理器就会发出页异常信号 此时缺页中断异常处理程序就可以把所请求的页加入到物理内存中,并且导致异常的指令会被重新执行
线性地址变换示意图
一个 32 位的线性地址被分成了三个部分,分别用来指定一个页目录项、 一个页表项和对应物理内存页上的偏移地址, 从而能间接地寻址到线性地址指定的物理内存位置 线性地址的位 31-22 共 10 个比特用来确定页目录中的目录项 位 21-12 用来寻址页目录项指定的页表中的页表项 最后的 12 个比特正好用作页表项指定的一页物理内存中的偏移地址 若该目录项有效(被使用),则该目录项中的页框地址指定了一个页表在物理内存中的基址 那么结合线性地址中的页表项指针 若该页表项有效,则根据该页表项中的指定的页框地址, 就可以最终确定指定线性地址对应的实际物理内存页的地址
线性地址对应的物理地址
如果需要从一个已知被使用的物理内存页地址,寻找对应的线性地址 则需要对整个页目录表和所有页表进行搜索 若该物理内存页被共享,我们就可能会找到多个对应的线性地址来 对于第一个进程(任务 0),其页表是在页目录表之后,共 4 页 对于应用程序的进程,其页表所使用的内存是在进程创建时向内存管理程序申请的,因此是在主内存区中
CR3
一个系统中可以同时存在多个页目录表,而在某个时刻只有一个页目录表可用 当前的页目录表是用CPU 的寄存器 CR3 来确定的,它存储着当前页目录表的物理内存地址
内存初始化
应用相关
磁盘管理
LVM
LVM主要思想是允许组织多个物理磁盘到一个分区 将很多磁盘组成卷组 然后在卷组上随意划分逻辑卷组
概念
卷组-VG
物理磁盘-PV
逻辑卷组-LV
存储单元-PE
逻辑卷寻址基本单位-LE
卷组描述符区域-VGDA
机制
系统启动LVM时激活VG 并将VGDA加载至内存 来识别LV的实际物理存储位置 当系统进行IO操作时 会根据VGDA建立的映射机制来访问实际的物理位置 LVM并不是取代文件系统 而是建立在文件系统基础上的分区手段
优缺点
优点
快照
快照同样采用写时拷贝 通过一个写时拷贝表记录新写入和修改的PE 而在修改时并不修改原有的PE 而是使用新的 如此就可以回溯倒快照了
可伸缩性
可以无须停机就能调整分区的大小
缺点
当分区的物理PE不连续时 会造成极大的新能损耗
RAID
用磁盘阵列的方式做数据存储
内存使用
调优
与磁盘回收相关调优
当空闲内存比阈值dirty_background_ratio低时 唤醒flusher线程开始将脏页写回磁盘调高阈值 可以减轻系统中内存不足的压力 dirty_background_ratio 占全部内存的百分比
与缓存相关的调优
交换与缓存的平衡可经/proc/sys/vm/swappiness来调整(范围值0~100) 此值越高 意味着越偏向将页面缓存数据保存在内存中且越容易进行交换 此值越低 意味着越偏向修剪页面缓存区而不进行交换
查看
系统当前可用内存
当前真正可用的物理内存=MemFree+Buffers+cached 已使用的物理内存=系统内存MemTotal-当前可用的物理内存
free
cat /proc/meminfo
MemTotal
除去启动前被BIOS保留的内存 剩下可供kernel支配的内存 这个值在系统运行期间一般是固定不变的
MemFree
表示当前没有被使用的内存大小 一般比较小(因为很多被用于文件缓存)
MemAvailable
包含其他的在用到的时候可以被回收的内存 例如命名页的文件缓存(Cached) slab或者其他的缓存(Buffer)
Buffers
一些网络传输或者虚拟设备之类的数据传输 后面没有真实的文件 这部分缓存就放在Buffer里 内核还用buffer存储一些文件元数据(比如目录inode)
Cached
内核中用于缓存文件内容的内存(缓存层的核心数据存储位置) 都是要求基于文件的背后缓存 例如tmpfs, mmap映射或共享库 打开进程文件的mapped内存 拷贝文件产生的没有映射的内存等
SwapCached
文件缓存的一种 是那些既缓存到swap文件又还在内存中缓存的数据(一般是先缓存出去 再加载回来) 如果tmpfs的内容被进行了swap 则该部分内容不再属于Cached 转而属于SwapCached
Active
Active(anon)+Active(file)
Inactive
Inactive(anon)+Inactive(file)
Active(anon)
匿名页(没有对应具体文件缓存的页) 包括应用进程申请使用的内存 ,buffer ,Shared memory 和tmpfs 这里统计不包括被mlock的内存
Inactive(anon)
Active(file)
命名页就是有文件在磁盘 也有内存缓存的这部分内存 占用情况如 文件预读 交换分区
Inactive(file)
Unevictable
包括了被lock的页 ,LRU_UNEVICTABLE的lru页面
Mlocked
被锁定的内存 (mlock调用)
SwapTotal
swap文件的总大小
SwapFree
swap的可用大小
Dirty
被标记为dirty的页面是要准备写回的 这个写回不但是文件的写回 还要把swap cache内存为dirty部分让交换分区进行交换
Writeback
正准备执行回写的内存 系统中全部的dirty pages=(Dirty + NFS_Unstable +Writeback)
AnonPages
匿名页 mmap系统调用的private映射属于匿名页 而public映射属于Cached 匿名页与进程相关 进程退出 匿名页释放
Mapped
Cached的页面很多是没有被映射的(如复制文件的残留cache内存) 很多是被实际映射的(如共享库 可执行文件 mmap的文件) 这个域就是Cached中被map的内存空间大小
Shmem
共享内存 tmpfs文件系统也属于这个类别
Slab
slab内存分配系统所有可分配内存大小
SReclaimable
可以被回收的slab内存
SUnreclaim
不可以被回收的slab内存
KernelStack
每一个用户线程都有对应的内核栈
PageTables
用于将虚拟地址转换为物理地址的表
NFS_Unstable
nfs文件系统未写入磁盘的缓存页
Bounce
用于高低端内存的数据拷贝 (现代CPU几乎不使用)
WritebackTmp
CommitLimit
Committed_AS
VmallocTotal
一共被使用vmalloc接口申请的内存总计 可以通过/proc/vmallocinfo看到内存是被谁申请的
VmallocUsed
被实际使用的内存大小
VmallocChunk
HardwareCorrupted
系统检查发现的有问题的硬件内存区域大小
AnonHugePages
透明大页的单独统计
CmaTotal
CMA连续内存分配器的内存分配情况
CmaFree
HugePages_Total
开启了大页之后 显示大页的使用情况
HugePages_Free
HugePages_Rsvd
HugePages_Surp
HugePagesize
DirectMap4K
为了加速内存页的映射 很多大页被直接映射 而不使用TLB 这里就是显示直接映射的内存大小
DirectMap2M
进程相关占用
cat /proc/{pid}/maps
常见问题
内存泄露
内存泄露是申请了的内存没有释放
内存空洞
内存空洞是申请并释放了的内存由于不处于堆顶而无法返还给系统 但这些内存还是能留给进程自身使用
地址空间
启动规划
实模式地址空间 逻辑地址=16位段寄存器:16位偏移
引导地址空间 工作在32位保护模式下 未分页 使用引导GDT且基地址为0 虚拟地址=物理地址
入口地址空间 工作在32位保护模式下 4MB分页 使用引导GDT 使用入口页表entrypgdir 1.虚拟地址[0,4MB)映射到物理地址[0,4MB) 虚拟地址=物理地址 2.虚拟地址[KERNBASE,KERNBASE+4MB)映射到物理地址[0,4MB) 虚拟地址=物理地址+KERNBASE
内核虚拟地址空间 工作在32位保护模式下 4KB分页 使用内核GDT的内核段 使用内核页表kpgdir 映射方式由kmap[]数组定义
用户虚拟地址空间 工作在32位保护模式下 4KB分页 使用内核GDT的用户段 使用fork等接口创建进程页表pgdir
分类示意
虚拟地址空间示意图
虚拟地址空间分配
进程虚拟地址数据结构关系
内存区组成部分
程序的可执行代码
程序的初始化数据
程序的未初始化数据
初始程序栈(用户栈)
所需共享库的可执行代码和数据
堆(程序动态请求的内存)
线性地址空间使用示意图
逻辑地址空间
进程代码和数据在其逻辑地址空间的分布
各任务的地址映射示意
任务0
任务1
其他任务