Operating Systems Three Easy Piece的读书笔记

绪论为什么需要操作系统

操作系统做的事情大概可以分为三个部分

  • 对资源(CPU, MEM)虚拟化
  • 处理并发问题
  • 提供持久化的文件系统

OSTEP虚拟化不适用于文件系统文件系统本身是程序间通信的一种方式不过现在移动端的iOSAndroid其实也在做文件系统的隔离 Docker可以算是对OS本身的虚拟化文件系统对应也被虚拟化了对文件系统的隔离算是个越来越强的需求了

设计目标

  • 提供抽象方法API
  • 降低抽象的额外开销
  • 提供应用程序的保护

最开始的操作系统只是一系列对硬件操作的API的集合库但是对资源的保护和虚拟化是库做不了的最先被虚拟化的资源是CPU通过对CPU时间片的管理一个CPU可以跑多个程序

想到看一些早期的数据库实现的论文写一个数据库前首先要选择一类机型有特定的CPU硬盘参数和驱动然后port一个操作系统在上面做线程, 文件系统的抽象那时候的操作系统估计做的就只是一些对硬件资源的抽象

进程

进程就是一个运行中的程序操作系统想运行一段程序需要经历

  • 将程序代码和静态数据(例如变量初始化的值从磁盘中加载到内存中因为虚拟内存机制加载可以是eager立刻将程序全部加载到内存中也可以是lazy只加载目前用到的部分
  • 为程序分配堆栈空间
  • 做一些初始化工作主要是I/O相关的例如UNIX会开启STDIN, STDOUT, STDERR这些文件
  • 跳转到main()函数处执行代码

操作系统将这样的一个运行中的程序的概念抽象为进程

对应的进程会有一些运行中可以读写的一些状态

  • 地址空间
  • 寄存器特殊的寄存器有Program CounterStack Pointerframe Counter等等
  • IO信息file descriptor

操作系统可以对进程做一些操作

  • 创建
  • 销毁
  • 等待等待一个进程结束
  • 获取进程状态
  • 一些杂项控制

当需要同时运行多个进程时OS需要让进程认为有无限个CPU可用通过对CPU资源的虚拟化来实现并发的运行例如分时系统每个进程都只能占用一定的时间片

简单的说进程的生命周期可以分成三类

  • 运行(RUNNING)进程执行中
  • 准备就绪(READY)进程可以被调度但尚未被调度
  • 阻塞BLOCKING进程被某些操作阻塞无法被调度例如执行某些IO操作IO操作未返回时

这三种状态可以相互流转

至于怎么流转取决于OS的调度策略

xv6进程状态:

  // the registers xv6 will save and restore
  // to stop and subsequently restart a process
  struct context
  {
    int eip;
    int esp;
    int ebx;
    int ecx;
    int edx;
    int esi;
    int edi;
    int ebp;
  };

  // the different states a process can be in
  enum proc_state
    { UNUSED,
      EMBRYO,
      SLEEPING,
      RUNNABLE,
      RUNNING,
      ZOMBIE
    };

  // the information xv6 tracks about each process
  // including its register context and state
  struct proc
  {
    char* mem;                  // Start of process memory
    uint sz;                    // Size of process memory
    char* kstack;               // Bottom of kernel stack
                                // for this process
    enum proc_state state;      // Process state
    int pid;                    // Process ID
    struct proc* parent;        // Parent process
    void* chan;                 // If !zero, sleeping on chan
    int killed;                 // If !zero, has been killed
    struct file* ofile[NOFILE]; // Open files
    struct inode *cwd;          // Current directory
    struct context context;     // Switch here to run process
    struct trapframe* tf;       // Trap frame for the
                                // current interrupt
  };

API

Unix关于进程的一些API

  • fork, 将目前的context复制创建出一个子进程子进程返回0父进程返回子进程的pid
  • wait, 等待一个进程完全停止
  • exec, 用一个新的程序完全替换掉当前进程

一种实现重定向的方式关掉STDOUTopen一个文件操作系统会从0寻找可用的文件描述符因为STDOUT比较小这个文件的fd就是STDOUT

有限直接执行机制

操作进程是如何执行一个进程的最简单的方法是PC跳转到进程的代码入口CPU直接执行进程的代码

限制操作

但是这样做之后每一个进程都会拥有CPU的所有权限可以用来读写其他进程的内存等等不应该访问的区域操作系统无法阻止进程做任何事情

为了防止这样的情况发生CPU引入了一套机制内核态和用户态在内核态下程序可以做任何事情操作系统运行在内核态下而操作系统执行的进程运行在用户态下操作系统提供了一系列的系统调用应用程序可以通过系统调用来访问一些资源

系统调用的实现是通过trap来做的当操作系统启动时会在内核态下注册一系列的trap跳转表跳转到操作系统的内部代码中进程执行系统调用时会通过trap进入内核态跳转到操作系统中的对应代码当系统调用完成时操作系统会将CPU的控制还给应用程序返回用户态

一次带系统调用的程序执行路径是这样的

  1. 操作系统启动时初始化trap跳转表CPU记下trap跳转的地址
  2. 操作系统从硬盘中将程序加载到内存中并分配堆栈空间寄存器初始化进程状态进入用户态跳转到程序的main函数中开始执行
  3. 进程执行到系统调用时通过cputrap进入到操作系统代码陷入内核态操作系统保存进程的寄存器等信息执行对应的操作
  4. 当操作系统执行完时将保存的进程上下文恢复返回用户态继续执行对应的程序
  5. 进程执行完后执行系统调用exit
  6. 操作系统清理进程的状态内存

中断执行

CPU在执行进程的代码时是无法执行操作系统的代码的操作系统也就无法控制进程的执行

解决这个问题一种方法是进程通过特殊的系统调用主动放弃对CPU的控制这个需要进程的主动配合

操作系统如果需要主动的掌握控制权必须要利用CPU的时钟中断启动时操作系统注册CPU的时钟中断启动计时器当一个时间片过去后会发生一次时钟中断CPU会跳转到操作系统注册的内存地址执行操作系统就相应掌握了CPU的控制权可以处理进程的调度了

切换线程执行是一次上下文切换的过程context swtich)上下文切换时操作系统会保存之前执行的进程的上下文加载将要运行的进程的上下文

当一次中断发生时如果另一次中断再次发生操作系统可以选择禁用中断也有一些锁机制防护对内部数据结构的并发修改

调度策略(policies)

OS怎么调度进程更合适呢

关于任务我们有一些假设

  1. 所有任务运行相等时间
  2. 所有任务启动的时间一致
  3. 启动任务后任务必须执行完
  4. 任务只使用CPU
  5. 操作系统知道所有进程的需要消耗的时间

有两种指标

  1. 周转时间turnaround time任务完成时间减去任务到达时间
  2. 响应时间Response Time第一次运行减去任务到达时间

FIFO

先到达先处理

12345都满足时是合理的

SJF

去掉假设1

先执行消耗时间最短的任务这样总和周转时间最短

STCF

去掉假设2,3

抢占式的算法可以打断一个任务执行

先执行完成时间最短的任务总和周转时间最短

RR

引入响应时间作为关注指标

每个任务执行一个时间片轮流执行

允许Overlap

去掉假设4,引入IO

将每个中间有IO的任务拆分成若干个独立的任务再使用STCF增加CPU的利用率

多级反馈队列调度算法

去掉假设5

思路设立多个队列按优先级由高到低排列CPU调度时从最高优先级队列开始取任务执行

  1. 当任务A的优先级 > 任务B任务A先执行
  2. 当两个任务优先级相等时两个任务以RR的方式执行
  3. 当任务到达时放入最高优先级队列中
  4. 如果任务在当前队列的时间片配额用完后被移入低一个等级的队列中
  5. 每过一段时间所有任务都被移入最高优先级的队列中