看看多久才会读完---买于20年双十一
【来自未来的更新】已于2021年5月16日看完!!!!
本篇为上集,戳这里直接看下集~~~~
目录
第1章-操作系统概述
第2章-硬件结构
第3章-操作系统结构
第4章-内存管理
第5章-进程与线程
进程的状态
进程的内存空间布局
进程控制块和内存上下文切换
线程
线程的由来
线程的定义
多线程的地址空间布局
线程控制块和线程本地存储
线程的基本接口-POSIX线程库
线程创建
线程退出
出让资源
合并操作
挂起与唤醒
进程的执行
执行进程
进程管理
进程间监控
进程组和会话组
Fork的优点
Fork的缺点
Fork的继任者们
第6章-操作系统调度
调度机制
长期调度
中期调度
短期调度
进程的分类
经典调度
先到先得FIFO
最短任务优先
最短完成时间任务优先
时间片轮转
优先级调度
多级队列
多级反馈队列
公平共享调度
从硬件角度
对硬件进行管理,处理各种错误
对硬件进行抽象,形成不依赖硬件的资源
从应用角度
提供不同的接口,满足不同类型的访问控制,应用间交互等服务
进行资源分配与管理
操作系统提供不同层次的接口
冯诺依曼-机,包含
指令集是ISA【指令集架构】的重要组成部分,AArch64属于RISC【精简指令集计算机】
特权级在AArch64中叫做异常级别,包括
何时从EL0切换到EL1
其中1和2为同步的CPU特权级切换
3为异步的CPU特权级切换
从EL0切换到EL1
寄存器是ISA的重要组成,包括
EL1下有两个页表基地址寄存器,这个和虚拟内存有关系
Cache
这个是为了加快CPU访问数据的速度,包括:
若干个缓冲行,每一个行包括
一个有效位
一个标记地址
为了通过物理地址找到对应的缓存,物理地址在逻辑上分为Tag,Set以及Offset三段
物理地址中Set段能够表示的最大数目叫做组
支持的最大Tag数叫做路
缓存结构和缓存寻址的图请看书
设计操作系统的原则【机制与策略分离】
策略:做什么【输入处理,启动加载....】
机制:如何做【调度方法...】
管理复杂系统的方法
模块化:分而治之,将复杂系统分解成一系列的模块,保证模块之间的界限,高耦合低内聚,使之有独立性
抽象:接口与实现分离,无需关心各个模块之间的内部实现
分层:将模块按一定的层次划分,约束内部模块之间的交互方式
层级:将功能相近的模块划分在一个子系统
操作系统的内核架构
简要结构:应用程序和操作系统在同一个地址空间,没有虚拟内存管理,特权级隔离等功能,任何一个模块出问题,系统就崩溃了
宏内核:所有的操作系统模块运行在内核态
微内核:将单个的内核功能拆分,作为服务部署在用户态,仅有很小部分的内核运行在内核态,服务提供进程间通信的功能使之互相协作
外核:操作系统与应用程序挂钩,应用程序要啥就装对应的功能,内核只负责对操作系统在多个操作系统之间的多路复用
多内核:通过多内核管理异构多核设备
应用程序是面向虚拟内存编写的,CPU会翻译地址到物理地址,操作系统来管理虚拟地址和物理地址的映射
设计时的三个目标
CPU通过MMU进行地址翻译,为了加速翻译,现代CPU都有TLB【转址旁路缓存】
有两种机制
分段机制不多写了,目前用的多的是分页机制的
分页机制
一个虚拟地址将被划分成两部分
多级页表也是一样的结构,多级页表允许部分空洞,单级页表则需要每一项都真实存在
AArch64 下的多级页表
虚拟地址低48位参与地址翻译,页表级数为4,虚拟页大小4KB
物理地址划分为连续的4KB大小物理页,一个虚拟页映射为一个物理页,低12位对应页内偏移量
整个页表的起始地址放在页表基地址寄存器中,对应的就是第0级的页表页
每一个页表页占用物理内存的一个物理页[4KB]
每一个页表项占用8字节,存访问权限。
因此,一个页表页包含512个页表项[4K/8],虚拟地址中对应于每一级页表的索引都是9位【2^9】
63-48位: 全0或者全1
47-39位:0级页表索引值
38-30位:1级页表索引值
29-21位:2级页表索引值
20-12位:3级页表索引值
11-0位: 页内偏移量
如何翻译呢?
TLB【转址旁路缓存】
多级页表的出现使得MMU翻译地址的过程要查找多个页表页中的页表项
为了减少次数,加入TLB加速翻译
TLB缓存了虚拟页号到物理页号的映射关系
MMU先把虚拟页号作为键值去查询TLB的缓存项
找到【TLB命中】就直接得到物理页号,否则【TLB未命中】就要查页表
由于这个TLB是CPU的一部分,所以它的大小是有限的,它是由硬件直接进行管理的,这样才能高效利用
TLB未命中,就去查页表,然后填进TLB
如果已满,按照预定的方式替换掉某一项
如果再次翻译一样的页号,那么就可以马上得到页表了。
TLB与当前页表不一致是需要刷新的,如何最小化刷新对应用程序带来的影响,则是操作系统与CPU一起需要处理的。
换页和缺页
换页:当物理内存不足时,操作系统把物理页的数据写到磁盘等容量更大的设备中,然后就可以回收物理页回来了,此时的物理页处于【已分配但未映射到物理内存】的状态
缺页:当应用程序访问了处于【已分配但未映射到物理内存】的状态的物理页,那么就会触发缺页异常,操作系统会调用预先设置的处理函数,然后找到一个空虚的物理页,将之前的数据重新写回到该物理页上,并且在页表上填写该虚拟地址到这一个物理页的映射。
由于换页涉及到磁盘操作,所以操作系统会引入预取机制优化。
当应用层申请虚拟内存的时候,操作系统可以把新分配的虚拟页标记为已分配但未映射至物理内存的状态,不需要为这个虚拟页分配对应的物理页。只有要访问时才会触发缺页异常,真正为虚拟页分配物理页,并在页表里填写映射。
初次访问时产生的缺页异常会导致访问延迟的增加,可以利用应用程序访问时的时空局限性改善。
虚拟页处于未分配或者已分配但未映射至物理内存状态时,应用程序访问该虚拟页会触发缺页异常。
Linux中应用程序的虚拟地址空间被实现由多个虚拟内存区域VMA组成
当应用程序发生缺页异常时,操作系统通过判断虚拟页是否属于该应用程序的某个虚拟内存区域区分该页所处的分配状态
若属于,则该页处于已分配但未映射至物理内存的状态
否则,该页处于未分配状态。
页替换策略
如果空闲的物理页已经用完或者小于某个阈值,策略便会选择某些物理页换出到磁盘,让出空间,最小化缺页异常发生的次数进而提升性能
常见的页替换策略有:
共享内存
允许同一个物理页再不同的应用程序间共享
基本用途是可以让不同应用程序之间互相通信
基于此,操作系统又衍生出写时拷贝和内存去重
写时拷贝
页表项中有部分位用来标识属性,包括标识虚拟页的权限
写时拷贝技术允许应用程序A和B以只读方式共享同一段物理内存
当应用程序对该内存区域进行修改时,便会触发缺页异常,在异常处理函数之中系统会发现由于应用程序写了只读内存,对应区域被标记为写时拷贝。于是操作系统会把这种异常的物理页重新拷贝一份,并将新拷贝的物理页以可读可写的方式重新映射给应用程序。
内存去重
操作系统定时在内存中扫描相同的物理页,找到映射这些物理页的虚拟页,只保留其中一个物理页,并将具有相同内容的其他虚拟页都用写时拷贝方式映射到这个物理页上,然后释放掉给其他的物理页用。
内存压缩
操作系统还会引入压缩算法对内存数据进行压缩,特别是资源不足时,把不太会使用的一部分数据压缩,节约出更多的内存空间
大页
解决TLB缓存项不够的问题,大页的大小可以到2M甚至1G,相比原来4K的大小,大幅度减小TLB的占用量
Linux还提供透明大页的机制,能够自动地降连续的4K页合并成2M的内存页.
物理内存分配机制
内存碎片
无法被利用的内存,直接导致内存的利用率下降
分为内部碎片和外部碎片
外部碎片:经过多次回收和分配之后,物理内存高度分散,导致空闲内存足够但是无法满足申请需求
内部碎片:当分配内存空间大于实际分配请求所需要的空间时,就会造成部分的内存浪费
上下文切换:通过保存和恢复进程在运行过程中的状态【上下文】,使进程可以暂停,切换和护肤,从而实现了CPU的资源调度。
用户栈: 栈保存了进程需要使用的各种临时数据,一般是自顶而下的,栈底在高地址,栈顶在低地址
代码库:进程运行依赖的共享代码库,标记为只读
用户堆:管理的是进程动态分配的内存,堆的扩展方向是自底向上。
数据与代码段:数据段主要保存全局变量的值,代码段保存的是进程执行所需的代码
内核部分:当进程在用户态运行时,内核内存对其不可见。只有进程进入内核态时,才能访问内存。
在内核中,每一个进程都通过一个数据结构来存它的相关状态,譬如进程标识符【PID】,进程状态,虚拟内存状态等,这种数据结构叫做进程控制块【PCB】
进程的上下文包括运行时的寄存器状态,它可以用于保存和恢复上一个进程在处理器运行的状态。使用上下文切换机制,就可以把前一个进程的寄存器保存到PCB,然后将下一个进程保存的状态写入寄存器,切换到它运行。
上下文切换一般是在内核态中运行的。
一个进程至少有一个线程,多个线程可以共享进程的资源。进程是在内存中运行的一段程序,而线程是这一段程序中的其中一个执行单元。
在linux中,进程一般是调用fork从进程中分裂出来的
一个进程调用fork后,会为该进程创建一个子进程。而调用fork的进程称作父进程。
之后便会形成两个完全独立的两个进程,将拥有不同的PID与虚拟内存空间。
对于父进程,返回值是子进程的PID,对子进程来说,返回值是0
注意调度器眼中,子进程和父进程是两个独立的个体,执行顺序不定,完全取决于调度器的决策
对每一个进程来说,运行过程中都会维护一个已打开文件的文件描述符【fd表】文件描述符会使用偏移量记录当前进程读取到某一个文件的位置,因为文件结构可能根据不同文件系统而变化,这样做有利于操作系统进行管理。
子进程和父进程拥有一样的fd表,因此在read操作的时候会对文件加锁,父子进程不可能读到完全一样的字符串。
操作系统的第一个进程是由操作系统创建的,特定而唯一,其他进程就像树一样派生出来。
线程之间共享地址空间,但又各自保存运行时所需要的状态【上下文】
它是操作系统种调度管理的最小单位
内核代码与数据 |
内核栈1 |
内核栈2 |
内核栈3 |
线程栈1 |
线程栈2 |
线程栈3 |
代码库 |
用户堆 |
数据 |
代码 |
两个重要特征
1. 分离的内核栈和用户栈
2. 共享的其他区域
内核栈由系统内核创建,用户栈不可见,直接受操作系统调度器管理。
为了实现内核栈和用户栈协作,操作系统会在两者之间维护一个关系,叫做多线程模型
类似于进程,线程也有自己的控制块,存储一些信息
对linux来说,用户态的线程控制块TCB可以认为是内核态的扩展,用来存储更多与用户态相关的信息,其中最重要的就是线程本地存储TLS
在多线程编程中,不同线程使用全局变量的时候,实际上访问的是该变量的不同拷贝,不会对其他线程产生影响。
线程库位每一个线程创建完全相同的TLS,保存在内存的不同地址之上,每一个全局变量的拷贝相对于TLS起始位置的偏移量都是一样的
由于TLS的结构相似性,对于TLS的寻址也比较特殊
不同线程的段寄存器FS保存着不同的TLS起始地址,当不同的线程访问同名的TLS时,最终访问了不同的地址
pthread_create
pthread_exit
pthread_yield
pthread_join
sleep
pthread_cond_wait
通过调用execve函数执行进程
- 将可执行文件的数据段和代码段载入当前进程的地址空间
- 重新初始化堆和栈
- 将PC寄存器设置到可执行文件代码段定义的入口点,该入口点最终会调用main函数
- 在需要设定环境变量时,main函数也可以扩展写成int main(int argc,char * argv[],char *envp[]),使得程序能够直接访问envp来获取到环境变量
Linux中,进程都是通过fork生成出来的
每一个进程都会记录自己的父进程和子进程,这样进程之间就构成了树关系
处于树根部的是init进程
在linux中,进程可以使用wait来对其子进程进行监控。
父进程调用waitpid堆子进程进行监控,如果子进程已经退出,那么waitpid就会立即返回,并设置状态值,否则会阻塞等待子进程退出,当waitpid退出后,父进程可以访问状态值变量来获取子进程的状态
僵尸进程: 终止了却没有释放对应资源的子进程
进程组是进程的集合,父进程和子进程属于同一个进程组
会话是进程组的集合,可以分为前台进程组和后台进程组,控制终端进程组等
每一个进程都有自己的进程id【PID】,进程组id【GID】和会话id【SID】
对于init进程来说,这三个值都是1
fork和exec的组合可以认为是将进程的创建的进一步解耦
fork强调进程之间的联系,如果父子进程之间有较强的联系的话就适合用fork
过于复杂,接口简洁,但是内部已经随着时代的变迁变得异常复杂
性能不佳,写时拷贝技术对于今天的应用来说需要耗费太多的时间
有安全漏洞
把调度分成
用于限制系统中真正被短期调度管理的进程数量,避免短期调度开销过大。
它的触发时间长,粗粒度的决定是不是要将一个新进程纳入调度管理,负责增加系统中可被调度的进程的数量
将内存使用的情况也考虑进来,避免内存使用过多,实际上算作换页机制的一部分,它会挂起系统中被短期调度管理的进程。
触发相对频繁,辅助换页机制,负责限制系统中可被调度的进程数量
是实际来做出调度决策的,负责进程状态的转换。
触发最为频繁,细粒度的负责进程的执行,做出对应的调度决策。
【优点】:简单直观,不需要预知任务的信息
【缺点】:
【优点】:选择运行时间最短的任务执行,短任务就可以不用等很久了
【缺点】:
调度器必须等一个任务执行完或者主动退出执行才能开始下一个调度,这种就是非抢占式调度。
对于最短完成时间任务优先来说,在任务达到的时候也会调度,还有可能打断目前正在执行的任务,这种就是抢占式调度。
【缺点】:
限定每一个任务的运行时间,完成以后就执行运行队列中的下一个任务
【优点】:不需要预知任务运行的时间,也不会出现长任务很吃亏的问题。
【缺点】:
每一个任务都会被分配预先设置好的优先级
每一个优先级对应一个队列,任务就被存放在这些队列里。
多个任务同时预备,那么调度器就会选择优先级高的队列中的任务执行,在同一优先级下,策略又不一样,可以采取不同的策略,譬如FIFO或者时间片。
在这种调度策略下
需要预知任务的运行时间
设置优先级需要提高IO密集型任务的优先级,因为IO密集型并不占用CPU太久。
【缺点】:
类似多级队列,但是它实现了优先级的动态设置。
【优点】:
会量化任务对系统资源的占用比例,从而实现资源公平调度,以份额量化每一个任务对CPU的使用。
实际中将任务分组,以组为单位分配份额,任务在组内继续分配份额。
优先级调度为了优化任务的周转时间,响应时间,而份额式的公平共享调度式为了让每个任务都可以得到对应的资源。
下篇链接戳这里