暑假读书笔记第五天
今日文章:
小林coding: Linux 内核 vs Windows 内核
小林coding:为什么要有虚拟内存?
b站数学建模老哥:《操作系统》内存管理
目录
- 操作系统结构
- Linux 内核
- Windows 内核
- 内存管理
- 虚拟内存
- 地址映射的作用
- 地址映射的实现
- 内存分段
- 内存分页
- 段页式内存管理
- 地址映射过程
- 内存扩充概述
- 虚拟存储技术
- 虚拟内存技术
- 缺页置换算法
- Linux 内存布局
其他:
往期打卡
操作系统结构
Linux 内核
内核作为应用连接硬件设备的桥梁,应用程序只需关心与内核交互,不用关心硬件的细节。
现代操作系统,内核一般会提供六项基本能力:
- 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力;
- 管理内存,决定内存的分配和回收,也就是内存管理的能力;管理硬件设备,
- 为进程与硬件设备之间提供通信能力,也就是硬件通信能力;
- 文件管理,内核在非结构化的存储硬盘之上建立了一个结构化的文件系统,结果是文件的抽象非常多地在整个系统中应用。
- 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。
- 网络管理,报文在某一个进程接手之前必须被收集,识别,分发,系统负责在程序和网络接口之间递送数据报文,它必须根据程序的网络活动来控制程序的执行。另外,所有的路由和地址解析问题都在内核中实现。
用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。因此,当程序使用用户空间时,我们常说该程序在用户态执行,而当程序使内核空间时,程序则在内核态执行。
应用程序如果需要进入内核空间,就需要通过系统调用,使用系统调用会产生中断,CPU收到中断后会跳转到内核态中断处理程序开始执行内核程序。内核处理完成后再把CPU执行权交回给用户程序,回到用户态。
Linux 内核设计的理念主要有这几个点:
-
MultiTask,多任务,可以并发并行执行程序
-
SMP,对称多处理,每个 CPU 的地位是相等的,对资源的使用权限也是相同的,每个程序都可以被分配到任意一个 CPU 上被执行。
-
ELF,可执行文件链接格式
-
它是 Linux 操作系统中可执行文件的存储格式,ELF 把文件分成了一个个分段,大致如下:
-
-
Monolithic Kernel,宏内核,系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。
-
与宏内核相反的内核架构是微内核,微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等。这样服务与服务之间是隔离的,单个服务出现故障或者完全攻击,也不会导致整个操作系统挂掉,提高了操作系统的稳定性和可靠性。
-
还有一种内核叫混合类型内核,它的架构有点像微内核,内核里面会有一个最小版本的内核,然后其他模块会在这个基础上搭建,然后实现的时候会跟宏内核类似,也就是把整个内核做成一个完整的程序,大部分服务都在内核中,这就像是宏内核的方式包裹着一个微内核。
-
三种内核架构内核态与用户态中组件的情况大致如下:
-
Windows 内核
当今 Windows 7、Windows 10 使用的内核叫 Windows NT,NT 全称叫 New Technology。
Windows 和 Linux 一样,同样支持 MultiTask(多任务) 和 SMP(对称多处理)
Window 的内核设计是混合型内核,图中可以看到内核中有一个 MicroKernel 模块,这个就是最小版本的内核,而整个内核实现是一个完整的程序,含有非常多模块。
Windows 的可执行文件格式叫 PE,称为可移植执行文件,扩展名通常是.exe
、.dll
、.sys
等。
PE 的结构如下:
内存管理
虚拟内存
现代操作系统内存管理涵盖分配回收、地址转换、内存扩充、共享与保护,核心是协调进程对内存资源的使用。
其核心机制 —— 隔离、保护、分配、扩充 —— 均建立在虚拟内存之上:地址转换实现地址映射,虚拟存储技术实现内存扩充。
早期操作系统内存管理不依赖虚拟内存,程序直接操作物理地址,内存扩充能力匮乏;隔离靠静态固定分区,保护仅依赖简单边界检查;因无法实现非连续分配,内存利用率低下。
地址映射的作用
51单片机没有硬件级的地址隔离或权限控制,烧录程序到ROM或程序操作RAM读写数据都直接操作物理地址
也就是说程序空间和数据空间都是共享的,而且没有内存管理单元(MMU,Memory Management Unit)无法避免程序间干扰
内存管理上的缺陷使得单片机难以并发执行多任务。还有一些原因如下:
硬件不支持高效任务切换,难以实现多程序的并发调度;仅依靠定时器中断,串口中断和外部中断,中断系统简单,难支持可靠的抢占式调度;单片机资源有限。
操作系统通过虚拟地址映射,隔离进程地址空间,解决多任务内存访问冲突。
程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
实际硬件里面的空间地址叫物理内存地址(Physical Memory Address)
操作系统会为每个进程独立分配一套虚拟地址空间
地址映射的实现
进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU,Memory Management Unit)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:
操作系统虚拟地址映射通过分页(Paging)和分段(Segmentation)两种基础机制实现,现代操作系统通常结合两者形成段页式架构
内存分段
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。
- 代码段(Code Segment):存放可执行指令(如函数体)和全局常量(如字符串常量),只读、可执行,生命周期与程序一致,也叫只读数据段;
- 数据段(Data Segment):存放已初始化的全局变量、静态变量(如
int a = 10;
),可读可写,生命周期与程序一致; - 栈段(Stack Segment):存放局部变量、函数参数、返回地址等,由编译器自动分配释放,遵循 “先进后出”,生命周期与函数调用栈绑定;
- 堆段(Heap Segment):存放动态分配的内存(如
malloc()
/new
申请的空间),由程序员手动分配释放,生命周期灵活。
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量
- 段选择因子保存在段寄存器里,段选择因子里面最重要的是段号,用作段表索引。段表中会保存对应段的基地址,段的界限和特权等级
- 虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量合法,就将基地址加上偏移量得到物理内存地址
每个进程都有自己的段表,由操作系统管理内存分配回收,解决了多任务访问内存冲突的问题,但由于分段大小不一,会产生内存碎片的问题,而且内存交换效率低
虽然现代操作系统采用离散内存管理技术,但是分段管理中 “段内必须连续”,离散管理只是段间离散,不必顺序分配内存
内存碎片分为内部碎片和外部碎片
- 内存分段管理按需分配内存,所以不会出现分配内存冗余的情况,没有内部内存碎片
- 由于段长不固定,多个段未必能恰好使用所有内存空间,会产生多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题
解决外部内存碎片的技术实现之一是内存交换
可以把音乐程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里,但读会时要让其紧挨着游戏占用的那512MB,这样就能空出连续的一段空间用来装载新程序。
用于内存交换的空间,在 Linux 系统里,就是 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。
但由于段长不固定,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿,内存交换效率低。
内存分页
如果分配内存不再按段长分配,而是以固定大小为单位分配,就可以解决外部碎片的问题,即内存分页(Paging)。
页与页之间是紧密排列的,所以不会有外部碎片,但段长不一定是页长的倍数,所以分配的页往往会有冗余内存,即有内部碎片产生
如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
但由于每个进程都需要存储自己的页表以访问理论上的虚拟地址空间,简单分页会有空间上的缺陷
在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。1个进程4MB,100个进程就是400MB,会占用相当一部分内存。而64位环境下虚拟地址空间可以达到TB甚至更高,会占用更多内存。
简单分页页表占用空间的关键是只有一级页表,所以无论实际用多少空间都要为该进程分配足以映射整个虚拟地址空间的页表,假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作。
采用多级列表的形式可以缓解页表浪费的情况,开始只创建一级列表,按需拓展下级具体页表。虽然映射相同的空间,多级列表需要存储更多的页表,但大多数进程往往不会占满虚拟地址空间,所以多级列表一般不会被完整创建。
多级列表通过按需创建缓解了页表内存浪费的问题
对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:
- 全局页目录项 PGD(Page Global Directory);
- 上层页目录项 PUD(Page Upper Directory);
- 中间页目录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry)
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。
可以利用局部性原理,把最常访问的几个页表项存储到访问速度更快的硬件来降低查表开销,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。
段页式内存管理
内存分段和内存分页并不是对立的,可以先分段再分页,组合起来在同一个系统中使用的,通常称为段页式内存管理。
- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
段页式地址变换的数据结构和多级页表相似
每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号
段页式地址变换中要得到物理地址须经过三次内存访问:
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
可用软、硬件相结合的方法实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的利用率。
地址映射过程
有快表(Translation Lookaside Buffer,旁路转换缓冲器)的分页存储方式地址映射过程大致如下
分段存储方式地址映射过程和分页存储方式差不过,下面是无快表的分段存储方式地址映射过程
无论是查询 TLB(快表)还是访问内存中的页表 / 段表,地址转换的全过程均由MMU(内存管理单元,Memory Management Unit) 硬件负责执行
内存扩充概述
虚拟存储技术
传统存储管理作用装入有一次性和驻留性缺点
- 作业必须一次性全部装入内存才能开始运行
- 一旦作业装入内存,就会一直驻留在内存中,直至作业运行结束
理论上,一个时间段内只需要访问作业的部分指令和数据即可让作业正常运行。一次全部装入并一直驻留大大降低了内存的实际利用率,且面对批量作业或大作业不得不扩充内存,造成浪费。
作业程序运行有时间局部性和空间局部性,统称为局部性原理:
- 如果执行了程序中的某条指令或访问了某个数据,那么近期该数据很可能再次被访问
- 如果程序访问了内存中的某个存储单元,那么近期其附近的存储单元也很可能需要被访问
虚拟存储技术基于局部性原理,将近期预计会用到的指令和数据放到更高速的缓存存储器中,将暂时用不到的指令和数据换回普通存储器,实现了只装入部分程序到内存就能正常运行。
虚拟内存技术
虚拟内存基于虚拟存储技术实现
虚拟内存的实现需要建立在离散分配的内存管理方式的基础上,与传统离散内存管理技术的区别是新增了两大功能,分别为请求调页和页面置换
- 访问的信息不在内存时,由操作系统负责将所需信息调入内存
- 内存空间不足时,由操作系统将暂时用不到的信息换出到外存
虚拟内存的扩充基于覆盖与交换技术:
- 覆盖技术:按逻辑分段程序,仅装入运行所需段,无需段调出(打破一次性加载)。
- 交换技术:内存紧张时,调出部分进程中可置换段至外存,为其他进程腾空间(打破驻留性)。
缺页置换算法
发生缺页(页面不在内存中)则产生缺页中断,执行页面置换算法换入所需页面
常见的页面置换算法有
- 先进先出FIFO,置换最早进入的
- 最近最近未使用LRU(least recently used),置换最久没用过的
- 最优页面置换算法OPT(Optimal),置换未来最晚用到的
Linux 内存布局
早期 Intel 的处理器从 80286 开始使用的是段式内存管理。但是很快发现,光有段式内存管理而没有页式内存管理是不够的,这会使它的 X86 系列会失去市场的竞争力。因此,在不久以后的 80386 中就实现了页式内存管理。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理。
但是这个 80386 的页式内存管理设计时,没有绕开段式内存管理,而是建立在段式内存管理的基础上,这就意味着,页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上一层地址映射。
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分
32位和64位系统地址空间范围分别如下
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
32位系统用户空间分布的情况大致如下:
用户空间内存,从低到高分别是 6 种不同的内存段:
- 代码段,包括二进制可执行代码;
- 数据段,包括已初始化的静态常量和全局变量;
- BSS 段,包括未初始化的静态变量和全局变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 (opens new window));
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;
灰色部分是「保留区」,因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址
在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
往期打卡
暑假读书笔记第四天
暑假读书笔记第三天
暑假读书笔记第二天
暑假读书笔记第一天