进程的切换
进程的切换本质上是保存当前进程的状态信息到该进程的PCB中,并载入接下来要运行的被调度算法选中的进程的PCB信息到CPU中。
并且我们要明白,PCB的信息都是在内核态中维护,所以进程的切换是在内核中完成的。
PCB的切换通常我们也称之为上下文切换(context switch)
进程的生命周期
- 进程执行结束后,他调用exit()
- 这个系统调用:
- 将此进程的“结果”作为一个参数
- 关闭所有打开的文件、连接等等
- 释放内存
- 释放大部分支持进程的操作系统数据结构
- 检查是否有父进程存活者:
- 如果有,它保留结果的值直到父进程需要它:这种情况下,进程没有真正死亡,但是它进入了僵尸(Zombie/defunct)状态
- 如果没有,释放所有的数据结构,这个进程死亡
- 清理所有等待的僵尸进程
- 进程终止是最终的垃圾收集(资源回收)
Critical Section(临界区)
临界区十一个代码区,是当一个进程中的一段代码需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域。
关于临界区上锁的几种锁的实现方式:
- 屏蔽硬件中断。这种方式直接屏蔽了时钟中断,从而可以防止操作系统对任务(线程/进程)的调度,但是这也仅限于单CPU的情况,对于多CPU或多核,这种方式无效。
- 软件实现,如Peterson算法,等等,但是这种算法比较复杂,时间开销也不小……
- 硬件体系支持的原子操作(软硬件结合实现的方式还是很牛逼的)
下面着重记录第三种方式:
硬件提供了一些原语:
中断禁用、原子操作等
大多数现代体系结构都是这样的
操作系统提供更高级的编程抽象来简化并行编程
例如:锁,信号量
从硬件原语中构建
锁是一个抽象的数据结构
一个二进制状态(锁定/解锁),两种方法
Lock::Acquire() 锁被释放前一直等待,然后得到锁
Lock::Release() 释放锁,唤醒任何等待的进程
大多数现代体系结构都提供特殊的原子操作指令
通过特殊的内存访问电路
针对单处理器和多处理器
Test-and-Set 测试和侧位(注意,这就是硬件提供的原子操作指令)
从内存中读取值
测试该值是否为 1 (然后返回真或假)
内存值设置为 1
交换 exchange
交换内存中的两个值
1 boolean TestAndSet(boolean *target){ // 注意,这些都是示意代码,实际代码是由汇编指令或内嵌汇编实现的 2 boolean rv = *target; 3 *target = TRUE; 4 return rv; 5 } 6 7 void Exchange(boolean* a, boolean* b){ 8 boolean temp = *a; 9 *a = *b; 10 *b = temp; 11 }
使用Test-and-Set实现的锁
使用 Exchange 实现的锁
硬件原子操作实现锁的好处:
适用于单处理器或共享主存的多处理器中任意数量的进程
简单并且容易证明
可以用于支持多临界区