进程间通信和线程同步

进程间的通信

为什么需要进程间通信?

由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

管道

匿名管道

  • 通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道。

  • 匿名管道是只能用于存在父子关系的进程间通信。

    image-20220717171135533

命名管道

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。

总结

  • 单向传输,先进先出

    不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则。

  • 效率低、编程复杂

    管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。且编程复杂。

消息队列

  • 自定义数据类型

    克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的

  • 消息队列通信的速度不是最及时的

    消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存

  • 通信速度快

    共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名

  • 需要保持同步

    但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

信号量

就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。

信号

信号是异步通信机制

  • 信号可以在应用进程和内核之间直接交互
  • 内核也可以利用信号来通知用户空间的进程发生了哪些系统事件

一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。

Socket

如果要与不同主机的进程间通信,那么就需要 Socket 通信了

可根据创建 Socket 的类型不同,分为三种常见的通信方式,

  • 一个是基于 TCP 协议的通信方式,
  • 一个是基于 UDP 协议的通信方式,
  • 一个是本地进程间通信方式,比如socketpair设置一对socket套接字。

线程通信间的方式

  • 因为是处于同一内存空间,通过全局变量即可实现通信,主要关注线程同步问题

    同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

线程同步

对共享资源的访问过程

  • 从内存取出共享资源
  • 加载到寄存器中进行运算处理
  • 将处理后的值存入内存

在这个过程中可能发生线程上下文切换,那么此时就会导致资源混乱,得到错误的结果。上面展示的情况称为竞争条件(race condition),将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。

互斥和同步概念

互斥

我们希望临界区代码是互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区,说白了,就是这段代码执行过程中,最多只能出现一个线程。

同步

所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。

互斥与同步的实现和使用

为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:

  • :加锁、解锁操作;
  • 信号量:P、V 操作;

任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。

信号量

通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。

另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:

  • P 操作:将 sem1,相减后,如果 sem < 0,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞;
  • V 操作:将 sem1,相加后,如果 sem <= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;

生产者-消费者问题

生产者-消费者问题描述:

  • 生产者在生成数据后,放在一个缓冲区中;
  • 消费者从缓冲区取出数据处理;
  • 任何时刻,只能有一个生产者或消费者可以访问缓冲区;

从问题分析可知

  • 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,需要互斥;
  • 缓冲区不为空时,消费者才能取数据;缓冲区空余大小不为空时,生产者才能生产数据。说明缓冲区数据大小和缓冲区空余大小需要同步。

那么我们需要三个信号量,分别是:

  • 互斥信号量 mutex:用于互斥访问缓冲区,初始化值为 1;
  • 资源信号量 N_Buffers:用于消费者询问缓冲区数据大小,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空);
  • 资源信号量 N_empty:用于生产者询问缓冲空余空间大小,有空位则生成数据,初始化值为 n (缓冲区大小);

伪代码如下:

#definde N 100
semaphore mutex=1;
semaphore N_buffers=0;
semaphore N_empty=N;

//生产者
void producer() {
    while(1) {
		P(N_buffers);//询问空槽数量
    	P(mutex);    //上锁
    	//添加缓存
    	V(mutex);    //解锁
    	V(N_buffers); //添加缓存区数据量
	}
}

//消费者

void comsumer() {
    
    while(1) {
        P(N_buffers);//询问数据量
        P(mutex);//上锁
        //取出缓存数据
        V(mutex);//解锁
        V(N_empty);//添加缓存区空余空
    }
    
}

哲学者就餐问题

先来看看哲学家就餐的问题描述:

  • 5 个老大哥哲学家,闲着没事做,围绕着一张圆桌吃面;
  • 巧就巧在,这个桌子只有 5 支叉子,每两个哲学家之间放一支叉子;
  • 哲学家围在一起先思考,思考中途饿了就会想进餐;
  • 奇葩的是,这些哲学家要两支叉子才愿意吃面,也就是需要拿到左右两边的叉子才进餐
  • 吃完后,会把两支叉子放回原处,继续思考

方案一

使用信号量标注每个筷子的互斥

image-20220718161133301

会出现死锁的问题,当哲学家都刚好同时拿起一把筷子,则会出现死锁。

方案二

每次只能一个哲学家就餐,效率低。

方案三

使得奇数哲学家、偶数哲学家、拿相邻筷子顺序一致,就不会出现各拿一根筷子的事件。

死锁

死锁?

当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁

死锁发生条件

当线程的某个任务需要获取多个资源才能完成,如果线程获取资源的顺序发生了交叉,那么就会出现死锁的情况。

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;

    互斥条件是指多个线程不能同时使用同一个资源。

  • 持有并等待条件;

    持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。

  • 不可剥夺条件;

    不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。

  • 环路等待条件;

    比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。

避免死锁的发生

  • 将多个资源统一成一个资源进行处理。
  • 将每个线程获取资源的顺序有序。
posted @ 2022-07-18 16:51  xfw121  阅读(67)  评论(0)    收藏  举报