操作系统

image-20240415204817080

  • 应用软件到计算机硬件的中间件

  • 管理硬件

    • CPU管理
    • 内存管理
    • 终端管理
    • 磁盘管理
    • 文件管理
    • 网络管理
    • 多核管理
    • 电源管理
  • 冯 诺伊曼存储程序思想

    • 将程序和数存放到计算机内部的存储器中,计算机在程序的控制下一步一步处理

      image-20240408200356742

  • 操作系统的启动

    • 引导扇区:512字节,启动设备的第一个扇区,启动设备信息被设置在其中
      • 其中的代码是汇编代码(更好的控制内存)
      • 引导扇区代码:bootsect.s
  • 操作系统接口

    • 表现为函数调用,又由系统提供,所以称为系统调用
  • 系统调用的实现

    • 用户态:不能访问内核空间数据

    • 内核态:可以访问任何数据

    • 如何分离:CPL DPL越小表示权限越高

      • 内核 DPL = 0
      • 用户 CPL = 3
      • CPL <= DPL方可调用,所以一般用户态无法访问内核态
    • 如何切换:中断(唯一方法)int 0x80

      image-20240408204828529

    • 完整过程

      image-20240408205211541

      • 开始判断用户态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):用来记录线程信息的数据结构

  • 进程状态

    • 新建态
    • 就绪态
    • 运行态
    • 阻塞态
    • 终止态

    image-20240408214304327

  • 进程队列

    • 就绪队列
    • 阻塞队列
  • 多进程组织: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

      image-20240409210815697

  • 初始化ThreadCreate()

    • 申请内存空间
    • 创建TCB
      • 创建内核栈、用户栈
    • 关联栈和TCB

1.4 用户级、核心级线程对比

image-20240409210345072

1.5 CPU调度策略

  • 进程阻塞的时候调用Schedule()函数获取下一个可执行的进程

  • IO约束型任务(前台进程特征,这类的优先级应该高一点)和CPU约束型任务

  • 调度算法:
    • FCFS:先来先服务
    • SJF:短作业优先
    • RR:时间片轮转(保证响应时间)

1.6 进程同步与信号量

  • 进程同步:让进程走走停停保证多进程合作

  • 信号量:用来解决只发送信号来实现同步的不足,更全面的表达进程的信息

    • 记录有多少进程睡眠和唤醒

      image-20240409220845574

    • 和信号的区别:信号只有1和0两个状态,信号量可以有多个状态

    • eg:一个资源数量是8,对应的信号量是2,则表示当前有2个资源可以使用,如果是-2则表示有两个进程等待这个资源

  • 信号量解决生产者消费者问题案例

    • 生产者停的条件:缓冲区满
    • 消费者停的条件:缓冲区空
    • 信号量:缓冲区当前大小,初始为buffer_size
    • 双方在执行的时候判断信号量
    • 注意:信号量是线程间互斥的资源(不能同时读写)

1.7 信号量的临界区保护

  • 共同修改信号量会造成错误,因此需要保护
  • 临界区:一次只允许一个进程进入的该进程的那段代码
  • 如何找到进程中的临界区代码:读写信号量的代码
  • 基本原则:互斥
  • 好的保护原则:有空让进;有限等待
  • 常见方法:
    • Peterson算法:结合了标记+轮转的算法
    • 面包店算法

1.8 死锁处理

  • 成因:
    • 互斥使用
    • 不可抢占
    • 请求和保持:进程必须占有资源再去申请新的资源
    • 循环等待:在资源分配图中有环路
  • 死锁预防:破坏死锁出现的条件
    • 一次性申请所有需要的资源(资料浪费,编程难)
    • 资源申请按序进行(资源浪费)
  • 死锁避免:检测每个资源请求,如果造成死锁就拒绝
    • 先试用在判断安全序列(算法时间复杂度高)
  • 死锁检测+恢复:检测到死锁的时候让进程回滚,让出资源
    • (恢复困难)
  • 死锁忽略:等重启就解决了
    • (用的多)

二 内存管理

2.1 内存使用与分段

  • 重定位

  • 交换

    image-20240410102659773

  • 使用过程

    image-20240410103406408

    • 进程1申请到一段内存,PCB指向2000地址,进程2申请到另一端内存,PCB指向1000地址
    • 当进程1执行的时候,PC指针取址100,放到CPU中
    • 根据进程1的基地址(2000)得到进程1的代码地址2100
    • 当需要切换到进程2的时候,先根据PCB切换到进程2的基地址1000
    • 执行进程2,PC指针继续取址执行
  • 内存分段:程序并不是整段放到内存中,分段放在不同的地方

    • 段+段内偏移

    • PCB中此时不只是一个基址了,而是每个段的基址(LDT表)

      image-20240410104432330

2.2 内存分区和分页

  • 内存分区
    • 可变分区:空闲分区表+已分配分区表
    • 固定分区
  • 内存分区问题
    • 内存外碎片
    • 如果需要内存紧缩的话就很耗时
  • 内存分页:
    • 会造成内碎片,但是可以根据页的大小来控制碎片的大小

2.3 多级页表与块表

  • 问题:为提高内存利用率,页应该很小,但是页小了页表就大了

  • 方法一:只存放用到的页

    • 带来的问题:页表中的页号不连续,需要比较、查找,线性的查找耗时
  • 方法二:多级页表:页目录表(章)+页表(节)

    image-20240410155109348

    • 空间利用率高,但是增加了访问的次数
  • 块表:TLB,一组相联快速存储,是寄存器

    • 一次比对就可以找到(类似于书签,redis)
    • 如果在块表中没有命中再查多级页表,然结果再放到TLB中
    • 关键是提高TLB的命中率

2.4 段页结合的内存管理

  • 程序员希望用段,物理内存希望用页

  • 虚拟内存

    image-20240410161014066

  • 对于用户来说是段式存储,对于物理内存来说是页式存储

  • 根据段号得到一个基址(虚拟地址),由基址得到一个页号,再根据页号查找页表得到真实的物理地址

    image-20240410161448350

2.5 内存换入换出

  • 用换入换出来实现“大内存”

    • 虚拟内存4G,实际内存1G,给用户一种4G的感觉
  • 换入:请求调页

    • 用户给一个基址,在虚拟内存中查发现物理地址中缺页,CPU从磁盘中载入相应资源到物理内存中
  • 换出:选择一页淘汰,换出到磁盘中

    • FIFO
    • MIN
    • LRU
  • LRU实现换出

    • 方法一:每页维护一个时间戳,淘汰最小的时间戳对应的页

    • 方法二:维护一个页码栈,淘汰栈底

    • 方法三近似实现Clock算法:每个页加一个引用位,每访问一页,硬件自动设置该位1,淘汰时如果是1就置为0,如果是0就淘汰

      image-20240410164923481

    • 方法四Clock改进:两个扫描指针:一个用来清除R位,移动速度快;一个用来淘汰页,移动速度慢

  • 另一个问题:进程分配的页框

    • 太多了内存利用率不高
    • 太少了导致缺页率高 ==> 磁盘的换入换出频率高 ==> 进程等待时间长,发生颠簸

三 磁盘、文件管理

3.1 IO与显示器

image-20240415205054190

  • 对外设的使用本质是向他们

    • 发送指令(out 指令)
    • 将out指令做成接口(文件视图
    • 进行设备的中断处理
  • printf:先创建缓存buf将格式化都输出在那里,然后再write(1, buf, …)

    • printf
    • 系统调用(wirte)
    • 字符设备接口(crw_table)
    • tty设备写(tty_wirte)
    • 显示器写(con_wirte)

3.2 键盘

image-20240415214935201

3.3 生磁盘的使用

  • 磁盘访问单位:扇区(512字节)
    • 升级到盘块,一次读取的内容变多,减少磁盘寻道(瓶颈)时间,用空间换时间
  • 只要往控制器中写柱面、磁头、扇区、缓存位置

  • 过程

    • 进程得到盘块号,算出扇区号
    • 用扇区号make req,用电梯算法add_request
    • 进程sleep
    • 磁盘中断处理
    • 算出cyl,head,sector
    • 调用outp()完成端口写