2021-05-11 现代操作系统 《现代操作系统 第4版》第2章 进程与线程——总结3(进程/线程间通信)

  • 多进程
    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
    Linux系统函数fork()可以在父进程中创建一个子进程,这样的话,在一个进程接到来自客户端新的请求时就可以复制出一个子进程让其来处理,父进程只需负责监控请求的到来,然后创建子进程让其去处理,这样就能做到并发处理。

  • 多线程
    线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

  • 线程和进程各自有什么区别和优劣呢?
    1、进程是资源分配的最小单位,线程是程序执行的最小单位。
    2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
    3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
    4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

  • 进程间通信:进程间通信的三个问题:
    1、一个进程如何把信息传递给另一个(线程好解决,因为同一个进程的线程共享一个地址空间)
    2、保证两个或者多个进程在涉及临界活动时不会彼此影响
    3、当存在依赖关系时确定适当次序(比如A进程产生数据,B进程打印数据,则B在打印之前需要等待)

  • 竞争条件
    在一些操作系统中,协作的进程可能共享一些彼此都能读写的公用存储区。两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序(比如说当两个进程A,B同时向共享内存中存储文件,首先各自查询能存储的槽位,当A找到槽位i能存储以后,被中断了,由另一个进程进行存储,也存在了槽位i中,执行完后调回到进程A,这时候就发生了覆盖),称为竞争条件。
    临界区
    如何避免部分条件?实际上凡涉及到共享内存、共享资源的情况都会引发竞争,要避免出现错误,必须对共享资源互斥访问。
    1、任何两个进程不能同时处于临界区
    2、不应对CPU的速度和数目做任何假设
    3、临界区外的进程不得阻塞其他进程(严格轮换发的缺点,当该进程执行完,另一个进程才能访问临界区)
    4、不得使进程在临界区外无休止等待

  • 忙等待的互斥
    1、屏蔽中断
    最简单的解决方案是使每个进程进入临界区后先屏蔽所有中断,在离开之前再打开中断。中断被关闭后,时钟中断也被屏蔽。CPU只有在发生时钟或其它中断时才会切换,因此关中断后CPU不会切换到其他进程。
    优点:屏蔽中断对于操作系统本身而言是一项很有用的技术。
    缺点:只是在单核中比较好用,屏蔽中断仅仅局限于其中一个单核,别的核同样会访问。其次屏蔽中断可能导致”竞争条件(比如就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件)。
    2、锁变量
    设想一个共享(锁)变量,初值为0,当一个进程想进入其临界区时,它首先测试这把锁,如果锁的值为0,则进程将其置为1并进入临界区。若锁已经为1,则进程将一直等待到值变成0。然后,这种做法由于锁本身就是共享内存,依然有可能发生竞争。
    问题:当将其设置位1之前,另一个进程被调度运行,将该锁变量设置位为1,这时候就会有两个进程进入临界区中。
    3、严格轮换法:
    通过一个程序看:
    在这里插入图片描述
    持续地检测一个变量直到它具有某一特定值就称为忙等待,忙等待是应该避免的,因为它会浪费CPU时间。一个适用忙等待的锁称为自旋锁
    如上程序,两个进程会严格交替进入临界区,当进程B的noncritical需要很长时间执行的时候,进程A需要等待B完成了才能继续,这违反了一开始的条件3(临界区外的进程不得阻塞其他进程),也不是一个很好的方案。
    缺点:当一个进程比另一个慢了很多的情况下,轮流进入临界区并不是一个好办法。
    4、Peterson解决方案 :(将锁变量思想与严格轮换发的思想相互结合,感觉就是进一步缩小了锁的范围)(缺点:忙等待)

#define FALSE   0
#define TRUE    1
#define N       2                       /* 进程数量 */

int turn;                               /* 现在轮到谁? */
int interested[N];                      /* 所有值初始化为0(FALSE) */

void enter_region(int process)          /* 进程是0或1 */
{
    int other;                          /* 其他进程号 */

    other = 1 - process;                /* 另一方进程的值,通过1-使他们分开 */
    interested[process] = TRUE;         /* 表明所感兴趣的 */
    turn = process;                     /* 设置标志 */
    while (turn == process && interested[other] == TRUE);   /* 空语句 */
}

void leave_region(int process)          /* 进程:谁离开? */
{
    interested[process] = FALSE;        /* 表示离开临界区 */
}
  • (格式)
    5、TSL指令:(需要硬件支持的一种方案,特别是在多处理器的计算机)(多看一下)
    指令:TSL RX, LOCK
    含义:测试并加锁,他将一个内存字lock读到寄存器RX(每个进程、线程都有)中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的。执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存(这也说明TSL指令和屏蔽中断的不同)。
    具体使用:①、为了使用TSL指令,要使用一个共享变量lock来协调对共享内存的访问。②、lock0时,任何进程都可以使用TSL指令将LOCK设置为“1”,并读写共享内存。③、进程结束后通过move指令将lock0.如图:
    比较:1、与屏蔽中断不同的是,TSL适用于多核 2、与锁变量相比,TSL对LOCK的操作具有原子性,也就是可以用XCHG替换的原因
    在这里插入图片描述
    进一步:使XCHG指令代替TSL,可以原子性的交换两个位置的内容(所有的Intelx86CPU在底层同步中使用XCHG指令,比如CAS指令,unsafe类库底层基本都是XCHG指令。)(mutex_region
    在这里插入图片描述
    缺点:忙等待。

  • 睡眠与唤醒
    1、Peterson解法和TSL或XCHG解法都有忙等待的缺点,这种方法不仅浪费了CPU时间,而且还可能引起预想不到的结果,比如根据线程优先级,低优先级的线程可能会一直处于忙等待(阻塞)。

    2、分析一个典型问题:生产者-消费者问题
    ①、两个进程共享一个公共的固定大小的缓冲区,其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息。
    在这里插入图片描述

  • 信号量 :(很重要)(semaphore
    1、使用一个整型变量来累计唤醒次数
    2、这个整型变量就是信号量,取值可以为0(表示没有保存下来的唤醒操作),也可以为正值(表示有一个或多个唤醒操作)。
    3、两种操作:downup(一般化的sleep,wakeup ,也就是java中的wait,notify)。
    4、down:对信号量执行down操作是检查其值是否大于0,如果是则减1并继续(用掉一个保存的唤醒信号),如果值为0,则进程将睡眠,而此时down操作并未结束。检查数值、改变数值以及可能发生睡眠操作均作为一个单一的、不可分割的原子操作完成(也就是说,信号量的变化,要变就变完,如果中途阻塞,则返回原值)
    5、up:对信号量的值增1,如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中的一个并允许其完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量仍为0,但是其上睡眠的进程却少了一个。递增信号量的值和唤醒一个进程同样也不可分割。(不会因某个进程执行up而阻塞)
    6、信号量可以用来实现同步
    7、通常将"up","down"作为系统调用实现,而且OS只需在以下操作(信号量的相关操作暂时屏蔽全部中断(对单核有效):测试信号量、更新信号量以及在需要时使用某个进程睡眠。这几个动作带来的屏蔽中断没有什么副作用。对于多个CPU每个信号量应由一个锁变量进行保护。通过TSL或XCHG指令来确保同一时刻只有一个CPU在对信号量进行操作(对多核的操作)
    **注意:**使用TSL或XCHG指令来防止几个CPU同时访问一个信号量,这和之前所说的"忙等待"是不一样的,比如生产者-消费者使用忙等待来等待对方腾出或填充缓冲区。信号量操作仅需要几个毫秒,而生产者和消费者则可能需要任意长的时间。mutex是二元信号量(每个进程在进入临界区前都执行一个down操作,并在刚刚退出时执行一个up操作,就能够实现互斥)。
    在这里插入图片描述
    8:信号量谨慎使用:当我们把生产者,消费者的程序中的down(mutex)和down(empty(full))都对调一下,就会发生严重的问题,当没有空槽时,生产者因为进入临界区(mutex0)而因为empty0则执行down陷入阻塞,与此同时消费者down(mutex),因为mutex为0消费者也陷入阻塞,这就造成了“死锁”。

  • 互斥量:(很重要
    1、如果不需要信号量的计数能力,有时可以使用信号量的一个简化版本,称为互斥量(mutex)。
    2、互斥量是一个可以处于两态之一(二进制位,也可以用整型量,0表示解锁,其他加锁)的变量:解锁和加锁。
    3、适用范围:仅仅适用于管理共享资源或一小段代码以及用户空间线程包
    4、例子:结合TSL,XCHG使用互斥量在这里插入图片描述
    在这里插入图片描述

    5、为什么互斥量适合在用户线程包使用?①、TSL指令中的例子mutex_region中的enter_region和mutex_lock的代码很相似,只不过前者的进程进入临界区失败时,它始终重复测试(忙等待),但可以通过时钟超时的作用,会调度其他进程进行进行。缺点就是:忙等待。②、在用户线程中,情形有所不同,没有“时钟超时”,但是!可以调用thread_yield将CPU放弃给另一个线程,这样就没有忙等待(并且thread_yield是在用户空间对线程调度程序的一个调用,所以运行非常快捷,都不需要任何内核的调用)。

  • 一个关于进程临界区的问题
    在Peterson和信号量中,都一个明显的问题,不同的进程有共享的变量?
    解决方法:①、有些共享数据结构,比如信号量,可以存放在内核中,仅通过系统调用来访问。
    ②、多数现代操作系统中,提供一种方法:不同的进程可以共享其部分地址空间。在这个方法中,缓冲区和其他数据结构可以共享。

  • 一些与互斥量相关的pthread调用
    在这里插入图片描述

    • 一些与条件变量相关的pthread调用:
      在这里插入图片描述
  • 管程
    1、一个管程(monitor)是一个由过程、变量及数据结构等组成的一个集合(相当于包含进程),它们组成一个特殊的模块或软件包。进程可在任何需要的时候调用管程中的过程,但他们不能在管程之外声明的过程中直接访问管程内部的数据结构。管程是一种语言概念,C语言不支持。
    2、特性:任一时刻管程中只能有一个活跃的进程,这一个特性能够有效的完成互斥。
    3、用法:将所有的临界区转换成管程过程中,决不会有两个进程同时执行临界区的代码。
    4、管程的特性使管程能实现有效的互斥,但是怎么使进程无法继续进行时被阻塞?引入条件变量(wait:阻塞,调用其他进程 signal:一般作为管程过程的最后一条语句,则就是执行signal的进程必须立即退出管程 )。
    5、管程的wait,signal比sleep,wakeup的优势在于:后者存在严重的竞争条件,但前者通过线程则不会发生这种条件。
    6、但是支持管程的语言不多

  • 生产者-消费者问题
    1、我们之前可以通过信号量,互斥量,管程解决这些访问公共内存的一个或多个CPU上的互斥问题。
    2、另一种方法:消息传递
    3、进程间通信方法使用两条原语send和receive。它们也是系统调用。

  • 消息传递
    消息传递(message passing)面临许多问题和设计难点,特别是位于网络中不同机器上的通信进程的情况。发送方和接收方可以达到如下一致:一旦接收到消息,接收方马上回送一条特殊的确认消息。如果发送方在一段时间间隔内未收到确认,则重发消息。(可以通过在消息中嵌入连续序号解决问题)
    1、设计要点:不可靠消息传递中的成功通信、消息系统还需要解决进程命名问题。
    2、消息传递的变体
    ①、通过信箱进行传递,信箱是用来对一定数量的消息进行缓冲的地方,发送来和接收方都有一个信箱在这里插入图片描述

  • 屏蔽(也是同步的一种思想)(barrier):
    1、屏障是用于进程组而不是用于双进程的生产者-消费者情形的。
    2、某些应用中划分了若干阶段,除非所有的进程都准备着手下一个阶段,否则任何进程都不能进入下一个阶段(可以在每个阶段的结尾安置屏障)。

  • 避免锁:最快的锁是根本没有锁。比如:读-复制-更新(RCU)将更新过程的“移除和再分配”过程分离开来
    窍门:在于确保每个读操作要么读取旧的数据版本,要么读取新的数据版本,但是不能读取新旧数据的一些奇怪组合(原子操作)。

  • 调度
    1、选择下一个要运行的进程
    2、多道程序设计系统中,多个线程或进程同时竞争CPU,当只有一个CPU可用时,那么就必须选择下一个要运行的进程。
    完成选择工作的这一部分称为调度程序,该程序使用的算法称为调度算法
    3、调度经常是按”线程级别“的。
    4、CPU是稀缺资源,每秒切换进程的次数太多,会消耗大量CPU。

  • 进程行为
    1、CPU密集型进程:具有较长时间的CPU集中使用和较小频度的I/O等待
    2、I/O密集型进程:具有较短时间的CPU几种使用和频繁的I/O等待。
    3、有必要之处,随着CPU变得越来越快,更多的进程倾向为I/O密集型。这种现象之所以是因为CPU的改进比磁盘的改进快得多。

  • 何时调度
    1、创建:在创建一个新进程之后,需要决定是运行父进程还是运行子进程。
    2、退出:在一个进程退出时必须做出调度决策。
    3、阻塞:当一个进程阻塞在I/O和信号量上或者由于其他原因阻塞时,必须选择另一个进程运行。
    4、中断:在一个I/O中断发生时,必须做出调度决策。
    非抢占式调度算法:挑选一个进程,然后让该进程运行直至被阻塞,或者直到进程自动释放CPU
    抢占式调度算法:选择一个进程,让该进程运行某个固定时段的最大值。如果时段结束进程任在运行则被挂起,调度程序选择另外一个进程运行。

  • 调度算法分类
    1、批处理:非抢占式,或长时间周期的抢占式算法都可。因为批处理系统中,不会有 用户始终等待(适用场景)。
    2、交互式:抢占式算法(避免一个进程长期霸占CPU)。
    3、实时式:抢占有时是不需要的,因为进程了解他们可能长时间得不到运行。

  • 调度算法的目标
    1、所有系统、公平——给每个进程公平的CPU份额 、平衡——保持系统的所有部分都忙碌 、策略强制执行——看到锁宣布的策略执行
    2、批处理系统、吞吐量——每小时(单位时间)最大作业数、周转时间——从提交到终止间的最小时间、CPU利用率——保持CPU时钟忙碌、
    3、交互式系统、响应时间——快速响应请求 、均衡性——满足用户的期望
    4、实时系统、满足截止时间——避免丢失数据 、可预测性——在多媒体系统中避免品质降低

  • 批处理系统中的调度
    1、先来先服务(队列)
    2、最短作业优先:适用于运行时间可以预知的另一个非抢占式的批处理调度算法。(最短执行时间优先)
    3、最短剩余时间优先:最短作业优先的抢占版本。(剩余的最短执行时间优先)

  • 交互式系统中的调度
    1、 轮转调度:(最古老,最简单,最公平且使用最广的算法)(轮盘式的)。每个进程被分配一个时间段,成为时间片,即允许该进程在该时间段中运行。如果在时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程。如果该进程在时间片结束前阻塞或结束,则CPU立即切换。调度程序只需要维护一张可运行进程列表。当一个进程用完他的时间片后,被移到队列的末尾。
    2、优先级调度(轮转调度的进一步):轮转调度做了一个所有的进程同等重要的假设。但是在实际情况下,每个进程被赋予一个优先级,允许优先级最高的可运行进程先运行。优先级可以被静态赋予或动态赋予。
    3、多级队列(优先级调度的进一步):对优先级越高得进程执行越少得时间片。随着进程优先级得不断降低,它的运行频度逐渐放慢(随着进程优先级得不断降低,它的运行频率逐渐放慢)
    4、最短进程优先:根据进程过去的行为进行推测,并执行估计运行时间最短的那个。
    5、保证调度:向用户做出明确得性能保证
    6、彩票调度:为进程提供各种资源得彩票,一旦需要做出调度,就抽一张彩票,拥有该进程得可以获得资源。
    7、公平分享调度

  • 实时系统中的调度 :
    1、实时系统的调度算法可以是静态的或动态的。
    2、前者在系统开始运行之前做出调度决策;后者在运行过程中进程调度决策。

  • 线程调度
    1、当若干进程都有多个线程时,就存在两个层次的秉性:进程和线程。在这样的系统中调度处理有本质差别,这取决于所支持的是用户级线程还是内核级线程(还是两种都支持)
    2、用户级线程:由于内核并不知道有线程存在,所以内核只能执行进程的操作。比如进程为A,A中的线程调度决定哪个线程执行,并且线程执行时不存时钟中断,所以该线程可以按其意愿任意执行多长时间。只能进行进程间的调用。
    **局限:**缺乏一个时钟中断运行过长的线程
    3、内核级进程:不需要考虑线程属于哪个进程。
    在这里插入图片描述

    4、比较:用户级线程和内核级线程之间的差别在于性能。用户级线程的线程切换需要 少量的机器指令,而内核级线程需要完整的上下文切换,修改内存映像(MMU),使高速缓存失效,这导致了若干数量级的延迟。另一方面,在使用内核级线程时,一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起

  • 哲学家就餐问题:对于互斥访问有限资源的竞争问题一类的建模过程十分有用。
    5、读者-写者问题:为数据库访问建立了一个模型。
    多个进程同时读数据库是可以接受的,如果一个进程正在写数据库,则所有的其他进程都不能访问该数据库即使读也不可以
    第一个读者对信号量db执行down操作。随后的读者只是递增一个计数器rc.当读者离开时,他们递减这个计数器,而最后一个读者则对信号量执行up,这样就允许一个被阻塞的写者可以访问该数据库
    这种策略的结果就是,如果有一个稳定的读者流存在,那么这些读者将在到达后被允许进入。而写者就始终被挂起,直到没有读者为止。为了避免这种情形,在一个读者到达,且一个写者在等待时,读者在写者之后被挂起,而不是立即允许进入。但这也是有缺点的,并发度和效率比较低。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值