进程间通信
进程间通信
进程通信的概念
在Linux中,进程间通信(IPC,Interprocess Communication)是指多个进程之间进行数据交换和同步的机制。由于进程拥有独立的地址空间,无法直接访问彼此的内存,因此需要借助操作系统提供的IPC机制来实现通信。
本质上通信就是让不同的进程看到同一份资源,然后对其进程读写操作获取或者写入数据。
进程间通信的主要方式
Linux中常见的进程间通信方式包括以下几种:
管道(Pipe)
匿名管道:用于具有亲缘关系的进程间通信(如父子进程)。数据单向流动,一端写,另一端读。
命名管道(FIFO):允许无亲缘关系的进程通过文件系统中的一个特殊文件进行通信。
消息队列(Message Queue)
消息队列允许进程通过发送和接收消息来通信,消息可以按类型区分,支持异步通信。
共享内存(Shared Memory)
多个进程共享同一块内存区域,通信速度最快,但需要同步机制(如信号量)来避免竞争条件。
信号量(Semaphore)
用于进程间的同步,通常与共享内存结合使用,防止多个进程同时访问共享资源。
信号(Signal)
用于通知进程发生了某种事件,是一种异步通信机制,常用于处理异常或中断。
套接字(Socket)
可用于不同主机或同一主机上的进程间通信,支持网络通信和本地通信。
文件(File)
进程可以通过读写文件来交换数据,但效率较低,通常用于简单的通信场景。
选择依据
选择IPC方式时,需考虑以下因素:
通信关系:是否有亲缘关系。
通信方向:单向还是双向。
数据量:大数据量适合共享内存或套接字。
同步需求:是否需要同步机制。
性能要求:共享内存性能最高,文件性能最低。
匿名管道
管道的概念
管道(Pipe)是Linux中一种最基本的进程间通信(IPC)机制,主要用于具有亲缘关系的进程(如父子进程)之间的通信。管道是单向的 ,数据只能从一端写入,从另一端读取。(重复:单向管道
)
管道的本质
管道的本质:内核中的环形缓冲区
匿名管道的本质是 内核中的一块内存缓冲区,这块缓冲区是一个 环形队列(Circular Buffer),用于存储进程间传递的数据。环形队列的特点是:
-
数据以 先进先出(FIFO) 的方式存储和读取。
-
缓冲区的大小是固定的(通常为 64KB)。
-
内核通过维护 读指针 和 写指针 来管理数据的读写。
环形缓冲区的工作原理
写操作:
-
数据从写指针位置开始写入。
-
写指针向前移动,如果到达缓冲区末尾,则绕回到缓冲区开头。
-
如果缓冲区已满,写操作会阻塞,直到有空间可用。
读操作:
-
数据从读指针位置开始读取。
-
读指针向前移动,如果到达缓冲区末尾,则绕回到缓冲区开头。
-
如果缓冲区为空,读操作会阻塞,直到有数据可读。
文件描述符的本质
在 Linux 中,文件描述符(File Descriptor, FD) 是一个非负整数,用于标识一个打开的文件或 I/O 资源。每个进程都有一个文件描述符表,用于记录该进程打开的文件或资源。
管道的文件描述符
当调用 pipe(fd) 时,内核会:
创建一个环形缓冲区。
分配两个文件描述符:
-
fd[0]:读端。
-
fd[1]:写端。
将这两个文件描述符与环形缓冲区关联。
文件描述符的本质是对内核资源的引用,通过它可以访问内核中的缓冲区。
内核如何管理管道
内核数据结构
在内核中,管道是通过以下数据结构实现的:
struct pipe_inode_info:
这是管道的核心数据结构,包含环形缓冲区的元信息,如:
-
缓冲区的大小。
-
读指针和写指针的位置。
-
等待队列(用于阻塞的读写操作)。
文件
struct file:
每个文件描述符对应一个 struct file 对象,其中包含:
-
指向 pipe_inode_info 的指针。
-
文件的打开模式(读或写)。
内核的工作流程
创建管道:
调用 pipe(fd) 时,内核会:
1,分配一个 pipe_inode_info 结构。
2,创建两个 struct file 对象,分别关联到 fd[0] 和 fd[1]。
3,将这两个文件描述符返回给用户进程。
写数据:
当进程调用 write(fd[1], buffer, size) 时:
-
内核检查环形缓冲区是否有足够的空间。
-
如果有空间,将数据从用户空间拷贝到内核缓冲区。
-
更新写指针。
-
如果缓冲区已满,进程会阻塞,直到有空间可用。
读数据:
当进程调用 read(fd[0], buffer, size) 时:
-
内核检查环形缓冲区是否有数据可读。
-
如果有数据,将数据从内核缓冲区拷贝到用户空间。
-
更新读指针。
-
如果缓冲区为空,进程会阻塞,直到有数据可读。
关闭管道:
当进程调用 close(fd[0]) 或 close(fd[1]) 时:
-
内核释放对应的文件描述符。
-
如果没有进程再使用管道,内核会释放环形缓冲区。
管道的阻塞与非阻塞
阻塞模式
默认情况下,管道的读写操作是阻塞的:
-
如果缓冲区为空,读操作会阻塞。
-
如果缓冲区已满,写操作会阻塞。
非阻塞模式
可以通过 fcntl() 将文件描述符设置为非阻塞模式:
-
如果缓冲区为空,读操作立即返回 -1,并设置 errno 为 EAGAIN。
-
如果缓冲区已满,写操作立即返回 -1,并设置 errno 为 EAGAIN。
管道的局限性
单向通信:
- 管道是单向的,只能一端写,另一端读。如果需要双向通信,需要创建两个管道。
亲缘关系:
- 匿名管道只能用于具有亲缘关系的进程(如父子进程)。
缓冲区大小有限:
- 管道的缓冲区大小通常为 64KB,超过后会阻塞写操作。
数据无格式:
- 管道传输的是字节流,没有消息边界。如果需要结构化数据,需要额外处理。
代码实例
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2]; // 文件描述符数组
pid_t pid; // 进程ID
char buffer[100]; // 用户空间缓冲区
// 创建管道
if (pipe(fd) == -1) {
perror("pipe"); // 如果失败,打印错误信息
return 1;
}
// 创建子进程
pid = fork();
if (pid < 0) {
perror("fork"); // 如果失败,打印错误信息
return 1;
}
if (pid == 0) { // 子进程
close(fd[1]); // 关闭写端
read(fd[0], buffer, sizeof(buffer)); // 从管道读取数据
printf("Child process received: %s\n", buffer); // 打印数据
close(fd[0]); // 关闭读端
} else { // 父进程
close(fd[0]); // 关闭读端
write(fd[1], "Hello from parent process!", 26); // 向管道写入数据
close(fd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
}
return 0;
}
命名管道
概念
命名管道(Named Pipe),也称为 FIFO(First In First Out),是一种特殊的文件类型,用于进程间通信(IPC)。与匿名管道不同,命名管道:
-
有一个文件系统中的路径名,可以被无亲缘关系的进程访问。
-
数据以先进先出的方式传输。
-
既可以用于本地进程间通信,也可以用于网络通信(通过文件系统共享)。
命名管道的定义方式
(1)在 Bash 中创建命名管道
在 Bash 中,可以使用 mkfifo 命令创建命名管道:
bash
mkfifo /tmp/my_fifo
/tmp/my_fifo 是命名管道的路径。
创建后,可以通过文件操作(如 cat、echo)使用命名管道。
示例:
bash
# 终端 1:写入数据
echo "Hello from terminal 1" > /tmp/my_fifo
# 终端 2:读取数据
cat /tmp/my_fifo
(2)在代码中创建命名管道
在 C 代码中,可以使用 mkfifo() 函数创建命名管道:
c
复制
#include <sys/types.h>
#include <sys/stat.h>
mkfifo("/tmp/my_fifo", 0666);
-
/tmp/my_fifo 是命名管道的路径。
-
0666 是权限模式,表示所有用户可读写。
命名管道的本质
(1)文件系统中的特殊文件
-
命名管道在文件系统中表现为一个特殊文件。
-
它不存储实际数据,而是作为进程间通信的桥梁。
(2)内核中的缓冲区
-
命名管道在内核中也是一个环形缓冲区,与匿名管道类似。
-
数据以先进先出的方式传输。
(3)文件描述符
-
进程通过 open() 打开命名管道,获取文件描述符。
-
通过文件描述符进行读写操作。
命名管道与匿名管道的不同
特性 | 命名管道(FIFO) | 匿名管道(Pipe) |
---|---|---|
文件系统可见性 | 是,有一个路径名 | 否,仅存在于内核中 |
进程关系 | 可用于无亲缘关系的进程 | 仅用于具有亲缘关系的进程 |
创建方式 | 通过 mkfifo 命令或 mkfifo() 函数 |
通过 pipe() 系统调用 |
持久性 | 持久存在,直到被删除 | 随进程结束而销毁 |
使用场景 | 本地或网络进程间通信 | 父子进程或兄弟进程间通信 |
通信方向 | 单向或双向(需创建两个 FIFO) | 单向 |
缓冲区大小 | 通常为 64KB | 通常为 64KB |
阻塞行为 | 默认阻塞,可设置为非阻塞 | 默认阻塞,可设置为非阻塞 |
内核实现 | 通过 struct inode 和 struct file 管理 |
通过 pipe_inode_info 管理 |
适用性 | 适用于无亲缘关系的进程 | 适用于有亲缘关系的进程 |
删除方式 | 使用 unlink() 或 rm 命令删除 |
随进程结束自动销毁 |
命名管道的原理与底层解读
(1)内核数据结构
-
命名管道在内核中通过 struct inode 和 struct file 管理。
-
数据存储在环形缓冲区中,与匿名管道类似。
(2)读写操作
写操作:
-
如果管道为空,写操作会阻塞,直到有进程读取数据。
-
如果管道已满,写操作会阻塞,直到有空间可用。
读操作:
-
如果管道为空,读操作会阻塞,直到有进程写入数据。
-
如果管道有数据,读操作会立即返回数据。
(3)阻塞与非阻塞模式
-
默认情况下,命名管道的读写操作是阻塞的。
-
可以通过 open() 的 O_NONBLOCK 标志设置为非阻塞模式:
-
非阻塞模式下,读操作立即返回(即使没有数据)。
-
非阻塞模式下,写操作立即返回(即使没有空间)。
通信案例
(1)Bash 示例
终端 1:写入数据
bash
echo "Hello from terminal 1" > /tmp/my_fifo
终端 2:读取数据
bash
cat /tmp/my_fifo
(2)C 代码示例
写入进程(writer.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
const char *fifo_path = "/tmp/my_fifo";
const char *message = "Hello from writer process!";
// 打开命名管道(写模式)
fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 向命名管道写入数据
if (write(fd, message, strlen(message) + 1) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
printf("Writer: Data written to FIFO.\n");
close(fd);
return 0;
}
读取进程(reader.c)
c
复制
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
const char *fifo_path = "/tmp/my_fifo";
char buffer[100];
// 打开命名管道(读模式)
fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 从命名管道读取数据
if (read(fd, buffer, sizeof(buffer)) == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("Reader: Data read from FIFO: %s\n", buffer);
close(fd);
return 0;
}
运行步骤:
(1)创建命名管道:
bash
mkfifo /tmp/my_fifo
(2)编译代码:
bash
gcc writer.c -o writer
gcc reader.c -o reader
(3)运行读取进程(会阻塞,等待数据):
bash
./reader
(4)运行写入进程:
bash
./writer
(5)输出:
Writer: Data written to FIFO.
Reader: Data read from FIFO: Hello from writer process!