【Linux 进程间通信(IPC)详解 · 第一篇】进程间通信(IPC)简介

目录&索引

  • 致读者
  • 0 前言
  • 1 概述
  • 2 进程、线程与信息共享
  • 3 名字空间
  • 4 出错处理
  • 5 小结


致读者

刚接触该书时甚是兴奋,书中思想令人振奋,以致深夜久不能寐,但毕竟书籍年代久远,刚好博主处于学习阶段,本着探索精神,搭配 man 手册进行编码学习。献上 W. Richard Stevens 所写书籍中的一句话——"理解某种特性的实现通常有助于了解如何使用该特性”。这不禁让人回想起侯捷先生所著《STL 源码剖析》的开篇,“源码之前,了无秘密”。

本文提炼自 UNIX 网络编程第 2 卷,下面会引用原书前言以明确内容提纲,致敬原书作者 W. Richard Stevens。


0 前言

进程间通信(IPC)几乎是所有 UNIX 程序性能的关键,理解 IPC 也是理解如何开发不同主机间网络应用程序的必要条件。

原书从Posix IPC 和 System V IPC 的内部结构开始讨论,全面深入地介绍 4 种 IPC 形式。

  • 原书前言
    大多数重要的程序都涉及进程间通信(Interprocess Communication, 简称 IPC)。这是受下述设计原则影响的自然结果:把应用程序设计为一组相互通信的小片段比将其设计为单个庞大的程序更好。从历史的角度看,应用程序有如下几种构建方法:
    1)用一个庞大的程序完成全部工作。程序的各部分可以实现为函数,函数之间通过参数、返回值和全局变量来交换信息;
    2)使用多个程序,程序之间用某种形式的 IPC 进行通信。许多标准的 UNIX 工具都是按照这种风格设计的,它们使用 shell 管道(IPC的一种形式)在程序之间传递信息;
    3)使用一个包含多个线程的程序,线程之间使用某种 IPC。这里仍然使用术语 IPC,尽管通信是在线程之间而不是在进程之间进行的。
    还可以把后两种设计形式结合起来——用多个进程来实现,其中每个进程包含几个线程。在这种情况下,进程内部的线程之间可以通信,不同进程之间也可以通信。
    上面讲述了可以把完成给定任务所需的工作分到多个进程中,或许还可以进一步分到进程内的多个线程中。在包含多个处理器(CPU)的系统中,多个进程也许可以(在不同的 CPU 上)同时运行,或许给定进程内的多个线程也能同时进行。因此,把任务分到多个进程或多个线程中,有望减少完成指定任务的时间。
    本书详细描述了以下 4 种不同的 IPC 形式:
    1)消息传递(匿名管道、FIFO 和消息队列);
    2)同步(互斥锁、条件变量、读写锁、文件与记录锁、信号量);
    3)共享内存(匿名的与具名的);
    4)远程过程调用(Solaris 门和 Sun RPC)。

    本书不讨论如何编写通过计算机网络通信的程序。这种通信通常涉及使用 TCP/IP 协议簇的套接字 API,相关主题在第 1 卷中详细讨论。
    有人可能会提出质疑:不应该使用单主机或非网络 IPC(本卷的主题),所有程序都应该在网络上的多台主机上同时运行。但在日常实践中,单主机 IPC 往往比网络通信快得多,而且有时还简单些。共享内存、同步等方法通常也只能用于单主机,跨网络时可能无法使用。经验和历史表明,非网络 IPC(本卷)与跨网络 IPC(第 1 卷)都是需要的。

1 概述

IPC 是进程间通信(interprocess communication)的简称。传统上该术语描述的是运行在某个操作系统之上的不同进程间各种消息传递(message passing)的方式。还讲述多种形式的同步(synchronization),因为像共享内存区需要某种形式的同步参与。

  • 管道(pipe):一个广泛使用的 IPC 形式,既可在程序中使用,也可以在 shell 中使用。管道的问题在于它们最初只能在具有共同祖先(指父子进程关系)的进程间使用,不过该问题已随命名管道(named pipe)即 FIFO 的引入而解决。匿名管道与命名管道均为半双工,数据只能单向流动。
  • 消息队列(message queue):消息的队列,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存(shared memory):映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  • 信号量(semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 套接字(socket):对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。(非该系列文章详细介绍内容,将在后续项目中单独介绍)
  • 远程过程调用(remote procedure call,简称 RPC):出现在 20 世纪 80 年代中期,它是从一个系统(客户主机)上某个程序调用另一个系统(服务器主机)上某个函数的一种方法。

2 进程、线程与信息共享

按照传统的 UNIX 编程模型,我们在一个系统上运行多个进程,每个进程都有各自的地址空间。UNIX 进程间的信息共享可以有多种方式。UNIX 进程间共享信息的三种方式如图:

【Linux 进程间通信(IPC)详解 · 第一篇】进程间通信(IPC)简介_第1张图片
  • 1)左边的两个进程共享存留于文件系统中某个文件的某些信息。为了访问这些信息,每个进程都得穿过内核(例如 read、write、lseek等)。当一个文件有待更新时,某种形式的同步是必要的,这样既可保护多个写入者,防止相互干扰,也可保护多个读出者,防止写入者的干扰。
  • 2)中间的两个进程共享驻留于内核中的某些信息。管道是这种共享类型的例子,消息队列和信号量也是。当前情况下,访问共享信息的每次操作涉及对内核的一次系统调用。
  • 3)右边的两个进程有一个双方都能访问的共享内存区。每个进程一旦设置好该共享内存区,就能不涉及内核而直接访问其中的数据。共享内存区的进程需要某种形式的同步。
  • 注意:没有任何东西限制任何 IPC 只能使用两个进程,IPC 适用于任何数目的进程。上图展示两个进程进行说明,是为了简单起见。

线程

从 IPC 角度看来,一个给定进程内的所有线程共享同样的全局变量。然而我们必须关注各个线程间对全局数据的同步访问。尽管同步不是一种明确的 IPC 形式,但它确实伴随许多形式的 IPC 使用,以控制对某些共享数据的访问。


3 名字空间

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

管道没有名字(因此不能用于无亲缘关系的进程间),但是 FIFO 有一个在文件系统中的路径名作为其标识符(因此可用于无亲缘关系的进程间)。对于一种给定的 IPC 类型,其可能的名字的集合称为它的名字空间(name space)。名字空间很重要,因为对于除匿名管道以外的所有形式的 IPC 来说,名字是客户与服务器彼此连接以交换信息的手段。

key_t 键与 ftok 函数

以 key_t 键与 ftok 函数为例,在System V 消息队列、System V 信号量、System V 共享内存区,这三种 IPC 使用 key_t 值作为它们的名字。头文件 把 key_t 这个数据类型定义为一个整数,它通常是一个至少 32 位的整数。这些整数值通常是由 ftok 函数赋予的。

函数 ftok 把一个已存在的路径名和一个整数标识符转换成一个 key_t 值,称为 IPC 键。

#include 
key_t ftok(const char *pathname, int id); // 返回: 若成功则为 IPC 键, 若出错则为 -1

4 出错处理

包裹函数

在现实程序中,我们必须检查每个函数调用是否返回错误。由于碰到错误时终止程序执行是个惯例,因此我们可以通过定义包裹函数(wrapper function)来缩短程序的长度。包裹函数执行实际的函数调用,测试其返回值,并在碰到错误时终止进程。我们使用的命名约定是将函数名的第一个字母改成大写字母,例如:

Sem_post(ptr);

定义这个包裹函数:

void Sem_post(sem_t *sem) {
     
    if (sem_post(sem) == -1) {
     
		err_sys("sem_post error");
	}
}

每当你遇到一个大写字母开头的函数名时,它就是我们所说的包裹函数。它调用一个名字相同但相应小写字母开头的实际函数。当碰到错误时,包裹函数总是输出一个错误消息后终止。

UNIX errno 值

每当在一个 UNIX 函数(同 Linux 函数)中发生错误时,全局变量 errno 将被设置成一个指示错误类型的正数,函数本身则通常返回 -1。

#include 
#include 
#include 
#include 

int main() {
     
    pid_t pid;
    if ((pid = fork()) < 0) {
      // 出错处理
        perror("fork()");
        exit(1);
    }
    if (pid) {
      // 子进程 fork() 返回 0, 父进程 fork() 返回正的子进程 pid 
        printf("In Parent printf Process! <%d>--><%d>--><%d>\n", getppid(), getpid(), pid);
    } else {
     
        printf("In Child printf Process! <%d>--><%d>\n", getppid(), getpid());
    }
    return 0;
}

5 小结

IPC 传统上是 UNIX 中一个杂乱不堪的领域。虽然有了各种各样的解决办法,但没有一个是完美的。讨论分为 4 个主要领域:

  • 1)消息传递(匿名管道、FIFO、消息队列);
  • 2)同步(互斥锁、条件变量、读写锁、信号量);
  • 3)共享内存区(匿名共享内存区、有名共享内存区);
  • 4)过程调用(Solaris 门、Sun RPC)。

考虑单个进程中的多个线程间 IPC 以及多个进程间的 IPC。


你可能感兴趣的:(操作系统,操作系统,多进程,多线程,linux,ipc)