进程间通信

虚拟CPU和虚拟内存的引入保证了进程的一个重要特性就是隔离,一个进程在执行过程中逻辑上总是认为自己占用了所有的CPU和内存,但是实际在底层,操作系统和硬件完成了很多工作才实现了隔离的特性(比如内核和时钟设备配合实现进程调度)。在多个进程之间,如果需要进行通信的话,隔离特性会造成一些通信的障碍。所以我们需要一些手段来跨越隔离,实现进程间通信(InterProcess Communication,IPC)。

1, 管道

一种最自然的通信方式就是依赖文件系统, 一个进程打开文件并读写信息,另一个进程也可以打开文件来获取信息。显然,这种通信方式要依赖磁盘文件,效率十分低下。

为了提升效率, 有名管道(named pipe,FIFO)就被设计出来了,有名管道是文件系统中一种专门用来读写的文件,但是通过有名管道进行通信的时候实际上并没有经过磁盘,而是经过内核的管道缓冲区进行数据传递。

如果对于拥有亲缘关系的进程而言,它们之间可以使用另一种匿名管道。匿名管道它不需要在文件系统创建单独的文件,相反它是进程在执行过程中动态创建和销毁的。

1.1 有名管道

有名管道在文件系统中以文件的形式存在,但它允许进程之间以先进先出的方式进行通信。它可以使用mkfifo可以创建管道文件,使用unlink删除管道文件。(man 3 mkfifo)

EgCode:

1.2 匿名管道

匿名管道/pipe函数是Linux系统中用于实现父子进程间通信的一种简单方式。 (man pipe)

  • 管道是单向的,数据从一端流入,从另一端流出,因此它们是半双工的。(这就意味着: 使用匿名管道,一个进程将数据写入管道pipe[1],而另一个进程则可以从管道读取数据pipe[0])

  • 匿名管道只使用于存在亲缘关系的进程之间进行通信(Eg: 父子进程)。

  • 先在一个进程中创建一个管道,然后再利用fork, 可以让父子进程同时持有管道, 就可以实现进程间通信了。(单个进程的管道的是没有什么价值的)

  • 如果要实现父子进程之间全双工通信,需要调用pipe函数两次来创建两条管道。而且, fork的次数和管道的数量无关,每一次使用 pipe函数就会在内核创建一个管道缓冲区。

EgCode:

1.3 popen

popen()函数是一个在 Linux系统中常用的库函数,它用于创建一个管道,并且同时启动一个新的进程,然后将这个启动的新进程的标准输入或标准输出与管道连接起来。(man 3 popen)

  • 这允许当前程序和新启动的程序之间进行数据交换。

  • popen() 函数的本质是对 pipe(), fork(), 等函数的高级封装。

EgCode1:

EgCode2:

EgCode3:

2, 共享内存

在进程的数量比较少的时候,使用管道进行通信是比较自然的思路。如果需要在任意两个进程之间使用匿名管道,它需要调用两次 pipe系统调用,但是随着进程数量增加, pipe的使用次数将会急剧增加。除此以外,使用管道通信的时候,数据要从写端拷贝到内核管道缓冲区, 再从缓冲区拷贝到读端,总共要进行两次拷贝,性能比较差。为了提升进程间通信的效率, 共享内存 (也有翻译成共享存储)的方式就诞生了。

对于进程而言,代码中所使用的地址都是虚拟地址,所操作的地址空间是虚拟地址空间。当进程执行的时候,如果发生访问内存的操作,操作系统和硬件需要能保证虚拟地址能够映射到物理内存上面( 这种地址转换相关的设备被称为内存管理单元MMU) (所以如果两个不同的进程即使使用相同的虚拟地址,它们所对应的物理内存地址是不一样的)。

共享内存就允许两个或者多个进程共享一个给定的物理存储区域 ,这使得不同的进程就可以通过共享内存进行通信了。(当然为了实现内存共享,内核会专门维护一个用来存储共享内存信息的数据结构 : 其中包括共享内存大小、权限、引用进程数量....)。

对于Linux的共享内存, 有两个常见的版本System VPOSIX接口

2.1 System V

2.1.1 生产标识

ftok函数的作用是根据条件产生一个独特的、用于标识作用的键值,使得在SystemV共享内存机制中, 不同的进程可以通过同一键值访问到同一片共享内存,从而实现进程间通信。(man 2 ftok)

2.1.2 创建共享内存

使用shmget函数可以根据键来获取一个共享内存段。

  • 函数的键值key通常由ftok函数生成。

  • 如果参数key设置为IPC_PRIVATE的宏,函数总会创建一个新的共享内存段,且只有创建这个共享内存的进程,及其具有亲缘关系的进程(子进程)可以访问。 (设置IPC_PRIVATE创建的共享内存又被称为私有内存)

  • 建议size为内存页大小的整数倍。

  • shmflg参数, 可以设置共享内存的读写权限。 (Eg: 0600 ...)

  • shmflg参数, 还可以配合逻辑标准使用。( IPC_CREAT、IPC_EXCL )

EgCode:

查看共享内存命令

如共享内存、以及信号量和消息队列一旦创建以后,即使进程已经终止,这些IPC并不会释放在内核中的数据结构,可以用使用命令ipcs来查看这些IPC的信息。

2.1.3 获取共享内存

shmat函数用于将共享内存段映射到当前进程的地址空间,允许进程访问共享内存段中的数据。(man 2 shmat)

2.1.4 共享内存通信

EgCode: 向共享内存写内容

EgCode: 从共享内存读内容

2.1.5 解除共享内存映射

使用 shmdt函数可以断开当前进程与共享内存段的连接,解除到共享内存段的映射(某种程度上等价于free)。(当一个进程通过 shmat函数将共享内存段附加到自己的地址空间后,使用 shmdt函数将其分离。) (man 2 shmdt)

EgCode:

2.1.6 修改共享内存属性: 仅了解

使用shmctl函数可以用于对共享内存段执行多种操作。根据参数的不同,可以执行不同的操作。 (man shmctl)

  • IPC_STAT可以用来获取存储共享内存段信息的数据结构;

  • IPC_RMID可以用来从内核删除共享内存段,当删除时,无论此时有多少进程映射到共享内存段,它都会被标记为待删除, 一旦被标记以后,就无法再建立映射了。当最后一个映射解除时,共享内存段就真正被移除。

  • IPC_SET可以用来修改共享内存段的所有者、所在组和权限;

EgCode:

2.2 POSIX

EgCode:

在上述例子中: 使用基本和System V一致

  1. 使用 shm_open 创建一个新的或打开一个共享内存对象。

  2. 使用 ftruncate 设置共享内存对象的大小。

  3. 使用 mmap 将共享内存映射到进程的地址空间。

  4. 通过 fork 创建一个新的进程。

  5. 在子进程中,向共享内存写入数据。

  6. 在父进程中,等待子进程结束后,从共享内存读取数据。

  7. 使用 munmap 解除映射。

  8. 使用 shm_unlink 删除共享内存对象。

 

ps1: 上述代码编译需要注意链接 -lrt (参考 man shm_open). (Eg: gcc test.c -o test -lrt)

ps2: shm_open 函数的第一个参数是一个POSIX共享内存对象的名字,而不是文件系统上的一个路径,而是一个标识符,应该以 / 开头的名字,用于创建和引用共享内存对象。 (这和ftok是不同的)

2.3 同时写入

共享内存可以实现多个进程同时对同一个数据进行访问和修改。

执行上述程序,所得到的结果和预期的200000并不一致。进程的切换是造成结果不一致 的原因:当其父进程进程尝试对共享资源进行访问的时候,比如将p[0]从共享内存取出到寄存器当中时,此时父进程进程有可能被切换到子进程进程,而子进程进程会修改p[0]的数值并写回到共享内存中,当重新切换回父进程进程的时候, 父进程进程会继续刚才的指令继续运行,就会将寄存器当中p[0]的内容写回内存中,这样的话子进程进程刚刚的修改就丢失掉了 (反之亦然)。这个多个进程同时写入造成结果出错误的情况被称为竞争条件。

man -k shm