0%

Linux 进程间通信

IPC 指进程间通信,Linux 进程间通信的方式有很多种,不同的通信方式有类似之处,也有很多不同之处,本文总结了 Linux 进程间通信的知识。

IPC 的作用

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几 M 字节之间
  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据
  • 事件通知:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

IPC 的方式

linux下进程间通信的几种主要手段简介:

管道(Pipe)及有名管道(namedpipe)

管道可用于具有亲缘关系进程间的通信(进程的亲缘关系通常是指父子进程关系),有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

信号(Signal)

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux 除了支持 Unix 早期信号语义函数 sigal 外,还支持语义符合 Posix.1 标准的信号函数sigaction(实际上,该函数是基于 BSD 的,BSD 为了实现可靠信号机制,又能够统一对外接口,用 sigaction 函数重新实现了 signal 函数)。

消息队列(Message Queue)

消息队列是消息的链接表,包括 Posix 消息队列 systemV 消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点

共享内存(Share Memory)

使得多个进程可以访问同一块内存空间,是最快的可用 IPC 形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥

信号量(Semaphore)

主要作为进程间以及同一进程不同线程之间的同步手段,是一种资源管理方式。

套接字(Socket)

更为通用的进程间通信机制,可用于不同机器之间的进程间通信(分布式系统)。起初是由 Unix 系统的 BSD 分支开发出来的,但现在一般可以移植到其它类 Unix 系统上:Linux 和 SystemV 的变种都支持套接字。

各种 IPC 介绍

管道

管道是由内核管理的一个缓冲区(buffer),相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待(阻塞),直到另一端的进程放入信息,所以管道是半双工的通信方式。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

从原理上,管道利用 fork 机制建立,从而让两个进程可以连接到同一个 PIPE 上。当在一个进程(父进程)中建立管道时,管道的两端都连接在该进程中。当 fork 复制进程的时候,会将这两个连接也复制到新的进程(子进程)。随后,每个进程关闭自己不需要的一个连接,一个关闭输入流,一个关闭输出流,这样两个进程就构成了单向数据流动的通信方式,Linux 使用 pipe 来创建匿名管道。管道中流动的数据是无格式的字节流。

由于基于 fork 机制,所以管道(匿名)只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了 FIFO 方式连接进程。FIFO 又叫做命名管道(named PIPE)。

FIFO(First in, First out) 为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以 FIFO 实际上也由内核管理,不与硬盘打交道。之所以叫 FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来(好像是传送带,一头放货,一头取货),从而保证信息交流的顺序。FIFO 只是借用了文件系统来为管道命名。写模式的进程向 FIFO 文件中写入,而读模式的进程从 FIFO 文件中读出。当删除 FIFO 文件时,管道连接也随之消失。FIFO 的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

信号

信号是由内核(kernel)管理的,它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。信号的产生方式多种多样,它可以是内核自身产生的,比如出现硬件错误(比如出现分母为0的除法运算,或者出现segmentation fault),内核需要通知某一进程;也可以是其它进程产生的,发送给内核,再由内核传递给目标进程。

内核中针对每一个进程都有一个表存储相关信息(房间的信箱)。当内核需要将信号传递给某个进程时,就在该进程相对应的表中的适当位置写入信号(塞入纸条),这样,就生成(generate)了信号。当该进程执行系统调用时,在系统调用完成后退出内核时,都会顺便查看信箱里的信息。如果有信号,进程会执行对应该信号的操作(signal action, 也叫做信号处理signal disposition),此时叫做执行(deliver)信号。从信号的生成到信号的传递的时间,信号处于等待(pending)状态(纸条还没有被查看)。我们同样可以设计程序,让其生成的进程阻塞(block)某些信号,也就是让这些信号始终处于等待的状态,直到进程取消阻塞(unblock)或者无视信号。

消息队列

消息队列(message queue)与PIPE相类似。它也是建立一个队列,先放入队列的消息被最先取出。不同的是,消息队列允许多个进程放入消息,也允许多个进程取出消息。每个消息可以带有一个整数识别符(message_type)。你可以通过识别符对消息分类 (极端的情况是将每个消息设置一个不同的识别符)。某个进程从队列中取出消息的时候,可以按照先进先出的顺序取出,也可以只取出符合某个识别符的消息(有多个这样的消息时,同样按照先进先出的顺序取出)。消息队列与 PIPE 的另一个不同在于它并不使用文件 API。最后,一个队列不会自动消失,它会一直存在于内核中,直到某个进程删除该队列。

消息队列一旦创建后即可由多进程共享.发送消息的进程可以在任意时刻发送任意个消息到指定的消息队列上,并检查是否有接收进程在等待它所发送的消息,若有则唤醒它;而接收消息的进程可以在需要消息的时候到指定的消息队列上获取消息.如果消息还没有到来.则转入睡眠状态等待。

共享内存

共享内存与多线程共享 global data 和 heap 类似。一个进程可以将自己内存空间中的一部分拿出来,允许其它进程读写。当使用共享内存的时候,我们要注意同步的问题。我们可以使用 semaphore 同步,也可以在共享内存中建立 mutex 或其它的线程同步变量来同步。由于共享内存允许多个进程直接对同一个内存区域直接操作,所以它是效率最高的 IPC 方式。

其特点是没有中间环节,直接将共享的内存页面映射到相互通信的进程各自的虚拟地址空间中,从而使多个进程可以直接访问同一个物理内存页面,如同访问自己的私有空间一样(但实质上不是私有的而是共享的)。因此这种进程间通信方式是在同一个计算机系统中的诸进程间实现通信的最快捷的方法。而它的局限性也在于此,即共享内存的诸进程必须共处同一个计算机系统,有物理内存可以共享才行。

信号量

信号量(semaphore)与互斥锁(mutex)类似,用于处理同步问题。我们说 mutex 像是一个只能容纳一个人的洗手间,那么 semaphore 就像是一个能容纳 N 个人的洗手间。其实从意义上来说,semaphore 就是一个计数锁(和信号 signal 完全不一样),它允许被 N 个进程获得。当有更多的进程尝试获得 semaphore 的时候,就必须等待有前面的进程释放锁。当 N 等于 1 的时候,semaphore 与 mutex 实现的功能就完全相同。许多编程语言也使用 semaphore 处理多线程同步的问题。一个 semaphore 会一直存在在内核中,直到某个进程删除它。

套接字

强大,可以单独一讲,主要用于网络通信,也可以用于计算机本地通信。

IPC 对象

  • 不同进程之间利用 IPC 对象来通信,通过不同进程访问同一个 IPC 对象实现了不同进程之间的通信。
  • IPC 对象存在于内核之中,不同的进程通过获取同一个 IPC 对象的 key,然后调用访问函数(msg_get, shm_get, sem_get)来创建或者访问一个 IPC 对象的句柄(id),然后通过这个 id 来访问共享的资源。
    • 不同的进程通过 ftok(传入一个目录或者文件的路径)来得到同一个 IPC 对象的 key,只要传入的路径相同。
    • 还可以通过传入 IPC_PRIVATE 来创建一个 IPC 对象,获取的 key 是 0,也就无法通过 key 来获取一个 IPC 对象(即使都是传入的 IPC_PRIVATE 也不行),类似于匿名管道。
      • 和匿名管道一样,这种用法一般用于父子进程之间,这样因为父进程创建的
    • 比如 shmget 获取共享内存ID,然后 shmat 来挂载共享内存,获取共享内存的地址(指针),然后通过对内存的直接复制操作来共享数据。