操作系统之进程篇(2)

进程间通信(InterProcess Communication,IPC):

进程通信中遇到的三个问题:

a) 进程之间如何进行信息的传递?

b) 多个进程在执行自己的核心代码时如何能够不相互影响?

c) 当进程之间出现相互依赖关系时,如何才能合理的调度进程的执行顺序!

1. 竞争情形:

当两个或多个进程同时读写某个共享资源的时候,程序运行的最终结果由各个进程的具体执行的情况所决定!

如何避免竞争情形的出现,那么我们首先引入关键代码区的定义:

程序中访问共享内存或其他共享资源的代码区被称为关键代码区!
如果我们可以保证在每一个时刻都没有多个

进程处于自己的关键代码区,那么我们当然就可以避免竞争情形的出现.

但为了能使并行的进程正确的进行合作并高效的利用共享资源,那么下面的四个条件应该的到满足:

  • 没有两个进程会同时出现在它们的代码区中;

  • 没有对CPU数量和速度的假设;

  • 运行在关键代码区之外的进程不能阻塞其他进程;

  • 没有进程可以永远等待进入其关键代码段中;
 2. Mutual
Exclusion With Busy Waiting:

2.1 停止中断:

实现进程之间互斥的最简单的一种方法就是:当一个进程进入自己的关键代码段之后,关闭所有的中断功能,当程序离开关键代码段之后,恢复这些中断功能。

显然这种方法是一个不好的方法,因为赋予用户进程关闭中断的能力并不是一件安全的事情,如果一个进程关闭中断之后不在打开这些中断,那么系统将会崩溃.

2.2 锁变量:

作为一种尝试,下面用一种软件解决方案:

设置单个,共享的锁变量,初始化为0,表示没有进程进入关键代码段。当一个进程要进去自己的关键代码段是,首先对锁变量进行测试,如果变量为0表示当前没有

其他进程在其关键代码段中,那么该进程进入关键代码段中,并将锁变量置为1。进程执行完关键代码段之后,将锁变量恢复成0.

但这个方法同样存在缺陷,当一个进程a检测到锁变量的值是0的时候,表明没有其他进程在其关键代码段中,在其将锁变量置为1之前,可能另外的一个进程b也检测到

锁变量是0,然后进程b被调度,执行,并将锁变量置为1。当进程a回来将锁变量重新置为1后,进程a同样也进入了关键代码区。这样冲突在所难免!

上述现象的根本原因是无法保证对锁变量的原子访问!

2.3 strict alternation:


while (TRUE) {

while (turn != 0)

; /* wait  */

critical_section();
turn = 1;
non_critical_section();
}
while (TRUE) {
while (turn != 1)

; /* wait */

critical_section();
turn = 0;
non_critical_section();
}

原理是用一个turn变量控制每个进程对关键代码段的访问,这种take
turns的方式在一个进程的速度比另外一个进程缓慢的时候效果并不好。

运行在关键代码区之外的进程不能阻塞其他进程!

2.4 Peterson‘s Solution:

Peterson算法是一个实现互斥锁并发程序设计算法,可以控制两个进程访问一个共享的单用户资源而不发生访问冲突。

算法使用两个控制变量flagturn.
其中flag[n]的值为真,表示ID号为n的进程希望进入该临界区.
标量turn保存有权访问共享资源的进程的ID号.

//flag[] is boolean array; and turn is an integer

flag[0]   = false;
flag[1]   = false;
turn;

P0: flag[0] = true;
    turn = 1;
    while (flag[1] == true && turn == 1)
    {
        // busy wait
    }
    // critical section
    flag[0] = false;
    // end of critical section

P1: flag[1] = true;
    turn = 0;
    while (flag[0] == true && turn == 0)
    {
        // busy wait
    }
    // critical section
    flag[1] = false;
    // end of critical section

上面的算法最复杂的情形莫过于两个进程同时要求进入关键代码段!还是将上面的代码修改一下为好:


1 #define FALSE 0
 2 #define TRUE  1
 3 #define N     2          /* numbers of process */
 4  
 5 int turn;                /* whose turn is it? */
 6 int interested[N];       /* all values initially 0 */
 7 
 8 void enter_region(int process)  /* process 0 or 1 */
 9 {
10     int other;                  /* the number of the other process */
11     
12     other = 1 - process;        /* the opposite process */
13 
14     interested[process] = TRUE;
15     turn = process; 
16     if(turn == process && interested[other] == TRUE)  /* null statement */; 
17 }
18 
19 void leave_region(int process)
20 {
21    interested[process] = FALSE;   
22 }

当process 1和process
2同时运行enter_region函数时,两个进程将interested[0,1]都设为1,但turn只能被设置成0或1,取决与执行turn = process;

语句的先后顺序,比如process 0先执行这条语句,那么最终turn将变成1,那么在process 1中将执行空循环,这样process
0就可以进入自己的关键代码段了!

2.5 The TSL Instuction

TSL(Test and Set Lock): 检查并设置;

许多计算机有这样的TSL指令,这条指令读取内存中某个word的内容进入寄存器之中,并在内存中的这个地址上设置一个非0值!

这样的一个测试并设置的过程是一个不可分割的原子操作!


1 enter_region:
2    tsl register, lock   | copy lock to register and set lock to 1
3    cmp register, #1     | was lock zero ?
4    jne enter_region     | if it was non zero, lock was set, so loop
5    ret                  | return to caller, critical region entered

7 leave_region:
8     move lock,#0        | store a 0 in lock
9     ret                 | return to caller

使用TSL方法的原其实是和锁变量的方法是相同的,只是在TSL方法用硬件手段为锁变量的访问和修改加上了原子性!

一个进程应该合理调用enter_region和leave_region。

3. 进程的睡眠和唤醒:

上述的方法都存在一个致命问题--busy waiting! 并有可能会出现priority inversion
problem(权限倒置问题: 权限高的进程反而得不到想要的资源)。

Priority inversion is a problem, not a solution. The typical example
is
a
low priority process acquiring a resource that a high priority process
needs
,
and then being preempted by a medium priority process, so the high priority
process is blocked on the resource while the medium priority one finishes
(effectively being executed with a lower priority)

3.2 生产者--消费者 问题:


 1 #define N 100    /* number of slots in the buffer */

2 int count = 0;   /* the initial number of items in  the buffer */
 3 
 4 void producer(void)
 5 {
 6    while(TRUE)   /* repeated forever */
 7    {
 8       produce_item();         /* generate next item */
 9       if(count == N) sleep(); /* if has no buffer slot, go to sleep */
10       enter_item();           /* put item in buffer */
11       count = count + 1;      /* increment count of items in buffer */
12       if(count == 1) wakeup(consumer);
13 /* if buffer is not empty! wake up the consumer */
14    }        
15 }
16 
17 void consumer(void)
18 {
19   while(TRUE)
20   {
21      if(count == 0) sleep();  /* if the buffer is empty, go to sleep */
22      remove_item();           /* take item out of buffer */
23      count = count - 1;       /* decrement count of items in buffer */
24      if(count == N - 1) wakeup(producer); /* if the buffer is not full, then wake up the producer */
25      consume_item();          /* print item */
26   }   
27 }

上面的生产者消费者问题可能出现race condition!由于生产者和消费者对count的访问是没有限制的,考虑如下情形:

  • 开始时buffer为空,consumer读取count发现为0,这时调度器决定终止consumer进程的执行(注意,这是调度器的安排,consumer并没有进入sleep状态)

  • producer进程执行,在buffer中放置一件产品,将count加1,然后向consumer进程发送一个wakeup信号。

  • 但由于consumer进程并不是处于asleep状态的,所以consumer进程丢弃了刚才这个wakeup信号!

  • cosumer再次运行,看到上次读取的count值0,然后进入sleep状态!

  • 迟早producer将填满整个buffer,这样两个进程都进入sleep状态!

3.3 信号量机制:

Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。

以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。
当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。

以上内容来自百度百科!

使用信号量机制来解决生产者消费者问题:


1 #define N 100   /* number of slots in the buffer */

2 typedef int semaphore; /* semaphores are a special kind of int */
 3 semaphore mutex = 1;   /* controls access to critical section */
 4 semaphore empty = N;   /* controls empty buffer slots */
 5 semaphore full = 0;    /* controls full buffer slots */
 6 
 7 void producer(void)
 8 {
 9    int item;
10    while(TRUE)
11    {
12       produce_item(&item);  /* generate something to put in buffer */
13       down(&empty);
14       down(&mutex);
15       enter_item(item);
16       up(&mutex);
17       up(&full);
18    }   
19 }
20 
21 void consumer(void)
22 {
23    int item;
24    while(TRUE)
25    {
26       down(&full);
27       down(&mutex);
28       remove_item(&item);
29       up(&mutex);
30       up(&empty);
31       consume_item(&item);
32    }       
33 }

这里分别设置了三个信号量,mutex信号量用来控制对buffer的互斥访问,full和empty用来进行buffer状态计数。

这里mutex信号量为互斥信号量,用来保证多个进程对资源的互斥访问,而full和empty是同步信号量,负责进程之间的同步工作!

操作系统之进程篇(2),布布扣,bubuko.com

时间: 2024-10-13 11:48:47

操作系统之进程篇(2)的相关文章

操作系统之进程篇(1)

1.进程介绍: 1.1 进程模型: 进程是一个程序的实际执行,包含了程序计数器的状态,寄存器和变量等等! 程序可以看成是一个状态的序列,程序在不同时刻呈现出不同的状态,而这种状态的前后交替过程可以看成是程序的执行过程.概念上来说,每个程序有自己的虚拟CPU,但在现实中CPU在不同的进程间来回切换,又称这种切换为伪并行! 进程和程序差别看似微小,实际上却是十分精妙; 可以将计算机执行程序的过程看成一次有趣的烹饪过程.食谱就是程序,厨师就是CPU,而食材是输入,得到的输出是鲜美可口的美食. 当厨师在

操作系统之进程篇(3)

1. 信号量机制的缺陷问题: 在上面的生产者消费者实例中,信号量的工作机制如下(我们以生产者的代码为例): 1 down(&empty); 2 down(&mutex); 3 enter_item(item); 4 up(&mutex); 5 up(&full); 如果交换1号和2号语句,变成: 1 down(&mutex); 2 down(&empty); 那么可能会出现下面的情形: mutex变成0,此时empty == 0,那么生产者阻塞; 此时消费者

操作系统之进程篇(4)--经典进程间通信(IPC)问题

1. 哲学家进餐问题: 问题描述: 五个哲学家在一个圆桌上进餐,每人的面前放了一盘意大利面,两个盘子之间有一个叉子,但是由于盘子里面的面条十分光滑,需要两个叉子才能进行就餐行为.餐桌的布局如下图所示: 假设哲学家的生活中只有两个活动:吃饭和思考[吃饭维持自身之生存,思考探究生存之意义],当然这样的哲学家在现实之中是不存在的.当一个哲学家在殚精竭虑之时,饥饿感随之而来,这是他会拿起左右手边的两个叉子来想享用这俗世之中的美味.酒足饭饱之后,又"躲进小楼成一统,管他春夏与秋冬"去了.问题是:

操作系统之进程篇1

1.进程的出现,让我们需要对进程进行分离存储,而有了内存管理:需要不同进程有条不紊的往前推进而有了进程调度. 2.为什么要有进程?什么是进程? 为了实现程序的并发执行,我们发明了进程.一个程序加载到内存后就变成了进程. 3.注意不是所有进程都一定要终结,实际上,许多系统进程是不会终结的,除非强制终止或关闭计算机. 4.什么时间造成进程的产生? 1)系统初始化:在一个系统初始化时,将有许多进程产生,产生的这些进程是系统正常运行必不可少的. 2)执行进程创立程序:如双击了一个可执行文件. 3)用户请

操作系统之进程篇2

1.进程调度主要要解决的问题是什么? 任意时刻到底由哪个进程执行,那些不执行.正在进展中的程序使用CPU的模式有3种:程序大部分时间在CPU上执行(CPU导向,又称计算密集型程序):程序大部分时间在进行输入输出(I/O导向,又称输入输出密集型程序):程序介入前两种模式之间(平衡型程序). 2.进程调度的目标? 达到极小化平均响应时间,极大化系统吞吐率,保持系统各个功能部件均处于繁忙的状态,并提供某种貌似公平的机制. 3.调度算法有哪些?各算法的利弊? 1)先来先服务FCFS(First Come

进程篇(1: 进程运行环境)--请参照本博客“操作系统”专栏

2014年5月30日  下午1:40:59 1. Unix 进程执行环境: 1.1 终止处理程序: ISO C 规定,一个程序可以登记多达32个函数,这些函数将由exit自动调用.我们称这些函数为终止处理程序(exit handler),并调用atexit函数来登记这些函数.该函数的原型如下: 1 #include <stdlib.h>2 3 int atexit(void (*function)(void)); exit调用这些终止程序的顺序与他们登记时的顺序相反(先登记后调用).同一个函数

进程篇(4: 基本进程控制:其他相关控制)--请参照本博客“操作系统”专栏

1. 更改进程的用户ID和组ID:为什么我们要更改用户ID和组ID的呢? 在UNIX系统中,特权是基于用户和组ID的.当用户需要增加特权,或要访问某个当前没有能力访问的文件时,我们需要更改自己的权限,以让新的ID具有合适的特权或访问权限.与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要跟换用户ID或组ID;一般而言,在设计应用程序时,我们总是试图使用"最小特权"模型.依照此模型,我们的程序应当值具有为完成特定的任务所需要的最小特权. NAME getuid, geteui

进程篇(2: C程序的存储空间布局)--请参照本博客“操作系统”专栏

1.  C程序的存储空间布局: C 程序由下面几个部分组成: 正文段(即是代码段): 这是由CPU执行的机器指令部分.通常,正文段是可以共享的,并常常是可读的,以防止程序因为意外原因而修改自身的代码! 初始化数据段(即数据段): 它包含了程序中需要明确的赋初值的变量. 非初始化数据段(bss段):在程序开始执行之前,内核将此段中的数据初始化为0或空指针. 栈.自动变量以及每次函数调用时所需保存的信息都存放在此段中.每次调用函数时,返回地址以及调用者的环境信息(如某些寄存器的值)都存放在栈中.然后

进程篇(3: 基本进程控制:进程的退出)--请参照本博客“操作系统”专栏

1. exit函数: 进程的五种正常的结束方式: 在main函数中执行return语句,这等效于exit; 调用exit函数.此函数由ISO C定义,其操作包括运行各终止处理程序,然后关闭所有标准I/O流等. 调用_exit或_Exit函数,ISO C定义了_Exit函数,目的是为了为进程提供一种无需运行终止处理程序和信号处理程序而终止的方法.并不处理标准I/O流! 进程的最后一个线程在其启动例程中执行返回语句,然后该进程以终止状态0返回. 进程的最后一个线程调用pthread_exit函数.