【Linux】IPC概述

概述

Linux 系统中有很多进程,免不了进程间进行通信,即IPC通信,linux 中有6种方式:管道(无名管道pipe和有名管道FIFO)、信号信号量消息队列共享内存套接字(socket)

  1. 管道(无名管道pipe和有名管道FIFO):无名管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此它还允许无亲缘关系进程间的通信;

  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;

  3. 消息队列(Message):消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

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

  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

  6. 套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。


管道

特点:

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

  • 写入与读取的顺序原则是:先进先出

  • 管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。

  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

  • 在默认情况下,当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小)

pipe无名管道

  • 只能用于父子进程或者兄弟进程之间( 具有亲缘关系的进程)。 比如fork或exec创建的新进程, 在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。 当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。

FIFO有名管道

  • 命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信,只要可以访问该路径,就能够彼此通过FIFO相互通信

管道和命名管道的区别

  • 对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行mkfifo myfifo就是创建一个命名通道,我们必须用open函数来显示地建立连接到管道的通道,而在管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。

  • 一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望有名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。


共享内存

  • 共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
  • 采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
  • Linux的内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。

mmap(内存映射——Memory Map)

  • 内存映射文件,是由一个文件到一块内存的映射。内存映射文件与 虚拟内存有些类似,通过内存映射文件可以保留一个地址的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。

  • 使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作

  • 使用内存映射文件不仅可以实现多个进程间的通信,还可以用于 处理大文件提高效率。因为我们普通的做法(I/O)是把磁盘上的文件先拷贝到内核空间的一个缓冲区再拷贝到用户空间(内存),用户修改后再将这些数据拷贝到缓冲区再拷贝到磁盘文件,一共四次拷贝。如果文件数据量很大,拷贝的开销是非常大的。那么问题来了,系统在在进行内存映射文件就不需要数据拷贝?mmap()确实没有进行数据拷贝,真正的拷贝是在在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,所以只进行一次数据拷贝。效率高于read/write。

系统调用mmap()用于共享内存的两种方式:

  1. 使用普通文件提供的内存映射:适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用mmap()

  2. 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。


套接字(socket)

  • socket网络编程可能使用得最多,经常用在网络上不同主机之间的通信。其实在同一主机内通信也可以使用socket来完成,socket进程通信与网络通信使用的是统一套接口,只是地址结构与某些参数不同。在使用socket创建套接字时通过指定参数domain是af_inet(ipv4因特网域)或af_inet6(ipv6因特网域)或af_unix(unix域,本地通信)来实现。

  • 套接字的创建和使用与管道是有区别的,套接字明确地将客户端与服务器区分开来,可以实现多个客户端连到同一服务器。


信号

信号是软件中断产生,用于进程间异步传递信息。一般在shell 中操作,进程获取信号进行处理,一共有64中信号,在shell中输入 kill -l 可查阅

信号的发送

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

进程对信号的响应

进程可以通过三种方式来响应一个信号:
1. 忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP
2. 捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数
3. 执行缺省操作,Linux对每种信号都规定了默认操作,注意,进程对实时信号的缺省反应是进程终止。

信号的注册监听

如果进程要处理某一信号,那么就要在进程中注册监听该信号
inux主要有两个函数实现信号的注册监听:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的监听;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的监听。sigaction()优于signal()主要体现在支持信号带有参数。


信号量

  • 信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。

  • 信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号)

  • p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行

  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1

  • 简单理解就是P相当于申请资源,V相当于释放资源


消息队列

  • 消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容。
  • 会存在多个消息队列,每个消息队列可以用IPC标识符唯一地进行识别,不同的消息队列直接是相互独立的。
  • msgget()创建一个新队列或打开一个存在的队列
  • msgsnd()向队列末端添加一条新消息
  • msgrcv()从队列中取消息,取消息是不一定遵循先进先出的,也可以按消息的类型字段取消息。

消息队列与有名管道的比较

  • 在有名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。

与有名管道相比,消息队列的优势在于
1. 消息队列也可以独立于发送和接收进程而存在,从而消除了在同步有名管道的打开和关闭时可能产生的困难。
2. 通过发送消息还可以避免有名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
3. 接收程序可以通过消息类型有选择地接收数据,而不是像有名管道中那样,只能默认地接收。