通信方式

进程之间是互相独立的,没有任何手段直接通信,因此我们需要借助操作系统来辅助它们

共享内存

Linux下采用共享内存的方式来使进程完成对共享资源的访问,它将磁盘文件复制到内存,并创建虚拟地址到该内存的映射,就好像该资源本来就在进程空间之中,此后我们就可以像操作本地变量一样去操作它们了,实际的写入磁盘将由系统选择最佳方式完成,例如操作系统可能会批量处理加排序,从而大大提高IO速度。

如何绑定一个共享内存到自己内存中呢?要知道在不同进程中使用malloc函数是会顺序分配空闲内存,而不会分配同一块内存,那么要如何去解决这个问题呢?

  • C 中函数辅助处理 Linux操作系统已经想办法帮我们解决了这个问题,在#include <sys/ipc.h>#include <sys/shm.h>头文件下,有如下几个shm系列函数:

    frok()函数用以标识系统IPC资源,例如这里的共享资源、下文的消息队列、管道…都属于IPC资源。

    • shmat函数:通过由shmget函数获取的标识符,建立由共享内存到进程独立空间的映射

    • shmdt函数:释放映射。

通过上述几个函数,每个独立的进程只要有统一的共享内存标识符便可以建立起虚拟地址到物理地址的映射,每个虚拟地址将被翻译成指向共享区域的物理地址,这样就实现了对共享内存的访问

共享内存对比其他几种方式是效率最高的,因为无需进行多次复制,直接对内存操作,不过要注意一点,操作系统并不保证任何并发问题,例如两个进程同时更改同一块内存区域,正如你和你的朋友在线编辑同一个文档中的同一个标题,这会导致一些不好的结果,所以我们需要借助信号量或其他方式来完成同步。

信号量

信号量是最先提出的一种为解决**同步不同执行线程问题的一种方法,进程与线程抽象来看大同小异,所以信号量同样可以用于同步进程间通信**。

信号量 s 是具有非负整数值的全局变量,由两种特殊的原子操作来实现,这两种原子操作称为 P 和 V :

简单来讲,就是P负责减,V负责加

  • P(s):如果 s 的值大于零,就给它减1,然后立即返回,进程继续执行。;如果它的值为零,就挂起该进程的执行,等待 s 重新变为非零值。

  • V(s):V操作将 s 的值加1,如果有任何进程在等在 s 值变为非0,那么V操作会重启这些等待进程中的其中一个(随机地),然后由该进程执行P操作将 s 重新置为0,而其他等待进程将会继续等待。

开始时,A率先写入资源,此时A调用P(s),将 s 减一,此时 s = 0,A进入共享区工作。

此时,进程B也想进入共享区修改资源,它调用P(s)发现此时s为0,于是挂起进程,加入等待队列。

A工作完毕,调用V(s),它发现s为0并检测到等待队列不为空,于是它随机唤醒一个等待进程,并将s加1,这里唤醒了B。

B被唤醒,继续执行P操作,此时s不为0,B成功执行将s置为0并进入工作区。

在Linux下并没有直接的P&V函数,而是需要我们根据这几个基本的sem函数族进行封装:

  • semget:初始化或获取一个信号量,这个函数需要接受ftok()的返回值以及初始s的值,它将全局计数变量s绑定在由ftok标识的共享资源上,并返回一个唯一标识的信号量组ID。
  • semop:这个函数接受上面函数返回的信号量组ID以及一些其他参数,根据参数的不同有一些不同的操作,他将对与该信号量组ID绑定的全局计数变量 s 进行一些操作,P&V操作便是基于此实现。
  • semctl:这个函数接受上面函数返回的信号量组ID以及一些其他参数,主要进行控制信号量相关信息,如删除该信号量等。

管道

管道是一种最基本的进程间通信机制。 管道由pipe函数来创建: 调用pipe函数,会在内核中开辟出一块缓冲区用来进行进程间通信,这块缓冲区称为管道,它有一个读端和一个写端。管道被分为匿名管道和有名管道。

匿名管道

匿名管道通过pipe函数创建,这个函数接收一个长度为2的Int数组,并返回1或0表示成功或者失败:

int pipe(int fd[2])

这个函数打开两个文件描述符,一个读端文件,一个写端,分别存入fd[0]和fd[1]中,然后可以作为参数调用writeread函数进行写入或读取,注意fd[0]只能读取文件,而fd[1]只能用于写入文件。

匿名管道怎么实现通信?其他进程又不知道这个管道,因为进程是独立的,其他进程看不到某一个进程进行了什么操作。

  • ‘其他’进程确实是不知道,但是它的子进程却可以,一个进程派生一个子进程,那么子进程将会复制父进程的内存空间信息,注意这里是复制而不是共享,这意味着父子进程仍然是独立的,但是在这一时刻,它们所有的信息又是相等的。因此子进程也知道该全局管道,并且也拥有两个文件描述符与管道挂钩,所以匿名管道只能在具有亲缘关系的进程间通信。

  • 还要注意,匿名管道内部采用环形队列实现,只能由写端到读端,由于设计技术问题,管道被设计为半双工的,一方要写入则必须关闭读描述符,一方要读出则必须关闭写入描述符。因此我们说管道的消息只能单向传递。

命名管道

命名管道有一个唯一的名称了,任何进程都可以访问这个管道,不再受限于匿名管道只能在具有亲缘关系的进程间通信这一点

操作系统将管道看作一个抽象的文件,但管道并不是普通的文件,管道存在于内核空间中而不放置在磁盘(有名管道文件系统上有一个标识符,没有数据块),访问速度更快,但存储量较小,管道是临时的,是随进程的,当进程销毁,所有端口自动关闭,此时管道也是不存在的,操作系统将所有IO抽象的看作文件,例如网络也是一种文件,这意味着我们可以采用任何文件方法操作管道,命名管道就利用了这种抽象。

消息队列

消息队列亦称报文队列,也叫做信箱,是Linux的一种通信机制,这种通信机制传递的数据会被拆分为一个一个独立的数据块,也叫做消息体,消息体中可以定义类型与数据,克服了无格式承载字节流的缺陷

同管道类似,它有一个不足就是每个消息的最大长度是有上限的,整个消息队列也是长度限制的。

信号

一个进程可以发送信号给另一个进程,一个信号就是一条消息,可以用于通知一个进程组发送了某种类型的事件,该进程组中的进程可以采取处理程序处理事件。

Linux下unistd.h头文件下定义了如图中的常量,当我们在shell命令行键入ctrl + c时,内核就会前台进程组的每一个进程发送SIGINT信号,中止进程。

套接字

Socket套接字是用与网络中不同主机的通信方式,多用于客户端与服务器之间,在Linux下也有一系列C语言函数,诸如socket、connect、bind、listen与accept

管道IPC 例子