操作系统(二): 进程与线程
本章解读
进程管理是操作系统重点中的重点,涵盖了操作系统中大部分的知识和考点。其主要包括四部分:进程与线程,处理器调度,同步与互斥,死锁。所以我准备分四个部分来解释这四个模块。进程与线程部分考纲内容如下:
1.进程概念
2.进程的状态与转换
3.进程控制
4.进程组织
5.进程通信:共享存储系统;消息传递系统;管道通信。
6.线程概念与多线程模型
正文
2.1 进程的概念
2.1.1 程序的顺序执行和并发执行
顺序执行特点:顺序性,封闭性,可再现性
并发执行特点:间断性(制约性),失去封闭性,不可再现性。
前趋图:是一个有向无循环图,用于描述进程之间执行的前后关系。Pi→Pj表示在Pj开始执行前Pi必须完成,Pi是Pj的直接前趋,Pj是Pi的直接后继。没有前趋的结点称为初始节点,没有后继的结点称为终止结点。
例题: ________是多道程序的基本特征。
A. 制约性 B. 顺序性 C.功能的封闭性 D.运行过程中的可再现性
答案:A
2.1.2 进程的特征
结构特征:进程由程序段、数据段、程序控制块(PCB)构成。所谓的创建进程,实质上是创建进程实体中的PCB;撤销进程实质是撤销进程的PCB。
动态性:进程是动态的,有一定的生命期;程序是静态的,它只是一组有序指令的结合,并存放于某种介质上。
并发性:多个进程同时存在于内存中,在同一段时间内并发执行。
独立性:进程实体是一个能独立运行、独立分配资源和独立接受调度的基本单位。
异步性:进程按相互独立,不可预知的速度向前推进。
2.1.3 进程的定义
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
例题:操作系统中,可以并行工作的基本单位是___(1)____,它也是核心调度及资源分配的基本单位,它是由____(2)____组成的,它与程序的重要区别之一是____(3)____。
(1) A. 作业 B.函数 C.进程 D.过程
(2) A.程序、数据和PCB B.程序、数据和标示符
C.程序、标示符和PCB D.数据、标示符和PCB
(3) A.程序有状态,而它没有 B.它有状态,而程序没有
C.程序可占有资源,而它不可 D.它能占有资源,而程序不能
答案:C A D
2.2 进程的状态与转换
三种基本状态:就绪态(进程已经分配到除了CPU以外的所有必要资源),执行态(获得CPU资源的就绪态进程),阻塞态(执行态进程因发生某事件而暂时无法执行就变成了阻塞态)。他们之间的关系如下:
挂起状态:挂起状态又称为静止状态。引入挂起状态的原因有:终端用户的调试请求,父进程请求,负荷调节的需要,操作系统的需要,对换的需要(物理内存资源紧张,将内存中阻塞态进程置换到外存上)。挂起态分静止就绪态和静止阻塞态,他们能分别与活动就绪态以及活动阻塞态进行状态转换,此外当静止阻塞态的进程所等待的事件发生时,将从静止阻塞态转变成静止就绪态。具有挂起状态的进程状态图如下:
创建状态:创建进程时,若该进程已经拥有了自己的PCB,但进程自身还未进入主存,则称该状态为创建状态。
终止状态:进入终止状态的进程以后不能再执行,但正在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其他进程收集。一旦其它进程完成了对终止状态进程信息的提取之后,操作系统将删除该进程。
具有创建、终止、挂起状态的进程状态图如下:
2.3 进程控制
进程的控制包括创建新进程,终止已完成或因某事件无法运行的进程,负责进程状态转换(阻塞与唤醒,挂起与激活)。进程控制是由操作系统的内核中用原语来实现的。
原语的主要作用是为了实现进程间通信和控制。原语其实是一段实现某种功能的程序指令,它与普通程序的区别是原语是原子操作,即它的功能要么全做,要么全不做,它不可能被调度程序中断。原语在操作系统中是一个很重要的概念,后面要讲的进程间同步问题所用到的信号量就是使用PV原语来实现的。原子操作在管态下执行,常驻内存。
2.3.1 进程的创建
进程之间是存在家族关系的,比如父子进程,在Linux下可以通过调用fork函数创建子进程。关于进程间的父子关系,只需记住两点即可:1、子进程可以继承父进程所拥有的所有资源(打开的文件,申请的缓冲区等) 2、当撤销父进程时,也必须同时撤销其所有的子进程。这和面向对象中的继承思想非常相似。
进程创建的事件有四类:用户登录,作业调度,提供服务,应用请求。
当操作系统发现要求创建新进程的时间后,便使用原语创建进程,其顺序如下:
申请空白PCB:申请的PCB具有唯一的标示符。
分配资源:给进程分配程序区和数据区的内存空间,并加载相应资源。
初始化PCB:初始化标识信息、处理器状态信息、处理机控制信息等。
将新进程插入就绪队列:如果进程就绪队列能够接纳新进程,则将新进程插入就绪队列。
2.3.2 进程的终止
引起进程的事件有三种:正常结束,异常结束,外界干预。
正常结束:程序运行到功能完毕时正常的退出。
异常结束:越界错误,保护错,非法指令,特权指令错,运行超时,等待超时,算数运算错,I/O故障。
外界干预:用户或操作系统干预,父进程请求,父进程终止。
当发生进程终止事件后,操作系统调用终止原语,按下列过程终止进程:1、根据被终止进程标示符找到其PCB,读出进程状态。 2、若该进程处在执行态,则终止该进程的执行。 3、若进程有子孙进程,终止它们。4、将被终止的进程的全部资源归还给其父进程,若没有父进程就归还给操作系统。5、将被终止进程的PCB从所在队列移除。
2.3.3 进程的阻塞与唤醒
引起进程的阻塞事件四种:请求系统服务(等待请求的服务和资源分配)、启动某种操作(等待某种操作完成,如I/O操作)、新数据尚未到达(等待进程间、网络中消息到来)、无新工作可做(服务型的进程等待新任务到来)。
进程阻塞过程:进程调用阻塞原语block把自己阻塞。要被阻塞的进程先停止执行,即将PCB中的状态由执行改为阻塞,并将PCB插入阻塞队列,然后调度程序调度其他进程给处理器执行。
进程唤醒过程:当被阻塞进程期待的事件出现,由有关进程(如之前占用资源现在释放的进程)调用唤醒原语wakeup将等待该事件的进程唤醒。首先把被唤醒的进程的PCB从对应事件的阻塞队列中移出,将其PCB中的状态由阻塞改为就绪,然后将该PCB插入到就绪队列中。
2.3.4进程的挂起与激活
进程的挂起:当出现某个引起进程挂起的事件(如用户进程请求将自己挂起,父进程请求将某子进程挂起等),操作系统调用挂起原语suspend将指定进程挂起,挂起的过程:将就绪进程挂起称为静态就绪进程,或将阻塞进程挂起为静止阻塞进程。然后将该进程的PCB复制到某指定的内存区域,以方便用户或父进程查看该进程的运行情况。
进程的激活:当发生激活进程的事件时,如父进程或用户进程请求激活对应进程,若该进程驻留在外存,而内存已有足够空间,则可将外存上的进程换入内存。操作系统调用激活原语active激活指定进程。首先将进程从外存调入内存,然后将进程的状态从静止就绪/静止阻塞转变成(活动)就绪态/(活动)阻塞态。若采用的是抢占调度策略,每当有新进程进入就绪队列时,会将新进入的进程和当前执行的进程比较,若新进入进程优先级大,这把处理器资源分给新进入的进程。
2.4 进程的组织
2.4.1 进程管理块(PCB Process Control Block)
对PCB的介绍在前面已经提到了,它其实是一个数据结构,在PCB中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。操作系统就是根据PCB来对并发执行的进程进行控制和管理的。PCB是进程存在的惟一标识。
下图为PCB大致包括的内容:
2.4.2 进程控制块的组织方式
即如何将整个系统中成千上万的PCB管理组织起来,方法分为链接方式和索引方法两种。
链接方式
索引方式
蝶叔我以前有写过一个以Linux作为平台的进程调度模拟器,其中关于PCB的组织方式就是利用类似于索引的方式进行,区别在于我将就绪队列按照优先级分为多个队列,不同的进程的PCB指针也就放在不同的就绪索引表里。这样,在处理器进行调度时,每次都从优先级高至低的顺序取出一个进程进行调度,这样就可以避免进程的饥饿了。
2.5 进程的通讯
本来这里将介绍进程间同步的问题,但由于进程同步是考试中的重点加难点(常作为一道大题),所以准备放在后面进行专门的讲解。现在先介绍进程间的通讯问题。
2.5.1 进程通信的概念
进程通信,指的进程之间的信息交换。进程通信分为低级通信和高级通信。进程的同步和互斥(信号量)就是低级通信,它本质上是使用原语操作共享内存中的数据结构的过程,效率低(不能用来传输大量数据信息)并且对用户不透明(所以才出现了考试信号量那道大题,哈哈)。进程间的高级通讯有共享内存、消息队列、管道、套接字等等,其中套接字不仅可用在本机通讯,还可用作网络通讯。
2.5.2 共享内存系统(共享存储器系统 Shared-Memory System)
相互通讯的进程可以通过共享某个内存区域,从而实现用该内存区域进行通讯。信号量是共享内存的一个特殊情况,信号量就只是共享了一个数据结构的内存。此外,高级通讯中的共享内存,先向系统申请一个共享内存的区域,并分配相应的关键字。若其他进程要访问共享内存,则提供这个关键字,若存在关键字对应的共享内存区域,则返回其描述符。
在Linux 中,使用shmget、shmat、shmdt、shmctl这四个函数操作共享内存,这里主要介绍前两个函数。
shmget用来创建或获得一个共享内存对象:
#define MY_SHM_ID 67480
intfd_shm;
fd_shm= shmget(MY_SHM_ID, 4096, 0666 | IPC_CREAT);
MY_SHM_ID是共享内存的关键字,若为0则为私有内存,常用于父子进程通信,若不为0则用于多进程通信。4096表示共享内存大小为4KB,第三个参数是共享内存的权限,如果该共享内存不存在则创建。
在其他进程中,若想访问这个共享内存,则fd_shm = shmget(MY_SHM_ID, 0, 0);因为该共享内存已存在,函数会返回共享内存的标识符。
接着,使用shmat把共享内存区对象映射到调用进程的地址空间:
void* pshm;
pshm= shmat(fd_shm, NULL, 0);
其它两个函数shmdt(断开共享内存连接),shmctl(共享内存管理)在这里介绍没有多大意义,介绍这些知识是给大家一个对共享内存更直观的认识。
2.5.3 消息传递系统(Message passing system)
消息传递系统是当前应用最为广泛的进程通信机制,消息队列和套接字都是基于这种机制工作的。此外,在微内核操作系统中,内核和服务器进程之间的通信都采用了消息传递机制。
消息传递可以分为直接通信和间接通信,它们的区别在于直接通信没有缓冲区,间接通信有缓冲区。直接通信中,源进程将消息直接发往目标进程;间接通信中,进程通信的消息都放在缓冲区(信箱)中。信箱又分三类:私用信箱(其他进程只能发送消息到信箱,信箱拥有者可以读取信息)、公用信箱(由操作系统创建,提供给批准的进程使用,这些进程可对信箱进行读写)、共享信箱(由某进程创建,创建时指明共享进程的名字,这样拥有者和共享者都能对信箱读写)。
由于网络通讯就是基于消息传递机制的,所以消息传递系统中的通信链路、消息格式其实就对应计算机网络中的数据链路层和网络层。比如,消息传递系统的通信链路分点对点传输和多点连接链路(广播)两类,通信方式也可分为单向通信链路(单工)和双向链路(全双工),消息格式由消息头和消息正文组成。
在Linux 中,使用msgget 、msgsnd、msgrcv、msgctl四个函数操作消息队列。
msgget用来创建或获得消息队列:
#defineMY_MSG_ID 52134
msgget(MY_MSG_ID,IPC_CREAT);打开关键字为MY_MSG_ID的消息队列,如果不存在则创建,返回消息队列的标识符。
msgsnd和msgrcv分别对应消息的发送和接收。
msgctl函数用来对消息队列进行控制和删除。
2.5.4 管道通信(pipe)
管道是用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件。当一个进程对管道进行操作时其它进程必须等待。只有确定了对方已存在的时候才进行通讯。
在Linux 中,用pipe函数创建匿名管道,用mkpipe函数创建命名管道,命名管道以文件的形式在对应目录下存在,用户可以查看到它,不过命名管道文件并非真正的文件,它存在于内存而非外存,这是通过虚拟文件系统(VFS)来实现的,所以对管道的操作可以使用低级文件操作,即open,read,write,close。
2.6 线程
2.6.1 线程的概念
线程引入的目的:进程由于自身占有着资源,在处理器进行调度时为了实现现场保护要进行较大的开销。所以,引入线程,是为了减少程序在并发执行时所付出的时间和空间上的开销。
线程具有许多传统进程的特点,故线程又称为轻型进程;传统进程称为重型进程。在引入了线程的操作系统中,一个进程至少由一个线程组成。
线程与进程的区别
1)调度:线程是处理器调度和分派的基本单位,进程作为资源拥有的基本单位。
2)并发性:引入线程之后,不仅仅进程之间可以并发执行,而且在一个进程中的多个线程之间也可以并发执行。在多处理器环境下,多个处理器同时也不能执行多个进程,而是多处理器同时执行同一进程中的多个线程。
3)拥有资源:一般情况线程不拥有独立的资源,而是共享该进程中的资源。
4)系统开销:线程的切换只需保存和设置少量的寄存器,不涉及现场保护操作,就切换代价而言线程远高于进程。
综上所述,进程的属性有:轻型实体,独立调度和分派的单位,可并发执行,共享进程资源。
线程控制块(TCB)类似于PCB,但它是轻型的,保存了线程的状态参数,如寄存器状态,堆栈指针,线程运行状态,优先级,专有存储器,信号屏蔽等。其中线程的运行状态只有三个基本状态:执行、就绪、阻塞。
进程的创建和终止:在Linux 中,创建线程使用pthread_create,终止进程使用pthread_exit函数。值得注意的是,非分离线程终止后并不会立即释放它的资源,而我们创建线程默认的都是非分离线程。所以,如果我们现在有这样一个线程:
voidfunc_thread(void *arg)
{
//什么也不做
pthread_exit();
}
如果用以下伪代码创建线程:
for(inti = 1 to 10000)
pthread_create(func_thread);
会发现系统不崩溃也残废,这就印证上面的说法。如果要让线程执行完毕后回收资源,则需要在调用者线程执行“等待线程终止”的命令,这个命令在Linux 为pthread_join函数。它会阻塞当前线程直到要等待的线程结束,然后回收它的资源。当然想让线程执行结束都自动回收其资源,可以在调用者线程中将创建的线程设为分离线程,使用pthread_detach或者pthread_attr_setdetachstate函数。
什么叫分离线程?分离线程即分离状态(detached)的线程,与其相反的就是结合态(非分离态 joinable)。我们知道线程共享进程的所有资源,那这个资源也就包括其它线程的信息,为了保证线程退出后其资源(如打开的文件,线程执行的状态记录等)可能会被其它线程用到,线程执行结束后相关的资源是不会自动回收的,这就是线程默认的结合态(可以理解为和其它线程结合在一起,不能随意回收资源)。要想线程执行结束后自动回收资源,就需要将线程指明为分离态。关于线程的分离态和结合态,大家可以看下这篇文章:
http://www.cnblogs.com/mydomain/archive/2011/08/14/2138454.html
2.6.2 线程的同步和通信
多线程操作系统中提供多种同步机制,如互斥锁,条件变量,信号量等等。详细的内容在后面会慢慢介绍。这里说一下条件变量:
如果只是用互斥锁,当线程A占有资源A想要资源B,线程B占有资源B想要资源A时,就会出现死锁。解决方法就是使用条件变量,因为线程A想要资源B时,资源A此时是线程A不会用到的,所以只要将它释放掉即可。条件变量就起到标记资源的状态的作用,通过互斥锁来改变条件变量进行资源的占有和释放。
2.6.3 线程的实现方式
线程的实现方式分三种:内核支持线程(KST kernel Supported Threads)、用户级线程(ULT User Level Threads)、内核线程和用户线程的组合。
内核支持线程:即线程是在内核的支持下运行的,无论用户级线程还是内核中的线程,他们的创建、撤销和调度都是内核来进行的。它的优点:多个线程可以并行执行在多个处理器上;当一个线程阻塞时,操作系统会调度其他线程占有处理器;内核里的线程速度快开销小。但缺点是用户级线程的操作需要从用户态进入到内核态,开销较大。
用户级线程:线程存在于用户空间里,对线程的创建、撤销和调度等操作都不需要系统调用。也就是说,就算系统内核不支持多线程,用户空间里的进程也可以拥有多线程。由于不会涉及到系统调用,并且线程调度比进程调度来的简单,所以使得线程切换的速度特别快。值得指出的是,用户级线程其实是将系统分给每个进程的时间片再进行时分复用的,比如一个进程的时间片为1,它有100个用户级线程,那么每个线程的时间只有1/100。用户级线程的优点就是节约了模式切换的开销和内核的资源;灵活性高,不同进程可以选择不同的线程调度算法;用户级线程的实现与操作系统平台无关,操作系统不知道用户级线程的存在,可以在不支持多线程的操作系统运行。但用户级线程有两个很大的缺点:一是当一个线程阻塞时,整个进程中所有的线程也就阻塞了。二是在多处理器平台上不能实现多线程在多处理器上并行执行。
组合方式:其实就是将内核支持线程和用户级线程结合在一起,取长补短,实现了内核支持线程的功能,又保证用户级线程调度的快速。
2.6.4 线程的实现
1)内核支持线程的实现
系统在创建一个新进程时,内核会为它分配一个任务数据区PTDA(Per Task Data Area),其中包括了若干线程的线程控制块TCB,操作系统内核通过这些TCB来分辨线程。
2)用户级线程的实现
用户级线程在用户空间实现,但它们都运行在一个中间系统之上。中间系统有两种,运行时系统和内核控制线程。
运行时系统(Runtime System):它实质上是用于管理和控制线程的函数的集合,其中包括创建、撤销、调度线程等操作的函数。用户级线程的操作不会直接使用系统调用,而是将相关的请求发给运行时系统,由运行时系统进行相关系统调用来和内核交流。
内核控制线程:这种线程又称为轻型线程LWP(Light Weight Process)。这是内核支持线程和用户级线程的组合方式中的线程调度,将一个用户级线程连接到LWP上,该用户线程就具有了内核支持线程的所有属性。如下图:
内核相当于一个服务器,而内核线程就对应了内核的各种功能性的服务。用户级线程可以通过LWP来访问内核,内核不知道用户级线程的存在,内核只能看到LWP。由于LWP数量有限,常常将LWP做成线程池,使多个用户级线程多路复用一个LWP。
用户级线程可以独立于内核运行,与内核无关。也就是说,当用户级线程不需要与内核通信时,并不需要LWP;当要通信时,就需要借助LWP,并且一个要通信的用户级线程就对应一个LWP。再说明白点,LWP就是内核线程提供给用户级线程访问内核的一个接口。
那么我们就要思考了,传统内核支持线程中的用户级线程和使用结合方式下的用户级线程有什么区别呢?内核支持线程中的用户线程的所有操作(创建、撤销、调度、通信等)都需要经过内核,那么这里的用户态和内核态的切换需要花费很多时间;而结合方式里,引入了LWP,在平常情况(不访问内核)下,用户级线程跟操作系统无关,是通过时分复用进程的时间片来工作的,当需要访问内核与内核通信时,用户级线程连接到一个LWP上,通过LWP与内核通信。这样,结合方式下的用户级线程在平常的调度等操作不需要访问内核,加快了执行效率,当需要访问内核时通过LWP来实现与内核通信,这样实现了扬长避短。
所以,下面两句话都是正确的:
1> 在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个LWP也将随之阻塞,进而使连接到LWP上的用户级线程也被阻塞。如果进程只包含一个LWP,则此时进程应阻塞。
2> 若进程包含多个LWP,当一个LWP阻塞时,进程中另一个LWP可继续执行;就算进程中所有的LWP全部阻塞,进程中的线程也依然能继续执行,只是不能再访问内核了。
想要了解更多的朋友可以看下这个链接:
http://blog.163.com/jiams_wang/blog/static/303391492012103010374038/
由于时间仓促,后面的内容就没有找习题了,相关教材参考书和网上有很多习题,大家自行找找。