操作系统深入
目录
目录1.png目录2.png
整体架构
冯若依曼模型
冯若依曼模型图- 电脑指令执行的过程
- CPU从PC(程序计数器)获得指令内存地址, 然后控制单元操作地址总线从内存拿到数据, 数据通过数据总线返回并存入指令CPU的寄存器中
- CPU分析指令寄存器中的指令, 如果是计算类型的指令交给逻辑运算单元, 如果是存储类型的指令交给控制单元执行。
- CPU 执行完指令后程序计数器的值通过自增指向下个指令, 不断循环执行直到程序结束
- x86架构表示一系列指令集
多核cpu
-
参考参考文章1的图, 图介绍
多核cpu.png
- 寄存器: 最靠近CPU的存储单元,访问速度0.5个时钟周期,时钟也称为定时器,cpu根据固定周期切换就是定时器其作用
- L1缓存: 各个cpu独享,缓存数据跟指令, 访问速度2-4个时钟周期
- L2缓存: 各个cpu独享, 访问速度10~20个时钟周期
- L3缓存: 各个cpu共享, 访问速度20~60个时钟周期
- 内存: 多个CPU共用,访问速度是200~300个时钟周期
- 固体硬盘SSD: SSD断电不会丢数据,比内存慢10~1000倍
- 机械盘HDD: HDD断电不会丢数据,访问速度比内存慢10W倍
- Cache Line: CPU读取数据是按照Cacheline = 64KB把数据加载到缓存中,所以L1,L2可能会触发伪共享(将数据划分到相同到CacheLine中, 当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会影响彼此的性能)。避免伪共享就是将数据划分到不同的CacheLine中,JDK8 新增加的 LongAdder就是这块知识。
- 大小从上到小一次增加,访问顺序从上到下查找
如何尽可能提高CPU缓存命中率
- 数据缓存: 顺序读取能够提升速度, 道理跟磁盘顺序写一样。数组就是连续内存连续读取。
- 指令缓存: CPU有分支预测器,能提前将可能要的指令放到缓存区,所以尽提供有规律的条件分支语句,能帮助提高缓存命中率。
- 线程绑定到CPU: 线程切换时每次切换回来用同一个核心CPU这样能复用L1,L2缓存,也就是绑定线程到CPU提高性能。
内存管理
虚拟内存
-
参考文章1的图,操作系统会将进程的虚拟地址做一个映射,映射到物理地址,这样看起来每个进程都是独占系统了。32位系统最大支持4G, 因为寻址2的32次方,64位就能支持很大了。
虚拟内存.png
内存管理单元MMU(Memory Management Unit)
-
参考文章1的图, MMU负责: 虚拟地址到物理地址的转换, 内存保护, 高速缓存控制。 MMU一般封装在CPU里面。
MMU.png
内存管理方式
- 一般现在是使用分段和分页来映射虚拟地址与物理地址之间的关系, 现在分页用的最多。不过分段的话有个好处就是linux内存布局中堆段数据段代码段都可以动态拓展。
页式存储管理
- 页表实现虚拟内存到物理内存的映射,页表保存在物理内存中。页page一般都是4k, 每次释放的时候都是按整个page来的,所以就不会有内存碎片的问题。当内存不够时,操作系统会使用页面置换算法,从物理内存置换一片页面出来。
- 内存保护可以通过查页表得知。
-
通过页表映射虚拟地址到物理地址,参考现代操作系统的图,拿16位的举例,前4位页号作为页表索引,页表中110 1中1表示在位,就是能映射到,如果不能映射到就可能产生缺页中断。查到的页框号高三位110和虚拟地址后12位结合产生物理地址。
页表映射.png - 32位操作系统占用4GB/4KB = 2^20 个页信息, 一个页4字节,那2的20次方就是4Mb,如果有1000个进程那页表占用的内存很大,常用解决方案是:
-
多级页表(参考现代操作系统的图): 避免不需要的页表也放入内存中,32位举例,前10位PT1用作顶级页表的索引,中间10位PT2用作二级页表索引,映射到物理地址就跟只有一级页表一样。
多级页表.png
- 内存映射文件: 将文件映射到虚拟地址空间
加速访问TLB(地址转换后援缓冲器也叫快表)
-
TLB是MMU的一部分,缓存的是最近使用的数据的页表项,参考现代操作系统的图
TLB.png
页面置换算法
- 当发生缺页中断时,cpu TRAP指令陷入内核,具体的linux使用页面置换算法有:
- 最优: 标记最后会被使用的页面,这个实现不了
- FIFO: 先进先出
- LRU: 最近最少访问算法
- LFU: 最少访问频率算法
- linux使用的页面置换算法, 改进的LRU。
linux内存管理图
linux内存管理图进程管理
进程跟线程的区别
- 进程是系统调度资源的最小单位,进程独占系统资源,而线程缺占用很少的资源。
- 进程由于资源比较多所以切换速度慢,线程资源少所以切换速度快。
- 由于线程共享进程资源所以线程通信方便,进程的IPC复杂一些。
进程状态图
-
参考参考文章1的图:
状态变化图.png - 阻塞: 比如I/O阻塞,等待某个事件返回
- 挂起: 进程从内存中抽出,放入到磁盘
PCB(进程控制块)
- PCB属于进程实体一部分,操作系统中会记录其数据结构
- 作用:
- 独立标记一个进程
- 进程管理的依据
- 进程调度的依据
- 进程通信的依据
- 包含了:
- 进程标识符: 唯一标识一个进程
- 处理机状态: 记录一些寄存器信息,cpu切换时restart用
- 进程调度信息: 进程优先级之类的信息
- 资源清单: 所打开的文件句柄,虚拟地址空间信息
PCB管理方式
-
进程创建销毁调度频繁所以采用链表方式:
链表管理.png
进程控制
进程创建
- 父进程可以创建子进程,子进程继承父进程所有资源, 大致流程:
- 分配Pid唯一标识号
- 分配所需资源
- 初始化PCB
- 资源允许的情况下进入就绪队列
进程终止
- 分为正常结束, 异常结束(越界, I/O异常), 外界干预(操作系统干预,父进程终止,父进程请求)
- 终止大致过程:
- 根据标识符,从PCB集合中找出该进程PCB
- 执行状态, 则直接终止, 如果有子进程,则等待子进程终止
- 全部资源还给操作系统或者父进程
- 从PCB链表中移除
进程调度
- 抢占式:CPU时间片轮转的形式调度进程,防止某个进程占用CPU开销很大。
- 调度算法:
- FCFS 算法: First Come First Severd,类似FIFO算法,对长作业有利,不适合I/O密集型
- SJF算法: Shortest Job First 最短作业优先算法, 对短作业有利
- HRRN算法: Highest Response Ratio Next 最高响应比优先算法, 1,2的折中,按照响应优先级来的
- RR算法: Round Robin 时间片轮转算法, 每个进程都按照固定的时间片运行,轮流运转
- HPF算法: Highest Priority First 最高优先级调度算法, 低优先级的可能一直无法执行
-
MFQ算法: Multilevel Feedback Queue 多级反馈队列调度算法, 多个队列中优先级不同,优先级高时间片短,新进程会先加入到最高优先级队列,下次执行则进程下一级优先级队列
MFQ算法.png
进程通信
- 进程间相互独立,但操作系统内核是共享的,所以要通信必须走内核。
管道
- 匿名管道: 单向流动,使用简单,效率低。linux常用的|就是匿名管道。实现依赖pipe。 本质就是在内核中弄个缓存。通信范围是父子进程。
- 命名管道: 与匿名管道区别它有管道文件
消息队列
- 它是保存在内核的消息链表,传递消息需要用户态 -> 内核态 -> 核心态。消息有对应的格式,我们平常用的rocketmq, kafka也属于消息队列。
共享内存
- 将一个公共部分比如文件映射到两个不同进程的虚拟地址,这时不涉及到用户态核心态切换,效率高。
信号量
- 整形的计数器,用于实现进程间同步与互斥,可与共享内存一起使用。信号量P,V
信号
- 异常状态下需要用到信号通知进程,信号是异步通信的,比如kill -9这种就是信号,进程可以响应,忽略信号,信号承载的信息少
socket
- 用于不同机器之前的通信
线程
-
线程状态流转图,参考文献3的图, start方法使线程变成可运行状态,run方法只是一个普通方法可重复调用。线程中断可以调用interrupt方法,但是至于线程什么时候响应,需要看具体代码,这里只是发出中断请求。不像cpu执行完一行代码就看下是否有需要响应的中断。
线程状态流转图.png - 对于操作系统而言,每个进程只有一个线程,因为每次调度都是这么调度的
- 进程是PCB,线程是TCB,也是操作系统能管,内线程模式下一个用户线程对应一个内核线程, java里面的线程与内核线程一一对应
- 轻量级线程本质上还是进程,只是大部分数据共享
- 协程运行在线程之上,n个协程对应一个线程
异常与中断
- 中断有专门的中断控制器统一进行处理,并通知相应设备。中断是处理器外部的中断信号,异常是处理器内部的内中断,内中断不能被屏蔽,一旦出现必须立即予以响应并进行处理。
-
参考文章9的图
异常与中断区别.png
switch if效率问题
- switch对于分支较多时效率高,用空间换时间,直接类似数组读取对应分支,不需要一个个判断。常量分布范围很大但实际有效值又比较少的情况,switch空间利用率较低。
- if需要一个个分支判断,但是if更灵活,可用作范围判断,switch只能是常量。
文件管理
-
参考文章1的图,虚拟文件系统定义了用户态可以访问的POSIX(可移植操作系统接口),
虚拟文件系统.png - 常用的文件系统分三种:
- 磁盘文件系统: EXT3之类
- 内存文件系统: 占用内存数据,/sys之类的
- 网络文件系统:常见的网盘挂载NFS
- 拓展: fat: u盘使用,NTFS: 对应u盘或者硬盘而言使用最广的
文件组成
- 重要的有两部分: inode节点和目录项
inode
- 文件存储最小单位是扇区,一般读取时会多个扇区一起读取,这种就做块,块是文件存储的最小单位,文件信息都存储在块中,inode节点存储了文件元数据信息,比如创建时间、inode编号、修改时间之类的,这些元数据信息存储在索引节点inode里面。操作系统用inode号码来识别不同的文件。对于操作系统而言文件名就是inode号码别名。
- iNode有个好处就是只有在文件打开时,iNode才在内存里面
一次查找/usr/ast/mbox的过程
- 文件系统定位到根目录/usr, 根目录iNode放在固定的磁盘位置
- 查找usr得到iNode是6
- iNode 6说明usr在块132上
- 根据第三步骤得到/usr/ast是iNode 26
- iNode 26说明/usr/ast在块406中
- 从块406得知/usr/ast/mbox的iNode是60
-
这个文件的iNode会被读取到内存中知道关闭
一次查找/usr/ast/mbox的过程.png
目录
- 对于操作系统而言,目录也是一种文件,为避免频繁读取磁盘里的目录文件,内核会把已经读过的目录文件用目录项这个数据结构缓存在内存
共享文件之软链接和硬链接:
- 硬链接:两个文件硬链接指的是两个文件inode相同,所以不能跨文件系统。只有两个文件删除,源文件才会被删除。
- 软链接:两个文件软链接指的是两个文件有不同的iNode,所以软链接可以跨文件系统。某个文件删除后,另个文件还存在不过找不到它了。
文件存储
- 大文件一般用iNode节点能够快速索引到具体的块
- 小文件可以考虑用连续内存类似数组,存储块
- 小文件也可以考虑用链表链接文件使用到的块信息
- 操作系统存储会根据文件大小改变存储块索引策略
空闲空间管理
- 管理空闲块,这样就不用一个个块查找看下是否空闲,然后放入文件里面的信息
- 类似redis的bitMap,标识是否存在,iNode用这个方法标识
文件系统四大对象
-
要访问文件,必须将文件挂载在目录树上面的目录,挂载目录称为挂载点,文件系统磁盘上布局,图是参考文章4的图
文件系统磁盘上布局.png
- 引导块: 引导操作系统用,一般启动时用到
- 超级块: 物理磁盘中文件系统结构的相关信息
- i节点: iNode,文件系统元数据信息
输入输出管理
I/O控制器跟驱动程序
-
参考文章1的图
设备控制器.png - 设备控制器是为了屏蔽操作系统操作I/O设备的细节,统一接口。输入输出设备可分为块设备跟字符设备,块设置是落盘的,比如磁盘,字符设备不落盘,比如鼠标
- CPU通过IO端口跟内存映射IO来跟I/O设备控制器中寄存器缓冲区通信
- 内存映射IO:设备控制中的寄存器映射到内存里,读写内存就可以读写设备控制器的缓冲区
驱动接口与设备控制器
-
参考文章1的图,驱动程序屏蔽了I/O外设设备控制器中寄存器使用细节,给操作系统提供统一接口
驱动程序.png
I/O控制
-
cpu与设备控制器交互读取数据流程,DMA参与后续流程,完成后通过中断通知Cpu, DMA减轻了CPU负担。有DMA在所以I/O不会一直占用CPU。
DMA传输数据.png
0拷贝
老式读写
-
从磁盘到网络数据传输的老式读写,参考文章1的图,真个过程4次用户态跟内核态的切换,2次DMA数据拷贝,2次CPU数据拷贝。
老式读写.png
mmap
-
参考文章1的图, 主要的优化是3共享缓存,减少了数据复制的次数
mmap.png
sendfile
-
在mmap的基础之上减少了上下文切换,实际工作中一般小文件采用0拷贝技术,而大文件会用异步IO。因为第2步,DMA将数据拷贝到内核缓存区,这个内核缓存区就是页Page, PageCache。页缓存大小有限,所以大文件不走这种形式。
sendfile.png
I/O分层
-
参考文章1的图
I/O分层.png - 读取数据是之上而下的, I/O分层主要分以下三层
- 文件系统层提供统一文件访问接口
- 通用块层包括块设备的 I/O 队列和 I/O 调度器。
- 设备层有设备控制器和驱动程序给最底层打交道。
死锁
死锁的产生条件
- 资源非抢占式
- 资源互斥
- 请求资源并保持
- 循环等待
死锁检测
-
有向图,根据资源分配有向图可以检测出死锁
资源分配有向图.png
死锁的避免
- 银行家算法:一个人去银行借钱的时候,声明借钱的数量,声明归还时间,银行审批通过则借贷.其实就是资源合理分配,并对资源进行安全性检查. 安全性检查其实就是所用请求当前资源的总和不超过当前可借贷的资源. 可重入锁也可预防,同一线程外层函数获得锁之后,内层递归函数依然有获取该锁的代码,但不影响。
死锁预防
- 破坏不可抢占条件:允许对资源实行抢夺。
- 破坏循环等待条件:对系统所有资源进行线性排序并赋予不同的序号,这样我们便可以规定进程在申请资源时必须按照序号递增的顺序进行资源的申请,当以后要申请时需检查要申请的资源的编号大于当前编号时,才能进行申请。
- 破坏请求和保持条件:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,需要先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它很快又要用到资源R。