笔记和练习博客总目录见开始读TLPI。本章简要介绍进程与线程可用于相互通信、同步执行行为的各类机制。后续章节将详述这些机制。43.1 A Taxonomy of IPC Facilities图 43-1 汇总了种类丰富的 UNIX 通信与同步机制并将其划分为三大功能类别通信这类机制用于进程间的数据交换。同步这类机制用于对进程或线程的执行动作进行同步。信号尽管信号的设计初衷主要用于其他用途但在某些场景下可作为一种同步手段。信号较少用作通信手段信号编号本身就是一种信息形式实时信号还可携带附属数据一个整数或指针。第 20 章至第 22 章将详细介绍信号。虽然其中部分机制用于同步但人们通常使用**进程间通信IPC**这一统称来指代全部这些机制。Figure 43-1: A taxonomy of UNIX IPC facilities如图 43-1 所示多种机制往往可以实现相近的 IPC 功能。造成这种现象主要有两点原因同类机制在不同 UNIX 变体上各自发展之后才被移植到其他 UNIX 系统。例如FIFO 在 System V 上诞生而流式套接字起源于 BSD。新机制被开发出来用于弥补早期同类机制在设计上的缺陷。例如 POSIX IPC 机制消息队列、信号量与共享内存就是为改进老旧的 System V IPC 机制而设计。 System V IPC是1980年代的优势在老UNIX系统的遗留代码POSIX IPC 是1990年代的设计更新API 更现代、易用新项目优先使用。在某些场景下图 43-1 中归为一类的机制实际提供的功能差异巨大。例如流式套接字可用于跨网络通信而 FIFO 仅支持同一主机上进程间通信。43.2 Communication Facilities图 43-1 列出的各类通信机制可供进程之间交换数据。这些机制也可用于同一进程内多个线程间的数据交换但这种场景很少用到因为线程可通过共享全局变量传递信息。 对于线程间通过共享全局变量传递信息写全局变量还是需要加锁以防止并发竞争问题。我们可将通信机制分为两类数据传输类机制这类机制的核心特征是读写模型。通信时一个进程向 IPC 设施写入数据另一个进程读取数据。这类机制需要在用户内存与内核内存之间发生两次数据拷贝写操作时数据从用户内存拷贝至内核内存读操作时数据再从内核内存拷贝到用户内存。图 43-2 展示管道的这一过程。共享内存共享内存让进程把信息放入一块进程间共享的内存区域以此完成数据交换。内核的实现方式是让每个进程页表项指向同一块物理内存页见第 1026 页图 49-2。进程只需将数据写入共享内存区域其他进程就能够访问该数据。由于通信无需系统调用也不需要在用户内存和内核内存之间拷贝数据共享内存的通信速度极快。Figure 43-2: Exchanging data between two processes using a pipeData transfer我们还可以进一步将数据传输类机制划分为以下子类别字节流管道、FIFO 以及流式套接字传输的数据属于无边界字节流。每次读操作可从 IPC 设施读取任意数量字节与写入方写入的数据块大小无关。该模型延续了传统 UNIX “文件即字节序列” 的思想。 原文此处笔误datagram sockets应为stream sockets流式套接字数据报 socket 属于消息模型。消息System V 消息队列、POSIX 消息队列以及数据报套接字所传输的数据是以带边界的消息为单位。每次读操作读取写入进程发出的整条消息。不能只读取消息的一部分、把剩余数据留在 IPC 设施中也不能在一次读操作里读取多条消息。伪终端伪终端是一种适用于特定场景的通信机制。第 64 章会详细介绍。数据传输类机制与共享内存之间存在几项共性区别特征尽管数据传输机制可以存在多个读取方但读操作具有消费性。读操作会取走数据这份数据不再能被其他进程获取。MSG_PEEK 标志可用于对套接字执行非破坏性读取61.3 节。UDP因特网域数据报套接字支持将单条消息广播或组播给多个接收方61.12 节。读写进程之间会自动同步。如果读取方试图从当前无数据的数据传输设施获取数据则默认情况下读操作将会阻塞直到有进程向该设施写入数据。Shared memory大多数现代 UNIX 系统提供三类共享内存System V 共享内存、POSIX 共享内存以及内存映射。后续章节介绍这些机制时我们会探讨它们之间的差异尤其参见 54.5 节。关于共享内存请注意以下通用要点尽管共享内存通信速度很快但这项速度优势被共享内存操作所需的同步开销抵消。例如当一个进程正在更新共享内存中的数据结构时其他进程不应当访问该结构。信号量是搭配共享内存最常用的同步手段。写入共享内存的数据对所有共享这块内存的进程均可见。这和前面数据传输机制的消费式读取语义形成鲜明对比。43.3 Synchronization Facilities图 43-1 中的同步机制可供进程协调各自的执行行为。借助同步进程可以避免诸如同时更新一块共享内存区域或同一文件片段这类操作。如果缺少同步这类并发更新可能导致应用程序输出错误结果。UNIX 系统提供以下同步机制信号量信号量是由内核维护的整型变量其取值永远不允许小于 0。进程可以增加或减少信号量的值。若尝试将信号量的值减至 0 以下内核会阻塞本次操作直到信号量的值上升到允许执行该操作为止。进程也可以请求非阻塞操作此时内核不会阻塞而是让调用立即返回并携带错误标识说明当前无法执行该操作。信号量的语义由应用程序定义。进程对信号量执行减操作例如从 1 减到 0以独占占用某个共享资源当资源操作完成后对信号量执行加操作释放该共享资源供其他进程使用。二元信号量取值只能为 0 或 1十分常用。但如果应用需要管理多份共享资源实例则会使用最大值等于资源数量的信号量。Linux 同时提供 System V 信号量与 POSIX 信号量二者功能大体相近。文件锁文件锁是专门为协调多进程操作同一文件而设计的同步方式也可用于管控其他共享资源的访问。文件锁分为两类读锁共享锁和写锁排他锁。多个进程可同时持有同一个文件或文件某区域的读锁。但只要一个进程持有文件或文件某区域的写锁其他进程就无法获取该文件或区域的读锁或写锁。Linux 通过flock()和fcntl()系统调用提供文件锁机制。flock()提供简易锁机制进程可对整个文件施加共享锁或排他锁。因其功能有限如今很少使用。fcntl()提供记录锁支持进程在同一个文件的不同区域上施加多组读锁与写锁。互斥锁与条件变量这类同步机制一般配合 POSIX 线程使用详见第 30 章。包含 NPTL 线程实现的 glibc 的 Linux 等部分 UNIX 实现也支持在进程间共享互斥锁与条件变量。SUSv3 标准允许但不强制要求实现支持进程间共享的互斥锁和条件变量。并非所有 UNIX 系统都提供该特性因此它们并不常用于进程间同步。 NPTL表示Native POSIX Thread LibraryLinux 原生 POSIX 线程库。进行进程间同步时机制的选择通常由功能需求决定。如果是协调对文件的访问文件记录锁一般是最优选择。对于其他类型共享资源的访问协调信号量往往是更好的方案。通信机制也可用于同步。例如在 44.3 节我们会演示如何利用管道实现父进程与其子进程间的行为同步。更普遍地说任意数据传输类机制都可用于同步同步操作体现为通过该设施交换消息。自 Linux 2.6.22 内核起Linux 通过eventfd()系统调用提供了一种额外的、非标准同步机制。该系统调用会创建一个 eventfd 对象内核会维护一个与之关联的 8 字节无符号整型变量。该系统调用返回指向此对象的文件描述符。向该文件描述符写入一个整数就会把这个整数加到对象内部的值上。如果对象的值为 0在该文件描述符上执行read()将会阻塞若对象值非 0read()会返回该值并将对象值重置为 0。此外可以使用poll()、select()或epoll检测该对象的值是否非零若非零则该文件描述符标记为可读。应用程序若要使用 eventfd 对象做同步必须先调用eventfd()创建对象再调用fork()创建子进程子进程会继承指向该对象的文件描述符。更多细节请参考eventfd(2)手册页。43.4 Comparing PC Facilities谈及 IPC我们会面临一系列选择初看之下容易令人困惑。在后续介绍各项 IPC 机制的章节中会专门设置小节将每种机制与其他同类机制进行对比。在接下来的内容里我们讨论若干通用考量点这些因素会决定 IPC 机制的选型。IPC object identification and handles for open objects进程若要访问一个 IPC 对象必须具备某种方式来标识该对象一旦对象完成 “打开”进程需要借助某种句柄来引用这个已打开对象。表 43-1 汇总了各类 IPC 机制在这两方面的特性。Table 43-1: Identifiers and handles for various types of IPC facilitiesFacility typeName used to identify objectHandle used to refer to object in programsPipeno namefile descriptorFIFOpathnamefile descriptorUNIX domain socketpathnamefile descriptorInternet domain socketIP address port numberfile descriptorSystem V message queueSystem V IPC keySystem V IPC identifierSystem V semaphoreSystem V IPC keySystem V IPC identifierSystem V shared memorySystem V IPC keySystem V IPC identifierPOSIX message queuePOSIX IPC pathnamemqd_t (message queue descriptor)POSIX named semaphorePOSIX IPC pathnamesem_t * (semaphore pointer)POSIX unnamed semaphoreno namesem_t * (semaphore pointer)POSIX shared memoryPOSIX IPC pathnamefile descriptorAnonymous mappingno namenoneMemory-mapped filepathnamefile descriptorflock() lockpathnamefile descriptorfcntl() lockpathnamefile descriptorFunctionality各类 IPC 机制之间存在功能差异这些差异会影响机制选型。我们首先总结数据传输类机制与共享内存之间的区别数据传输类机制采用读写操作传输的数据只能被一个读取进程消费。写入方与读取方之间的流量控制以及同步机制读取方试图从空的 IPC 设施读取数据时会阻塞均由内核自动处理。该模型非常契合很多应用程序的设计。另一些应用场景则更适合共享内存模型。共享内存允许一个进程将数据暴露给所有共享该内存区域的其他进程。通信 “操作” 十分简单 —— 进程访问共享内存中的数据方式和访问自身虚拟地址空间内其他内存完全一样。但另一方面开发者需要自行处理同步有时还要处理流量控制会增加共享内存方案的复杂度。该模型适合需要维护共享状态的应用例如共享数据结构。对于各类数据传输机制有以下要点值得关注部分数据传输机制以字节流形式传输数据管道、FIFO、流式套接字另一些则是面向消息的消息队列、数据报套接字。选用哪种方式取决于应用场景。应用程序也可以在字节流机制之上自行实现面向消息模型使用分隔符、定长消息或是携带总消息长度的消息头参见 44.8 节。和其他数据传输机制相比System V 与 POSIX 消息队列的一大特色可以为消息设置数字类型或优先级消息的送达顺序可以不同于发送顺序。管道、FIFO 和套接字基于文件描述符实现。这些 IPC 机制都支持多种 I/O 模型详见第 63 章I/O 多路复用select()、poll()系统调用、信号驱动 I/O以及 Linux 特有的epoll接口。这类技术最主要的优势是允许应用程序同时监视多个文件描述符判断其中任意一个是否可执行 I/O 操作。与之相对System V 消息队列不使用文件描述符因此不支持上述这些 I/O 模型。在 Linux 上POSIX 消息队列同样基于文件描述符实现并支持上面提到的各类 I/O 技术。但该行为并未在 SUSv3 标准中规定大多数其他系统实现并不支持这一特性。POSIX 消息队列提供通知机制当原本空的队列收到消息时可以向进程发送信号或者创建一个新线程。UNIX 域套接字具备一项特性支持将文件描述符从一个进程传递给另一个进程。借助该特性一个进程打开文件后可以把这个文件提供给原本无法访问该文件的其他进程。我们将在 61.13.3 节简要介绍该特性。UDP因特网域数据报套接字允许发送方将消息广播或组播给多个接收者。我们会在 61.12 节简要介绍该特性。对于进程同步机制有以下要点值得关注通过fcntl()施加的记录锁其归属权属于加锁进程。内核利用该归属属性检测死锁即两个或多个进程各自持有锁互相阻塞对方后续锁请求的场景。一旦发生死锁内核会拒绝其中一个进程的锁请求并从fcntl()调用返回错误标识出现死锁。System V 和 POSIX 信号量不具备归属属性内核不会对信号量做死锁检测。当持有锁的进程终止时fcntl()施加的记录锁会自动释放。System V 信号量通过撤销undo机制提供类似能力但该机制并非在所有场景下都可靠见 47.8 节。POSIX 信号量没有与之对应的功能。Network communication在图 43-1 列出的全部 IPC 方式中只有套接字支持进程跨网络通信。套接字通常运行在两个域之一UNIX 域用于同一主机上进程间通信因特网域用于 TCP/IP 网络互联的不同主机之间的进程通信。将使用 UNIX 域套接字的程序修改为因特网域套接字版本往往只需少量改动。因此基于 UNIX 域套接字开发的应用可以用较低成本改造为具备网络通信能力的程序。Portability现代 UNIX 实现大多支持图 43-1 列出的绝大多数 IPC 机制。但是 POSIX IPC 机制消息队列、信号量、共享内存的普及程度不如对应的 System V IPC在老旧 UNIX 系统上尤其明显。Linux 直到 2.6.x 内核系列才实现 POSIX 消息队列并完整支持 POSIX 信号量。因此从可移植性角度考虑System V IPC 可能优于 POSIX IPC。System V IPC design issuesSystem V IPC 机制独立于传统 UNIX I/O 模型设计因此存在若干特殊之处导致其编程接口使用起来更为复杂。对应的 POSIX IPC 机制正是为了解决这些问题而设计。以下几点需要特别留意System V IPC 是无连接的。这类机制不存在类似文件描述符那样、用于指代已打开 IPC 对象的句柄。后续章节我们有时会提到 “打开” 一个 System V IPC 对象但这只是简称实际指获取引用该对象的句柄。内核不会记录进程已经 “打开” 该对象这和其他 IPC 对象不同。这意味着内核无法维护当前正在使用该对象的进程引用计数。因此应用程序需要额外编写代码来判断何时可以安全删除对象。System V IPC 的编程接口和传统 UNIX I/O 模型不统一使用整型 key 与 IPC 标识符而非路径名与文件描述符其 API 本身也过于复杂。这点在 System V 信号量上体现得尤为明显参见 47.11 节与 53.5 节。与之相对内核会为 POSIX IPC 对象维护打开引用计数简化了对象删除时机的判断。此外POSIX IPC 提供的接口更简洁并且和传统 UNIX 模型保持一致。Accessibility表 43-2 的第二列汇总了各类 IPC 对象的一项重要特性权限方案该方案规定哪些进程可以访问此对象。下面针对各类权限方案补充细节部分 IPC 机制例如 FIFO 和套接字的对象名称存在于文件系统中访问权限由关联的文件权限掩码决定掩码规定了属主、属组和其他用户的权限15.4 节。虽然 System V IPC 对象并不存放在文件系统内但每个对象都附带权限掩码其语义和文件权限类似。少数 IPC 机制管道、匿名内存映射仅允许亲缘进程访问。这里的亲缘进程指通过fork()创建的进程。若两个进程要访问该对象必须由其中一个进程创建对象再调用fork()。fork()发生后子进程继承指向该对象的句柄于是父子进程就可以共享该对象。POSIX 未命名信号量的可访问性由承载该信号量的共享内存区域的访问权限决定。进程若要对文件加锁必须拥有指向该文件的文件描述符实际就是进程要有打开该文件的权限。因特网域套接字的访问连接或发送数据报本身没有权限限制。如有需要访问控制必须在应用程序内部实现。Table 43-2: Accessibility and persistence for various types of IPC facilitiesFacility typeAccessibilityPersistencePipeonly by related processesprocessFIFOpermissions maskprocessUNIX domain socketpermissions maskprocessInternet domain socketby any processprocessSystem V message queuepermissions maskkernelSystem V semaphorepermissions maskkernelSystem V shared memorypermissions maskkernelPOSIX message queuepermissions maskkernelPOSIX named semaphorepermissions maskkernelPOSIX unnamed semaphorepermissions of underlying memorydependsPOSIX shared memorypermissions maskkernelAnonymous mappingonly by related processesprocessMemory-mapped filepermissions maskfile systemflock() file lockopen() of fileprocessfcntl() file lockopen() of fileprocessPersistence**持久化persistence**这一术语指 IPC 对象的生命周期参见表 43-2 第三列。我们可以区分三类持久化特性进程级持久化具备进程级持久化的 IPC 对象只要至少还有一个进程保持打开该对象对象就会持续存在。一旦所有进程都关闭该对象内核会释放与此对象相关的全部资源所有未读取的数据都会被销毁。管道、FIFO、套接字是进程级持久化 IPC 机制的例子。FIFO 的数据持久化和它的名称持久化不是一回事。FIFO 在文件系统中拥有文件名即便所有引用 FIFO 的文件描述符都已关闭这个文件名仍然保留。内核级持久化内核级持久化的 IPC 对象会一直存在直到被显式删除或者系统关机。对象的生命周期与是否有进程打开它无关。举例来说一个进程可以创建对象、写入数据然后关闭对象或直接退出后续另一个进程可以打开该对象并读取里面的数据。System V IPC 与 POSIX IPC 都属于内核级持久化机制。后续章节介绍这些机制时示例程序会利用这个特性针对每一种机制我们分别编写独立程序用于创建对象、删除对象以及执行通信或同步操作。文件系统级持久化具备文件系统级持久化的 IPC 对象系统重启后信息依然保留对象会持续存在直到被显式删除。唯一拥有文件系统级持久化特性的 IPC 对象就是基于内存映射文件实现的共享内存。Performance在某些场景下不同 IPC 机制会呈现出明显的性能差异。但在后续章节中我们一般不会做性能对比原因如下IPC 机制的性能未必是应用整体性能中的关键因素也不一定是 IPC 选型的唯一依据。各类 IPC 机制的相对性能会随不同 UNIX 实现、不同 Linux 内核版本而变化。最重要的一点IPC 机制的性能取决于具体使用方式与运行环境。相关影响因素包括每次 IPC 交互传输的数据单元大小、IPC 对象上积压的未读数据量、传输每个数据单元是否需要进程上下文切换以及系统上的其他负载。如果 IPC 性能至关重要那么没有任何方式可以替代在与目标系统一致的环境下运行贴合业务场景的基准测试。为此可以编写一层抽象软件层向应用屏蔽底层 IPC 机制的细节之后在该抽象层下替换不同的 IPC 实现以此测试各类方案的性能。43.5 Summary本章概述了进程以及线程之间用于通信与行为同步的各类机制。Linux 提供的通信机制包括管道、FIFO、套接字、消息队列与共享内存同步机制包含信号量与文件锁。在很多场景下完成同一个任务我们可以在多种通信、同步技术之间做选择。本章从多个维度对比了不同方案旨在突出那些会影响技术选型的差异点。后续章节我们将对每一种通信与同步机制展开更为详尽的讲解。