蚊章汇 - IPC
type: channel
link: https://linuxcn.bwsl.wang/tag/IPC/
items: 免费电子书《Linux 进程间通信指南》介绍
items: Linux 下的进程间通信：套接字和信号
items: Linux 下的进程间通信：使用管道和消息队列
items: Linux 下的进程间通信：共享存储

免费电子书《Linux 进程间通信指南》介绍
type: item
date: 2020-01-30T19:56:51+08:00
description: 这本免费的电子书使经验丰富的程序员更深入了解 Linux 中进程间通信（IPC）的核心概念和机制。让一个软件过程与另一个软件过程进行对话是一个微妙的平衡行为。但是，它对于应用程序而言可能是至关重要的功能，因此这是任何从事复杂项目的程序员都必须解决的问题。你的应用程序是否需要启动由其它软件处理的工作；监视外设或网络上正在执行的操作；或者检测来自其它来源的信号，当你的软件需要依赖其自身代码之外的东西来知道下一步做什么或什么时候做时，你就需要考虑 进程间通信 inter-process communication （IPC）。这在 Unix 操作系统上已经由来已久了，这可能是因为人们早期预期软件会来自各种来源。按照相同的传统，Linux 提供了一些同样的 IPC 接口和一些新接口。Linux 内核具有多种 IPC 方法，util-linux 包包含了 ipcmk、ipcrm、ipcs 和 lsipc 命令，用于监视和管理 IPC 消息。显示进程间通信信息在尝试 IPC 之前，你应该知道系统上已经有哪些 IPC 设施。lsipc 命令提供了该信息。RESOURCE DESCRIPTION LIMIT USED USE% MSGMNI Number of message queues 32000 0 0.00% MSGMAX Max size of message (byt.. 8192 - - MSGMNB Default max size of queue 16384 - - SHMMNI Shared memory segments 4096 79 1.93% SHMALL Shared memory pages 184[...] 25452 0.00% SHMMAX Max size of shared memory 18446744073692774399 SHMMIN Min size of shared memory 1 - - SEMMNI Number of semaphore ident 32000 0 0.00% SEMMNS Total number of semaphore 1024000.. 0 0.00% SEMMSL Max semaphores per semap 32000 - - SEMOPM Max number of operations p 500 - - SEMVMX Semaphore max value 32767 - -你可能注意到，这个示例清单包含三种不同类型的 IPC 机制，每种机制在 Linux 内核中都是可用的：消息（MSG）、共享内存（SHM）和信号量（SEM）。你可以用 ipcs 命令查看每个子系统的当前活动：$ ipcs ------ Message Queues Creators/Owners --- msqid perms cuid cgid [...] ------ Shared Memory Segment Creators/Owners shmid perms cuid cgid [...] 557056 700 seth users [...] 3571713 700 seth users [...] 2654210 600 seth users [...] 2457603 700 seth users [...] ------ Semaphore Arrays Creators/Owners --- semid perms cuid cgid [...]这表明当前没有消息或信号量阵列，但是使用了一些共享内存段。你可以在系统上执行一个简单的示例，这样就可以看到正在工作的系统之一。它涉及到一些 C 代码，所以你必须在系统上有构建工具。必须安装这些软件包才能从源代码构建软件，这些软件包的名称取决于发行版，因此请参考文档以获取详细信息。例如，在基于 Debian 的发行版上，你可以在 wiki 的构建教程部分了解构建需求，而在基于 Fedora 的发行版上，你可以参考该文档的从源代码安装软件部分。创建一个消息队列你的系统已经有一个默认的消息队列，但是你可以使用 ipcmk 命令创建你自己的消息队列：$ ipcmk --queue Message queue id: 32764编写一个简单的 IPC 消息发送器，为了简单，在队列 ID 中硬编码：#include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <string.h> struct msgbuffer { char text[24]; } message; int main() { int msqid = 32764; strcpy…

Linux 下的进程间通信：套接字和信号
type: item
date: 2019-06-03T07:44:52+08:00
description: 学习在 Linux 中进程是如何与其他进程进行同步的。本篇是 Linux 下进程间通信（IPC）系列的第三篇同时也是最后一篇文章。第一篇文章聚焦在通过共享存储（文件和共享内存段）来进行 IPC，第二篇文章则通过管道（无名的或者命名的）及消息队列来达到相同的目的。这篇文章将目光从高处（套接字）然后到低处（信号）来关注 IPC。代码示例将用力地充实下面的解释细节。套接字正如管道有两种类型（命名和无名）一样，套接字也有两种类型。IPC 套接字（即 Unix 套接字）给予进程在相同设备（主机）上基于通道的通信能力；而网络套接字给予进程运行在不同主机的能力，因此也带来了网络通信的能力。网络套接字需要底层协议的支持，例如 TCP（传输控制协议）或 UDP（用户数据报协议）。与之相反，IPC 套接字依赖于本地系统内核的支持来进行通信；特别的，IPC 通信使用一个本地的文件作为套接字地址。尽管这两种套接字的实现有所不同，但在本质上，IPC 套接字和网络套接字的 API 是一致的。接下来的例子将包含网络套接字的内容，但示例服务器和客户端程序可以在相同的机器上运行，因为服务器使用了 localhost（127.0.0.1）这个网络地址，该地址表示的是本地机器上的本地机器地址。套接字以流的形式（下面将会讨论到）被配置为双向的，并且其控制遵循 C/S（客户端/服务器端）模式：客户端通过尝试连接一个服务器来初始化对话，而服务器端将尝试接受该连接。假如万事顺利，来自客户端的请求和来自服务器端的响应将通过管道进行传输，直到其中任意一方关闭该通道，从而断开这个连接。一个迭代服务器（只适用于开发）将一直和连接它的客户端打交道：从最开始服务第一个客户端，然后到这个连接关闭，然后服务第二个客户端，循环往复。这种方式的一个缺点是处理一个特定的客户端可能会挂起，使得其他的客户端一直在后面等待。生产级别的服务器将是并发的，通常使用了多进程或者多线程的混合。例如，我台式机上的 Nginx 网络服务器有一个 4 个 工人 worker 的进程池，它们可以并发地处理客户端的请求。在下面的代码示例中，我们将使用迭代服务器，使得我们将要处理的问题保持在一个很小的规模，只关注基本的 API，而不去关心并发的问题。最后，随着各种 POSIX 改进的出现，套接字 API 随着时间的推移而发生了显著的变化。当前针对服务器端和客户端的示例代码特意写的比较简单，但是它着重强调了基于流的套接字中连接的双方。下面是关于流控制的一个总结，其中服务器端在一个终端中开启，而客户端在另一个不同的终端中开启：服务器端等待客户端的连接，对于给定的一个成功连接，它就读取来自客户端的数据。为了强调是双方的会话，服务器端会对接收自客户端的数据做回应。这些数据都是 ASCII 字符代码，它们组成了一些书的标题。客户端将书的标题写给服务器端的进程，并从服务器端的回应中读取到相同的标题。然后客户端和服务器端都在屏幕上打印出标题。下面是服务器端的输出，客户端的输出也和它完全一样：Listening on port 9876 for clients... War and Peace Pride and Prejudice The Sound and the Fury示例 1. 使用套接字的客户端程序#include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include "sock.h" void report(const char* msg, int terminate) { perror(msg); if (terminate) exit(-1); /* failure */ } int main() { int fd = socket(AF_INET, /* network versus AF_LOCAL */ SOCK_STREAM, /* reliable, bidirectional: TCP */ 0); /* system picks underlying protocol */ if (fd < 0) report("socket", 1); /* terminate */ /* bind the server's local address in memory */ struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); /* clear the byt…

Linux 下的进程间通信：使用管道和消息队列
type: item
date: 2019-05-12T21:43:00+08:00
description: 学习在 Linux 中进程是如何与其他进程进行同步的。本篇是 Linux 下进程间通信（IPC）系列的第二篇文章。第一篇文章 聚焦于通过共享文件和共享内存段这样的共享存储来进行 IPC。这篇文件的重点将转向管道，它是连接需要通信的进程之间的通道。管道拥有一个写端用于写入字节数据，还有一个读端用于按照先入先出的顺序读入这些字节数据。而这些字节数据可能代表任何东西：数字、员工记录、数字电影等等。管道有两种类型，命名管道和无名管道，都可以交互式的在命令行或程序中使用它们；相关的例子在下面展示。这篇文章也将介绍内存队列，尽管它们有些过时了，但它们不应该受这样的待遇。在本系列的第一篇文章中的示例代码承认了在 IPC 中可能受到竞争条件（不管是基于文件的还是基于内存的）的威胁。自然地我们也会考虑基于管道的 IPC 的安全并发问题，这个也将在本文中提及。针对管道和内存队列的例子将会使用 POSIX 推荐使用的 API，POSIX 的一个核心目标就是线程安全。请查看一些 mq\_open 函数的 man 页，这个函数属于内存队列的 API。这个 man 页中有关 特性 的章节带有一个小表格：接口特性值mq_open()线程安全MT-Safe上面的 MT-Safe（MT 指的是 多线程 multi-threaded ）意味着 mq_open 函数是线程安全的，进而暗示是进程安全的：一个进程的执行和它的一个线程执行的过程类似，假如竞争条件不会发生在处于相同进程的线程中，那么这样的条件也不会发生在处于不同进程的线程中。MT-Safe 特性保证了调用 mq_open 时不会出现竞争条件。一般来说，基于通道的 IPC 是并发安全的，尽管在下面例子中会出现一个有关警告的注意事项。无名管道首先让我们通过一个特意构造的命令行例子来展示无名管道是如何工作的。在所有的现代系统中，符号 | 在命令行中都代表一个无名管道。假设我们的命令行提示符为 %，接下来考虑下面的命令：## 写入方在 | 左边，读取方在右边 % sleep 5 | echo "Hello, world!" sleep 和 echo 程序以不同的进程执行，无名管道允许它们进行通信。但是上面的例子被特意设计为没有通信发生。问候语 “Hello, world!” 出现在屏幕中，然后过了 5 秒后，命令行返回，暗示 sleep 和 echo 进程都已经结束了。这期间发生了什么呢？在命令行中的竖线 | 的语法中，左边的进程（sleep）是写入方，右边的进程（echo）为读取方。默认情况下，读取方将会阻塞，直到从通道中能够读取到字节数据，而写入方在写完它的字节数据后，将发送 流已终止 end-of-stream 的标志。（即便写入方过早终止了，一个流已终止的标志还是会发给读取方。）无名管道将保持到写入方和读取方都停止的那个时刻。在上面的例子中，sleep 进程并没有向通道写入任何的字节数据，但在 5 秒后就终止了，这时将向通道发送一个流已终止的标志。与此同时，echo 进程立即向标准输出（屏幕）写入问候语，因为这个进程并不从通道中读入任何字节，所以它并没有等待。一旦 sleep 和 echo 进程都终止了，不会再用作通信的无名管道将会消失然后返回命令行提示符。下面这个更加实用的示例将使用两个无名管道。我们假定文件 test.dat 的内容如下：this is the way the world ends下面的命令：% cat test.dat | sort | uniq会将 cat（ 连接 concatenate 的缩写）进程的输出通过管道传给 sort 进程以生成排序后的输出，然后将排序后的输出通过管道传给 uniq 进程以消除重复的记录（在本例中，会将两次出现的 “the” 缩减为一个）：ends is the this way world下面展示的情景展示的是一个带有两个进程的程序通过一个无名管道通信来进行通信。示例 1. 两个进程通过一个无名管道来进行通信#include <sys/wait.h> /* wait */ #include <stdio.h> #include <stdlib.h> /* exit functions */ #include <unistd.h> /* read, write, pipe, _exit */ #include <string.h> #define ReadEnd 0 #define WriteEnd 1 void report_and_exit(const char* msg) { perror(msg); exit(-1); /** failure **/ } int main() { int pipeFDs[2]; /* two file descr…

Linux 下的进程间通信：共享存储
type: item
date: 2019-05-07T20:26:00+08:00
description: 学习在 Linux 中进程是如何与其他进程进行同步的。本篇是 Linux 下进程间通信（IPC）系列的第一篇文章。这个系列将使用 C 语言代码示例来阐明以下 IPC 机制：共享文件共享内存（使用信号量）管道（命名的或非命名的管道）消息队列套接字信号在聚焦上面提到的共享文件和共享内存这两个机制之前，这篇文章将带你回顾一些核心的概念。核心概念进程是运行着的程序，每个进程都有着它自己的地址空间，这些空间由进程被允许访问的内存地址组成。进程有一个或多个执行线程，而线程是一系列执行指令的集合：单线程进程就只有一个线程，而多线程的进程则有多个线程。一个进程中的线程共享各种资源，特别是地址空间。另外，一个进程中的线程可以直接通过共享内存来进行通信，尽管某些现代语言（例如 Go）鼓励一种更有序的方式，例如使用线程安全的通道。当然对于不同的进程，默认情况下，它们不能共享内存。有多种方法启动之后要进行通信的进程，下面所举的例子中主要使用了下面的两种方法：一个终端被用来启动一个进程，另外一个不同的终端被用来启动另一个。在一个进程（父进程）中调用系统函数 fork，以此生发另一个进程（子进程）。第一个例子采用了上面使用终端的方法。这些代码示例的 ZIP 压缩包可以从我的网站下载到。共享文件程序员对文件访问应该都已经很熟识了，包括许多坑（不存在的文件、文件权限损坏等等），这些问题困扰着程序对文件的使用。尽管如此，共享文件可能是最为基础的 IPC 机制了。考虑一下下面这样一个相对简单的例子，其中一个进程（生产者 producer）创建和写入一个文件，然后另一个进程（消费者 consumer）从这个相同的文件中进行读取： writes +-----------+ reads producer-------->| disk file |<-------consumer +-----------+在使用这个 IPC 机制时最明显的挑战是竞争条件可能会发生：生产者和消费者可能恰好在同一时间访问该文件，从而使得输出结果不确定。为了避免竞争条件的发生，该文件在处于读或写状态时必须以某种方式处于被锁状态，从而阻止在写操作执行时和其他操作的冲突。在标准系统库中与锁相关的 API 可以被总结如下：生产者应该在写入文件时获得一个文件的排斥锁。一个排斥锁最多被一个进程所拥有。这样就可以排除掉竞争条件的发生，因为在锁被释放之前没有其他的进程可以访问这个文件。消费者应该在从文件中读取内容时得到至少一个共享锁。多个读取者可以同时保有一个共享锁，但是没有写入者可以获取到文件内容，甚至在当只有一个读取者保有一个共享锁时。共享锁可以提升效率。假如一个进程只是读入一个文件的内容，而不去改变它的内容，就没有什么原因阻止其他进程来做同样的事。但如果需要写入内容，则很显然需要文件有排斥锁。标准的 I/O 库中包含一个名为 fcntl 的实用函数，它可以被用来检查或者操作一个文件上的排斥锁和共享锁。该函数通过一个文件描述符（一个在进程中的非负整数值）来标记一个文件（在不同的进程中不同的文件描述符可能标记同一个物理文件）。对于文件的锁定， Linux 提供了名为 flock 的库函数，它是 fcntl 的一个精简包装。第一个例子中使用 fcntl 函数来暴露这些 API 细节。示例 1. 生产者程序#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FileName "data.dat" #define DataString "Now is the winter of our discontent\nMade glorious summer by this sun of York\n" void report_and_exit(const char* msg) { perror(msg); exit(-1); /* EXIT_FAILURE */ } int main() { struct flock lock; lock.l_type = F_WRLCK; /* read/write (exclusive versus shared) lock */ lock.l_whence = SEEK_SET; /* base for seek offsets */ lock.l_start = 0; /* 1st byte in file */ lock.l_len = 0; /* 0 here means 'until EOF' */ lock.l_pid = getpid(); /* process id */ int fd; /* file …