unix网络编程2-IPC-1-简介

继续学习《unix网络编程卷2: 进程间通信》,本篇主要记录第一章简介的相关内容。

1.1 概述

在unix操作系统的历史中,消息传递经历了如下几个阶段。

  • 管道

    管道是第一个广泛使用的IPC形式,既可在程序中使用,也可以在shell中使用。管道的问题在于它们只能在具有共同祖先(指父子进程关系)的进程间使用,不过该问题已随有名管道即FIFO的引入解决了。

  • System V 消息队列

    它可以用在同一主机上有亲缘关系或无亲缘关系的进程之间。

    有亲缘关系 意味着进程具有共同的祖先,这些有亲缘关系的进程是从祖先进程经过一次或多次fork派生来的。考虑到IPC,父进程可以在调用fork前建立某种形式的IPC(如管道或消息队列),因为它知道随后派生的两个子进程将穿越(across) fork继承该IPC对象

    从理论上说,所有Unix进程与init进程都有亲缘关系,它是在系统自举(reboot)时启动所有初始化进程的祖先进程。

    从实践上说,进程的亲缘关系开始于一个登录shell(称为一个会话)以及由该shell派生的所有进程。

  • Posix消息队列

    它可以用在同一主机上有亲缘关系或无亲缘关系的进程之间。

  • 远程过程调用(RPC)

    它是从一个系统(客户主机)上某个程序调用另一个系统(服务器主机)上某个函数的一种方法,是作为显式网络编程 的一种替换方法开发的。

    既然客户和服务器之间通常传递一些信息(被调用的函数的参数和返回值),而且RPC可以用在同一主机上的客户和服务器之间,因此可以认为RPC是另一种形式的消息传递。


unix系统中各种同步形式的演变如下:

  • 记录上锁(record locking)
  • System V 信号量
  • Posix 信号量
  • 互斥锁(mutex)和条件变量
  • 读写锁(read-write lock)

1.2 进程、线程与信息共享

下图介绍了Unix进程间共享信息的三种方式。

  1. 左边的两个进程共享的信息在文件系统中某个文件中。为访问这些信息每个进程都需要穿越内核(例如read、write、lseek等)。当一个文件有待更新时,某种形式的同步是必要的,这样既可保护多个写入者,防止相互串扰,也可以保护一个或多个读出者,防止写入者的干扰。
  2. 中间的两个进程共享驻留于内核中的某些信息。管道是这种共享类型的一个例子,System V 消息队列和System V信号量也是。现在访问共享信息的每次操作涉及对内核的一次系统调用。
  3. 右边的两个进程有一个双方都能访问的共享内存区。每个进程一旦设置好该共享内存区,就能根本不涉及内核而访问其中的数据,共享该内存区的进程需要某种形式的同步

:上面为了简述只写了两个进程,技术上支持任意数目的进程。

从IPC角度看,一个给定的进程内所有线程共享相同的全局变量(即共享内存区的概念对这种模型来说是内在的)。然而我们必须关注的是各个线程之间对全局数据的同步访问,同步尽管不是一种明确的IPC形式,但它却是伴随许多形式的IPC使用,以控制对某些共享数据的访问。

本书主要讲述进程间的IPC和线程间的IPC。假设一个线程环境:如果管道为空,调用线程就阻塞在它的read调用上,直到某个线程往该管道写入数据。在支持线程的系统上,只有对空管道调用read的那个线程阻塞,同一进程中的其余线程才可以继续执行。向该空管道写数据的工作既可以由同一进程中的另一个线程去做,也可以由另一个进程中的某个线程去做。

1.3 IPC对象的持续性

任意类型的IPC对象的**持续性(persistence)**:该类型的一个对象一直存在多长时间。

下图展示了三种类型的持续性:

  1. 随进程持续的(process-persistent)

    IPC对象一直存在到打开该对象的最后一个进程关闭该对象为止。例如管道和IFIO就是这种对象。

  2. 随内核持续的(kernel-persistent)

    IPC对象一直存在到内核重新自举(reboot)或显示删除该对象为止。例如System V的消息队列、信号量和共享内存区就是此类对象。Posix的消息队列、信号量和共享内存区必须至少是随内核持续的,但也可以是随文件系统持续的

  3. 随文件系统持续的(filesystem-persistent)

    IPC对象一直存在到显示删除该对象为止。即使内核重新自举了,该对象还是保持其值。Posix消息队列、信号量和共享内存区如果是使用映射文件实现的(非必需条件),那么他们就是filesystem-persistent的。

在定义一个IPC对象的持续性时我们必须小心,因为它并不总是像看起来的那样。

管道内的数据是在内核中维护的,但管道具备的是随进程的持续性而不是随内核的持续性,最后一个将某个管道打开着用于读进程关闭该管道后,内核将丢弃所有的数据并删除数据。

尽量FIFO在文件系统中有名字,它们也只是具备随进程的持续性,因为最后一个将某个FIFO打开着的进程关闭该FIFO后,FIFO中的数据都被丢弃。

下图展示了本书中讲述的各种IPC对象的持续性。

虽然列表中没有任何类型的IPC具备随文件系统的持续性,但是之前说过三种类型的posix IPC可能会具备该持续性,这取决于它们的实现。

显然向一个文件写入数据提供了随文件系统的持续性,但这通常不作为一种IPC形式的调用。多数形式的IPC并没有在系统重新自举后继续存在的打算,因为进程不可能跨越重新自举继续存活

对于一种给定形式的IPC,要求它具备文件系统的持续性可能会使其性能降低,而IPC的一个设计目标是高性能。

1.4 名字空间(name spaces)

两个或多个无亲缘关系的进程使用某种类型的IPC对象来彼此交换信息时,该IPC对象必须有一个某种形式的名字(name)或标识符(identifier),这样其中一个进程(往往是服务器)可以创建该IPC对象,其余进程则可以指定同一个IPC对象。

管道没有名字(因此不能用于无亲缘关系的进程间),但是FIFO有一个在文件系统中的Unix路径名作为其标识符(因此可以用于无亲缘关系的进程间)。

对于一种给定的IPC类型,其可能的名字的集合称为它的名字空间

名字空间非常重要,因为对于除普通管道外的所有形式的IPC来说,名字是客户与服务器彼此连接以交换消息的手段。

下图给出了各种形式IPC的名字空间。

尽管posix.1标准化了信号量,它们仍然是可选的特性。每种特性有强制未定义可选三种选择,如下图所示,对于可选的特性,指出了每种特性受支持是(通常在<unistd.h>头文件中)定义的常值的名字,例如_POSIX_THREAD。

1.5 fork、exec和_exit对IPC对象的影响

我们需要理解forkexec_exit函数对于所讨论的各种形式的IPC对象的影响,_exit使用exit调用的一个函数。

1.6 出错处理:包裹函数

包裹函数包装了实际调用的函数,运行实际调用的函数并检测其返回值,遇到错误时终止进程,在一定程度上减少了代码量。本书命名约定是将第一个字母改为大写字母。如:

lib/wrapunix.c
1
2
3
4
5
6
void
Sem_post(sem_t *sem)
{
if (sem_post(sem) == -1)
err_sys("sem_post error");
}