重拾操做系统

主要是把在重读 《现代操做系统》 中以为有价值的东西,以 Tips 的形式记录下来。不可能面面俱到,可是若是有必定的基础应该是会回想起不少知识的。具体解释将会以连接形式补充。GitHub
相关阅读 : 重拾数据结构node

1. 进程与线程

1.1 进程

主要是为了支持伪并发能力git

  • 运行态 : 实际占用 CPU 资源
  • 就绪态 : 可运行,可是因为没有时间片而被暂停等待 CPU 从新调度
  • 阻塞态 : 外部某种事件致使(资源不足)不可运行

CPU 利用率 = 1 - p ^ n

p : IO 等待时间比

n : 进程数github

1.2 线程

每个进程有一个地址空间和一个控制线程,主要是使某个进程内的任务之间不被相互阻塞,实现一种进程内并行操做的假象。建立销毁更加轻量级。算法

共享一组资源,协同完成任务。每一个线程有本身的堆栈区(由于要区分同一进程内的线程,CPU 调度要进行状态的保存)编程

线程模型
用户空间中实现线程
内核中实现线程
混合实现

1.3 进程间通讯(IPC)

1.竞争条件

两个或者多个进程读写某些共享数据缓存

2.临界区

将共享内存的访问代码称为临界区,确保在每一个时刻两个进程不可能同时处于临界区中,这样能够避免竞争条件。核心思想为互斥。安全

并发程序准确高效要知足一下四个条件数据结构

  • 任何两个进程不能同时处于其临界区
  • 不该对 CPU 的速度和数量作任何假设
  • 临界区外运行的程序不得阻塞其余进程
  • 不得使进程无限期等待进入临界区
3.忙等待的互斥

互斥实现方案并发

屏蔽中断

每一个进程进入临界区后当即屏蔽全部中断,这样 CPU 没法进行进程切换,就要离开临界区是打开中断。函数

锁变量

设置一个共享锁变量,初始值为 0。当一个进程想要进入临界区,必须检测锁的值是否为 0,是则置 1 进入临界区。不是则等待其余进程退出临界区时释放锁直到本身能获取到锁开始进入临界区。

锁变量仍是会产生竞争条件

严格轮换法

一直循环等待直到出现容许该进程进入临界区的条件才开始运行,十分消耗 CPU 资源。

避免了竞争条件,可是临界区外运行的程序会发生阻塞

用于忙等待的锁称为自旋锁。

A:
while (TRUE) {
    while (turn != 0);
    critical_region();
    turn = 1;
    noncritical_region();
}

B:
while (TRUE) {
    while (turn != 1); 
    critical_region();
    turn = 0;
    noncritical_region();
}复制代码
Peterson 解法

一种互斥算法

#define FALSE 0
#define TRUE 1
#define N 2

int turn;
int interested[N];

void enter_region(int process) {
    int other;
    other = 1 - process;
    interested[process] = TRUE;
    turn = process;
    // 若是有另外一个程序进入临界区的话则一直空循环
    while (turn == process && interested[other] == TRUE);
}

void leave_region(int process) {
    interested[process] = FALSE;
}复制代码
4.睡眠与唤醒

前面的弊端是忙等待会消耗 CPU 资源。若是在等待进入临界区时能够挂起,等到某个信号到达再唤醒就能够避免这种状况了。

生产者-消费者问题

利用资源缓冲区实现进程间的协调

#define N 100 
int count = 0;

void producer(void) {
    int item;
    while (TURE) {
        item = produce_item();
        if (count == N) {
            sleep();
        }
        insert_item(item);
        count = count + 1;
        if (count == 1) {
            wakeup(consumer);
        }
    }
}

void consumer(void) {
    int item;
    while (TURE) {
        if (count == 0) {
            sleep();
        }
        item = remove_item();
        count = count - 1;
        if (count == N - 1) {
            wakeup(producer);
        }
        consume_item(item);
    }
}复制代码
5.信号量

引入一个信号量来累计唤醒次数,能够为 0 或正数

使用 down 和 up 操做代替 sleep 和 wakeup

#define N 100
typedef int semaphore
semaphore mutex = 1;  // 控制对临界区的访问
semaphore empty = N; // 计数缓冲区的空槽数目
semaphore full = 0; // 计数缓冲区的满槽数目

void producer(void) {
    int item;
    while (TRUE) {
        utem = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer(void) {
    int item;
    while (TRUE) {
        down(&full);
        down(&mutex);
        item = remove_item();
        up(&mutex);
        up(&empty);
        consume_item(item);
    }
}复制代码
  • mutex : 用于互斥,保证任一时刻只有一个进程读写缓冲区
  • full && empty : 实现同步,保证某种时间的顺序发生或者不发生
6.互斥量

仅仅适用于管理共享资源或一小段代码

7.管程
8.消息传递
9.屏障
1.4 调度

当有多个进程处于就绪态时就面临调度的选择。

内核管理线程时调度能够认为是线程级别的。

进程行为有 计算密集型I/O 密集型。而如今因为 CPU 改进速度加快,进程行为更倾向于后者,因此应该多运行该类进程保持 CPU 的利用率。

调度算法
  1. 批处理

    • 先来先服务
    • 最短做业优先
    • 最短剩余时间优先
  2. 交互式

    • 轮转调度(每一个进程一个时间片,用完就轮转)
    • 优先级调度
    • 多级队列
    • 最短进程优先 (aT0 + (1 - a)T1
    • 保证优先
    • 彩票调度
    • 公平分享调度
  3. 实时

线程调度

和系统支持的线程实现方式有关(理解 : 线程表存在哪的区别)

用户级线程 : 内核并不知道,内核只是选中该进程,至于进程中跑哪一个线程由用户态调度决定。

内核级线程 : 直接调度某个进程内的线程。

以上两种方式在性能上有主要差异 : 前面说起 I/O 操做实际上是很耗时的,因此在进程间切换比在线程间切换更加耗时。由于线程轻量,而进程完成切换要完整的山下文切换,修改内存映像。并且同一进程内的线程 I/O 访问 cache 的局部性更高,不一样进程间切换的清理缓存,这也会消耗时间。

2. 存储管理

主要思想就是内存抽象

2.1 空闲内存管理

使用位图的存储管理
使用链表的存储管理

2.2 虚拟内存

程序产生的地址为虚拟地址,在没有虚拟内存的操做系统上,直接将地址输送到内存总线上。而有虚拟内存的操做系统上,把虚拟地址输送到 MMU(Memory Management Unit),由 MMU 将虚拟地址映射为为物理地址。

分页

虚拟地址空间 : 页面 物理内存地址 : 叶框 4k大小

虚拟地址 = 虚拟页号(高位) + 偏移量(低位)

页表 : 把虚拟地址的页面映射为页框,每一个进程都有本身的页表

加速分页方法 : 转换检测缓冲区(TLB)主要是优先在 TLB 中查找页面号。

大内存页表

  1. 多级页表
  2. 倒排页表 : 每一个页框一个表项 + TLB 快表

2.3 页面置换算法

最优页面置换算法不可实现,由于没法肯定将来。

1.最近未使用页面置换算法(NRU)

设置访问(读、写)位 R,页面修改位 M。

2.先进先出页面置换算法(FIFO)
3.第二次机会页面置换算法

设置一个检测最老页面位 R

4.时钟页面置换算法

链表实现页面选择

5.最近最少使用页面置换算法(LRU)

利用矩阵模拟 : 增长自身权重减小其余权重,行置 1,列置 0。

6.用软件模拟 LRU

老化算法

7.工做集页面置换算法
8.工做集时钟页面置换算法
算法 注释
最优算法 不可实现,但可做为基准
NRU(最近未使用)算法 LRU 的很粗糙近似
FIFO(先进先出)算法 可能抛弃重要页面
第二次机会算法 比 FIFO 有大的改善
时钟算法 现实的
LRU(最近最少使用)算法 很优秀,但很难实现
NFU(最不常用)算法 LRU 的相对粗略的近似
老化算法 很是近似 LRU 的有效算法
工做集算法 实现起来开销很大
工做集时钟算法 好的有效算法

2.4 内存映射文件

进程发起系统调用,把文件映射到其虚拟地址空间的一部分。通常实现是开始不加载,在程序访问时在按页加载。

// Linux 待填

2.5 实现

分页工做
  • 进程建立时 : 操做系统要肯定程序和数据在初始时有多大,并为它们建立一个页表,操做系统还要在内存中为页表分配空间并对其进行初始化。
  • 进程运行时 : 页表必须在内存中(反之不须要),而且在磁盘交换区中分配空间。
  • 调度一个进程执行时 : 为新进程充值 MMU,刷新 TLB,更换页表。
  • 缺页中断发生时 : 操做系统必须经过读硬件寄存器肯定是哪一个虚拟地址形成了缺页中断经过该信息计算须要哪一个页面,定位磁盘位置并找到合适的页框来存放新页面,必要的话要置换老页面,而后把所需页面读入页框。最后,备份程序计数器,是程序计数器指向引发缺页中断的指令,并从新执行该指令。
  • 进程退出时 : 释放页表,页面和页面在硬盘上占的空间。
缺页中断处理
  1. 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前的指令的各类状态信息保存在特殊的 CPU 寄存器中。
  2. 启动一个汇编代码例程保存通用寄存器和其余易失信息,以避免被操做系统破坏。这个例程将操做系统作为一个函数来调用。
  3. 当操做系统发现一个缺页中断时,尝试发现须要哪一个虚拟页面。一般一个硬件寄存器包含了这一信息,若是没有的话,操做系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看他在缺页中断时正在作什么。
  4. 一旦知道了发生缺页中断的虚拟地址,操做系统检查这个地址是否有效,并检查存取与保护是否一致,若是不一致,向进程发出一个信号或杀掉该进程。若是地址有效且没有保护错误发生,系统则检查是否有空闲页框。若是没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
  5. 若是选择的页框“脏”了,安排该页面写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其余进程运行直至磁盘传输结束。不管如何,该页框被标记为忙,以避免由于其余缘由而被其余进程占用。
  6. 一旦页框“干净”后(不管是马上仍是在写回磁盘后),操做系统查找所需页面在磁盘上的地址,经过磁盘操做将其装入。该页面被装入后,产生缺页中断的进程仍然被挂起,而且若是有其余可运行用户进程,则选择另外一个用户进程运行。
  7. 当磁盘中断发生时,代表该页已被装入,页表已经更新能够反映他的位置,页框也被标记为正常状态。
  8. 恢复发生缺页中断指令之前的状态,程序计数器从新定向这条指令。
  9. 调度引起缺页中断的进程,操做系统返回调用他的汇编语言例程。
  10. 该例程恢复寄存器和其余状态信息,放回到用户空间继续执行,就好像缺页中断没有发生过同样。

2.6 分段

段是逻辑实体,大小不固定。

2.7 分段和分页结合 : MULTICS

还有 Intel Pentuium 未介绍

34 位的 MULTICS 虚拟地址

段号 页号 页内偏移
18 6 10
  1. 根据段号找到段描述符
  2. 检查该段的页表是否存在内存中。若是在,则找到他;若是再也不,则产生一个段错误。若是访问违反了段的保护要求就要求发出一个越界错误(陷阱)。
  3. 检查所请求虚拟页面的页表项,若是该页面再也不内存中则产生一个缺页中断,若是在内存就从页表中取出这个页面在内存中的起始地址。
  4. 把偏移量加到页面的起始地址上,获得要访问的字在内存中的地址。
  5. 最后进行读或写操做。

3. 文件系统

文件系统存放在磁盘上。多数磁盘划分为一个或多个分区,每一个分区中有一个独立的文件系统。磁盘的 0 号盘扇区称为主引导记录(Master Boot Record, MBR),用来引导计算机。在 MBR 的结尾是分区表,该表给出了每一个分区的其实和结束地址。表中的一个分区被标记为活动分区。在计算机被引导时,BIOS 读入并执行 MBR。MBR 作的第一件事是肯定活动分区,读入它的第一个块,称为引导块,并执行。

整个分区:

MBR 分区表 磁盘分区 磁盘分区 磁盘分区...

磁盘分区:

引导块 超级块 空闲空间管理 i 节点 根目录 文件和目录

3.1 文件实现

连续分配

把每一个文件做为一连串连续数据块存储在磁盘上。

链表分配

一个文件由几个磁盘块组成。

在内存中采用表的链表分配

把每一个磁盘块的指针字放在内存的一个表中

i 节点

每一个文件赋予一个称为 i 节点(index-node)的数据结构,列出文件属性和文件快的磁盘地址。

4. 输入/输出

4.1 I/O 硬件原理
I/O 设备

块设备 : 以块为单位传输,可寻址

字符设备 : 以字符为单位收发字符流,不可寻址

设备控制器
内存映射 I/O
直接存储器存取

DMA 工做原理:

  1. CPU 对 DMA 控制器进行编程
  2. DMA 请求磁盘传送数据到内存
  3. 磁盘传送数据到内存
  4. 磁盘给 DMA 控制器应答
  5. 完成中断

5. 死锁

5.1 资源

在进程对设备,文件等取得了排他性访问权限的时候,有可能会出现死锁。这类须要排他性使用的对象称为资源。

可抢占资源

能够从拥有它的进程中抢占而不会产生任何反作用。(存储器)

不可抢占资源

指在不引发相关的计算失败的状况下,没法把他从占有它的进程处抢占过来。( CD 刻录)

资源使用步骤:

  1. 请求资源
  2. 使用资源
  3. 释放资源

5.2 死锁概述

若是一个进程集合中的每一个进程都在等待只能由该进程集合中的其余进程才能引起的事件,那么,该进程集合就是死锁的。

资源死锁条件

发生资源死锁的四个必要条件:

  1. 互斥条件 : 每一个资源要么已经分配了一个进程,要么就是可用的。
  2. 占有和等待条件 : 已经获得了某个资源的进程能够再请求新的资源。
  3. 不可抢占条件 : 已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。
  4. 环路等待条件 : 死锁发生时,系统中必定有两个或两个以上的进程组成的一条环路,该环路中的每一个进程都在等待着下一个进程所占有的资源。

5.3 死锁检测与死锁恢复

死锁检测主要是判断当前空闲资源在某种合理分配下是否能使全部进程都运行完而且最终资源都可以释放。

恢复方法 :

  1. 利用抢占式恢复
  2. 利用回滚恢复
  3. 利用杀死进程恢复

5.4 死锁避免

资源轨迹图
安全状态和不安全状态
单个资源的银行家算法
多个资源的银行家算法

5.5 死锁预防

死锁避免从本质上来讲是不可能的,由于他要获取将来的信息。

破坏互斥条件

若是资源不被一个进程独占死锁不会发生。(假脱机打印机)

破坏占有和等待条件

开始执行前请求全部资源就不会形成等待。另外一种是请求资源时先释放本身所持有的资源,再尝试一次请求资源。

破坏不可抢占条件

针对某些资源进行虚拟化,实现可抢占。

破坏环路等待条件

保证每一个进程在任什么时候刻只能占用一个资源若是要请求另一个资源它必须先释放第一个资源。另外一种是将全部资源统一编号,进程能够在任什么时候刻提出资源请求,可是请求必须按照资源编号顺序(升序)提出。

5.6 其余问题

两阶段加锁
通信死锁
活锁
饥饿
相关文章
相关标签/搜索