2.4 进程通信
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。
为了保证安全,一个进程不能直接访问另一个进程的地址空间。
进程通信,是指进程之间的信息交换,通常有低级和高级之分。
- 低级进程通信
- ①效率低,生产者每次只能向缓冲区中投放一个产品(消息),消费者每次只能从缓冲区中取得一个消息;
- ②通信对用户不透明,OS只为进程之间的通信提供了共享存储器,而关于进程之间通信所需要的共享数据结构的设置、数据的传送、进程的互斥与同步等,都必须由程序员去实现,这对于用户而言显然是非常不方便的。本书第4章中将会介绍进程的互斥与同步,由于它们的实现需要在进程间交换少量的信息,不少学者也将它们归为低级进程通信。
- 高级进程通信
- ①使用方便,OS隐藏了实现进程通信的具体细节,向用户提供了一组高级通信命令(原语),用户可以方便地直接利用它来实现进程之间的通信,或者说通信过程对用户是透明的,这样就大大减少了通信程序编制上的复杂性;
- ②高效地传送大量数据,用户可以直接利用高级通信命令(原语)来高效地传送大量数据。
2.4.1 进程通信的类型
随着OS的发展,用于进程之间实现通信的机制也在发展,并已由早期的低级通信机制发展为能传送大量数据的高级通信机制。目前,高级通信机制可归结为4类:共享存储器系统、管道通信系统、消息传递系统以及客户机-服务器系统。
1.共享存储器系统
在共享存储器系统(shared-memory system)中,相互通信的进程共享某些数据结构或存储区,进程之间能够通过这些空间进行通信。据此,又可把它们分成以下两种类型。
(1)基于共享数据结构的通信方式。在这种通信方式中,要求各进程共享某些数据结构,以实现各进程间的信息交换,如在生产者-消费者问题中的有界缓冲区。OS仅提供共享存储器,由程序员负责对共享数据结构进行设置和对进程间同步进行处理。这种通信方式仅适用于传送相对较少量的数据,通信效率低下,属于低级进程通信。
(2)基于共享存储区的通信方式。为了传送大量数据,在内存中划出了一块共享存储区,各进程可通过对该共享存储区的读/写来交换信息、实现通信,数据的形式和位置(甚至访问)均由进程负责控制,而非OS。这种通信方式属于高级进程通信。需要通信的进程在通信前,先向系统申请获得共享存储区中的一个分区,并将其附加到自己的地址空间中,进而便可对其中的数据进行正常的读/写,读/写完成或不再需要时,将分区归还给共享存储区即可。
2.管道通信系统
所谓“管道”(pipe),是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),会以字节流形式将大量的数据送入管道;而接收管道输出的接收进程(即读进程),则会从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故称之为管道通信。这种方式首创于UNIX系统,由于它能有效地传送大量数据,因而又被应用于许多其他OS中。
为了协调双方的通信,管道机制必须提供以下3方面的协调能力。①互斥,即当一个进程正在对管道执行读/写操作时,其他(另一)进程必须等待。②同步,即当写(输入)进程把一定数量(如4KB)的数据写入管道后,便去睡眠(等待),直到读(输出)进程取走数据后,再把它唤醒。当读进程读一空管道时,也应睡眠(等待),直至写进程将数据写入管道后,再把它唤醒。③确定对方是否存在,只有确定了对方已存在时,才能进行通信。
3.消息传递系统
- 在该机制中,进程不必借助任何共享数据结构或存储区,而是会以格式化的消息(message) 为单位,将通信的数据封装在消息中,并利用OS提供的一组通信命令(原语),在进程间进行消息传递,完成进程间的数据交换。
- 该方式隐藏了通信实现细节,使通信过程对用户透明,降低了通信程序设计的复杂性和错误率,成为当前应用得最为广泛的一类进程通信机制。例如:在计算机网络中,消息又称为报文;在微内核OS中,微内核与服务器之间的通信无一例外地都采用了消息传递机制;由于该机制能很好地支持多处理机系统、分布式系统和计算机网络系统,因此也成为这些系统中最主要的通信机制。
- 基于消息传递系统(message passing system)的通信方式属于高级通信方式,其因实现方式的不同又可进一步分成两类:①直接通信方式,是指发送进程利用OS所提供的发送原语,直接把消息发送给目标进程;②间接通信方式,是指发送进程和接收进程都通过共享中间实体(称为信箱)的方式进行消息的发送和接收,进而完成进程间的通信。
4.客户机 - 服务器系统
前面所述的共享内存、消息传递等技术,虽然可以用于实现不同计算机间进程的双向通信,但客户机-服务器系统(client-server system)的通信机制,在网络环境的各种应用领域,已成为当前主流的通信机制,其主要的实现方法分为3类:套接字、远程过程调用和远程方法调用。
- (1)套接字。
套接字(socket) 起源于20世纪70年代加州大学伯克利分校开发的伯克利软件套件(berkeley software distribution,BSD),其是UNIX系统下的网络通信接口。最初,套接字被设计用于同一台主机上多个应用程序之间的通信(即进程间的通信),主要是为了解决多对进程同时通信时端口和物理线路的多路复用问题。随着计算机网络技术的发展以及UNIX系统的广泛使用,套接字逐渐成为了非常流行的网络通信接口之一。
一个套接字就是一个通信标志类型的数据结构,包含通信目标地址、通信使用的端口号、通信网络的传输层协议、进程所在的网络地址以及针对客户或服务器程序所提供的不同系统调用或API等,是进程通信和网络通信的基本构件。套接字是针对客户机-服务器模型而设计的。通常,套接字可分为以下两类。
① 基于文件型。通信进程都运行在同一主机的网路环境下,而套接字是基于本地文件系统支持的。一个套接字会关联一个特殊的文件,通信双方通过对这个特殊文件进行读/写而实现通信,其原理类似于前面所讲的管道通信。
② 基于网络型。该类型通常采用非对称方式通信,即发送者需要提供接收者的名称。- 通信双方的进程运行在不同主机的网络环境下。通信被分配了一对套接字,其中一个属于接收进程(或服务器端),另一个属于发送进程(或客户端)。一般地,发送进程发出连接请求时,会随机申请一个套接字,与此同时主机会为之分配一个端口,并使其与该套接字绑定,而不再分配给其他进程(套接字 socket = (IP 地址:端号))。接收进程拥有全局公认的套接字和指定的端口(如文件传输协议(file transfer protocol,FTP)服务器的监听端口号为21,Web或超文本传输协议(hypertext transfer protocol,HTTP)服务器的监听端口号为80),并通过监听端口来等待客户请求。因此,任何进程都可以向它发出连接请求和信息请求,以方便进程之间通信连接的建立。接收进程一旦收到请求,就会接受来自发送进程的连接,并完成连接,这表示在主机间传输的数据可以准确地分送到通信进程,实现进程间的通信;当通信结束时,系统通过关闭接收进程的套接字来撤销连接。
- 套接字的优势在于,它不仅适用于同一台计算机内部的进程通信,也适用于网络环境中不同计算机间的进程通信。由于每个套接字都拥有唯一的套接字号(也称为套接字标识符),即系统中所有的连接都持有唯一的一对套接字及端口,对于来自不同应用程序进程或网络连接的通信能够被方便地加以区分,这确保了通信双方之间逻辑链路的唯一性,便于实现数据传输的并发功能,而且隐藏了通信设施与实现细节,支持采用统一的端口进行通信。
- (2)远程过程调用和远程方法调用。
- 远程过程调用(remote procedure call,RPC)是一个通信协议,用于通过网络连接的系统。该协议允许运行于一台主机(本地)系统上的进程调用另一台主机(远程)系统上的进程,而对程序员则表现为常规的过程调用,无须额外为此编程。需要特别说明的是,如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程方法调用。
- 负责处理远程过程调用的进程有两个,一个是本地客户进程,另一个是远程服务器进程。这两个进程通常也被称为网络守护进程,主要负责在网络间传递消息。一般情况下,这两个进程都处于阻塞状态,等待消息。
- 为了使远程过程调用看上去与本地过程调用一样,使得调用者感觉不到此次调用的过程是在其他主机上(远程)执行的,RPC引入了存根(stub) 的概念:在本地客户端,每个能够独立运行的远程过程都拥有一个客户存根(client stubborn),本地进程调用远程过程,实际是调用该过程关联的存根;与此类似,在每个远程进程所在的服务器端上,其所对应的实际可执行进程也存在一个服务器存根与其关联。本地客户存根与对应的远程服务器存根一般也处于阻塞状态,等待消息。
- 远程过程调用的主要步骤是:
- ①本地过程调用者以一般方式调用远程过程在本地关联的客户存根,传递相应的参数,然后将控制权转移给客户存根;
- ②执行客户存根,完成包括过程名和调用参数等信息的消息建立,将控制权转移给本地客户进程;
- ③本地客户进程完成与服务器的消息传递,并将消息发送到远程服务器进程;
- ④远程服务器进程接收消息后,转入执行,并根据其中的远程过程名找到对应的服务器存根,然后将消息发送给该服务器存根;
- ⑤该服务器存根接收到消息后,由阻塞状态转入执行状态,拆开消息并从中取出过程调用的参数,然后以一般方式调用服务器端关联的远程过程;
- ⑥在服务器端的远程过程运行完毕后,将结果返回给与之关联的服务器存根;
- ⑦该服务器存根获得控制权后运行,将结果打包为消息,并将控制权转移给远程服务器进程;
- ⑧远程服务器进程将消息发送回客户端;
- ⑨本地客户进程接收到消息后,根据其中的过程名将消息存入关联的客户存根,并再次将控制权转移给客户存根;
- ⑩客户存根从消息中取出结果,返回给本地过程调用者进程,并完成控制权的转移。这样,本地过程调用者再次获得控制权,并且得到了所需的数据,能够继续运行。
- 显然,上述步骤的主要作用在于,将客户端的本地过程调用转化为客户存根,再转化为服务器端的本地过程调用。对客户与服务器来说,这一过程的中间步骤是不可见的。因此,调用者在整个过程中并不知道该过程的执行是在远程(而非本地)。
2.4.2 消息传递通信的实现方式
当进程之间进行通信时,源进程可以直接或间接地将消息发送给目标进程,因此可将进程通信分为直接和间接两种。常见的直接消息传递系统和信箱通信就分别采用了这两种通信方式。
1.直接通信(直接消息传递系统)
- 直接消息传递系统采用直接通信方式,即 利用OS所提供的发送命令(原语),直接把消息发送给目标进程。
-
(1)直接通信原语。
- ① 对称寻址方式。
该方式要求发送进程和接收进程都必须以显式方式提供对方的标识符。通常,系统会提供下列两条通信命令(原语)。
send(receiver, message):发送一个消息给接收进程。
receive(sender, message):接收发送进程发来的消息。
例如,原语send(P2, m1)表示将消息m1发送给接收进程P2;而原语receive(P1, m1)则表示接收由P1发来的消息m1。
对称寻址方式的不足在于,一旦改变进程的名称,就可能需要检查所有其他进程的定义,有关对该进程旧名称的所有引用都必须查找到,以便将它们修改为新名称。显然,这样的方式不利于实现进程定义的模块化。 - ② 非对称寻址方式。
在某些情况下,接收进程可能需要与多个发送进程进行通信,因此无法事先指定发送进程。例如,用于提供打印服务的进程,可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程的原语中不需要命名发送进程,而只需要填写表示源进程的参数,即完成通信后的返回值;发送进程则仍需要命名接收进程。该方式的发送和接收原语可表示为如下。
send(P, message):发送一个消息给进程P。
receive(id, message):接收来自任何进程的消息,id变量可设置为进行通信的发送方进程id或其名字。
- ① 对称寻址方式。
-
(2)消息的格式。
在消息传递系统中所传递的消息,必须具有一定的消息格式。在单处理机系统中,由于发送进程和接收进程处于同一台机器中,有着相同的环境,因此消息的格式比较简单,可采用比较短的定长消息格式,以减少对消息的处理和存储开销。该方式可用于办公自动化系统中,为用户提供快速的便笺式通信。但这种方式对于需要发送较长消息的用户是不方便的。
为此,可采用变长消息格式,即进程所发送消息的长度是可变的。对于变长消息,系统无论在处理方面还是存储方面,都可能会付出更多的开销,但其优点在于方便了用户。 -
(3)进程的同步方式。
当进程之间进行通信时,同样需要有进程同步机制,以使各进程间能协调通信。不论是发送进程,还是接收进程,在完成消息的发送或接收后,都存在两种可能性,即进程继续发送/接收,或者阻塞。由此,我们可得到3种情况。- ①发送进程阻塞,接收进程阻塞。这种情况主要用于进程之间紧密同步,发送进程和接收进程之间无缓冲。
- ②发送进程不阻塞,接收进程阻塞。这是一种应用最广的进程同步方式。平时,发送进程不阻塞,因而它可以尽快地把一个或多个消息发送给多个目标;而接收进程平时则处于阻塞状态,直到发送进程发来消息时才会被唤醒。
- ③发送进程和接收进程均不阻塞。这也是一种较常见的进程同步方式。平时,发送进程和接收进程都在忙于自己的事情,仅当发生某事件而使它们无法继续运行时,它们才会把自己阻塞起来进行等待。
-
(4)通信链路。
为了使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。有两种方式建立通信链路。- 第一种方式是:由发送进程在通信之前用显式的“建立连接”命令(原语),请求系统为之建立一条通信链路,在链路使用完后拆除链路。这种方式主要用于计算机网络中。
- 第二种方式是:发送进程无须明确提出建立链路的请求,只须使用系统提供的发送命令(原语),系统就会自动为之建立一条链路。这种方式主要用于单处理机系统中。
根据通信方式的不同,又可把链路分成两种:
- ①单向通信链路,只允许发送进程向接收进程发送消息,或者反向进行;
- ②双向通信链路,允许进程A在向进程B发送消息的同时,进程B向进程A发送消息。
-
2.间接通信(信箱通信)
- 信箱通信采用间接通信方式,即进程之间的通信需要通过某种中间实体(如共享数据结构等)实现。该实体建立在随机存储器的共享缓冲区上,用来暂存发送进程发送给目标进程的消息;接收进程可以从该实体中取出发送进程发送给自己的消息,通常把这种中间实体称为信箱(或邮箱),每个信箱都有一个唯一的标识符。消息在信箱中可以被安全保存,只允许核准的目标用户对其进行随时读取。因此,利用信箱通信方式既可实现实时通信,又可实现非实时通信。
-
(1)信箱的结构。
信箱被定义为一种数据结构。在逻辑上,其可以分为两个部分:①信箱头,用于存放信箱的描述信息,如信箱标识符、信箱的拥有者标识符、信箱口令、信箱的空格数等;②信箱体,由若干个可以存放消息(或消息头)的信箱格组成,信箱格的数目以及每格的大小是在创建信箱时确定的。在消息传递方式上,最简单的方式是单向传递,当然消息也可以双向传递。图2-15所示为双向通信链路(双向信箱)示意。
-
(2)信箱通信原语。
系统为信箱通信提供了若干条原语,分别用于下列情况。- ①信箱的创建和撤销。进程可利用信箱创建原语来建立一个新信箱,创建者进程应给出信箱名字、信箱属性(公用、私用或共享);对于共享信箱,还应给出共享者的名字。当进程不再需要读信箱时,可用信箱撤销原语将之撤销。
- ②消息的发送和接收。当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下列通信原语进行通信。
send(mailbox, message):将一个消息发送到指定信箱。
receive(mailbox, message):从指定信箱中接收一个消息。
-
(3)信箱的类型。
信箱可由OS创建,也可由用户进程创建,创建者是信箱的拥有者。据此,可把信箱分为以下3类。-
①私用信箱,用户进程可为自己建立一个新信箱,并将其作为该进程的一部分。信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱。这种私用信箱可采用单向通信链路的信箱来实现。当拥有该信箱的进程结束时,信箱也会随之消失。
-
②公用信箱,由OS创建,并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该信箱,也可获得该信箱发送给自己的消息。显然,公用信箱应采用双向通信链路的信箱来实现。通常,公用信箱在系统运行期间始终存在。
-
③共享信箱,由某进程创建,在创建时或创建后,须指明它是可共享的,同时须指出共享进程(用户)的名字。信箱的拥有者和共享者,都有权获得信箱发送给自己的消息。
利用信箱通信时,在发送进程和接收进程之间,存在以下4种关系:
-
①一对一关系,发送进程和接收进程可以建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。
-
②多对一关系,允许提供服务的进程与多个用户进程进行交互,也称为客户/服务器交互(client/server interaction)。
-
③一对多关系,允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方式向接收者(多个)发送消息。
-
④多对多关系,允许建立一个公用信箱,使得多个进程既能向信箱中投递消息,又能从信箱中取走属于自己的消息。
-
-
2.4.3 实例 :Linux 进程通信
Linux作为一种新兴的OS,几乎支持所有的UNIX系统下常用的进程通信方法,包括管道、信号、消息队列、共享内存、信号量、套接字等。
1.管道
管道是进程间通信中最古老的一种方式,它分为无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行在同一台机器上的任意两个进程间的通信。
- (1)无名管道由pipe()函数创建:
1 #include <unistd.h>
2 int pipe(int filedis[2]);
其中,参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下面的例子示范了如何在父进程和子进程之间实现通信。
1 #define INPUT 0
2 #define OUTPUT 1
3 void main( ) {
4 int file_descriptors[2];
5 pid_t pid; /* 定义子进程号 /
6 char buf [256];
7 int returned_count;
8 pipe(file_descriptors); / 创建无名管道 /
9 if((pid = fork()) == -1) { / 创建子进程 /
10 printf(“Error in fork/n”);
11 exit(1);
12 }
13 if(pid == 0) { / 执行子进程 /
14 printf(“in the spawned (child) process…/n”);
15 / 子进程向父进程写数据,关闭管道的读端 /
16 close(file_descriptors[INPUT]);
17 write(file_descriptors[OUTPUT], “test data”, strlen(“test data”));
18 exit(0);
19 }
20 else { / 执行父进程 /
21 printf(“in the spawning (parent) process…/n”);
22 / 父进程从管道读取子进程写的数据,关闭管道的写端 */
23 close(file_descriptors[OUTPUT]);
24 returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
25 printf(“%d bytes of data received from spawned process: %s/n”,returned_count, buf);
26 }
27 }
-
(2)有名管道可由两种命令行方式创建:函数mkfifo和系统调用mknod。下面的两种方式都在当前目录下生成了一个名为myfifo的有名管道。
- 方式一:mkfifo(“myfifo”,“rw”);
- 方式二:mknod myfifo p;
生成了有名管道后,就可以使用一般的文件I/O函数(如open、close、read、write等)来对它进行操作。下面给出了一个简单的例子,假设我们已经创建了一个名为myfifo的有名管道。
1 #include <stdio.h>
2 #include <unistd.h>
3 void main( ) {
4 FILE * in_file, out_file;
5 int count = 1;
6 char buf[80];
7 in_file = fopen(“mypipe”, “r”); / 读有名管道 /
8 if (in_file == NULL) {
9 printf(“Error in fdopen./n”);
10 exit(1);
11 }
12 while ((count = fread(buf, 1, 80, in_file)) > 0) printf(“received from pipe: %s/n”, buf );
13 fclose(in_file);
14 out_file = fopen(“mypipe”, “w”); / 写有名管道 */
15 if (out_file == NULL) {
16 printf(“Error in fdopen./n”);
17 exit(1);
18 }
19 sprintf(buf,“this is test data for the named pipe example./n”);
20 fwrite(buf, 1, 80, out_file);
21 fclose(out_file);
22 }
2.信号
使用信号进行通信是一种比较复杂的通信方式,用于通知接收进程有某种事件发生。信号除了可以用于进程间通信外,还可以被进程发送给其自身。Linux除了支持UNIX系统早期信号语义函数signal() 外,还支持语义符合可移植操作系统接口(portable operating system interface,POSIX)标准的信号函数sigaction。
3.消息队列
Linux消息队列支持POSIX消息队列和System V消息队列。消息队列用在运行于同一台机器上的进程间通信中,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中以消息链表的形式出现。消息链表中节点的结构用msg声明。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程可以读取队列中的消息。消息队列克服了信号承载信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.共享内存
共享内存可以使运行在同一台机器上的进程间通信最快,因为数据无须在不同的进程间进行复制。通常由一个进程在内存中创建一块共享存储区,其余进程对这块存储区进行读/写。
得到共享存储区的方式有两种:映射/dev/mem设备和内存映像文件。前一种方式不会给系统带来额外的开销,但在现实中并不常用,因为它控制的将会是实际的物理内存,而在Linux系统下,这只有通过限制Linux系统存取的内存才可以实现,这当然不太实际。常用的方式是通过shmXXX函数族来实现利用共享存储区进行存储。
首先使用shmget() 函数获得一个共享存储标识符:
1 #include <sys/types.h>
2 #include <sys/ipc.h>
3 #include <sys/shm.h>
4 int shmget(key_t key, int size, int flag);
shmget()函数类似于大家所熟悉的malloc()函数,系统会将其请求分配size大小的内存用作共享存储区。当共享存储区被创建后,其余进程即可通过调用shmat() 将其连接到自身的地址空间中:
void *shmat(int shmid, void *addr, int flag);
shmid为shmget()函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值就是该进程数据段所连接的实际地址,进程可以对此地址所对应的内存进行读/写操作。
使用共享存储区来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量会被用于实现对共享存储数据存取的同步,另外,也可通过使用shmctl() 函数设置共享存储区的某些标志位(如SHM_LOCK、SHM_UNLOCK等)来实现。
5.信号量
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常会被作为一种锁机制,用于防止某进程正在访问共享资源(如共享内存)时,其他进程也来访问该资源。因此,信号量主要作为进程间以及同一进程内不同线程间的同步手段。
6.套接字
套接字编程是实现Linux系统和其他大多数OS中进程间通信的主要方式之一。我们熟知的WWW服务、FTP服务、Telnet服务等都是基于套接字编程来实现的。除了适用于异地计算机进程间通信之外,套接字同样适用于本地同一台计算机内部的进程间通信。
2.5 线程的概念
20世纪60年代中期,人们在设计多道程序OS时,引入了进程的概念,从而解决了在单处理机环境下的程序并发执行问题。此后在长达20年的时间里,在多道程序OS中一直以进程为能够拥有资源并独立调度(运行)的基本单位。直到20世纪80年代中期,人们才提出了比进程更小的基本单位——线程(thread) 的概念,并试图用它来提高程序并发执行的程度,以进一步改善系统的服务质量。特别是在进入20世纪90年代后,多处理机系统得到了迅速发展,由于线程能更好地提高程序的并发执行程度,近些年推出的多处理机OS无一例外地都引入了线程,用于改善OS的性能。
还没引入进程之前,系统中各个程序只能串行执行。引入了进程之后,系统中各个程序就能并行执行。
一个程序有多个功能,如微信的视频、聊天、传送文件,而进程是程序的一次执行,但程序的多个功能显然不可能是由一个程序顺序处理就能实现的。
2.5.1 线程的引入
如果说在OS中引入进程的目的是使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在OS中再引入线程,则是为了减少程序在并发执行时所付出的时空(时间和空间)开销,以使OS具有更好的并发性。
1.进程的两个基本属性
首先让我们来回顾进程的两个基本属性。①进程是一个可拥有资源的独立单位。一个进程要能独立运行,就必须拥有一定的资源,包括用于存放程序正文和数据的磁盘,内存地址空间,以及它在运行时所需要的I/O设备、已打开的文件、信号量等。②进程同时又是一个可独立调度和分派的基本单位。一个进程要能独立运行,它还必须是一个可独立调度和分派的基本单位。每个进程在系统中均有唯一的PCB,系统可以根据PCB来感知进程的存在,也可以根据PCB中的信息对进程进行调度,还可将断点信息保存在进程的PCB中。反之,可利用进程PCB中的信息来恢复进程运行的现场。正是由于具有这两个基本属性,进程才成为了一个能独立运行的基本单位,从而也就构成了进程并发执行的基础。
2.程序并发执行所须付出的时空开销
为使程序能并发执行,系统必须进行以下这一系列的操作:
- ①创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源(如内存空间、I/O设备等),并建立相应的PCB;
- ②撤销进程,系统在撤销进程时,又必须先对其所占有的资源执行回收操作,然后再撤销PCB;
- ③进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,并设置新选中进程的CPU环境,这一过程须花费不少的处理机时间。
据此可知,由于进程是一个资源的拥有者,因而在创建、撤销和切换中,系统必须为之付出较大的时空开销。这就限制了系统中所设置进程的数目,而且进程切换也不宜过于频,从而限制了程序并发执行程度的进一步提高。
3.线程——作为调度和分派的基本单位
如何能使多个程序更好地并发执行,同时又能尽量减少系统的开销,已成为近年来设计OS时所追求的重要目标。有不少研究OS的学者们想到,可以将进程的两个基本属性分开,由OS分开处理,即并不把“作为调度和分派的基本单位”同时作为拥有资源的基本单位,以实现“轻装上阵”;而对于拥有资源的基本单位,又不对之施以频繁的切换。正是在这种思想的指导下,形成了线程的概念。
随着VLSI技术和计算机体系结构的发展,出现了对称多处理机(symmetrical multi-processing,SMP)系统。它为提高计算机的运行速度和系统吞吐量提供了良好的硬件基础。但要使多个CPU很好地协调运行,充分发挥它们的并行处理能力以提高系统性能,则还必须配置性能良好的多处理机OS。但利用传统的进程概念和设计方法,已难以设计出适用于SMP系统的OS,最根本的原因是进程“太重”,这致使为实现多处理机环境下的进程创建、调度与分派,均须花费较大的时空开销。如果在OS中引入线程,以线程为调度和分派的基本单位,则可以有效改善多处理机系统的性能。因此,一些主要的OS(如UNIX、Windows等)厂家,又进一步对线程技术做了开发,使之适用于SMP系统。
2.5.2 线程与进程的比较
线程具有传统进程所具有的很多特征,因此又称为轻型进程(light-weight process,LWP)或进程元;相应地把传统进程称为重型进程(heavy-weight process,HWP),它相当于只有一个线程的任务。下面将从调度的基本单位、并发性、拥有资源等方面对线程和进程进行比较。
1.调度的基本单位
在传统OS中,进程作为独立调度和分派的基本单位,能够独立运行。其在每次被调度时,都需要进行上下文切换,开销较大。
而在引入线程的OS中,已把线程作为调度和分派的基本单位,因而线程是能独立运行的基本单位。当进行线程切换时,仅须保存和设置少量寄存器的内容,切换代价远低于进程。
在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,必然会引起进程的切换。
2.并发性
在引入线程的OS中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,甚至还允许一个进程中的所有线程都能并发执行。同样,不同进程中的线程也能并发执行。这使得OS具有了更好的并发性,从而能更加有效地提高资源利用率和系统吞吐量。例如,在文字处理机中可设置三个线程:第一个线程用于显示文字和图形,第二个线程通过键盘读入数据,第三个线程在后台检查拼写和语法。再如,在网页浏览器中可设置两个线程:第一个线程用于显示图像或文本,第二个线程用于从网络中接收数据。
此外,有的应用程序需要执行多个相似的任务。例如,一个网络服务器经常会接收到许多客户的请求,如果仍采用传统单线程的进程来执行该任务,则每次只能为一个客户提供服务。但如果在一个进程中可以设置多个线程,并使其中的一个线程专用于监听客户的请求,则每当有一个客户提出请求时,系统便会立即创建一个线程来处理该客户的请求。
3.拥有资源
进程可以拥有资源,并可作为系统中拥有资源的一个基本单位。然而,线程可以说是几乎不拥有资源,其仅有的一点儿必不可少的资源也是为了确保自身能够独立运行。例如,在每个线程中都应具有用于控制线程运行的线程控制块(thread control block,TCB),用于指示被执行指令序列的程序计数器,用于保留局部变量、少数状态参数和返回地址等的一组寄存器,以及堆栈。
线程除了拥有自己的少量资源外,还允许多个线程共享它们共属的进程所拥有的资源,这一点首先表现在:属于同一进程的所有线程都具有相同的地址空间,这意味着线程可以访问该地址空间中的每一个虚地址;此外,线程还可以访问其所属进程所拥有的资源,如已打开的文件、定时器、信号量机构等的内存空间,以及线程所申请到的I/O设备等。
4.独立性
在同一进程中的不同线程之间的独立性,要比不同进程之间的独立性低得多。这是因为,为防止进程之间彼此干扰和破坏,每个进程都拥有独立的地址空间和其他资源,它们除了共享全局变量外,不允许自身以外的进程访问自己地址空间中的地址。但是同一进程中的不同线程,往往是为了提高并发性以及满足进程间的合作需求而创建的,它们可以共享进程的内存地址空间和资源,如每个线程都可以访问它们所属进程地址空间中的所有地址,一个线程的堆栈可以被其他线程读/写,甚至完全清除。由一个线程打开的文件,可以供其他线程读/写。
5.系统开销
在创建(撤销)进程时,系统要为它分配(向它回收)TCB和其他资源(如内存空间和I/O设备等)。OS为此所付出的开销,明显大于线程创建/撤销时所付出的开销。类似地,在进行进程切换时,涉及进程上下文的切换,而线程的切换代价则远低于进程的(寄存器的值不需要改变)。例如,在Solaris 2 OS中,进程的创建耗时约为线程的30倍,而进程上下文切换的耗时约为线程的5倍。此外,由于一个进程中的多个线程具有相同的地址空间,线程之间的同步和通信也比进程简单。因此,在一些OS中,线程的切换、同步以及通信都无须OS内核的干预。
6.支持多处理机系统
在多处理机系统中,对于传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。但对于多线程进程,其可以将一个进程中的多个线程分配到多个处理机上,并行运行,这无疑能够加速进程的完成。因此,现代多处理机系统都无一例外地引入了多线程。
2.5.3 线程状态和线程控制块
1.线程执行的 3 个状态
与传统的进程一样,各线程之间也存在着共享资源和相互合作的制约关系,这致使线程在执行时也具有间断性。相应地,线程在执行时,也具有下述3种基本状态:
- ①执行状态,指线程已获得处理机而正在执行;
- ②就绪状态,指线程已具备各种执行条件,只须再获得CPU便可立即执行;
- ③阻塞状态,指线程在执行中因某事件而受阻,进而处于暂停状态,例如,当一个线程执行从键盘读入数据的系统调用时,该线程就会被阻塞。线程状态之间的转换和进程状态之间的转换是一样的,如图2-6所示。
2.线程控制块
如同每个进程有一个PCB一样,系统也为每个线程配置了一个TCB,将所有用于控制和管理线程的信息均记录在TCB中。
TCB中通常含有:
- ①线程标识符,为每个线程赋予一个唯一的线程标识符;
- ②一组寄存器(包括程序计数器、状态寄存器和通用寄存器等)的内容;
- ③线程执行状态,描述线程正处于何种执行状态;
- ④优先级,描述线程执行的优先程度;
- ⑤线程专有存储区,用于在线程切换时存放现场保护信息和与该线程相关的统计信息等;
- ⑥信号屏蔽,即对某些信号加以屏蔽;
- ⑦堆栈指针,线程在执行时,经常会进行过程调用,而过程调用时通常会出现多重嵌套的情况,这样,就必须把每次过程调用中所使用的局部变量以及返回地址保存起来。为此,应为每个线程设置一个堆栈,用它来保存局部变量和返回地址。相应地,在TCB中,也须设置两个指向堆栈的指针:指向用户自己堆栈的指针和指向核心栈的指针。前者是指当线程运行在用户态时,使用用户自己的用户栈来保存局部变量和返回地址;后者是指当线程运行在内核态时,使用系统的核心栈来保存局部变量和返回地址。
3.多线程 OS 中的进程属性
多线程OS中的进程通常都包含多个线程,并会为它们提供资源。OS支持一个进程中的多个线程并发执行,但此时的进程已不再是一个执行的实体。多线程OS中的进程具有以下属性。
(1)进程是一个可拥有资源的基本单位。在多线程OS中,进程仍作为系统资源分配的基本单位,任一进程所拥有的资源包括:用户的地址空间、实现进程(线程)间同步和通信的机制、已打开的文件和已申请到的I/O设备以及一张由核心进程维护的地址映射表,该表用于实现用户程序的逻辑地址到其内存物理地址的映射。
(2)多个线程可并发执行。通常一个进程含有若干个相对独立的线程,其数目可多可少,但至少要有一个线程。由进程为这些(个)线程提供资源和运行环境,以使它们能并发执行。在OS中的所有线程都只能属于某一个特定进程。实际上,现在把传统进程的执行方法称为单线程方法,如传统的UNIX系统能支持多用户进程,但只能支持单线程方法。将每个进程支持多个线程执行的方法,称为多线程方法,例如,Java的运行环境是单进程多线程的,Windows 2000、Solaris、Mach等的运行环境则是多进程多线程的。
(3)进程已不是可执行的实体。在多线程OS中,把线程作为独立运行(或称调度)的基本单位。此时的进程已不再是一个基本的可执行实体。虽然如此,进程仍具有与“执行”相关的状态。例如,进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的、与进程状态有关的操作,也会对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;再如,在把某个进程激活时,属于该进程的所有线程也都将被激活。
2.6 线程的实现
2.6.1 线程的实现方式
线程已在许多系统中实现,但各系统中实现的方式并不完全相同。在有的系统中,特别是在一些数据库管理系统(如infomix)中,所实现的是用户级线程;而在另一些系统中,如Windows XP、Linux、Mac OS X和OS/2等,所实现的是内核支持线程;此外,在Solaris等系统中,所实现的则是这两种线程的组合。
1.内核支持线程
- 在OS中的所有进程,无论是系统进程还是用户进程,都是在OS内核的支持下运行的,是与内核紧密相关的。而内核支持线程(kernel supported thread,KST),同样也是在内核的支持下运行的,它们的创建、阻塞、撤销和切换等也都是在内核空间实现的。为了对内核支持线程进行控制和管理,在内核空间也为每个内核支持线程设置了一个TCB,内核根据该TCB来感知某线程的存在,并对其加以控制。当前大多数OS都支持KST。
- KST的实现方式有4个主要优点:
- ①在多处理机系统中,内核能够同时调度同一进程中的多个线程并行运行;
- ②如果进程中的一个线程被阻塞,则内核可以调度该进程中的其他线程来占有处理机并运行,也可运行其他进程中的线程;
- ③内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;
- ④内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。
- 内核支持线程的主要缺点是:对于用户的线程切换而言,其模式切换的开销较大;在同一个进程中,从一个线程切换到另一个线程时需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核中实现的,系统开销较大。
- KST的实现方式有4个主要优点:
- 1.线程的管理工作由谁来完成?
2.线程切换是否需要CPU变态?
3.操作系统是否能意识到内核级线程的存在?
4.这种线程的实现方式有什么优点和缺点?- 1.内核级线程的管理工作由操作系统内核完成。
2.线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
3.操作系统会为每个内核级线程建立相应的 TCB(Thread Control Block,线程控制块)通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程”
4.优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。缺点:一个用户进程会占用多个内核级线程线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
- 1.内核级线程的管理工作由操作系统内核完成。
2.用户级线程
- 用户级线程(user level thread,ULT) 是在用户空间中实现的,其对线程的创建、撤销、同步与通信等功能都无须内核支持,即ULT与内核无关。一个系统中的ULT数目可以达到数百个甚至数千个。由于这些线程的TCB都设置在用户空间,而线程所执行的操作又无须内核支持,因而内核完全不知道ULT的存在。
- 值得说明的是,对于设置了ULT的系统,其调度仍是以进程为单位进行的。在采用时间片轮转调度算法时,各个进程轮流执行一个时间片,这对于各进程而言貌似是公平的,但假如在进程A中包含了1个ULT,而在进程B中包含了100个ULT,那么,进程A中线程的运行时间将会是进程B中各线程运行时间的100倍;相应地,进程A的运行速度也要快上100倍,因此说实质上并不公平。
- 假如系统中设置的是KST,则调度便会以线程为单位进行。在采用时间片轮转调度算法时,各个线程轮流执行一个时间片。同样假定进程A中只有1个KST,而进程B中有100个KST。此时,进程B可以获得的CPU时间是进程A的100倍,且进程B可使100个系统调用并发执行。
- 使用ULT方式有许多优点,介绍如下。
- ①线程切换不需要转换到内核空间。对一个进程而言,其所有线程的管理数据结构均在该进程的用户空间中,管理线程切换的线程库也在用户空间运行,因此进程不用切换到内核方式来做线程管理,从而节省了模式切换的开销。
- ②调度算法可以是进程专用的。在不干扰OS调度的情况下,不同的进程可以根据自身需要选择不同的调度算法,以对自己的线程进行管理和调度,而与OS的低级调度算法无关。
- ③用户级线程的实现与OS平台无关,因为面向线程管理的代码属于用户程序的一部分,所有的应用程序都可以共享这段代码。因此,ULT甚至可以在不支持线程机制的OS平台上实现。
- 使用ULT方式的主要缺点介绍如下。
- ①系统调用的阻塞问题。在基于进程机制的OS中,大多数系统调用都会使进程阻塞,因此,当线程执行一个系统调用时,不仅该线程会被阻塞,而且进程内的所有线程均会被阻塞。而在KST方式下,进程中的其他线程仍然可以运行。
- ②在单纯的ULT实现方式中,多线程应用不能利用多处理机可以进行多重处理的这一优点。内核每次分配给一个进程的仅有一个CPU,因此,进程中仅有一个线程能执行,在该线程放弃CPU之前,其他线程只能等待。
- 1.线程的管理工作由谁来完成?
2.线程切换是否需要CPU变态?
3.操作系统是否能意识到用户级线程的存在?
4.这种线程的实现方式有什么优点和缺点?- 1.用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
2.用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。
3.在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。“用户级线程”就是“从用户视角看能看到的线程”
4.优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
- 1.用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
3.两种线程的组合方式
- 有些OS把ULT和KST这两种线程进行组合,提供了组合方式的ULT/KST线程。在组合方式线程系统中,内核支持多个KST的建立、调度和管理,同时也允许用户应用程序建立、调度和管理ULT。一些KST对应多个ULT,这是ULT通过时分多路复用KST来实现的,即将ULT对部分或全部KST进行多路复用,并且程序员可按应用需要和机器配置对KST的数目进行调整,以达到较好的效果。在组合方式线程中,同一个进程内的多个线程可以同时在多处理机上并行执行,而且在阻塞一个线程时,并不需要将整个进程阻塞。因此,组合方式多线程模型能够结合ULT和KST两者的优点,并克服它们各自的不足。由于ULT和KST的连接方式不同,从而形成了3种不同的多线程模型:多对一模型、一对一模型和多对多模型。
- (1)多对一模型,将多个ULT映射到一个KST上。如图2-16(a)所示,这些ULT一般属于一个进程,运行在该进程的用户空间,对这些线程的调度和管理也是在该进程的用户空间中完成的。仅当ULT需要访问内核时,才会将其映射到一个KST上,但每次只允许一个线程进行映射。该模型的主要优点是:线程管理的开销小,效率高。其主要缺点是:如果一个线程在访问内核时发生阻塞,则整个进程都会被阻塞;此外,在任一时刻,只有一个线程能够访问内核,多个线程不能同时在多个处理机上运行。
- (2)一对一模型,将每个ULT映射到一个KST上。如图2-16(b)所示,为每个ULT都设置了一个KST与之连接。该模型的主要优点是:当一个线程阻塞时,允许调度另一个线程运行,所以它提供了比多对一模型更好的并发性能。此外,在多处理机系统中,它允许多个线程并行地运行在多处理机系统上。该模型的唯一缺点是:每创建一个ULT,相应地就需要创建一个KST,开销较大,因此需要限制整个系统的线程数。Windows 2000、Windows NT、OS/2等系统上都实现了该模型。
- (3)多对多模型,将许多ULT映射到同样数量或较少数量的KST上。如图2-16(c)所示,KST的数目可以根据应用进程和系统的不同而变化,其可以比ULT数少,也可以与之相等。该模型结合了上述两种模型的优点,它可以像一对一模型那样使一个进程的多个线程并行地运行在多处理机系统上,也可以像多对一模型那样减少线程管理开销并提高效率。
- (1)多对一模型,将多个ULT映射到一个KST上。如图2-16(a)所示,这些ULT一般属于一个进程,运行在该进程的用户空间,对这些线程的调度和管理也是在该进程的用户空间中完成的。仅当ULT需要访问内核时,才会将其映射到一个KST上,但每次只允许一个线程进行映射。该模型的主要优点是:线程管理的开销小,效率高。其主要缺点是:如果一个线程在访问内核时发生阻塞,则整个进程都会被阻塞;此外,在任一时刻,只有一个线程能够访问内核,多个线程不能同时在多个处理机上运行。
2.6.2 线程的具体实现
不论是进程还是线程,都必须直接或间接地取得内核的支持。由于KST可以直接利用系统调用为它服务,故其对应的线程控制相当简单;而ULT则必须借助于某种形式的中间系统的帮助方能取得内核的服务,故其对应的线程控制要较复杂。
1.KST 的实现
- 在仅设置了KST的OS中,一种可能的线程控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区(per task data area,PTDA),其中包括若干个TCB空间,如图2-17所示。在每个TCB中可保存线程标识符、优先级、线程运行的CPU状态等信息。虽然这些信息与ULT的TCB中的信息相同,但它们被保存在了内核空间中。
- 每当进程要创建一个线程时,便会为新线程分配一个TCB,同时将有关信息填入该TCB中,并为之分配必要的资源,如为线程分配数百至数千个字节的栈空间和局部存储区,于是新创建的线程便有条件立即执行。当PTDA中的所有TCB空间已用完而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统即可再为之分配新的TCB空间;在撤销一个线程时,也应回收该线程的所有资源和TCB。由此可见,KST的创建和撤销均与进程的相似。在有的系统中,为了减少在创建和撤销一个线程时的开销,在撤销一个线程时,并不会立即回收该线程的资源和TCB,这样,当以后再要创建一个新线程时,便可直接将已被撤销但仍持有资源的TCB作为新线程的TCB。
KST的调度和切换与进程的调度和切换十分相似,也分抢占式和非抢占式两种。线程的调度同样可采用时间片轮转调度算法、优先级调度算法等。当线程调度选中一个线程后,便会将处理机分配给它。当然,线程在调度和切换上所花费的开销要比进程的小得多。
2.ULT 的实现
ULT是在用户空间实现的。所有ULT都具有相同的结构,它们都运行在一个中间系统上。
当前有两种方式实现的中间系统,即运行时系统与核心线程。
- (1)运行时系统。
所谓运行时系统(runtime system),实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤销线程的函数、用于控制线程同步和通信的函数以及用于实现线程调度的函数等。正因为有这些函数,才能使ULT与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为ULT与内核之间的接口。
在传统OS中,进程在切换时必须先由用户态转为内核态,再由核心线程来执行切换任务;而ULT在切换时则无须转入内核态,而是由运行时系统中的线程切换过程来执行切换任务,该过程将线程的CPU状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,并将新线程堆栈中的CPU状态装入CPU相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。由于ULT的切换无须进入内核,且切换操作简单,因而其切换速度非常快。
不论是在传统OS中,还是在多线程OS中,系统资源都是由内核管理的。在传统OS中,进程是利用OS提供的系统调用来请求系统资源的,系统调用通过软中断(如trap) 机制进入OS内核,由内核来完成相应资源的分配。ULT是不能利用系统调用的。当线程需要系统资源时,其须将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源。 - (2)核心线程。
- 核心线程又称为LWP。每一个进程都可拥有多个LWP。同ULT一样,每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先级、CPU状态等信息,另外还有栈和局部存储区等。LWP也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个ULT运行时,只须将它连接到一个LWP上,它便能具有KST的所有属性。这种线程实现方式就是组合方式。
- 一个系统中的ULT数量可能很大,为了节省系统开销,不可能设置太多的LWP,而是会把这些LWP做成一个缓冲区,称之为“线程池”。用户进程中的任一ULT都可以连接到线程池中的任一LWP上。为使每一个ULT都能利用LWP与内核通信,可以使多个ULT多路复用一个LWP,但只有当前连接到LWP上的ULT才能与内核通信,其余线程或阻塞、或等待LWP。
- 而每个LWP都要连接到一个KST上,这样,通过LWP即可把ULT与KST连接起来,ULT可通过LWP来访问内核,但内核所看到的总是多个LWP而非ULT。亦即,由LWP实现了内核与ULT之间的隔离,从而使ULT与内核无关。图2-18所示为将LWP作为中间系统时ULT的实现方法。
- 当ULT不需要与内核通信时,并不需要LWP;而当其要通信时,便须借助LWP,而且每个要通信的ULT都需要一个LWP。例如,在一个任务中,如果同时有5个ULT发出了对文件的读/写请求,这就需要有5个LWP来予以帮助,即由LWP将对文件的读/写请求发送给相应的KST,再由后者执行具体的读/写操作。如果一个任务中只有4个LWP,则只能有4个ULT的读/写请求被传送给KST,余下的1个ULT必须等待。
- 在KST执行操作时,如果其发生阻塞,则与之相连接的多个LWP也将随之阻塞,进而使连接到LWP上的ULT也被阻塞。如果进程中只包含一个LWP,此时进程也会阻塞。这种情况与前述的传统OS一样,在进程执行系统调用时,该进程实际上是阻塞的。但如果一个进程中含有多个LWP,则当一个LWP阻塞时,进程中的另一个LWP可以继续执行;即使进程中的所有LWP全部阻塞,进程中的线程也仍能继续执行,只是不能再去访问内核而已。
2.6.3 线程的创建与终止
如同进程一样,线程也是有生命期的,它由创建而产生、由终止而消亡。相应地,在OS中也就有用于创建线程的函数(或系统调用)和用于终止线程的函数(或系统调用)。
1.线程的创建
应用程序在启动时,通常仅有一个线程在执行,我们把该线程称为“初始化线程”,它的主要功能是创建新线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序入口的指针、堆栈的大小以及用于调度的优先级等。在线程的创建函数执行完后,将返回一个线程标识符供以后使用。
2.线程的终止
- 当一个线程完成了自己的任务(工作)后,或是线程在运行中出现异常情况而须被强行终止时,由终止线程通过调用相应的函数(或系统调用)对它执行终止操作。但有些线程(主要是系统线程)一旦被建立起来之后,便会一直运行下去而不被终止。在大多数OS中,线程被终止后并不会立即释放它所占有的资源,只有当进程中的其他线程执行了分离函数后,被终止的线程才会与资源分离,此时的资源才能被其他线程利用。
- 已被终止但尚未释放资源的线程,仍可被需要它的线程所调用,以使其重新恢复运行。为此,调用线程须调用一条被称为“等待线程终止”的连接命令,以与该线程进行连接。当一个调用线程调用“等待线程终止”的连接命令而试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后,其才能与指定线程进行连接并继续执行;若指定线程已被终止,则调用线程不会被阻塞,而是会继续执行。
2.7 本章小结
- 本章从程序的执行方式入手,先后引入了OS中的两个重要概念:进程和线程。程序的执行方式有顺序执行和并发执行两种。在顺序执行方式下,单个程序独占内存运行,系统的运行效率低;在并发执行方式下,多个程序占用内存并轮流在CPU上运行,系统的运行效率得到了提升。
- 进程就是指正在运行的程序,它在运行过程中会改变状态,这些状态是根据进程当前的活动来定义的,包括创建、就绪、运行、阻塞和终止等。OS中的每个进程都是通过与之一一对应的PCB来实现控制和管理的。进程控制包括:进程创建、进程终止、进程阻塞与唤醒、进程挂起与激活等,这些控制操作需要用原语的方式来完成。进程间可以相互通信,通信方法多样,常用的有管道、信号、消息队列、共享内存、信号量、套接字等。
- 为了提高程序并发执行的程度,引入了比进程更小的单位——线程。引入线程后,在资源共享、用户响应、经济性和多处理机架构等方面有诸多好处,能够进一步改善系统的性能。引入线程后,进程是资源分配的单位,线程是CPU调度的单位。线程可分为KST和ULT两种,不同的系统会支持某一种线程,或者两种都支持。由于ULT和KST的连接方式不同,形成了3种不同的多线程模型:多对一模型、一对一模型和多对多模型。