CS-Notes 操作系统读书笔记
进程与线程
这是一个超链接
1. 进程是资源分配的最小单位
进程控制块(PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。
2. 线程是独立调度的基本单位
一个进程中可以有多个线程,它们共享进程资源
区别:
1. 进程是资源分配的最小单位,线程是独立调度的基本单位,线程不拥有资源,线程可以访问隶属进程的资源
2. 线程是独立调度的基本单位,在同一进程中,线程切换不会影响进程切换。
3. 系统开销,由于创建或者撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。
在线程切换的时候,只需要保存和设置少量寄存器内容,开销很小
4. 通信方面,线程间可以通过直接读写同一进程中的数据进行通信,但是进程通讯需要借助IPC
进程状态间的切换
就绪状态 运行状态 阻塞状态
只有就绪状态和运行状态可以相互转换
其他都是单向转换,就绪状态通过调度算法获取CPU时间,转为运行状态,而运行状态的进程,在分配给它的CPU时间片使用完成后会变成就绪状态,等待下次调度
阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括CPU时间,缺少CPU时间会从运行态转换为就绪态
进程调度算法
批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量跟周转时间
1. FCFS 先来先服务
非抢占式调度算法,按照请求的顺序进行调度
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业有需要执行很长时间,短作业就得等待
2. 短作业优先(SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能被饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度
3. 最短剩余时间优先(SRTN)
最短作业优先的抢占式版本,按剩余运行的时间进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较,如果新的进程需要的时间更少,就挂起当前进程,运行新的进程,否则就新的进程等待
交互式系统有大量的用户交互操作
1. 时间片轮转
将所有就绪进程按FCFS的原则排成一个队列,每次调度时,将CPU时间分配给队首进程,该进程可以执行一个时间片,当时间片使用完毕时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
因为进程切换都要保存
进程切换过于频繁,在进程切换上就会花过多的时间
2. 优先级调度
为每个进程分配一个优先级,按照优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级
3. 多级反馈队列
一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
进程同步
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,都需要先进行检查
同步与互斥
同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
互斥:多个进程在同一时刻只有一个进程能进入临界区
信号量是一个整形变量,可以对其执行down和up操作,也就是常见的P和V操作
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
使用信号量实现生产者-消费者问题
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE) {
int item = produce_item();
down(&empty);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
consume_item(item);
up(&mutex);
up(&empty);
}
}
管程
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。
管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
进程通信
管道pipe,通过调用pipe创建,fd[0]读取
fd[1]用于写
只支持半双工通信
只能在父子进程或者兄弟进程间使用
附上代码
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define INPUT 0
#define OUTPUT 1
/*
只能适用于父子进程之间的通信,无名管道
*/
int main(){
int fd[2];
pid_t pid;
char buf[256];
int returned_count;
pipe(fd);
pid = fork();
if(pid < 0){
perror(" error fork \n");
return 0;
}else if(pid == 0){
printf("this is son\n");
close(fd[INPUT]);
write(fd[OUTPUT], "hello world", strlen("hello world"));
return 0;
}else{
printf(" this is father\n");
close(fd[OUTPUT]);
returned_count = read(fd[INPUT], buf, sizeof(buf));
printf("the message is %s\n", buf);
}
return 0;
}
FIFO
命名管道,去除了管道只能在父子进程中使用的限制
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define P_FIFO "/tmp/p_fifo"
int main(int argc, char ** argv){
char cache[105];
int fd;
memset(cache, 0, sizeof(cache));
if(access(P_FIFO, F_OK) == 0){
execlp("rm", "-f", P_FIFO, NULL);
printf("access.\n");
}
if(mkfifo(P_FIFO, 0777) < 0){
printf("error fail\n");
return -1;
}
fd = open(P_FIFO, O_RDONLY | O_NONBLOCK);
while(1){
memset(cache, 0, sizeof(cache));
if(read(fd, cache, 100) == 0){
printf("no data:\n");
}else{
printf("yes have data: %s\n", cache);
}
sleep(1);
}
close(fd);
return 0;
}
消息队列
消息队列可以独立于读写进程存在,从而避免了FIFO中同步管道的打开和关闭时的困难
避免了FIFO的同步阻塞问题,不需要进程自己提供同步方法
读进程也可以根据消息类型有选择地接受消息,而不是像FIFO那样只能默认地接收
信号量:它是一个计数器,能为多个进程提供共享数据对象的访问
共享存储:允许多个进程共享一个给定的存储区,因为数据不需要在进程之间复制,所以这是最快的一种IPC,需要使用信号量来同步对共享存储的访问,多个进程可以将同一个文件映射到他们的地址空间从而实现共享内存
套接字:socket编程
死锁
四个必要条件:
1. 互斥:每个资源都分配给了一个进程,要么就是可用的
2. 占有跟等待:已经得到了某个资源的进程可以再请求新的资源
3,不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占用它的进程进行显式地释放
4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源
处理方法:
1.鸵鸟策略
把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
2.死锁检测与死锁恢复
不试图阻止死锁,每当检测到死锁发生时,就采取策略进行恢复
1)每种类型一个资源的死锁检测就是通过检测有向图是否存在环来实现的。通过一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的存在。
2)每种类型多个资源的死锁检测
参考github的CS-Notes
3.死锁恢复
利用抢占恢复,利用回滚恢复,通过杀死进程恢复
死锁预防
1.破坏互斥条件
2.破坏占有跟等待条件
3.破坏不可抢占条件
4.破坏环路等待
银行家算法:参考CS-notes
内存管理
这是一个超链接
虚拟内存
虚拟内存的目的是为了让物理内存扩充为更大的逻辑内存,从而让程序获得更多的可用内存
为了更好的管理内存,操作系统将内存抽象地址空间,每个程序都拥有自己的地址空间,这个地址空间被分割成很多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。
分页系统地址映射
内存管理单元MMU管理着地址空间和物理空间的转换,其中页表存储着页(程序地址空间)和页框(物理内存空间)的映射表
参考github上的CS-Notes的图
页面置换算法
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。
页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。
OPT算法理论上最优秀
LRU算法最近最久未被使用,为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。
NRU算法每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:
R=0,M=0
R=0,M=1
R=1,M=0
R=1,M=1
当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。
NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。
FIFO算法,先进先出
第二次机会算法,参考github CS-Notes
分段
虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。
下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。
分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。
段页式
程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分为大小相同的页,这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能
分页与分段的比较
对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。
地址空间的维度:分页是一维地址空间,分段是二维的。
大小是否可以改变:页的大小不可变,段的大小可以动态改变。
出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
设备管理
磁盘结构:
盘面(Platter):一个磁盘有多个盘面;
磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
制动手臂(Actuator arm):用于在磁道之间移动磁头;
主轴(Spindle):使整个盘面转动。
磁盘调度算法:
读写一个磁盘块的时间的影响因素有:
旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
实际的数据传输时间
其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。
1.先来先服务
FCFS按照磁盘请求的顺序进行调度
优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使得平均寻道时间可能较长
2. 最短寻道时间优先SSTF
优先调度与当前磁头所在磁道举例最近的磁道,虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
3. 电梯算法SCAN
电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。
因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。