##第二章、进程与线程
操作系统最核心的概念就是进程,它是对正在运行的程序的一个抽象,也可以理解为对处理器的抽象。即使可用的CPU可用,但是依然可以支持多进程(伪)并发操作。
伪并行:严格的说,在一瞬间,CPU只能运行一个进程,但是在多道程序设计系统中,每个进程各运行几十或几百毫秒,那一秒内CPU就可以同时运行多个进程,给人产生了并行的错觉。
一个进程就是一个正在执行的程序,它包括程序计数器(PC)、寄存器、变量的当前值等。为了更好理解,本章假设计算机只有一个CPU。下图是对多个进程在CPU上运行的理解:
在任意时刻,总是只有一个进程在运行。
四种主要事件会导致进程创建:
####2.1.3 进程的终止
四种主要事件会导致进程的终止:
UNIX系统中,进程 创建另一个进程后,父进程和子进程就以某种形式继续保持联系,进程只有一个父进程,但可以有多个子进程。
Windows中没有进程层次的概念,所有进程都是地位相等的,但父进程创建爱你子进程后会得到一个令牌(称为句柄),该句柄可以用来控制子进程,但父进程有权把这个令牌传递给其他进程,这样,就不存在层次概念了。
进程有三种状态: 1.就绪; 2.运行;3.阻塞。进程总是在这三种状态见切换:
进程准备好后就可以运行了,但是这个时候其他程序在运行,因此进入就绪状态,当调度选择了这个进程时,它就可以运行了。而正在运行的进程,由于CPU给的时间用完了,但还没执行完毕,这时就会进入就绪转态,等待下次调度进入CPU。如果正在运行的进程由于需要I/O操作或者其他因素,需要等待一段时间才可以继续运行,就会进入阻塞状态,如果I/O执行完毕,就会进入就绪状态,再次等待运行。
为了实现进程模型,操作系统维护一张表格,即进程表,每个进程占用一个进程表项 (有的作者称这个表项为进程控制块), 典型的进程表项大致信息如下:
进程的切换都是一次中断,所有的中断都是从保存寄存器开始,对于当前进程而言,通常是保存在进程表项中, 中断的处理和调度过程如图:
CPU利用率 = 1 - p^n. p为一个进程等待I/O操作的时间与其在内存中时间的比值,n为程序的数量。
一个进程包含一个线程,对现在的系统而言,通常有多个线程。
多线程是在多进程的基础上继续的演化来的, 都是为了提升CPU的利用率,因为CPU是一种很宝贵的资源,因减少浪费。假设一个进程要执行三道数学题,如果没有线程的概念,那这个进程只能一道一道的去完成,执行第一道产生I/O操作时,就会阻塞,知道阻塞完成了才会继续执行。这样效率就比较低,而如果有了多个线程,每个线程执行一道数学题,互不干扰,这个线程就可以一直处于运行状态,抢到时间片的几率就大了,就可不断执行。并且,如果当前运行进程少或者其他进程也在阻塞,那没人用CPU,就是一种浪费,所以多线程可以尽量减少进程的阻塞,从而可以让CPU一直有事可做。当然,这只是一种比喻。 还有就是,进程切换的开销远大于线程切换的开销,毕竟,线程切换都是在同一个进程中。
进程间的线程不像同进程那样有很大独立性,所有线程有完全一样的地址空间,共享同样的全局变量,因此没有跨进程通信的麻烦。每个线程也有自己的程序计数器、寄存器、堆栈、转态等:
每个进程的数据、打开的资源、文件、代码都是相同的,每个线程都可以取用,但是每个线程都有寄存器、堆栈等。
用户线程:在用户空间实现线程,内核对线程一无所知, 内核依然按照老的方式,把进程当做单线程进程。
用户线程优缺点:
优点:
缺点:
内核线程:在内核空间实现线程。
最优实现:用户线程和内核线程混合实现。
进程间通信是很重要也很常用的的一个概念,主要围绕三个问题:
多个进程同时读写某些共享数据,就存在竞争。
多个进程同时需要访问的**【程序片段】称为临界区**。
为了避免竞争条件,引入互斥的概念,设计的方案应该满足以下四个条件:
首先明白两个概念, 同步与互斥
互斥:散步在不同进程的代码片段,当某个进程访问了其中一个片段时,此时其他进程就不能访问了,只能等该进程结束后才可访问。
同步:是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。显然,同步是一种更为复杂的互斥。
互斥的方案有:、屏蔽中断、锁变量、严格轮换发、Perterson解法、TSL指令。
屏蔽中断:效果太差,不会这么做
锁变量:理想化,软件难以实现
严格轮换法:需要用到忙等待,非常浪费CPU。导致其他进程可能被阻塞。 连续测试一个值直到某个值出现,称为忙等待, java示例代码如下:
while(true) {
while(a != 0) {
break;
}
}
如果a一直等于0,则无限循环下去。
Peterson解法:一个不需要严格轮换的软件互斥算法。
TSL指令:硬件支持的方案,需要用到TSL指令:
TSL RX, LOCK
TSL指令将 内存自LOCK读到寄存器RX中,然后在该内存地址上存在一个非零值。读字和写字由操作系统保证是不可分割的(原子性),CPU将锁住内存总线,以禁止其他CPU在本指令结束前访问内存。
一个可替代TSL的指令是XCHG,本质和TSL解决办法一致。
Perterson解法和TSL/XCHG解法都是正确可行的,他们的本质都是:当一个进程想进入临界区,先检查是否允许进入,若不允许则原地等待,直到允许为止。 但很浪费时间。引入生产者消费者,但没从根本解决。
信号量:将检查值、修改值以及可能发生的睡眠操作作为一个单一的、不可分割的原子操作完成,原子性由操作系统保证。在完成前,其他进程不允许访问信号量。
互斥量:信号量的简化版本,不需要计数能力,只需要两种状态:解锁和加锁。一个二进制位就可表示它,不过通常用整型。
管程:是编程语言的组成部分,编译器知道他们的特殊性。任意时刻管程中只有一个活跃进程。 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。管程具有面向对象编程的特点。
额外概念,同步原语:保证同步执行的代码语句。大致理解为只有将同步的代码执行完毕,才能顺序执行下一段代码。
通常用于进程组,把他们的执行划分了不同阶段,每个阶段末尾设置一个屏障,只有所有进程都到达屏障,才能继续运行下一个阶段。
多个进程同时竞争CPU,应该选择哪一个进程执行,这就由操作系统的 调度程序完成,它内部实现了调度算法。
进程切换:占用CPU资源的使用者发生了切换。需要保存当前进程在PCB(进程控制块)的执行上下文(寄存器、数据、打开资源文件等),然后恢复下一个进程的上下文。
调度:从就绪进程队列中挑一个进程去CPU上执行。
何时调度:
调度算法目标如下:
批处理系统中的调度算法:
交互式系统的调度算法:
1、轮转调度:也就是大家轮流来,一个进程分配固定时间片,时间到了还没执行完,则移动到就绪队列队尾,下一个进程接着来。
2、优先级调度:优先级高的进程先运行,统一优先级的则按照轮转调度。:
3、多级队列:举个例子,高优先级的进程先运行一个时间片,然后是次高级队列每个进程运行2个时间片,然后再次一级运行四个时间片。每个进程运行一次后,优先级降低一级。
4、最短进程优先
5、保证调度
6、彩票调度
7、公平分享调度
实时系统的调度:硬实时调度:在绝对截止时间前完成,软实时调度:在某个时间前后完成调度。
**调度策略与机制分离:**将调度算法以某种形式参数化,具体参数由用户进程写入。调度机制位于内核,调度策略可由用户进程决定。
**线程调度:**与进程调度类似。