操作系统
操作系统
应用软件到计算机硬件的中间件
管理硬件
- CPU管理
- 内存管理
- 终端管理
- 磁盘管理
- 文件管理
- 网络管理
- 多核管理
- 电源管理
冯 诺伊曼存储程序思想
将程序和数存放到计算机内部的存储器中,计算机在程序的控制下一步一步处理
操作系统的启动
- 引导扇区:512字节,启动设备的第一个扇区,启动设备信息被设置在其中
- 其中的代码是汇编代码(更好的控制内存)
- 引导扇区代码:bootsect.s
- 引导扇区:512字节,启动设备的第一个扇区,启动设备信息被设置在其中
操作系统接口
- 表现为函数调用,又由系统提供,所以称为系统调用
系统调用的实现
用户态:不能访问内核空间数据
内核态:可以访问任何数据
如何分离:CPL DPL越小表示权限越高
- 内核 DPL = 0
- 用户 CPL = 3
- CPL <= DPL方可调用,所以一般用户态无法访问内核态
如何切换:中断(唯一方法)int 0x80
完整过程
- 开始判断用户态CPL=3 > 内核 DPL= 0,不能直接访问,走系统调用
- 根据系统调用号(exa=72)发送 int 0x80指令到达系统(因为_system_call的DPL=3,故可以调用)
- 当调用_system_call后CPL置为0,此时再访问内核就可以了
系统调用核心:
- 用户程序中包含一段int指令代码
- 操作系统写中断处理,获取想调程序的编号
- 操作系统更具编号执行相应代码
一 CPU管理
- CPU工作原理
- 自动的取指-执行:从指定的地址中取出指令执行
- 只需要设置要PC初值即可,后续可以不断的自动取指执行
并发:一个CPU上交替执行多个程序(为了提高利用率)
线程切换
- 需要 记录线程切换出去时候的信息?用进程来描述
1.1 多进程图像
开机的时候
fork()
了第一个进程,随后每执行一个子任务,fork()
新的进程PCB(Process Control Block)
:用来记录进程信息的数据结构TCB(Thread Control Block)
:用来记录线程信息的数据结构进程状态
- 新建态
- 就绪态
- 运行态
- 阻塞态
- 终止态
进程队列
- 就绪队列
- 阻塞队列
多进程组织:PCB + 状态 + 队列
多进程如何交替
- 进程A启动磁盘读写
- 修改进程A状态为阻塞态
- 将进程A放入阻塞队列
schedule()
:进行上下文切换getNext()
:在就绪队列中找到下一个进程B(线程的调度)switch_to()
:- CPU中的信息保存在进程A对应的PCB中,方便下次恢复
- 读取进程B中的PCB信息,恢复信息
1.2 用户级线程
- 进程 = 一个资源 + 多个指令执行序列
- 多个进程共享资源,可以保留并发的特点,且避免了进程切换的代价
- 一个线程一个栈
yield()
:线程的切换- 将当前线程的下一条指令压入栈中(方便后续返回时读取到命令)
- 切换TCB
- 根据TCB来切换栈
- PC切换(从栈从弹出PC指针)
- 执行新的线程
create()
:生成新的线程- 申请内存生成栈
- 申请内存生成TCB,关联TCB和栈
- 将第一条命令放到栈中
1.3 内核级线程
用户级线程:每个线程每个栈
内核级线程:因为既要在用户态跑又要在内核态跑,所以有两套栈(内核栈,用户栈)
切换
switch_to()
:首先线程A从用户态A1中断进入内核态A2
根据内核态A2的TCB找到切换的线程B的内核态B2的TCB
然后再根据B2通过
iret
中断返回得到线程B的用户态B1
初始化
ThreadCreate()
- 申请内存空间
- 创建TCB
- 创建内核栈、用户栈
- 关联栈和TCB
1.4 用户级、核心级线程对比
1.5 CPU调度策略
进程阻塞的时候调用
Schedule()
函数获取下一个可执行的进程IO约束型任务(前台进程特征,这类的优先级应该高一点)和CPU约束型任务
- 调度算法:
- FCFS:先来先服务
- SJF:短作业优先
- RR:时间片轮转(保证响应时间)
1.6 进程同步与信号量
进程同步:让进程走走停停保证多进程合作
信号量:用来解决只发送信号来实现同步的不足,更全面的表达进程的信息
记录有多少进程睡眠和唤醒
和信号的区别:信号只有1和0两个状态,信号量可以有多个状态
eg:一个资源数量是8,对应的信号量是2,则表示当前有2个资源可以使用,如果是-2则表示有两个进程等待这个资源
信号量解决生产者消费者问题案例
- 生产者停的条件:缓冲区满
- 消费者停的条件:缓冲区空
- 信号量:缓冲区当前大小,初始为buffer_size
- 双方在执行的时候判断信号量
- 注意:信号量是线程间互斥的资源(不能同时读写)
1.7 信号量的临界区保护
- 共同修改信号量会造成错误,因此需要保护
- 临界区:一次只允许一个进程进入的该进程的那段代码
- 如何找到进程中的临界区代码:读写信号量的代码
- 基本原则:互斥
- 好的保护原则:有空让进;有限等待
- 常见方法:
- Peterson算法:结合了标记+轮转的算法
- 面包店算法
1.8 死锁处理
- 成因:
- 互斥使用
- 不可抢占
- 请求和保持:进程必须占有资源再去申请新的资源
- 循环等待:在资源分配图中有环路
- 死锁预防:破坏死锁出现的条件
- 一次性申请所有需要的资源(资料浪费,编程难)
- 资源申请按序进行(资源浪费)
- 死锁避免:检测每个资源请求,如果造成死锁就拒绝
- 先试用在判断安全序列(算法时间复杂度高)
- 死锁检测+恢复:检测到死锁的时候让进程回滚,让出资源
- (恢复困难)
- 死锁忽略:等重启就解决了
- (用的多)
二 内存管理
2.1 内存使用与分段
重定位
交换
使用过程
- 进程1申请到一段内存,PCB指向2000地址,进程2申请到另一端内存,PCB指向1000地址
- 当进程1执行的时候,PC指针取址100,放到CPU中
- 根据进程1的基地址(2000)得到进程1的代码地址2100
- 当需要切换到进程2的时候,先根据PCB切换到进程2的基地址1000
- 执行进程2,PC指针继续取址执行
内存分段:程序并不是整段放到内存中,分段放在不同的地方
段+段内偏移
PCB中此时不只是一个基址了,而是每个段的基址(LDT表)
2.2 内存分区和分页
- 内存分区
- 可变分区:空闲分区表+已分配分区表
- 固定分区
- 内存分区问题
- 内存外碎片
- 如果需要内存紧缩的话就很耗时
- 内存分页:
- 会造成内碎片,但是可以根据页的大小来控制碎片的大小
2.3 多级页表与块表
问题:为提高内存利用率,页应该很小,但是页小了页表就大了
方法一:只存放用到的页
- 带来的问题:页表中的页号不连续,需要比较、查找,线性的查找耗时
方法二:多级页表:页目录表(章)+页表(节)
- 空间利用率高,但是增加了访问的次数
块表:TLB,一组相联快速存储,是寄存器
- 一次比对就可以找到(类似于书签,redis)
- 如果在块表中没有命中再查多级页表,然结果再放到TLB中
- 关键是提高TLB的命中率
2.4 段页结合的内存管理
程序员希望用段,物理内存希望用页
虚拟内存
对于用户来说是段式存储,对于物理内存来说是页式存储
根据段号得到一个基址(虚拟地址),由基址得到一个页号,再根据页号查找页表得到真实的物理地址
2.5 内存换入换出
用换入换出来实现“大内存”
- 虚拟内存4G,实际内存1G,给用户一种4G的感觉
换入:请求调页
- 用户给一个基址,在虚拟内存中查发现物理地址中缺页,CPU从磁盘中载入相应资源到物理内存中
换出:选择一页淘汰,换出到磁盘中
- FIFO
- MIN
- LRU
LRU实现换出
方法一:每页维护一个时间戳,淘汰最小的时间戳对应的页
方法二:维护一个页码栈,淘汰栈底
方法三近似实现Clock算法:每个页加一个引用位,每访问一页,硬件自动设置该位1,淘汰时如果是1就置为0,如果是0就淘汰
方法四Clock改进:两个扫描指针:一个用来清除R位,移动速度快;一个用来淘汰页,移动速度慢
另一个问题:进程分配的页框
- 太多了内存利用率不高
- 太少了导致缺页率高 ==> 磁盘的换入换出频率高 ==> 进程等待时间长,发生颠簸
三 磁盘、文件管理
3.1 IO与显示器
对外设的使用本质是向他们
- 发送指令(out 指令)
- 将out指令做成接口(文件视图)
- 进行设备的中断处理
printf:先创建缓存buf将格式化都输出在那里,然后再write(1, buf, …)
- printf
- 系统调用(wirte)
- 字符设备接口(crw_table)
- tty设备写(tty_wirte)
- 显示器写(con_wirte)
3.2 键盘
3.3 生磁盘的使用
- 磁盘访问单位:扇区(512字节)
- 升级到盘块,一次读取的内容变多,减少磁盘寻道(瓶颈)时间,用空间换时间
只要往控制器中写柱面、磁头、扇区、缓存位置
过程
- 进程得到盘块号,算出扇区号
- 用扇区号make req,用电梯算法add_request
- 进程sleep
- 磁盘中断处理
- 算出cyl,head,sector
- 调用outp()完成端口写