第八章 进程同步
竞争条件
竞争条件:多个进程并发访问和操作统一数据并且执行结果和特定的访问顺序有关。
多个进程可能共享一块内存,同时共享全局变量可能导致数据不一致
临界区:能访问共享资源的代码
能解决竞争条件的办法必须满足以下四个条件:
- 互斥:如果进程P1在临界区内执行,其他进程不能再临界区执行
- 有空让进:临界区无进程,则允许其他进程进入
- 有限等待:从一个进程请求进入临界区到这个请求被允许期间,其他进程允许进入临界区的次数有上限
- 不对CPU的速度和数量做出假设
解决办法
软件上
First Try : 不满足有空让进,因为当P1出临界区后再立即进入临界区,P0就无法进入临界区。(无限推迟)????
Second Try: 两个进程同时执行代码:flag[i]=true,则出现死锁
Third Try(Peterson 算法) 相较于第二次尝试:不会产生死锁--因为turn的值不是0就是1,当do循环里的第一行和第二行代码两个进程同时执行的时候,Turn的值最后0或1,这样某个进程就能进入临界区,而另一个就进入不了。
相较于第一次尝试:
面包店算法(因为Perterson算法只能满足两个进程,不能满足多个进程)
//n是要进入临界区的所有进程的数量 bool choosing[n] = {false,false,...,false}; int number[n] = {0,...,0}; do{ choosing[i] = true; number[i] = max(number[0],...,number[n-1])+1; //按照严格地FIFL机制,防止客户取到相同的号码牌 choosing[i] = false; for(j=0;j<n;j++){ while(choosing[j]);//等没有进程取号码牌,号码牌稳定的时候 while((number[j]!=0)&&(number[j],j)<(number[i],i)); //首先number不等于0,表示这个进程要参与排队 //被扫描的号码要是比自己的号码小,要等待,先比较号码,再比较顾客编号 //防止出现饥饿现象:最多等n-1个进程(有限等待) //有空让进:不懂 //互斥:肯定的呀,将number[i]作为互斥锁,前面的进程没搞完后面的就一直在循环里 } CS; number[i] = 0; // 不参与排队 }while(1);
硬件上
- 关中断:让临界区变为原子型,不能被打断
缺点:用户态不应该关中断,它是一个需要很高权限的指令;对于多核处理器系统来说,关掉中断很难应付
- 特殊指令:TSL 和SWAP(原子型函数)
bool TSL(bool &target){ bool rv = target; target = true; return rv; } bool SWAP(bool &a,bool &b){ bool temp = a; a = b; b = temp; }
利用上述特殊指令来解决问题:
bool lock = false; do{ while(TSL(lock));//用lock来进行互斥 CS; lock = false; }while(1);
bool lock = false; do{ bool key = true; while(key == true) SWAP(lock,key); CS; lock = false; }while(1);
以上两种算法都不满足有限等待,能进入临界区的进程是随机选中的,而不是严格的排队机制
进行改进
bool lock = false; waiting[n] = {false,...,false}; do{ waiting[i] = true; bool key = true; while(waiting[i]&&key) key = TSL(lock); CS; int j = (i+1)%n; while(j!=i && !waiting[j]) j = (j+1)%n; if(j==i) lock = false; //为啥这就是lock 其他都是waiting[j]=false else waiting[j] = false; }while(1);
以上算法的缺点:盲等待--成本上升
信号量
两个标准原子操作:P和V
typedef struct{ int value=1;//通常初始值为1 它的绝对值等于waiting队列进程个数 struct PCB *L; }semaphore; void P(semaphore *S){ S->value--; if(S->value <0){ add(当前进程) to S->L (当前进程)->state = WAITING;//加入等待队列 scheduler(); } } void V(semaphore *S){ S->value++; if(S->value <=0) remove a proc(A) from S->L; A->state = ready;//唤醒进程A }
监视器
信号量和监视器的关系
结论
第九章 内存管理
内存管理介绍:
内存管理的任务:提供一个虚拟的机器接口,让每一个进程都以为是自己独占内存
基本方法:
MS-DOS操作系统中采用OVERLAY的方法:就是只将按需加载到内存中,而不是将整个进程加载到内存中。
Overlay的思想:无论进程在运行的时候占有多大的内存,在某一段时间内,他只会访问其中的一部分。
Q1:支持多线程的系统,系统已经没有内存可以使用,但某个正在运行的进程还需要申请更多的内存,需要怎么办?
Answer: Swap--将不在运行的进程交换到Backing-store中,释放它的内存,分配给要使用的进程。当调度器重新调度时,OS再从backing-store中加载到内存继续运行。注:backing-store一般是快速、大容量的硬盘,
swap的思想:内存不足的时候,向backing-store借一部分。
多任务环境会给内存管理带来一系列问题:
重定位
cpu生成逻辑地址,并不是在内存中的实际地址,无法完成绝对的地址绑定。这样的程序只能被加载到0地址的内存运行,如果该程序被加载到其他非0的地址,必须对程序中所引用的地址进行修改才能运行,这个修改的过程叫做重定位。
逻辑地址:程序中引用的地址(表达代码空间存储的需求);
物理地址:系统中内存单元所看到的地址;
内存管理单元:专门完成逻辑地址到物理地址转换的硬件单元,一般是CPU的一部分
地址绑定
编译时绑定:逻辑地址等于物理地址--手机的bootLoader
加载时绑定:重定位(偏移量固定)
执行时绑定:物理内存小于逻辑内存(偏移量不确定)
内存保护(需要硬件支持)
通过对逻辑地址和物理地址的绑定,指定进程只能访问内存的某一个区间,不能超界。一般用MMU通过硬件来检查。
内存分配(动态内存分配问题)
空闲内存称为Hole,出现问题:随着进程的加载和进程结束后内存的释放,内存的空闲空间分散开,导致外部碎片太多,必须设计专门的算法来进行内存的分配和释放来减少外部碎片,提高内存的使用率。
首次适应:找到第一个能放下进程的hole
最佳适应:找到最小的能放下进程的hole(最慢)
最坏适应:将Hole降序排列,找第一个hole(最快)
另外一种方法:将系统内存分为固定大小的内存块,以块为单位进行内存的分配和释放,最终分给进程的内存比进程需要的多,形成内部碎片。
分页内存管理(硬件和操作系统共同完成)(实现分段函数)
解决的问题:进程所占用的物理内存连续,系统不能产生太多外部碎片,整个进程swap-in/out很耗时。
- 将逻辑地址分成两部分,page number和page offset
- 地址转换:在page table的帮助下,MMU把CPU产生的逻辑地址转换为物理地址,根据page number访问页表,来找到帧号,帧号乘以页的大小,加上偏移量,就是物理地址。
- 页的大小必须是2的n次方
- 具体计算要清楚。
- 每次内存访问都需要两次内存操作:CPU的两个寄存器一个保存页表的地址,另一个保存页表的大小。为了提高地址转换的效率,MMU包含了一个高速缓存称为TLBs
分段内存管理
段页式内存管理