操作系统原理 第二章 进程管理(2)

2.4 经典进程的同步问题

2.4.2 哲学家进餐问题

1.利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。其描述如下:

Var chopstick: array[0, …, 4] of semaphore;
所有信号量均被初始化为1, 第i位哲学家的活动可描述为:     
      repeat
   	 wait(chopstick[i]);
    	wait(chopstick[(i+1) mod 5]);
      		…
   	 eat;
                             …
   	 signal(chopstick[i]);
   	 signal(chopstick[(i+1) mod 5]);
     	            …
   	 think;
  until false; 

可采取以下几种解决方法:
(1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2) 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
(3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、 2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
2.利用AND
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本质上就是前面所介绍的AND同步问题,故用AND信号量机制可获得最简洁的解法。

Var chopsiick array [0, …, 4] of semaphore∶ =(1,1,1,1,1);
processi
repeat
think;
Sswait(chopstick[(i+1) mod 5], chopstick [i]);
eat;
Ssignat(chopstick [(i+1) mod 5], chopstick [i]);
until false;

2.4.3 读者-写者问题

1.利用记录型信号量解决读者-写者问题
为实现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。

读者-写者问题可描述如下:

Var rmutex, wmutex:semaphore∶   =1,1;
    Readcount:integer∶   =0;
    begin
    parbegin
     Reader:begin
        repeat
         wait(rmutex);
         if readcount=0 then wait(wmutex);
          Readcount∶   =Readcount+1;
         signal(rmutex);
           …
         perform read operation;
           …
wait(rmutex);
         readcount∶   =readcount-1;
         if readcount=0 then signal(wmutex);
         signal(rmutex);
        until false;
       end
    writer:begin
        repeat
         wait(wmutex);
         perform write operation;
         signal(wmutex);
        until false;
       end
    parend
  end

2.利用信号量集机制解决读者-写者问题

Var RN integer;
L, mx:semaphore∶ =RN,1;
begin
parbegin
reader:begin
repeat
Swait(L,1,1);
Swait(mx,1,0);

perform read operation;

Ssignal(L,1);
until false;
end
writer:begin
repeat
Swait(mx,1,1; L,RN,0);
perform write operation;
Ssignal(mx,1);
until false;
end
parend
end

2.6 进程通信

2.6.1 进程通信的类型

1.共享存储器系统

  • 基于共享数据结构的通信方式
  • 基于共享存储区的通信方式

2.消息传递系统
不论是单机系统、多机系统,还是计算机网络,消息传递机制都是用得最广泛的一种进程间通信的机制。在消息传递系统中,进程间的数据交换,是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。程序员直接利用系统提供的一组通信命令(原语)进行通信。操作系统隐藏了通信的实现细节,大大减化了通信程序编制的复杂性,而获得广泛的应用。消息传递系统的通信方式属于高级通信方式。又因其实现方式的不同而进一步分成直接通信方式和间接通信方式两种。

3.管道(Pipe)通信
所谓“管道”,是指用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程), 以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。这种方式首创于UNIX系统,由于它能有效地传送大量数据,因而又被引入到许多其它操作系统中。

为了协调双方的通信,管道机制必须提供以下三方面的协调能力:

  1. 互斥,即当一个进程正在对pipe执行读/写操作时,其它进程必须等待。
  2. 同步,指当写进程把一定数量的数据写入pipe,便去睡眠等待,知道读进程取走数据后,再把他唤醒。当读进程读到一空pipe时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
  3. 确定对方是否存在,只有确定了对方已存在时,才能进行通信。

2.6.2 消息传递通信的实现方法

1.直接通信方式
这是指发送进程利用OS所提供的发送命令,直接把消息发送给目标进程。此时,要求发送进程和接收进程都以显式方式提供对方的标识符。通常,系统提供下述两条通信命令(原语):
Send(Receiver, message); 发送一个消息给接收进程;
Receive(Sender, message); 接收Sender发来的消息;
例如,原语Send(P2, m1)表示将消息m1发送给接收进程P2; 而原语Receive(P1,m1)则表示接收由P1发来的消息m1。

在某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程。例如,用于提供打印服务的进程,它可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程接收消息的原语中的源进程参数,是完成通信后的返回值,接收原语可表示为:
Receive (id, message);

我们还可以利用直接通信原语,来解决生产者-消费者问题。当生产者生产出一个产品(消息)后,便用Send原语将消息发送给消费者进程;而消费者进程则利用Receive原语来得到一个消息。如果消息尚未生产出来,消费者必须等待,直至生产者进程将消息发送过来。生产者-消费者的通信过程可分别描述如下:

repeat
      …
    produce an item in nextp;
      …
    send(consumer, nextp);
   until false;
   repeat
    receive(producer, nextc);
      …
    consume the item in nextc;
  until false; 

2.间接通信方式
(1) 信箱的创建和撤消。
进程可利用信箱创建原语来建立一个新信箱。创建者进程应给出信箱名字、信箱属性(公用、私用或共享);对于共享信箱, 还应给出共享者的名字。当进程不再需要读信箱时,可用信箱撤消原语将之撤消。
(2) 消息的发送和接收。
当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下述通信原语进行通信。
Send(mailbox, message); 将一个消息发送到指定信箱;
Receive(mailbox, message); 从指定信箱中接收一个消息;

信箱可由操作系统创建,也可由用户进程创建,创建者是信箱的拥有者。据此,可把信箱分为以下三类。

  • 私用信箱
    用户进程可为自己建立一个新信箱,并作为该进程的一部分。信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中。这种私用信箱可采用单向通信链路的信箱来实现。 当拥有该信箱的进程结束时,信箱也随之消失。
  • 公用信箱
    它由操作系统创建,并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该信箱中,也可从信箱中读取发送给自己的消息。显然,公用信箱应采用双向通信链路的信箱来实现。通常,公用信箱在系统运行期间始终存在。
  • 共享信箱
    它由某进程创建,在创建时或创建后,指明它是可共享的,同时须指出共享进程(用户)的名字。信箱的拥有者和共享者,都有权从信箱中取走发送给自己的消息。

利用信箱通信时,在发送进程和接收进程之间,存在以下四种关系:

1.一对一关系。这是可为发送进程和接收进程建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。
2.多对一关系。允许提供服务的进程与多个用户进程之间进行交互,也称为客户/服务器交互。
3.一对多关系。允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方式,向接受者(多个)发送消息。
4.多对多关系。允许建立一个公用信箱,让多个进程都能向信箱中投递消息;也可从信箱中取走属于自己的信息。

2.6.3 消息传递系统实现中的若干问题

1.通信链路
为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。有两种方式建立通信链路。第一种方式是:由发送进程在通信之前,用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路;在链路使用完后,也用显式方式拆除链路。
这种方式主要用于计算机网络中。第二种方式是发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路。这种方式主要用于单机系统中。
根据通信链路的连接方法,又可把通信链路分为两类: ① 点—点连接通信链路,这时的一条链路只连接两个结点(进程);② 多点连接链路,指用一条链路连接多个(n>2)结点(进程)。而根据通信方式的不同,则又可把链路分成两种: ① 单向通信链路,只允许发送进程向接收进程发送消息; ② 双向链路,既允许由进程A向进程B发送消息,也允许进程B同时向进程A发送消息。

2.消息的格式
在某些OS中,消息是采用比较短的定长消息格式,这减少了对消息的处理和存储开销。这种方式可用于办公自动化系统中,为用户提供快速的便笺式通信;但这对要发送较长消息的用户是不方便的。在有的OS中,采用另一种变长的消息格式,即进程所发送消息的长度是可变的。系统在处理和存储变长消息时,须付出更多的开销,但方便了用户。 这两种消息格式各有其优缺点,故在很多系统(包括计算机网络)中,是同时都用的。

3.进程同步方式

  1. 发送进程阻塞、接收进程阻塞。
  2. 发送进程不阻塞、接收进程阻塞。
  3. 发送进程和接收进程均不阻塞。

2.6.4 消息缓冲队列通信机制

1.消息缓冲队列通信机制中的数据结构

  1. 消息缓冲区。在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区。
  2. PCB中有关通信的数据项。在利用消息缓冲队列通信机制时,在设置消息缓冲队列的同时,还应增加用于对消息队列进行操作和实现同步的信号量,并将它们置入进程的PCB中。

2.发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间,设置一发送区a,见图所示,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区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.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 

3.接收原语


接收原语描述如下:
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

2.7 线程

2.7.1 线程的基本概念

1.进程操作
为使程序能并发执行,系统还必须进行一下的一些列操作。

  • 创建进程
  • 撤销进程
  • 进程切换

2.线程的属性

  • 轻型实体
  • 独立调度和分派的基本单位
  • 可并发执行
  • 共享进程资源

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

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

4.线程的创建和终止
在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”。它可根据需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。
终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。

5.多线程OS中的操作
在多线程OS中,进程是作为拥有系统资源的基本单位,通常的进程都包含多个线程并为它们提供资源,但此时的进程就不再作为一个执行的实体。 多线程OS中的进程有以下属性:

  • 作为系统资源分配的单位
  • 可包括多个线程
  • 进程不是一个可执行的实体

2.7.2 线程间的同步和通信

1.互斥锁(mutex)
互斥锁是一种比较简单的、用于实现进程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开锁都较低, 因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态, 即开锁(unlock)和关锁(lock)状态。 相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁lock操作用于将mutex关上,开锁操作unlock则用于打开mutex。

2.条件变量
每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待, 直至所等待的资源成为可用的。
线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述资源状态的数据结构,以了解资源的情况。 只要发现所需资源R正处于忙碌状态,线程便转为等待状态, 并对mutex执行开锁操作后,等待该资源被释放; 若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex执行开锁操作。

在这里插入图片描述
3.信号量机制

  1. 私用信号量
    当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构是存放在应用程序的地址空间中。私用信号量属于特定的进程所有,OS并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为0(空), 也不能将它传送给下一个请求它的线程。
  2. 公用信号量
    公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则OS会自动将该信号量空间回收,并通知下一进程。可见,公用信号量是一种比较安全的同步机制。

2.7.3 内核支持线程和用户级线程

1.内核支持线程
这里所谓的内核支持线程,也都同样是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等,也是依靠内核实现的。此外,在内核空间还为每一个内核支持线程设置了一个线程控制块, 内核是根据该控制块而感知某线程的存在的,并对其加以控制。
2.用户级线程
用户级线程仅存在于用户空间中。对于这种线程的创建、 撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。可见,这种线程是与内核无关的。

2.7.4 线程控制

1.内核支持线程的实现
在这里插入图片描述
2.用户级线程的实现

  1. 运行时系统(Runtime System)
    所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合, 其中包括用于创建和撤消线程的函数、 线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
  2. 内核控制线程
    这种线程又称为轻型进程LWP(Light Weight Process)。 每一个进程都可拥有多个LWP, 同用户级线程一样, 每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先级、 状态, 另外还有栈和局部存储区等。 它们也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。
    利用轻型进程作为中间系统
发布了107 篇原创文章 · 获赞 68 · 访问量 7756

猜你喜欢

转载自blog.csdn.net/weixin_43092232/article/details/105245669