程序的顺序执行如下图,其中I代表输入,C代表计算,P代表打印。程序顺序执行时的特征有顺序性、封闭性(独占全机资源)、可再现性。
程序的并发执行如下图,其中I代表输入,C代表计算,P代表打印。输入程序在输入第一个程序后,在计算程序对该程序进行计算的同时,可由输入程序再输入第二个程序,从而使第一个程序的计算操作可与第二个程序的输入操作并发执行。程序并发执行的特征有间断性、失去封闭性、不可再现性。
进程基本概念
进程是程序的一次执行过程。由程序段、相关的数据段和PCB(进程控制块)三部分便构成了进程实体(或称为进程映像)。进程的特征有动态性(它由创建而产生,由调度而执行,由撤消而消亡)、并发性、独立性(进程是一个独立运行、分配资源和调度的基本单位)、异步性。
进程的状态
- 就绪态:进程已获得除CPU外的所有必要资源。系统中通常有一个就绪队列。
- 执行态:程序正在执行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,则有多个进程处于执行状态。
- 阻塞态:正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃CPU而处于暂停状态。致使进程阻塞的典型事件有:请求I/O,申请缓冲空间等。系统会有一个或多个请求资源阻塞队列。
- 创建态:进程正在创建。创建一个进程一般要通过两个步骤:首先,为一个新进程创建PCB,并填写必要的管理信息;其次,把该进程转入就绪状态并插入就绪队列之中。
- 终止态:进程的终止也要通过两个步骤:首先等待操作系统进行善后处理,然后将其PCB清零,并将PCB 空间返还系统。
引入挂起。
挂起(suspend) vs. 阻塞(pend)
参考:https://www.cnblogs.com/jason-liu-blogs/archive/2012/12/19/2825202.html
- 挂起的挂起和恢复是一种主动行为,而阻塞则是一种*自发(迫不得已的自己阻塞自己)行为,是在等待事件或资源时任务的表现。
- 任务调度是操作系统来实现的,任务调度时,直接忽略挂起状态的任务,但是会顾及处于阻塞下的任务,当阻塞下的任务等待的资源就绪后,就可以转为就绪了。可以这样理解,只要是挂起状态,操作系统就不在管理这个任务了。
进程控制块(PCB)
为了描述和控制进程的运行,系统为每个进程定义了一个数据结构——进程控制块,它是进程实体的一部分,记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。OS是根据PCB来对并发执行的进程进行控制和管理的。PCB是进程存在的惟一标志。
PCB中所含信息:
- 进程标识符:进程标识符用于惟一地标识一个进程。内部标识符它通常是一个进程的序号,主要是为了方便系统使用。外部标识符由创建者提供,通常是由字母、数字组成,往往是由用户(进程)在访问该进程时使用。父进程标识及子进程标识、用户标识。
- 处理机状态:处理机状态信息主要是由处理机的各种寄存器中的内容组成的。① 通用寄存器,又称为用户可视寄存器;② 指令计数器,其中存放了要访问的下一条指令的地址;③ 程序状态字PSW,其中含有状态信息,如条件码、执行方式、中断屏蔽标志等;④ 用户栈指针。
- 进程调度信息:与进程调度和进程对换有关的信息,包括:① 进程状态,指明进程的当前状态,作为进程调度和对换时的依据;② 进程优先级,用于描述进程使用处理机的优先级别的一个整数,优先级高的进程应优先获得处理机;③ 进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待CPU的时间总和、进程已执行的时间总和等;④ 事件,指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
- 进程控制信息:进程控制信息包括:① 程序和数据的地址,指进程的程序和数据所在的内存或外存地(首)址,以便再调度到该进程执行时,能从PCB中找到其程序和数据;② 进程同步和通信机制,指实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等,它们可能全部或部分地放在PCB 中;③ 资源清单,即一张列出了除CPU 以外的、进程所需的全部资源及已经分配到该进程的资源的清单;④ 链接指针,它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。
PCB的组织方式有链接方式和索引方式。
进程控制
进程控制是进程管理中最基本的功能,负责进程的状态转换。进程控制一般是由OS的内核中的原语来实现的。原语是由若干条指令组成的,用于完成一定功能的一个过程。它与一般过程的区别在于:它们是原子操作,即一个操作中的所有动作要么全做,要么全不做,是一个不可分割的基本单位,因此,在执行过程中不允许被中断。原子操作在管态下执行,常驻内存。
子进程与父进程。子进程可以继承父进程所拥有的资源,例如,继承父进程打开的文件,继承父进程所分配到的缓冲区等。当子进程被撤消时,应将其从父进程那里获得的资源归还给父进程。此外,在撤消父进程时,也必须同时撤消其所有的子进程。
进程创建
引起创建的事件:
- (1) 用户登录。用户登录后系统将为该终端建立一个进程,并把它插入就绪队列中。
- (2) 作业调度。
- (3) 提供服务。当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供用户所需要的服务。
- (4) 应用请求。在上述三种情况下,都是由系统内核为它创建一个新进程;而第4 类事件则是基于应用进程的需求,由它自己创建一个新进程。
一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语Creat( )按下述步骤创建一个新进程:
- 申请空白PCB。为新进程申请获得惟一的数字标识符,并从PCB 集合中索取一个空白PCB。
- 为新进程分配资源。为新进程的程序和数据以及用户栈分配必要的内存空间。
- 初始化进程控制块。PCB 的初始化包括:① 初始化标识信息,将系统分配的标识符和父进程标识符填入新PCB中;② 初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶;③ 初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式方式提出高优先级要求。
- 将新进程插入就绪队。如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
进程终止
引起终止的事件:
- (1)正常结束。
- (2)异常结束。越界错误、保护错、非法指令、 特权指令错、运行超时、等待超时、算术运算错、I/O 故障。
- (3)外界干预。操作员或操作系统干预、父进程请求、父进程终止。
如果系统中发生了上述要求终止进程的某事件,OS便调用进程终止原语,按下述过程去终止指定的进程:
- 根据被终止进程的标识符,从PCB 集合中检索出该进程的PCB,从中读出该进程的状态。
- 若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度。
- 若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防它们成为不可控的进程。
- 将被终止进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
- 将被终止进程(PCB)从所在队列(或链表)中移出,等待其他程序来搜集信息。
进程阻塞与唤醒
引起阻塞与唤醒的事件:
- (1)请求系统服务 。
- (2)启动某种操作。
- (3)新数据尚未到达。
- (4)无新工作可做。
阻塞过程:正在执行的进程,当发现上述某事件时,由于无法继续执行,于是进程便通过调用阻塞原语block()把自己阻塞。先立即停止执行,把进程控制块中的现行状态由“执行”改为“阻塞”,并将PCB插入阻塞队列。后转调度程序进行重新调度,将处理机分配给另一就绪进程并进行切换。
唤醒过程:当被阻塞进程所期待的事件出现时,则由有关进程(比如用完并释放了该I/O 设备的进程)调用唤醒原语wakeup( ),将等待该事件的进程唤醒。首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。
进程挂起与激活
引起挂起的事件:
- (1)终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂时使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态称为挂起状态。
- (2)父进程请求。有时父进程希望挂起自己的某个子进程,以便考查和修改该子进程,或者协调各子进程间的活动。
- (3)负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
- (4)操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
挂起过程:当出现了引起进程挂起的事件时,系统将利用挂起原语suspend( )将指定进程或处于阻塞状态的进程挂起。首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪; 对于活动阻塞状态的进程,则将之改为静止阻塞。
激活过程:当发生激活进程的事件时系统将利用激活原语active( )将指定进程激活。激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞,便将之改为活动阻塞。假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。
进程同步
进程同步的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性。共享资源形成进程间间接制约关系,相互合作形成进程间直接制约关系。
生产者-消费者问题,counter就是一个临界资源变量,因为但不互斥访问它时会出现问题。
临界资源有硬件临界资源和软件临界资源,多个进程必须互斥的对它进行访问。把在每个进程中访问临界资源的那段代码称为临界区。
一个进程的代码段可以划分如下:
同步机制遵循准则:
- 空闲让进。
- 忙则等待。
- 有限等待。应保证有限时间内可以进入临界区,避免死等。
- 让权等待。当不能进去临界区时,应释放CPU,避免忙等。
进程同步的机制有:信号量机制、管程机制。
信号量机制
介绍了整型信号量,记录型信号量,AND型信号量,信号量集。
整型信号量为一个用于表示可用资源数目的整型量S,除了初始化以外,仅支持两个原子操作,保证了操作不能被中断,wait(S)和signal(S),也称为P和V操作。这种机制没有遵循让权等待,会出现忙等。
记录型信号量由于它采用了记录型的数据结构而得名的,它解决了忙等问题。如果S.value的初值为1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。
相应的wait(S)和signal(S)操作:
AND型信号量应用于一个进程需要先获得两个或更多的共享资源后方能执行其任务的场景。假定现有两个进程A和B,他们都要求访问共享数据D 和E。当然,共享数据都应作为临界资源。为此,可为
这两个数据分别设置用于互斥的信号量Dmutex 和Emutex,并令它们的初值都是1。但是会出现死锁问题。
最后,进处于僵持状态。在无外力作用下,两者都将无法从僵持状态中解脱出来。我们称此时的进已进入死锁状态。当进程同时要求的共享资源愈多时,发生进程死锁的可能性也就愈大。
AND 同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。亦即,对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。
当一次需要N 个某类临界资源时,便要进行N 次wait(S)操作,显然这是低效的。此外,在有些情况下,当资源数量低于某一下限值时,便不予以分配。因而,在每次分配之前,都必须测试该资源的数量,看其是否大于其下限值。基于上述两点,可以对AND 信号量机制加以扩充,形成一般化的“信号量集”机制
利用信号量实现进程互斥,解决共享资源问题。为使多个进程能互斥地访问某临界资源,只须为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。
利用信号量实现前趋关系,解决相互合作问题。设有两个并发执行的进程P1和P2。P1中有语句S1;P2中有语句S2。我们希望在S1执行后再执行S2。为实现这种前趋关系,我们只须使进程P1和P2共享一个公用信号量S,并赋予其初值为0,将signal(S)操作放在语句S1后面;而在S2语句前面插入wait(S)操作,即
在进程 P1中,用S1;signal(S);
在进程 P2中,用wait(S);S2;
如下一个例子,应该设置a,b,c,d,e,f,g信号量,并初始化为0。
管程机制
信号量机制要求每个要访问临界资源的进程都必须自备同步操作wait(S)和signal(S),这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。故引入进程同步工具——管程。
代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块,我们称之为管程。管程被请求和释放资源的进程所调用。管程由四部分组成:① 管程的名称;② 局部于管程内部的共享数据结构说明;③ 对该数据结构进行操作的一组过程;④ 对局部于管程内部的共享数据设置初始值的语句。所有进程要访问临界资源时,都必须经过管程(相当于通过围墙的门)才能进入,而管程每次只准许一个进程进入管程,从而实现了进程互斥。
管程和进程的区别:
- (1) 二者都定义了数据结构,但进程定义的是私有数据结构PCB,管程定义的是公共数据结构,如消息队列等;
- (2) 二者都存在对各自数据结构上的操作,但进程是由顺序程序执行有关的操作,而管程主要是进行同步操作和初始化操作;
- (3) 设置进程的目的在于实现系统的并发性,而管程的设置则是解决共享资源的互斥使用问题;
- (4) 进程通过调用管程中的过程对共享数据结构实行操作,该过程就如通常的子程序一样被调用,因而管程为被动工作方式,进程则为主动工作方式;
- (5) 进程之间能并发执行,而管程则不能与其调用者并发;
- (6) 进程具有动态性,而管程则是操作系统中的一个资源管理模块,供进程调用。
当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,*长时间地等待。为了解决这个问题,引入了条件变量。通常,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量,对这些条件变量的访问,只能在管程中进行。
条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作即可表示为x.wait和x.signal,其含义为:
① x.wait:正在调用管程的进程因x 条件需要被阻塞或挂起,则调用x.wait 将自己插入到x 条件的等待队列上,并释放管程,直到x条件变化。此时其它进程可以使用该管程。
② x.signal:正在调用管程的进程发现x 条件发生了变化,则调用x.signal,重新启动一个因x 条件而阻塞或挂起的进程。如果存在多个这样的进程,则选择其中的一个,如果没有,则继续执行原进程,而不产生任何结果。这与信号量机制中的signal操作不同,因为后者总是要执行s:=s+1操作,因而总会改变信号量的状态。
经典的进程同步问题较有代表性的是“生产者—消费者”问题、“读者—写者问题”、“哲学家进餐问题”等等。
再看生产者-消费者问题
使用记录型信号量,利用互斥信号量mutex 实现诸进程对缓冲池的互斥使用。利用信号量empty和full分别表示缓冲池中空缓冲区和满缓冲区的数量。在每个程序中的多个wait 操作顺序不能颠倒,应先执行对资源信号量的wait操作,然后再执行对互斥信号量的wait操作,否则可能引起进程死锁。
使用AND信号量。
使用管程。首先便是为它们建立一个管程,并命名为ProclucerConsumer,或简称为PC。其中包括两个过程:(1) put(item)过程。生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量count 来表示在缓冲池中已有的产品数目,当count≥n 时,表示缓冲池已满,生产者须等待。(2) get(item)过程。消费者利用该过程从缓冲池中取出一个产品,当count≤0 时,表示缓冲池中已无可取用的产品,消费者应等待。
其中put(item)和get(item)如下:
哲学家进餐问题
有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
使用记录型信号量,看能会出现死锁,如当5个哲学家同时拿起左边的筷子时。解决办法可以使用AND信号量,仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
读者-写者问题
一个数据文件或记录,可被多个进程共享,我们把只要求读该文件的进程称为“Reader进程”,其他进程则称为“Writer 进程”。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个Writer 进程和其他Reader 进程或Writer 进程同时访问共享对象,因为这种访问将会引起混乱。
使用记录型信号量解决读者—写者问题为实现Reader 与Writer 进程间在读或写时的互斥而设置了一个互斥信号量Wmutex。另外,再设置一个整型变量Readcount 表示正在读的进程数目。由于只要有一个Reader 进程在读,便不允许Writer 进程去写。因此,仅当Readcount=0,表示尚无Reader 进程在读时,Reader 进程才需要执行Wait(Wmutex)操作。若Wait(Wmutex)操作成功,Reader 进程便可去读,相应地,做Readcount+1 操作。同理,仅当Reader 进程在执行了Readcount 减1操作后其值为0 时,才须执行signal(Wmutex)操作,以便让Writer进程写。又因为Readcount是一个可被多个Reader 进程访问的临界资源,因此,也应该为它设置一个互斥信号量rmutex。
进程通信
有7中常见的通信方式:
-
管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
-
有名管道(Names Pipes): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
-
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
-
消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺。
-
信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
-
共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
-
套接字(Sockets): 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
信号量机制是进程同步工具,交换信息量少,属于低级通信,下面介绍几种高级通信机制共享存储器系统、管道通信系统以及消息传递系统。
共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。
- (1) 基于共享数据结构的通信方式。在这种通信方式中,要求诸进程公用某些数据结构,借以实现诸进程间的信息交换。如在生产者—消费者问题中,就是用有界缓冲区这种数据结构来实现通信的。这种通信方式是低效的,只适于传递相对少量的数据。
- (2) 基于共享存储区的通信方式。为了传输大量数据,在存储器中划出了一块共享存储区,诸进程可通过对共享存储区中数据的读或写来实现通信。进程在通信前,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字;若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者,继之,由申请者把获得的共享存储分区连接到本进程上;此后,便可像读、写普通存储器一样地读、写该公用存储分区。
管道通信。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步、确认对方是否存在。
消息传递系统中,进程间的数据交换是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。程序员直接利用操作系统提供的一组通信命令(原语),不仅能实现大量数据的传递,而且还隐藏了通信的实现细节,使通信过程对用户是透明的。分为直接通信和间接通信(利用中间信箱)两种通信方式。
直接通信方式指发送进程利用OS所提供的发送命令,直接把消息发送给目标进程。此时,要求发送进程和接收进程都以显式方式提供对方的标识符。
间接通信方式指进程之间的通信需要通过作为共享数据结构的实体。该实体用来暂存发送进程发送给目标进程的消息;接收进程则从该实体中取出对方发送给自己的消息。通常把这种中间实体称为信箱。消息在信箱中可以安全地保存,只允许核准的目标用户随时读取。因此,利用信箱通信方式,既可实现实时通信,又可实现非实时通信。系统为信箱通信提供了若干条原语,分别用于信箱的创建、撤消和消息的发送、接收等。信箱分为私有信箱(用户创建,仅用户可收,其余只能发)、公有信箱(系统创建,都可收可发)、共享信箱(用户创建,指定共享者,用户和共享者均可收发)。
有两种方式建立通信链路。第一种方式是由发送进程在通信之前用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路,在链路使用完后,也用显式方式拆除链路,这种方式主要用于计算机网络中。第二种方式是发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路,这种方式主要用于单机系统中。
消息缓冲通信机制如下:
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区a,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区a 中所设置的消息长度a.size来申请一缓冲区i,接着把发送区a 中的信息复制到缓冲区i中。为了能将i挂在接收进程的消息队列mq 上,应先获得接收进程的内部标识符j,然后将i 挂在j.mq 上。由于该队列属于临界资源,故在执行insert操作的前后,都要执行wait和signal操作。
接收进程调用接收原语receive(b),从自己的消息缓冲队列mq 中摘下第一个消息缓冲区i,并将其中的数据复制到以b 为首址的指定消息接收区内。
线程
操作系统引入线程,是为了减少程序在并发执行时所付出的时空开销,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使OS具有更好的并发性。通常一个进程都拥有若干个线程,至少也有一个线程,线程共享进程资源。线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。
线程的状态参数和状态和进程一样,状态参数有① 寄存器状态,它包括程序计数器PC 和堆栈指针中的内容;② 堆栈,在堆栈中通常保存有局部变量和返回地址;③ 线程运行状态,用于描述线程正处于何种运行状态;④ 优先级,描述线程执行的优先程度;⑤ 线程专有存储器,用于保存线程自己的局部变量拷贝;⑥ 信号屏蔽,即对某些信号加以屏蔽。基本状态有就绪态,执行态,阻塞态。
在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”。它可根据需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。
终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。在大多数的OS中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用。虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与该线程进行连接。
线程间同步和通信
在多线程OS中通常提供多种同步机制,如互斥锁、条件变量、计数信号量以及多读、单写锁等。
互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低。互斥锁有两种状态,即开锁(unlock)和关锁(lock)状态。。其中的关锁lock操作用于将mutex 关上,开锁操作unlock则用于打开mutex。当一个线程需要读/写一个共享数据段时,命令首先判别mutex 的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;而如果mutex 是处于开锁状态,则将mutex 关上后便去读/写该数据段。在线程完成对数据的读/写后,必须再发出开锁命令将mutex 打开,同时还须将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待mutex打开的队列上。
每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。利用互斥锁和条件变量来实现对资源R的访问。线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源R 正处于忙碌状态,线程便转为等待状态,并对mutex 执行开锁操作后,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex 执行开锁操作。下面给出了对上述资源的申请(左半部分)和释放(右半部分)操作的描述。
信号量机制有私有信号量和公有信号量。
私用信号量属于特定的进程所有,其数据结构存放在应用程序的地址空间中,OS并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为0(空),也不能将它传送给下一个请求它的线程。
公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的,其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则OS会自动将该信号量空间回收,并通知下一进程。
线程的实现
操作系统对线程的实现方式有三种:内核支持线程、用户级线程、同时实现前两者。
内核支持线程中,线程的创建、撤消和切换等都是依靠内核,在内核空间实现的。系统在创建一个新进程时,便为它分配一个任务数据区PTDA(Per Task Data Area),其中包括若干个线程控制块TCB空间。在每一个TCB中可保存线程标识符、优先级、线程运行的CPU状态等信息。每当进程要创建一个线程时,便为新线程分配一个TCB,将有关信息填入该TCB中,并为之分配必要的资源。当PTDA中的所有TCB 空间已用完,而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统可再为之分配新的TCB空间;在撤消一个线程时,也应回收该线程的所有资源和TCB。
内核支持线程的优点:
- 在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;
- 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;
- 内核支持线程具有很小的数据结构和堆栈,开销小;
- 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。
内核支持线程的缺点:对于用户的线程切换而言,其模式切换的开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行。
用户级线程中,线程的任务控制块都是设置在用户空间,而线程所执行的操作也无须内核的帮助,因而内核完全不知道用户级线程的存在,因而使线程的切换速度特别快。对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。设置了内核支持线程的系统,则调度便是以线程为单位进行的。
所有的用户级线程都具有相同的结构,它们都运行在一个中间系统的上面。当前有两种方式实现的中间系统,即运行时系统和内核控制线程。
运行时系统实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等。用户级线程在切换时则不需转入核心态,而是由运行时系统中的线程切换过程来执行切换任务。该过程将线程的CPU状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,将新线程堆栈中的CPU状态装入到CPU相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。当线程需要系统资源时,是将该要求传送给运行时系统,由运行时系统通过相应的系统调用来获得系统资源的。
内核控制线程又称为轻型进程LWP,每一个进程都可拥有多个LWP,同用户级线程一样,每个LWP都有自己的数据结构(如TCB),它们也可以共享进程所拥有的资源。LWP 可通系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而把这些LWP 做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到LWP池中的任何一个LWP上。为使每一用户级线程都能利用LWP与内核通信,可以使多个用户级线程多路复用一个LWP,但只有当前连接到LWP上的线程才能与内核通信,其余进程或者阻塞,或者等待LWP。而每一个LWP都要连接到一个内核级线程上,这样,通过LWP可把用户级线程与内核线程连接起来,用户级线程可通过LWP来访问内核,但内核所看到的总是多个LWP 而看不到用户级线程。亦即,由LWP 实现了在内核与用户级线程之间的隔离,从而使用户级线程与内核无关。
用户级线程的优点:
- 线程切换不需要转换到内核空间,模式切换开销小;
- 调度算法可以是进程专用的;
- 用户级线程的实现与操作系统平台无关,因为对于线程管理的代码是在用户程序内的,属于用户程序的一部分,所有的应用程序都可以对之进行共享。因此,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。
用户级线程的缺点:
- 系统调用的阻塞问题。在基于进程机制的操作系统中,大多数系统调用将阻塞进程,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。
- 在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点。内核每次分配给一个进程的仅有一个CPU,因此进程中仅有一个线程能执行,在该线程放弃CPU之前,其它线程只能等待。
组合方式有些操作系统把用户级线程和内核支持线程两种方式进行组合,在组合方式线程系统中,内核支持线程的建立、调度和管理有内核完成,同时,也允许用户应用程序建立、调度和管理用户级线程。
参考
《计算机操作系统》 第三版 汤小丹