文章目录
1. 进程间通信介绍
首先简单介绍 程间通信的目的、发展与分类:
1.1 进程间通信的目的
进程间通信的主要目的是 使独立运行的进程能够相互协作、共享数据和资源,以实现更复杂的任务和功能。通过进程间通信,不同的进程可以在系统中进行协调合作,实现以下几个主要目标:
数据共享:进程间通信允许多个进程访问和共享相同的数据或资源,从而避免了数据复制的开销,提高了运行效率。
信息传递:进程可以通过通信机制向其他进程发送消息、通知或信号,以实现进程间的协调和同步。
资源共享:进程间通信使得多个进程可以共享系统资源,如文件、设备、内存等,从而更加高效地利用系统资源。
进程协作:不同的进程可以通过通信实现协同工作,各自承担不同的任务,最终完成一个更大规模的工作。
并发控制:通过进程间通信,可以实现对共享资源的并发访问控制,确保各个进程安全地访问共享资源,避免竞争条件和死锁问题的发生。
1.2 进程间通信的发展
进程间通信发展主要分为下面三个阶段:
管道(Pipes): 管道是最早的进程间通信方式之一,主要用于在同一台计算机上的父子进程或兄弟进程之间进行通信。管道是单向的,通常用于将一个进程的输出连接到另一个进程的输入。
System V进程间通信(System V IPC): System V IPC是一组通信机制,包括消息队列(Message Queues)、信号量(Semaphores)和共享内存(Shared
Memory)。这些机制允许不同进程之间在同一计算机上进行数据交换和共享。这些通信机制通常比管道更灵活,允许更复杂的数据结构和通信模式。POSIX进程间通信: POSIX进程间通信是对System V IPC的一种替代,提供了更简单和更具移植性的进程间通信方法。它包括消息队列、信号量、共享内存等,但使用起来更符合POSIX标准,因此在不同UNIX系统之间更具可移植性。
1.3 进程间通信的分类
根据上面介绍的进程间通信的发展,可以给进程间通信作如下分类:
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
1.4 命令: ipcs && ipcrm
ipcs 和 ipcrm 都是在 UNIX/Linux 系统下用于管理和操作进程间通信(IPC)资源的命令。下面对它们进行详细解释:
-
ipcs命令:ipcs命令用于显示当前系统中的 IPC 资源信息。- 可以使用不同的选项来查看不同类型的 IPC 资源,包括消息队列、共享内存和信号量。
- 常见的选项包括:
-q:显示消息队列的信息。-m:显示共享内存的信息。-s:显示信号量的信息。
- 示例:
ipcs -q可以查看当前系统中的消息队列信息。
-
ipcrm命令:ipcrm命令用于删除 IPC 资源,包括消息队列、共享内存和信号量。- 可以指定不同的选项来删除特定类型的 IPC 资源。
- 常见的选项包括:
-q:删除指定的消息队列。-m:删除指定的共享内存。-s:删除指定的信号量。
- 示例:
ipcrm -q <消息队列ID>可以删除指定的消息队列。
需要注意的是,使用
ipcrm命令删除 IPC 资源需要谨慎操作,因为删除后无法恢复,并且可能会影响正在使用该资源的进程。在使用ipcrm命令时,务必确保正确指定要删除的资源类型和标识符。
2. 管道
2.1 管道 概念
管道是Unix中最古老的进程间通信的形式。
我们把从 一个进程连接到另一个进程的一个数据流称为一个“管道”
在Unix和类Unix系统中,管道通常是通过 | 符号来表示的,它可以将一个进程的标准输出连接到另一个进程的标准输入,形成一个数据流的传输通道。
管道的基本特点包括:
- 单向传输: 管道是单向的,一个进程的输出只能传递给另一个进程的输入,而不能反向传输。
- 线性连接: 管道连接的进程通常是线性的,即一个进程的输出可以直接连接到另一个进程的输入,形成一个线性的数据流传输。
- 实时数据流: 管道传输的数据是实时的,即一个进程写入管道的数据可以立即被另一个进程读取。

2.2 匿名管道 / pipe
pipe() 是一个系统调用,用于创建匿名管道(Anonymous Pipe),它是进程间通信(IPC)的一种简单机制:
pipe() 函数的原型
#include <unistd.h>
int pipe(int pipefd[2]);
参数
pipefd:一个整型数组,包含两个文件描述符:pipefd[0]:用于从管道读取数据的文件描述符(读端)。pipefd[1]:用于向管道写入数据的文件描述符(写端)。
返回值
- 成功时返回 0。
- 失败时返回 -1,并设置
errno来指示错误的类型。

下面是一个简单的例子,演示了如何使用 pipe() 在父子进程之间进行通信:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
pid_t cpid;
char buf;
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 创建子进程
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { // 子进程
close(pipefd[1]); // 关闭写端
// 从管道中读取数据
while (read(pipefd[0], &buf, 1) > 0) {
write(STDOUT_FILENO, &buf, 1);
}
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]); // 关闭读端
_exit(EXIT_SUCCESS);
} else { // 父进程
close(pipefd[0]); // 关闭读端
// 向管道中写入数据
const char *msg = "Hello from parent";
write(pipefd[1], msg, sizeof(msg));
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
exit(EXIT_SUCCESS);
}
}
在这个例子中,父进程创建了一个管道,然后创建了一个子进程。父进程向管道写入消息,子进程从管道读取消息并将其输出到标准输出
2.3 理解管道
① 文件描述符角度 理解管道
通过下面三步去理解:



通过这种方式,父进程和子进程利用共享的文件描述符实现了进程间的通信,即管道在文件描述符层面上的工作原理。
② 内核角度 探寻管道本质

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”
2.4 管道读写规则
对于不同情况,管道有以下的读写规则:
-
读端没有数据可读时的行为:
- 如果管道没有设置为非阻塞模式(
O_NONBLOCK disable):“读取操作(read调用)会阻塞,即进程会暂停执行,直到有数据可读为止。 - 如果管道设置了非阻塞模式(
O_NONBLOCK enable):读取操作将立即返回-1,并且errno会被设置为EAGAIN或EWOULDBLOCK,表示当前没有数据可用。
- 如果管道没有设置为非阻塞模式(
-
管道满时的行为:
- 当写入端向管道写入数据而管道已经满了时: 如果管道没有设置为非阻塞模式,写入操作(write调用)会阻塞,直到有进程从管道中读取数据,释放空间。
- 如果管道设置了非阻塞模式: 写入操作将立即返回-1,并且
errno会被设置为EAGAIN,表示管道已满,无法写入更多数据。
-
管道关闭的影响:
- 当所有写入端对应的文件描述符(
write端)被关闭时,读取操作(read调用)会返回0,表示已经读取到了所有数据,并且没有更多数据可以读取了。 - 当所有读取端对应的文件描述符(
read端)被关闭时,写入操作(write调用)会产生信号SIGPIPE。默认情况下,这会导致写入进程终止,除非进程通过信号处理机制忽略或处理该信号。
- 当所有写入端对应的文件描述符(
-
原子性的保证:
- 在Linux中,当要写入的数据量不超过
PIPE_BUF(通常是4096字节): 时,写入操作是原子性的,即要么写入的数据全部成功,要么一个字节也不会写入。这保证了小于等于PIPE_BUF大小的写操作是原子的。 - 当要写入的数据量大于
PIPE_BUF时: Linux不再保证写入的原子性。这时候写入操作可能会被信号打断,或者只写入部分数据。
- 在Linux中,当要写入的数据量不超过
2.5 管道的特点
-
亲缘关系进程之间通信:
- 管道通常由一个进程创建,并且通常用于具有亲缘关系(父子关系或兄弟关系)的进程之间进行通信。(管道在创建时与创建它的进程相关联)
-
流式通信:
- 管道提供流式服务,数据可以按顺序从一个进程流向另一个进程,类似于数据流的形式。
-
管道生命周期与进程相关:
- 管道的生命周期通常与创建它的进程相关联。一旦所有引用该管道的进程都关闭了对应的文件描述符,管道就会被操作系统自动释放。
-
管道生命周期与进程相关:
- 管道操作在内核中会进行同步和互斥管理,确保在多进程访问时数据的正确传输和读写操作的安全性。
-
半双工特性:
- 管道是半双工的,这意味着数据只能在一个方向上流动。如果需要双向通信,通常需要创建两个管道,或者采用其他的进程间通信机制,比如命名管道或共享内存。
-
数据传输的原子性:
- 当要写入的数据量不超过PIPE_BUF(通常为4096字节)时,Linux系统会保证写操作的原子性。这意味着要么写入的所有数据都成功传输到管道,要么一个字节也不会写入。

(利用管道实现双向通信)
2.6 命名管道
命名管道(Named Pipe)是一种特殊类型的文件,它允许无关的进程间进行双向通信。:与匿名管道不同的是,命名管道可以在文件系统中被命名,并通过文件名来进行访问,因此它也被称为 FIFO(First In, First Out)。
① 特点和用途:
- 文件系统中的特殊文件:
- 命名管道在文件系统中表现为一个特殊类型的文件,类似于普通文件,但具有特定的FIFO属性。
- 无关进程的通信:
- 不像匿名管道那样仅限于具有亲缘关系的进程,命名管道可以由任意进程访问,只要它们知道该管道的文件名。
- 双向通信:
- 命名管道支持双向通信,进程可以同时在管道的两端进行读写操作。
- 生命周期与文件系统相关联:
- 命名管道的生命周期与文件系统相关联。一旦创建并且没有进程引用它,文件系统会在最后一个进程关闭它时将其删除。
- 用途:
- 命名管道通常用于解决多个进程间的数据传输问题,特别是在需要非阻塞通信、长期存在的通信链路或者需要在无关进程间传递大量数据时。
② 创建和使用命名管道:
- 在
Unix/Linux系统中,可以直接在命令行使用命令mkfifo来创建命名管道,例如:
mkfifo mypipe
这将在当前目录创建一个名为 mypipe 的命名管道文件。然后可以像操作普通文件一样,通过文件名在不同的进程中进行数据读写操作。
命名管道也可以在程序中创建,利用mkfifo:
int mkfifo(const char *filename,mode_t mode);
③ 注意事项:
- 命名管道的数据是以字节流的形式进行传输的,因此需要进程在读取时处理好数据的分隔和解析。
- 使用命名管道时需要注意同步和互斥问题,以避免多进程同时操作导致的数据错乱或丢失。
④ 命名管道的打开规则
-
以读模式打开 FIFO:
- 如果以只读方式(
O_RDONLY)打开 FIFO,并且O_NONBLOCK没有设置(即阻塞模式),那么进程将阻塞,直到有其他进程以写方式打开该 FIFO 为止。这种情况下,打开操作会一直等待,直到 FIFO 可以被成功打开为止。 - 如果以只读方式打开 FIFO 并且设置了
O_NONBLOCK(非阻塞模式),那么open函数会立即返回成功,即使当前没有进程以写方式打开该 FIFO。
- 如果以只读方式(
-
以写模式打开 FIFO:
- 如果以只写方式(
O_WRONLY)打开 FIFO,并且O_NONBLOCK没有设置(阻塞模式),那么进程将阻塞,直到有其他进程以读方式打开该 FIFO。打开操作会一直等待,直到 FIFO 可以被成功打开为止。 - 如果以只写方式打开 FIFO 并且设置了
O_NONBLOCK(非阻塞模式),那么open函数会立即返回失败,并且错误码为ENXIO,表示没有其他进程以读方式打开该 FIFO,无法进行写操作。
- 如果以只写方式(
⑤ 实例1:用命名管道实现文件拷贝
我们用示例代码展示:如何使用命名管道在两个进程之间进行文件拷贝:
- 读取进程(读文件并写入命名管道):
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_FILE "myfifo"
#define MAX_BUFFER_SIZE 1024
int main() {
int fd_fifo;
FILE *fp_source;
char buffer[MAX_BUFFER_SIZE];
// 打开命名管道(写模式)
fd_fifo = open(FIFO_FILE, O_WRONLY);
// 打开源文件(需要拷贝的文件)
fp_source = fopen("source.txt", "r");
// 从源文件读取内容,并写入命名管道
while (fgets(buffer, MAX_BUFFER_SIZE, fp_source) != NULL) {
write(fd_fifo, buffer, sizeof(buffer));
}
// 关闭文件和管道
fclose(fp_source);
close(fd_fifo);
return 0;
}
- 写入进程(从命名管道读取数据并写入目标文件):
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_FILE "myfifo"
#define MAX_BUFFER_SIZE 1024
int main() {
int fd_fifo;
FILE *fp_dest;
char buffer[MAX_BUFFER_SIZE];
ssize_t bytes_read;
// 打开命名管道(读模式)
fd_fifo = open(FIFO_FILE, O_RDONLY);
// 创建目标文件(拷贝后的文件)
fp_dest = fopen("destination.txt", "w");
// 从命名管道读取内容,并写入目标文件
while ((bytes_read = read(fd_fifo, buffer, sizeof(buffer))) > 0) {
fwrite(buffer, 1, bytes_read, fp_dest);
}
// 关闭文件和管道
fclose(fp_dest);
close(fd_fifo);
return 0;
}
此时只需执行读进程与写进程就可以直接实现文件拷贝
⑥ 实例2:用命名管道实现server&client通信
在这个例子中,我们将展示如何使用命名管道(FIFO)来实现一个简单的服务器(server)和客户端(client)之间的通信。服务器将从客户端接收消息,并且可以向客户端发送响应。
- 服务器端
- 服务器端负责接收来自客户端的消息,并可以发送响应。
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_FILE "myfifo"
int main() {
int fd_fifo;
char read_buffer[BUFSIZ];
char write_buffer[BUFSIZ];
int bytes_read;
// 创建命名管道(如果不存在)
mkfifo(FIFO_FILE, 0666);
printf("Server started, waiting for clients...\n");
// 打开命名管道(读模式)
fd_fifo = open(FIFO_FILE, O_RDONLY);
while (1) {
// 从命名管道中读取数据
bytes_read = read(fd_fifo, read_buffer, sizeof(read_buffer));
if (bytes_read > 0) {
read_buffer[bytes_read] = '\0';
printf("Received: %s\n", read_buffer);
// 模拟处理消息(这里简单地回复消息)
sprintf(write_buffer, "Server received: %s", read_buffer);
// 打开命名管道(写模式)
int fd_write = open(FIFO_FILE, O_WRONLY);
write(fd_write, write_buffer, strlen(write_buffer) + 1);
close(fd_write);
}
}
// 关闭命名管道
close(fd_fifo);
unlink(FIFO_FILE);
return 0;
}
- 客户端
- 客户端向服务器发送消息,并等待服务器的响应。
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_FILE "myfifo"
int main() {
int fd_fifo;
char write_buffer[BUFSIZ];
char read_buffer[BUFSIZ];
int bytes_read;
// 打开命名管道(写模式)
fd_fifo = open(FIFO_FILE, O_WRONLY);
while (1) {
// 从标准输入读取消息
printf("Enter message to send: ");
fgets(write_buffer, sizeof(write_buffer), stdin);
write_buffer[strlen(write_buffer) - 1] = '\0'; // 去除末尾的换行符
// 将消息写入命名管道
write(fd_fifo, write_buffer, strlen(write_buffer) + 1);
// 打开命名管道(读模式)
int fd_read = open(FIFO_FILE, O_RDONLY);
// 读取服务器的响应
bytes_read = read(fd_read, read_buffer, sizeof(read_buffer));
if (bytes_read > 0) {
printf("Server response: %s\n", read_buffer);
}
close(fd_read);
}
// 关闭命名管道
close(fd_fifo);
return 0;
}
2.7 匿名管道与命名管道的区别
有以下主要区别:
-
命名和访问方式:
- 匿名管道:没有在文件系统中显示的文件名,仅存在于内存中。它只能用于具有亲缘关系的进程间通信,通常通过
pipe系统调用创建,返回两个文件描述符用于读和写。 - 命名管道:在文件系统中有名字(路径),因此可以被多个无关的进程访问。通过
mkfifo命令或者mkfifo系统调用来创建,它创建的文件实际上是一个 FIFO 文件,可以像普通文件一样操作。
- 匿名管道:没有在文件系统中显示的文件名,仅存在于内存中。它只能用于具有亲缘关系的进程间通信,通常通过
-
进程关系要求:
- 匿名管道:仅限于具有亲缘关系(如父子进程或兄弟进程)的进程之间进行通信。
- 命名管道:允许任意无关的进程通过文件系统中的路径来访问和进行通信。
-
生命周期和持久性:
- 匿名管道:在进程关闭相关的文件描述符或终止时自动销毁,不存在持久性。
- 命名管道:持久存在于文件系统中,直到显式删除或系统关闭时才会被删除。
-
命名和访问方式:
- 匿名管道:适用于需要临时、快速的单向数据传输,例如父子进程间的简单数据交换。
- 命名管道:适用于长期存在的、需要多个进程之间双向通信的场景,如服务器和客户端之间的通信。
-
访问权限:
- 匿名管道:进程间创建管道时共享访问权限,通常无需额外的权限管理。
- 命名管道:在文件系统中以文件的形式存在,因此可以通过文件系统的权限机制来控制访问。
匿名管道适用于简单的进程间通信需求,而命名管道则提供了更灵活和持久的通信方式,适用于更复杂和长期的通信场景。
1578

被折叠的 条评论
为什么被折叠?



