进程管理(二)

来源:互联网 发布:mac桌面文件复制到u盘 编辑:程序博客网 时间:2024/05/19 23:10


1.经典进程同步问题
2.进程通信
3.线程


经典进程同步问题

  • 生产者—消费者问题(producer – consumer Problem)
  • 哲学家就餐问题(Dining-Philosophers Problem)
  • 读者-写者问题(Readers and Writers Problem)

生产者—消费者问题

利用记录型信号量解决生产者—消费者问题

生产者和消费者之间应满足下列二个同步条件:
  • 只有在缓冲池中至少有一个缓冲区已存入消息后,消费者才能从中提取消息,否则消费者必须等待。
  • 只有缓冲池中至少有一个缓冲区是空时,生产者才能把消息放入缓冲区,否则生产者必须等待。
当缓冲区无穷大时,同步条件发生了什么改变?
生产者和消费者问题的互斥情况:
  • 全互斥(生产者与生产者之间,消费者与消费者之间的互斥)
  • 生产者/消费者内部对栈指针(in/out)互斥(生产者与消费者之间互斥)
Var mutex,empty,full: semaphore:=1,n,0;
  buffer:array[0,…,n-1] of item;
  in,out:  integer:=0,0;

begin
 parbegin
 proceducer:  
      begin
    repeat
                 …        
        producer an item nextp;
                 …
              wait(empty);     
              wait(mutex);
              buffer(in):=nextp;         
              in:=(in+1) mod n;         
              signal(mutex);         
              signal(full);         
          until false;
      end
    consumer: 
      begin
    repeat
    wait(full);                  // wait()操作顺序不可以颠倒,signal()可以
      wait(mutex);       

    nextc:=buffer(out);
              out:=(out+1) mod n;
    signal(mutex);
    signal(empty);
    consumer the item in nextc;
    until false;
   end
      parend
end 


若生产者之间及消费之之间互斥,但生产者与消费者之间不互斥:设置两个互斥信号量mutex1和mutex2,分别对in和out指针互斥。
若缓冲区无限大:取消wait(empty)和signal(empty)的操作。

哲学家进餐问题

利用记录型信号量解决哲学家进餐问题



经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。其描述如下: 
Var chopstick: array[0,…,4] of semaphore; 
所有信号量均被初始化为1,第i位哲学家的活动可描述为:
  repeat
    wait(chopstick[i]);
    wait(chopstick[(i+1)mod 5]);
      ...
    eating;
      ...
    signal(chopstick[i]);
    signal(chopstick[(i+1)mod 5]);
      ...
    thinking;
  until false; 

在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行wait(chopstick[i]); 成功后,再去拿他右边的筷子,即执行wait(chopstick[(i+1)mod 5]);又成功后便可进餐。进餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两个相邻的哲学家同时进餐,但有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量chopstick均为0; 当他们再试图去拿右边的筷子时,都将因无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决方法:
(1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2) 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
(3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定,将是1、2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。

读者-写者问题

条件要求:
1,写者不能同时写;
2,读者可以同时读;
3,读者读时写者不能写,写者写时读者不能读。

利用记录型信号量解决读者—写者问题

为实现Reader与Writer进程间在读或写时的互斥而设置了一个互斥信号量wmutex。另外,再设置一个整型变量rcount表示正在读的进程数目。由于只要有一个Reader进程在读,便不允许Writer进程去写。因此,仅当rcount=0,表示尚无Reader进程在读时,Reader进程才需要执行Wait(wmutex)操作。若Wait(wmutex)操作成功,Reader进程便可去读,相应地,做rcount+1操作。同理,仅当Reader进程在执行了rcount减1操作后其值为0时,才须执行signal(wmutex)操作,以便让Writer进程写。又因为rcount是一个可被多个Reader进程访问的临界资源,因此,也应该为它设置一个互斥信号量rmutex。 
读者—写者问题可描述如下:
  Var rmutex,wmutex:  semaphore:=1,1;
    rcount:  integer:=0;
    begin
       parbegin
                     writer:  … …
                     reader  … …
            parend
          end 


Reader:  
begin
 repeat
     wait(rmutex);
     if rcount=0 then wait(wmutex);
        rcount:=rcount+1;
        signal(rmutex);
        perform read operation;
        wait(rmutex);     
        rcount:=rcount-1;
        if rcount=0 then  signal(wmutex);
        signal(rmutex);
 until false;
end
Writer:  
begin
  repeat 
     wait(wmutex);
     perform write operation;
     signal(wmutex);
 until false;
end


进程通信

进程之间有两种信息要交换:
  • 低级通信(只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量和管程机制。优点是速度快。缺点是:----传送信息量小:效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。----编程复杂:用户直接实现通信的细节,编程复杂,容易出错):信号量(semaphore)、管程(monitor)、锁(lock) ...
  • 高级通信(能够传送任意数量的数据):消息缓冲(Message)、信箱方式(mail box)、共享存储区(share memory)、管道通信(pipe line)、剪贴板(clipboard)、套接字(socket)…
进程间通信程序
  • 直接通信:信息直接传递给接收方,如管道。在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址;在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址。
  • 间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如消息队列。通常收方和发方的数目可以是任意的。

1.共享存储器系统

(1) 基于共享数据结构的通信方式。在这种通信方式中,要求诸进程公用某些数据结构,借以实现诸进程间的信息交换。如在生产者—消费者问题中,就是用有界缓冲区这种数据结构来实现通信的。这里,公用数据结构的设置及对进程间同步的处理,都是程序员的职责。这无疑增加了程序员的负担,而操作系统却只须提供共享存储器。因此,这种通信方式是低效的,只适于传递相对少量的数据。 
(2) 基于共享存储区的通信方式。为了传输大量数据,在存储器中划出了一块共享存储区,诸进程可通过对共享存储区中数据的读或写来实现通信。这种通信方式属于高级通信。进程在通信前,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字;若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者,继之,由申请者把获得的共享存储分区连接到本进程上;此后,便可像读、写普通存储器一样地读、写该公用存储分区。
UNIX的共享存储区系统调用:
  • 创建或打开共享存储区(shmget):依据用户给出的整数值key,创建新区或打开现有区,返回一个共享存储区ID。
  • 连接共享存储区(shmat):连接共享存储区到本进程的地址空间,可以指定虚拟地址或由系统分配,返回共享存储区首地址。父进程已连接的共享存储区可被fork创建的子进程继承。
  • 拆除共享存储区连接(shmdt):拆除共享存储区与本进程地址空间的连接。
  • 共享存储区控制(shmctl):对共享存储区进行控制。如:共享存储区的删除需要显式调用shmctl(shmid, IPC_RMID, 0);

2.消息传递系统

消息传递系统(Message passing system)是当前应用最为广泛的一种进程间的通信机制。在该机制中,进程间的数据交换是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。程序员直接利用操作系统提供的一组通信命令(原语),不仅能实现大量数据的传递,而且还隐藏了通信的实现细节,使通信过程对用户是透明的,从而大大减化了通信程序编制的复杂性,因而获得了广泛的应用。 
特别值得一提的是,在当今最为流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。又由于它能很好地支持多处理机系统、分布式系统和计算机网络,因此它也成为这些领域最主要的通信工具。消息传递系统的通信方式属于高级通信方式。又因其实现方式的不同而进一步分成直接通信方式和间接通信方式两种。 

直接通信方式

这是指发送进程利用OS所提供的发送命令,直接把消息发送给目标进程。此时,要求发送进程和接收进程都以显式方式提供对方的标识符。通常,系统提供下述两条通信命令(原语):Send(Receiver,message),发送一个消息给接收进程;Receive(Sender,message),接收Sender发来的消息;例如,原语Send(P2,m1)表示将消息m1发送给接收进程P2;而原语Receive(P1,m1)则表示接收由P1发来的消息m1。 
在某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程。例如,用于提供打印服务的进程,它可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程接收消息的原语中,表示源进程的参数,也是完成通信后的返回值,接收原语可表示为:Receive (id,message); 
还可以利用直接通信原语来解决生产者—消费者问题。当生产者生产出一个产品(消息)后,便用Send原语将消息发送给消费者进程;而消费者进程则利用Receive原语来得到一个消息。如果消息尚未生产出来,消费者必须等待,直至生产者进程将消息发送过来。生产者—消费者的通信过程可分别描述如下: 
producer:  
   begin
 repeat
  produce an item in nextp;
        send(consumer,nextp);
    until false;
   end
consumer:
     begin
  repeat
            receive(producer,nextc);
   consume the item in nextc;
  until false;
  end
 

间接通信方式

间接通信方式指进程之间的通信需要通过作为共享数据结构的实体。该实体用来暂存发送进程发送给目标进程的消息;接收进程则从该实体中取出对方发送给自己的消息。通常把这种中间实体称为信箱。消息在信箱中可以安全地保存,只允许核准的目标用户随时读取。因此,利用信箱通信方式,既可实现实时通信,又可实现非实时通信。
系统为信箱通信提供了若干条原语,分别用于信箱的创建、撤消和消息的发送、接收等。
(1) 信箱的创建和撤消。进程可利用信箱创建原语来建立一个新信箱。创建者进程应给出信箱名字、信箱属性(公用、私用或共享);对于共享信箱,还应给出共享者的名字。当进程不再需要读信箱时,可用信箱撤消原语将之撤消。
(2) 消息的发送和接收。当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下述通信原语进行通信:Send(mailbox,message),将一个消息发送到指定信箱;Receive(mailbox,message),从指定信箱中接收一个消息;
信箱可由操作系统创建,也可由用户进程创建,创建者是信箱的拥有者。据此,可把信箱分为以下三类。 
1) 私用信箱
用户进程建立。信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中。这种私用信箱可采用单向通信链路的信箱来实现。当拥有该信箱的进程结束时,信箱也随之消失。
2) 公用信箱
操作系统创建,并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该信箱中,也可从信箱中读取发送给自己的消息。显然,公用信箱应采用双向通信链路的信箱来实现。通常,公用信箱在系统运行期间始终存在。
3) 共享信箱
用户进程创建并指明是可共享的信箱,还要给出共享此信箱的进程名。共享进程有权从信箱中读取消息。  

消息缓冲队列通信机制

1) 消息缓冲区

在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区。它可描述如下:
type message buffer=record
             sender ;发送者进程标识符
             size ; 消息长度
             text ; 消息正文
             next ; 指向下一个消息缓冲区的指针
          end 

2) PCB中有关通信的数据项

在操作系统中采用了消息缓冲队列通信机制时,除了需要为进程设置消息缓冲队列外,还应在进程的PCB中增加消息队列队首指针,用于对消息队列进行操作,以及用于实现同步的互斥信号量mutex和资源信号量sm。在PCB中应增加的数据项可描述如下:
type processcontrol block=record
              ...
                 mq   ; 消息队列队首指针
                 mutex ; 消息队列互斥信号量
              sm ; 消息队列资源信号量
              ...
             end 



1.发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区a,见图2-14 所示。把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区a中所设置的消息长度a.size来申请一缓冲区i,接着把发送区a中的信息复制到缓冲区i中。为了能将i挂在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源,故在执行insert操作的前后,都要执行wait和signal操作。
发送原语可描述如下:
  procedure send(receiver,a)
   begin
    getbuf(a.size,i);  根据a.size申请缓冲区;
    i.sender:= a.sender; 将发送区a中的信息复制到消息缓冲区i中;
    i.size:=a.size;
    i.text:=a.text;
    i.next:=0;
    getid(PCB set,receiver.j);获得接收进程内部标识符;
    wait(j.mutex);
    insert(j.mq,i);   将消息缓冲区插入消息队列;
    signal(j.mutex);
    signal(j.sm);
      end 

2.接收原语
接收进程调用接收原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区内。接收原语描述如下:
procedure receive(b)
  begin
    j:= internal name;  j为接收进程内部的标识符;
    wait(j.sm);
    wait(j.mutex);
    remove(j.mq,i);  将消息队列中第一个消息移出;
    signal(j.mutex);
    b.sender:=i.sender; 将消息缓冲区i中的信息复制到接收区b;
    b.size:=i.size;
    b.text:=i.text;
  end 




3.管道通信

所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。写进程以字符流形式将大量的数据送入管道;而读进程则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。


为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
(1) 互斥,即当一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待。
(2) 同步,指当写(输入)进程把一定数量(如4 KB)的数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
(3) 确定对方是否存在,只有确定了对方已存在时,才能进行通信。 

三种通信方式的比较:






线程


线程的基本概念

1.线程的引入

如果说,在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。为了说明这一点,我们首先来回顾进程的两个基本属性:① 进程是一个可拥有资源的独立单位;② 进程同时又是一个可独立调度和分派的基本单位。正是由于进程有这两个基本属性,才使之成为一个能独立运行的基本单位,从而也就构成了进程并发执行的基础。然而,为使程序能并发执行,系统还必须进行以下的一系列操作。 

1) 创建进程

系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB。

2) 撤消进程

系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB。

3) 进程切换

对进程进行切换时,由于要保留当前进程的CPU环境和设置新选中进程的CPU环境,因而须花费不少的处理机时间。

换言之,由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。正因如此,在系统中所设置的进程,其数目不宜过多,进程切换的频率也不宜过高,这也就限制了并发程度的进一步提高。 
如何能使多个程序更好地并发执行同时又尽量减少系统的开销,已成为近年来设计操作系统时所追求的重要目标。有不少研究操作系统的学者们想到,若能将进程的上述两个属性分开,由操作系统分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以做到“轻装上阵”;而对于拥有资源的基本单位,又不对之进行频繁的切换。正是在这种思想的指导下,形成了线程的概念。 


2.线程与进程的比较

1)调度

在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位,把传统进程的两个属性分开,使线程基本上不拥有资源,这样线程便能轻装前进,从而可显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。

2) 并发性

在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。例如,在一个未引入线程的单CPU操作系统中,若仅设置一个文件服务进程,当该进程由于某种原因而被阻塞时,便没有其它的文件服务进程来提供服务。在引入线程的操作系统中,则可以在一个文件服务进程中设置多个服务线程。当第一个线程等待时,文件服务进程中的第二个线程可以继续运行,以提供文件服务;当第二个线程阻塞时,则可由第三个继续执行,提供服务。显然,这样的方法可以显著地提高文件服务的质量和系统的吞吐量。

3) 拥有资源

不论是传统的操作系统,还是引入了线程的操作系统,进程都可以拥有资源,是系统中拥有资源的一个基本单位。一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源,如已打开的文件、I/O设备等,可以供该进程中的所有线程所共享。 

4) 系统开销

在创建或撤消进程时,系统都要为之创建和回收进程控制块,分配或回收资源,如内存空间和I/O设备等,操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地,在进程切换时,涉及到当前进程CPU环境的保存及新被调度运行进程的CPU环境的设置,而线程的切换则仅需保存和设置少量寄存器内容,不涉及存储器管理方面的操作,所以就切换代价而言,进程也是远高于线程的。此外,由于一个进程中的多个线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易。在一些操作系统中,线程的切换、同步和通信都无须操作系统内核的干预。 

3.线程的属性

在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有下述属性。

(1) 轻型实体。

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、 能保证其独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器,保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

(2) 独立调度和分派的基本单位。

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。

(3) 可并发执行。

在一个进程中的多个线程之间可以并发执行,甚至允许在一个进程中的所有线程都能并发执行;同样,不同进程中的线程也能并发执行。

(4) 共享进程资源。

在同一进程中的各个线程都可以共享该进程所拥有的资源,这首先表现在所有线程都具有相同的地址空间(进程的地址空间)。这意味着线程可以访问该地址空间中的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。

4.线程的状态

(1) 状态参数。

在OS中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项:① 寄存器状态,它包括程序计数器PC和堆栈指针中的内容;② 堆栈,在堆栈中通常保存有局部变量和返回地址;③ 线程运行状态,用于描述线程正处于何种运行状态;④ 优先级,描述线程执行的优先程度;⑤ 线程专有存储器,用于保存线程自己的局部变量拷贝;⑥ 信号屏蔽,即对某些信号加以屏蔽。 

(2) 线程运行状态。

如同传统的进程一样,在各线程之间也存在着共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。相应地,线程在运行时也具有下述三种基本状态: ① 执行状态,表示线程正获得处理机而运行;② 就绪状态,指线程已具备了各种执行条件,一旦获得CPU便可执行的状态;③ 阻塞状态,指线程在执行中因某事件而受阻,处于暂停执行时的状态。 

5.线程的创建和终止

在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”。它可根据需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。 
如同进程一样,线程也是具有生命期的。终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。但有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。在大多数的OS中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用。
虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。 

线程间的同步和通信

1.互斥锁(mutex)

互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁lock操作用于将mutex关上,开锁操作unlock则用于打开mutex。 
当一个线程需要读/写一个共享数据段时,线程首先应为该数据段所设置的mutex执行关锁命令。命令首先判别mutex的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;而如果mutex是处于开锁状态,则将mutex关上后便去读/写该数据段。在线程完成对数据的读/写后,必须再发出开锁命令将mutex打开,同时还须将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待mutex打开的队列上。
另外,为了减少线程被阻塞的机会,在有的系统中还提供了一种用于mutex上的操作命令Trylock。当一个线程在利用Trylock命令去访问mutex时,若mutex处于开锁状态,Trylock将返回一个指示成功的状态码;反之,若mutex处于关锁状态,则Trylock并不会阻塞该线程,而只是返回一个指示操作失败的状态码。

2.条件变量

在许多情况下,只利用mutex来实现互斥访问可能会引起死锁,我们通过一个例子来说明这一点。有一个线程在对mutex 1执行关锁操作成功后,便进入一临界区C,若在临界区内该线程又须访问某个临界资源R,同样也为R设置另一互斥锁mutex 2。假如资源R此时正处于忙碌状态,线程在对mutex 2执行关锁操作后必将被阻塞,这样将使mutex 1一直保持关锁状态;如果保持了资源R的线程也要求进入临界区C,但由于mutex 1一直保持关锁状态而无法进入临界区,这样便形成了死锁。为了解决这个问题便引入了条件变量。
每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。
现在,我们看看如何利用互斥锁和条件变量来实现对资源R的访问。线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源R正处于忙碌状态,线程便转为等待状态,并对mutex执行开锁操作后,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex执行开锁操作。下面给出了对上述资源的申请(左半部分)和释放(右半部分)操作的描述。 
Lock mutex               Lock mutex
  check data structures;               mark resource as free;
  while(resource busy);             unlock mutex;
   wait(condition variable);     wakeup(condition variable);
  mark resource as busy;
     unlock mutex;
 
原来占有资源R的线程在使用完该资源后,便按照右半部分的描述释放该资源,其中的wakeup(condition variable)表示去唤醒在指定条件变量上等待的一个或多个线程。在大多数情况下,由于所释放的是临界资源,此时所唤醒的只能是在条件变量上等待的某一个线程,其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件,该文件允许多个线程同时对它执行读操作。在这种情况下,当一个写线程完成写操作并释放该文件后,如果此时在该条件变量上还有多个读线程在等待,则该线程可以唤醒所有的等待线程。

3.信号量机制

1)私用信号量

当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。私用信号量属于特定的进程所有,OS并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为0(空),也不能将它传送给下一个请求它的线程。

2)公用信号量

公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则OS会自动将该信号量空间回收,并通知下一进程。可见,公用信号量是一种比较安全的同步机制。 

线程的实现方式

1.内核支持线程

对于通常的进程,无论是系统进程还是用户进程,进程的创建、 撤消,以及要求由系统设备完成的I/O操作,都是利用系统调用而进入内核,再由内核中的相应处理程序予以完成的。进程的切换同样是在内核的支持下实现的。因此我们说,不论什么进程,它们都是在操作系统内核的支持下运行的,是与内核紧密相关的。 
这里所谓的内核支持线程KST(Kernel Supported Threads),也都同样是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等也是依靠内核,在内核空间实现的。此外,在内核空间还为每一个内核支持线程设置了一个线程控制块,内核是根据该控制块而感知某线程的存在,并对其加以控制。

2.用户级线程

用户级线程ULT(User Level Threads)仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。可见,这种线程是与内核无关的。我们可以为一个应用程序建立多个用户级线程。在一个系统中的用户级线程的数目可以达到数百个至数千个。由于这些线程的任务控制块都是设置在用户空间,而线程所执行的操作也无须内核的帮助,因而内核完全不知道用户级线程的存在。
值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程而言似乎是公平的。但假如在进程A中包含了一个用户级线程,而在另一个进程B中含有100个用户级线程,这样,进程A中线程的运行时间将是进程B中各线程运行时间的100倍;相应地,其速度要快上100倍。
假如系统中设置的是内核支持线程,则调度便是以线程为单位进行的。在采用轮转法调度时,是各个线程轮流执行一个时间片。同样假定进程A中只有一个内核支持线程,而在进程B中有100个内核支持线程。此时进程B可以获得的CPU时间是进程A的100倍,且进程B可使100个系统调用并发工作。 



原创粉丝点击