进程的描述与控制
前驱图和程序执行
前驱图:
一种DAG,描述进程之间执行的前后关系。
程序的顺序执行:
顺序性、封闭性、可再现性。
程序的并发执行:
间断性、失去封闭性、不可再现性。
进程的描述
进程的定义:
- 系统利用 PCB(进程控制块)来描述进程的基本情况和活动过程,进而控制和管理进程。
- 由程序段、相关的数据段和 PCB 三部分构成进程实体。一般将进程实体简称为进程。
- 所谓创建进程,实质上是创建进程实体中的 PCB;而撤消进程,实质上是撤消进程的 PCB。
进程的特征:
- 动态性。
由创建而产生,由调度而执行,由撤消而消亡。 - 并发性。
- 独立性。
进程实体是一个能独立运行、独立分配资源和独立接受调度的基本单位。 - 异步性。
进程的三种基本状态:
- 就绪状态。
进程已分配到除 CPU 以外的所有必要资源。 - 执行状态。
- 阻塞状态。
正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态。
通常将处于阻塞状态的进程也排成一个队列,称为阻塞队列。在较大的系统中则根据阻塞原因的不同会设置多个阻塞队列。
进程的创建状态和终止状态:
- 创建状态。
进程创建的步骤:1)由进程申请一个空白PCB。2)向PCB中填写用于控制和管理进程的信息。3)为进程分配运行时所必须的资源。4)把进程转入就绪状态并插入到就绪队列中。 - 终止状态。
进程终止的步骤:1)等待操作系统进行善后处理。2)将其 PCB 清零,并将 PCB 空间返还系统。
在系统进行善后处理后,进程仍在系统中保留一个记录,其中保存状态码和一些计时统计数据,供其它进程收集。一旦其它进程完成了对其信息提取之后,操作系统将删除该进程,即将其 PCB 清零,并将 PCB 空间返还系统。
挂起操作的引入:
基于如下需要:1)终端用户的请求。2)父进程请求。3)负荷调节的需要。4)操作系统的需要。
在引入挂起原语Suspend和激活原语Active后导致的几种状态转换:
1)活动就绪(Readya) –> 静止就绪(Readys)。2)活动阻塞(Blockeda) –> 静止阻塞(Blockeds)。
3)静止阻塞(Blockeds) –> 静止就绪(Readys)。 [ 处于该状态的进程在其所期待的事件出现后发生状态转换 ]
4)静止就绪(Readys) –> 活动就绪(Readya)。5)静止阻塞(Blockeds) –> 活动阻塞(Blockeda)。
进程管理中的数据结构:
- 操作系统中用于管理控制的数据结构。
内存表、设备表、文件表和用于进程管理的进程表(又被称为进程控制块 PCB)。 - 进程控制块 PCB 的作用(操作系统中最重要的记录型数据结构)。
1)作为独立运行基本单位的标志。
PCB 是进程存在于系统中的唯一标志。
2)能实现间断性运行方式。
可以将 CPU 现场信息保存在中断进程的 PCB 中,以在再次被调度时恢复。
3)提供进程管理所需要的信息。
4)提供进程调度所需要的信息。
5)实现与其它进程的同步与通信。 - 进程控制块中的信息。
1)进程标识符。
外部标识符、内部标识符(唯一的数字标识符)。
2)处理机状态。
即处理机的上下文,由处理机的各种寄存器中的内容组成。寄存器包括:通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
3)进程调度信息。
进程状态、进程优先级、进程调度所需要的其它信息(例如已执行的时间总和)、事件(阻塞原因)。
4)进程控制信息。
程序和数据的地址、进程同步和通信机制、资源清单(列出了除 CPU 外的全部资源;还有一张已分配到的资源清单)、链接指针(本进程所在队列中的下一进程的 PCB 的首地址)。 - 进程控制块的组织方式。
1)线性方式。2)链接方式。3)索引方式。
进程控制
进程控制一般是由 OS 的内核中的原语来实现的。
操作系统内核:
通常将一些与硬件紧密相关的模块、各种常用设备的驱动程序以及运行频率较高的模块,都安排在紧靠硬件的软件层次中,将它们常驻内存,即通常被称为的 OS 内核。目的在于两方面:一是便于对这些软件进行保护,防止遭受其他应用程序的破坏;二是可以提高 OS 的运行效率。
为防止 OS 本身及关键数据遭受应用程序破坏,通常将处理机的执行状态分成:
1)系统态:又称管态、内核态,能执行一切指令,访问所有的寄存器和存储区;
2)用户态:又称目态,仅能执行规定的指令,访问指定的寄存器和存储区。
大多数 OS 内核包含以下两大方面的功能:
1. 支撑功能。
1)中断处理。2)时钟管理。3)原语操作。
2. 资源管理功能。
1)进程管理。2)存储器管理。3)设备管理。
进程的创建:
- 进程的层次结构。
当子进程被撤销时,应将从父进程获得的资源归还给父进程;当父进程被撤销时,也必须同时撤销其所有的子进程。 - 进程图。
- 引起创建进程的事件。
1)用户登录。2)作业调度。3)提供服务。4)应用请求。 - 进程的创建。
OS 调用进程创建原语Creat进行创建新进程:
1)申请空白 PCB,为新进程申请获得唯一的数字标识符,并从 PCB 集合中索取一个空白 PCB。
2)为新进程分配其运行所需的资源。
3)初始化进程控制块:初始化标识信息、初始化处理机状态信息、初始化处理机控制信息
4)如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
进程的终止:
- 引起进程终止的事件。
1)正常结束。3)异常结束。3)外界干预。 - 进程的终止过程(下述过程即之前所谓的操作系统进行善后处理)。
1)根据被终止进程的标识符,从 PCB 集合中检索出该进程的 PCB。
2)若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度。
3)还应将其所有子孙进程予以终止,以防它们成为不可控的进程。
4)将被终止进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
5)将被终止进程(PCB)从所在队列(或链表)中移出,等待其他程序来搜集信息。
进程的阻塞与唤醒:
- 引起进程阻塞和唤醒的事件。
1)向系统请求共享资源失败。
2)等待某种操作的完成。
3)新数据尚未到达。
4)等待新任务的到达。 - 进程阻塞过程。
进程通过调用阻塞原语 block 把自己阻塞。阻塞是进程自身的一种主动行为。 - 进程唤醒过程。
由有关进程(比如用完并释放了该 I/O 设备的进程)调用唤醒原语 wakeup。
进程的挂起与激活:
- 进程的挂起。
利用挂起原语 suspend。 - 进程的激活过程。
利用激活原语 active。
假如采用的是抢占调度策略,则每当有静止就绪进程被激活而插入就绪队列时,便应检查是否要进行重新调度。
进程同步
进程同步的基本概念
1 . 两种形式的制约关系。
1)间接相互制约关系。
共享系统资源。特别地,对于临界资源只能是互斥访问。
2)直接相互制约关系。
为完成同一项任务而相互合作。
2. 临界资源。
通过经典问题生产者-消费者问题来说明为什么要对互斥资源互斥的访问:
int in=0,out=0,count=0; //共享变量:生产、消费指针,产品数目
item buffer[n]; //共享数组变量:包含n个缓冲区的缓冲池
void producer(){ //生产者进程
while(1){
produce an item in nextp; //生产一个产品nextp
...
while(count==n) //判断缓冲池是否已满
;
buffer[in]=nextp; //产品nextp投放到in指针所指的缓冲区
in=(in+1)%n; // in指针向后移动一下
count++; //产品数目加1
}
};
void consumer(){ //消费者进程
while(1){
while(count==0) //产品数不等于0时,可以消费
;
nextc=buffer[out]; //out所指缓冲区即为要消费的产品
out=(out+1)%n; //out指针向后移动
count--; //产品数目减1
consume an item in nextc; //开始消费产品nextc
...
}
};
如果生产者程序和消费者程序并发执行就会出现差错,问题就在于这两个进程共享变量 count。
count–,count++这两个操作在用机器语言实现时, 常可用下面的形式描述:
register1=count;——————– register2=count;
register1=register1+1;———— register2=register2-1;
count=register1;——————– count=register2;
倘若两段程序中各语句交叉执行的顺序不是一段执行后再执行另一段,则count会得到错误值。
解决此问题的关键是应把变量 count 作为临界资源处理,亦即,令生产者进程和消费者进程互斥地访问变量 count。
3. 临界区。
在每个进程中访问临界资源的那段代码称为临界区。
在临界区前面要加上一段称为进入区的代码,用于对欲访问的临界资源进行检查,看它是否正被访问。
在临界区后面也要加上一段称为退出区的代码,用于将临界区正被访问的标志恢复为未被访问的标志。
除进入区、临界区和退出区之外的其它部分的代码,都称为剩余区。
4. 同步机制应遵循的规则。
1)空闲让进。 2)忙则等待。 3)有限等待。 4)让权等待。
硬件同步机制
1 . 关中断。
在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。在此过程中不响应中断,从而不会引发调度。关中断会影响系统效率,且不适用于多 CPU 系统。
2. 利用 Test-and-Set 指令实现互斥。
// 指令描述
boolean TS(boolean *lock)
{
boolean old;
old=*lock;
*lock=TRUE;
return old;
}
// 实现互斥的循环进程结构
do{
...
while TS(&lock);
critical section;
lock=False;
remainder section;
}while(TRUE);
3 . 利用 Swap 指令实现进程互斥。
// 指令描述
void swap(boolean *a, boolean *b)
{
boolean temp;
temp=*a;
*a=*b;
*b=temp;
}
// 实现互斥的循环进程结构
do{
key=True;
do{
swap(&lock,&key);
}while(key!=False);
critical section;
lock=False;
remainder section;
}while(True);
上述硬件指令能有效地实现进程互斥,但当临界资源忙碌时,其它访问进程会一直处于“忙等”状态,不符合“让权等待”的原则,造成处理机资源的浪费。
信号量机制
——Dijkstra
1 . 整型信号量
定义为一个用于表示资源数目的整型量 S。
仅能通过两个标准的原子操作 wait(S) 和 signal(S) 来访问。这两个操作也被分别称为 P、V 操作。
P:
wait(S){
while(S<=0);
S--;
}
V:
signal(S){
S++;
}
在整型信号量机制中,并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。
2. 记录型信号量
记录型信号量机制则是一种不存在“忙等”现象的进程同步机制。但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量 value 外,还应增加一个进程链表指针 list,用于链接上述的所有等待进程。
typedef struct{
int value;
struct process_control_block *list;
}semaphore;
wait(semaphore *S){
S->value--;
if(S->value<0) block(S->list);
}
signal(semaphore *S){
S->value++;
if(S->value<=0) wakeup(S->list);
}
S->value 的初值表示系统中某类资源的数目,因而又称为资源信号量。
wait(S) 中执行 block(S->list) 后的 S->value 的绝对值表示在该信号量链表中已阻塞进程的数目。
如果 S->value 的初值为 1,此时的信号量转化为互斥信号量,用于进程互斥。
3. AND 型信号量
假定有两个进程都要访问两个不同的临界资源。而又由于某些原因两进程对这两个临界资源的P操作次序不同,那么则可能会出现两进程各自获得其中一个临界资源,并分别在等待对方释放另一个临界资源。此时已进入死锁状态。
AND 同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。亦即,对若干个临界资源的分配,采取原子操作方式:要么全部分配,要么一个也不分配。
AND 同步也称为同时 wait 操作。
Swait(S1,S2,…,Sn) {
while(TRUE) {
if(Si>=1 && … && Sn>=1) {
for(i=1; i<=n; i++) Si--;
break;
else {
place the process in the waiting queue associated with the first Si found with Si<1,and set the program count of this process to the beginning of Swait operation.
// 上述英文解释不是很清楚,所以这里仅需知道将进程阻塞在第一个<1的临界资源Si的list上。
}
}
}
Ssignal(S1,S2,…,Sn) {
while(TRUE) {
for(i=1; i<=n; i++) {
Si++;
Remove all the process waiting in the queue associated with Si into the ready queue.
// 唤醒临界资源Si的list上的某个进程。
}
}
}
4 . 信号量集
一次需要 N 个某类临界资源。
每次分配之前,都必须测试该资源的数量Si,看其是否大于其分配下限值ti,允许则分配相应的需求值di。
Swait(S1,t1,d1,…,Sn,tn,dn);
Ssignal(S1,d1,…,Sn,dn);
特殊情况:
Swait(S,1,1)。蜕化为一般的记录型信号量(S>1 时)或互斥信号量(S=1 时)。
Swait(S,1,0)。当 S≥1 时,允许任意多个进程进入某特定区;当 S 变为 0 后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。
信号量的应用
- 利用信号量实现进程互斥。
wait(mutex)和 signal(mutex)必须成对地出现。 - 利用信号量实现前趋关系。
将 signal(S) 操作放在进程P1语句的后面;而在进程P2语句的前面插入 wait(S) 操作。将 S 被初始化为 0,这样,若 P2 先执行必定阻塞,只有在 P1 执行后 P2 才能执行。
管程机制
- 管程的定义。
代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块,我们称之为管程。
对于请求访问共享资源的诸多并发进程,可以根据资源的情况接收或阻塞,确保每次仅有一个进程进入管程,执行这组过程,使用共享资源,达到对共享资源所有访问的统一管理,有效地实现进程互斥。 - 条件变量。
为了解决进程在管程中时被阻塞或挂起所造成的问题,引入条件变量 condition。
需要对每个条件变量都予以说明,形式为:condition x1,x2;对条件变量的操作仅仅是 wait 和 signal。含义如下:
x.wait:正在调用管程的进程因 x 条件需要被阻塞或挂起,则调用 x.wait 将自己插入到 x 条件的等待队列上,并释放管程,直到 x 条件变化。
x.signal:正在调用管程的进程发现 x 条件发生了变化,则调用 x.signal,重新启动(唤醒)一个因 x 条件而阻塞或挂起的进程。如果存在多个这样的进程,则选择其中的一个,如果没有,则继续执行原进程,而不产生任何结果。
经典的进程同步问题
生产者-消费者问题
1 . 利用记录型信号量解决。
int in = 0, out = 0; //对缓冲区进行循环使用
item buffer[n];
seamphere mutex = 1, empty = n, full = 0;
//互斥信号量,空缓冲池数量资源信号量,满缓冲池数量资源信号量
void producer() //生产者
{
do
{
produce an item nextp;
...
wait(empty);
wait(mutex);
buffer[in] = nextp;
in = (in+1)%n;
signal(mutex);
signal(full);
}while(true);
}
void consumer() //消费者
{
do
{
wait(full);
wait(mutex);
nextc = buffer[out];
out = (out+1)%n;
signal(mutex);
signal(empty);
...
consume the item in nextc;
}while(true);
}
在每个程序中的多个 wait 操作顺序不能颠倒,应先执行对资源信号量的wait 操作,然后再执行对互斥信号量的 wait 操作,否则可能引起进程死锁。反例:加入顺序颠倒。当缓冲池满了的时候,生产者进程尝试进行生产,则先执行互斥信号量的 wait 会成功,然后执行资源信号量的 wait 会被阻塞,并且导致互斥信号量没有被释放,消费者进程无法进行消费,所以生产者进程也一直不会被唤醒,从而造成死锁。
2. 利用 AND 信号量解决。
在记录型信号量代码中用 Swait(S1,S2) 代替两个 wait,用 Ssignal(S1,S2) 代替两个 signal。
3. 利用管程解决。
建立一个管程,并实现两个过程、两个条件变量。生产者和消费者进入临界区通过这调用两个过程即可。
// 创建管程
Monitor producerconsumer{
item buffer[N];
int in,out;
int count;
condition notfull,notempty;
public:
void put(item x){
if(count>=N) cwait(notfull); //wait不满条件变量
buffer(in)=x;
in=(in+1) % N;
count++;
csignal(notempty);
}
void get (item x){
if(count<=0) cwait(notempty); //wait非空条件变量
x=buffer(out);
out=(out+1) % N;
count--;
csignal(notfull);
}
{ in=0; out=0; count=0; } //初始化
} PC;
哲学家进餐问题
五个哲学家共用一个圆桌,且每人两旁都有一个筷子,即共享5个筷子,进餐需要拿到两旁的两个筷子。
1. 利用记录型信号量解决。
为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。有一种情况会造成死锁,即哲学家同时先拿起左边(或右边)的筷子,然后去请求另一个筷子,导致无限等待。可以规定至多只允许有四位哲学家同时去拿左边(或右边)的筷子等方法来预防死锁问题。
2. 利用 AND 信号量机制解决。
读者-写者问题
允许多个同时读,但只允许一个写。
1. 利用记录型信号量解决。
semaphore rmutex = 1, wmutex = 1;
//互斥访问readcount; 实现读-写、写-写互斥
int readcount = 0;
void reader() {
do {
wait(rmutex);
if(readcount == 0) wait(wmutex);
readcount++;
signal(rmutex);
...
perform read operation;
...
wait(rmutex);
readcount--;
if(readcount == 0) signal(wmutex);
signal(rmutex);
} while(true);
}
void writer() {
do {
wait(wmutex);
perform write operation;
signal(wmutex);
} while(true);
}
对于 wait(rmutex); if(readcount == 0) wait(wmutex); 。当写进程正在执行时,第一个读进程会执行成功第一个 wait 操作且阻塞在第二个 wait 操作队列上,之后的所有读进程都会阻塞在第一个 wait 操作队列上。因此,当写进程执行完之后如果唤醒了第一个读进程,然后读进程会不断唤醒其它读进程。
2. 利用信号量集机制解决。
int RN; //增加限制:最多允许RN个读者同时读
semaphore L = RN, mx = 1;
//读者数量资源信号量; 实现读-写、写-写互斥
int readcount = 0;
void reader() {
do {
Swait(L, 1, 1);
Swait(mx, 1, 0);
...
perform read operation;
...
Ssignal(L, 1);
} while(true);
}
void writer() {
do {
Swait(mx, 1, 1; L, RN, 0);
perform write operation;
signal(mx, 1);
} while(true);
}
进程通信
进程通信的类型
- 共享存储器系统。
1)基于共享数据结构的通信方式。
仅适于传递相对少量的数据,属于低级通信。
2)基于共享存储区的通信方式。
属于高级通信。 - 管道通信系统。
所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件。管道机制必须提供以下三方面的协调能力:1)互斥。2)同步。3)确定对方是否存在。 - 信息传递系统。
进程不必借助任何共享存储区或数据结构,而是以格式化的消息为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语),在进程间进行消息传递,完成进程间的数据交换。
1)直接通信方式。
2)间接通信方式。 - 客户机-服务器系统。
是网络环境中主流的通信实现机制。实现方法:
1)套接字。
一个套接字就是一个通信标识类型的数据结构,是进程通信和网络通信的基本构件。
基于文件型的套接字:原理类似于管道。
基于网络型的套接字:该类型通常采用的是非对称方式通信,即发送者需要提供接收者命名。在通信结束时通过关闭进程(或服务器端)的套接字撤销连接。
2)远程过程调用和远程方法调用。
远程过程调用 RPC 是一个通信协议,允许运行于一台主机系统上的进程调用另一台主机系统上的进程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程方法调用。
在本地客户端,每个能够独立运行的远程过程都拥有一个客户存根,本地进程调用远程过程实际是调用该过程关联的存根。同样在服务器端,其所对应的实际可执行进程也存在一个服务器存根与其关联。
远程过程调用的步骤作用在于将客户过程的本地调用转化为客户存根,然后发送给服务器存根,再转化为服务器过程的本地调用,对客户与服务器来说,它们的中间步骤是不可见的。
消息传递通信的实现方式
- 直接消息传递系统(直接通信方式)。
1)直接通信原语。
对称寻址方式:要求发送进程和接收进程都必须以显式方式提供对方的标识符。即一个通信命令只能发送给(或接受)自一个进程。
非对称寻址方式:接收进程不同于对称寻址方式,不需要命名发送进程,只需填写表示源进程的参数即可。即可以接受来自任何进程的消息。
2)消息的格式:定长和变长。
3)进程的同步方式:进程在发送或接受消息后,存在两种可能性:或者继续发送(或接受)或者阻塞。于是得到三种同步方式:发送 接收进程都阻塞、发送进程不阻塞 接收进程阻塞、发送 接收进程都不阻塞。
4)通信链路:两进程通信必须建立一条通信链路。第一种方式:通过显式的“建立连接”命令请求系统建立;第二种方式:无须明确提出请求,只须利用系统提供的发送命令,系统会自动为之建立一条链路。 - 信箱通信(间接通信方式)。
通过某种中间实体来暂存消息。该实体建立在随机存储器的公用缓冲区上,称为邮箱或信箱。
1)信箱的结构。
信箱头:存放有关信箱的描述信息。
信箱体:若干个存放消息的信箱格组成。
2)信箱通信原语:创建、撤销;发送、接收。
3)信箱的类型。
私用邮箱:只有拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中。
公用邮箱:由操作系统创建,并提供给系统中的所有核准进程使用(发送和接收)。
共享邮箱:它由某进程创建,在创建时或创建后指明它是可共享的,同时须指出共享进程(用户)的名字。信箱的拥有者和共享者都有权从信箱中取走发送给自己的消息。
直接消息传递系统实例——消息缓冲队列通信机制
1 . 消息缓冲队列通信机制中的数据结构。
1)消息缓冲区。
typedef struct message_buffer {
int sender; //发送者进程标识符
int size; //消息长度
char *text; //消息正文
struct message_buffer *next; //指向下一个消息缓冲区指针
}
2)PCB 中有关通信的数据项。
typedef struct processcontrol_block {
...
struct message_buffer *mq; //消息队列队首指针
semaphore mutex; //消息队列互斥信号量
semaphore sm; //消息队列资源信号量
...
} PCB;
2 . 发送原语。
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区 a,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区 a 中所设置的消息长度a.size 来申请一消息缓冲区 i,接着把发送区 a 中的信息复制到缓冲区 i 中。为了能将 i 挂在接收进程的消息队列 mq 上,应先获得接收进程的内部标识符 j,然后将 i 挂在 j.mq 上。且对队列需要互斥访问。
void send(receiver, a) {
getbuf(a.size, i);
i.sender = a.sender;
i.size = a.size;
copy(i.text, a.text);
i.next = 0;
getid(PCBset, receiver.j); //获得接收进程内部的标识符
wait(j.mutex);
insert(&j.mq, i);
signal(j.mutex)l
siganl(j.sm);
}
3 . 接收原语。
从自己的消息缓冲队列 mq 中摘下第一个消息缓冲区 i,并将其中的数据复制到以 b 为首址的指定消息接收区内。
void receive(b) {
j = internal name;
wait(j.sm);
wait(j.mutex);
remove(j.mq, i);
signal(j.mutex);
b.sender = i.sender;
b.size = i.size;
copy(b.text, i.text);
releasebuf(i);
}
线程的基本概念
线程的引入
引入进程,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量。引入线程,则是为了减少程序在并发执行时所付出的时空开销,使 OS 具有更好的并发性。
- 进程的两个基本属性。
1)进程是一个可拥有资源的独立单位。
2)进程同时又是一个可独立调度和分派的基本单位。 - 程序并发执行所需付出的时空开销。
由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销,因此限制了系统中设置进程的数量。 - 线程。
设法将进程的上述两个属性分开,由 OS 分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位;而对于拥有资源的基本单位,又不对之进行频繁的切换。正是在这种思想的指导下,形成了线程的概念。
线程和进程的比较
线程又称为轻型进程或进程元,传统进程称为重型进程。
- 调度的基本单位。
在传统的 OS 中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。
在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。 - 并发性。
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行。 - 拥有资源。
不论是传统的 OS,还是引入了线程的 OS,进程都可以拥有资源,并作为系统中拥有资源的一个基本单位。线程本身并不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源,例如线程控制块等。
允许一个进程中的多个线程贡献该进程所拥有的资源,这主要表现在:属于同一进程的所有线程都具有相同的地址空间。 - 独立性。
在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。因为进程之间是为了防止彼此干扰和破坏;而同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的。 - 系统开销。
进程的创建、撤销和切换所付出的开销都明显大于线程的创建、撤销和切换的开销。 - 支持多处理机系统。
对于单线程进程,不管多少处理机,同一时刻该进程只能运行在一个处理机上。对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上并行执行。
线程的状态和线程控制块
- 线程运行的三种状态。
1)执行状态。2)就绪状态。3)阻塞状态。 - 线程控制块 TCB
所有用于控制和管理线程的信息都记录在线程控制块 TCB 中。线程控制块 TCB 中的信息:
1)线程标识符。2)一组寄存器。3)线程运行状态。4)优先级。5)线程专有存储器。6)信号屏蔽。7)堆栈指针,在指向的堆栈中通常保存有局部变量和返回地址。 - 多线程 OS 中的进程属性。
1)进程是一个可拥有资源的基本单位。
2)通常一个进程中都含有若干个(至少一个)相对独立的线程。由进程为这些(个)线程提供资源及运行环境。多个线程可并发执行。
3)进程已不再是一个可执行的实体,而把线程作为独立运行的基本单位。虽然如此,进程仍具有与执行相关的状态。例如,所谓进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的与进程状态有关的操作,也对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;又如,在把某进程激活时,属于该进程的所有线程也都将被激活。
线程的实现
线程的实现方式
- 内核支持线程 KST。
线程的创建、撤消和切换等都是在内核空间实现的。为每一个内核支持线程设置一个线程控制块以对内核线程进行控制和管理。
缺点:对于用户的线程切换而言,其模式切换的开销较大。因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。 - 用户级线程 ULT。
用户级线程是在用户空间中实现的。对线程的创建、撤消、线程之间的同步与通信等功能,都无须内核的支持,即用户级线程是与内核无关的。
值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。
缺点:一个线程被阻塞,它所在的进程中的所有线程都会被阻塞。且多线程应用不能利用多处理机进行多重处理的优点,因为内核每次分配给一个进程的仅有一个 CPU,而进程中仅有一个线程能执行。 - 组合方式。
把用户级线程和内核支持线程两种方式进行组合,提供了组合方式 ULT/KST 线程。一些内核支持线程对应多个用户级线程,这是用户级线程通过时分多路复用内核支持线程来实现的。
由于连接方式的不同,有三种不同的模型:
1)多对一模型。将用户级线程映射到一个内核控制线程,这些用户级线程一般属于一个进程。在任一时刻,只有一个线程能够访问内核,类似于前面的单纯的方式 2。
2)一对一模型。每一个用户级线程映射到一个内核支持线程。每创建一个用户线程,相应地就需要创建一个内核支持线程,开销较大。
3)多对多模型。许多用户级线程映射到同样数量或更少数量的内核支持线程上。克服了上述两种模型的缺点。
线程的实现
- 内核支持线程的实现。
系统在创建一个新进程时,便为它分配一个任务数据区 PTDA,其中包括若干个线程控制块 TCB 空间。
每当进程要创建一个线程时,便为新线程分配一个 TCB,将有关信息填入该 TCB 中,并为之分配必要的资源。
有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。 - 用户级线程的实现。
用户级线程是在用户空间实现的。所有的用户级线程都运行在一个中间系统上。两种方式实现中间系统:
1)运行时系统。
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
不论在传统的 OS 中,还是在多线程 OS 中,系统资源都是由内核管理的。在传统的 OS 中,进程是利用 OS 提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap)机制进入 OS 内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。
2)内核控制线程。
这种线程又称为轻型进程 LWP。LWP 可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个 LWP 上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。
每一个 LWP 都要连接到一个内核级线程上,这样,通过 LWP 可把用户级线程与内核线程连接起来,用户级线程可通过 LWP 来访问内核,但内核所看到的总是多个 LWP 而看不到用户级线程。
在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个 LWP 也将随之阻塞,进而使连接到 LWP 上的用户级线程也被阻塞。
线程的创建和终止
在 OS 中有用于创建线程的函数(或系统调用)和用于终止线程的函数(或系统调用)。
- 线程的创建。
应用程序在启动时,通常仅有一个“初始化线程”,它的主要功能是用于创建新线程。 - 线程的终止。
有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。
继续加油~