进程间通信(IPC)方式
文章目录
进程间通信(IPC, Inter-Process Communication)常用方式大致可以分为 7 大类:
1. 管道(Pipe)
1.1 无名管道(Anonymous Pipe)
父子进程间常用,单向通信
1. 创建匿名管道
pipe
:
创建一个管道,参数为输出型参数,打开两个文件描述符fd
,返回值为0
表示打开失败。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
int main() {
int pipefd[2] = {0};
if (pipe(pipefd) != 0) {
std::perror("pipe");
return 1;
}
// 父进程读取数据,子进程写入数据
// pipefd[0] 为读取端,pipefd[1] 为写入端
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
close(pipefd[0]); // 关闭读取端
const char* msg = "hello world\n";
while (true) {
ssize_t written = write(pipefd[1], msg, std::strlen(msg));
if (written == -1) {
std::perror("write");
break;
}
sleep(1);
}
std::exit(0);
} else {
// 父进程
close(pipefd[1]); // 关闭写入端
while (true) {
char buffer[64] = {0};
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0) {
std::cout << "child quit" << std::endl;
break;
} else if (s > 0) {
buffer[s] = '\0'; // 手动添加终止符
std::cout << "child say: " << buffer;
} else {
std::perror("read");
break;
}
}
}
return 0;
}
子进程写入
数据,父进程读出
数据,这样就实现了简单的父子进程间的通信
注意: 因为管道是面向字节流的,字符串之间没由规矩分隔符,如果读取速度慢于写入速度,可能读端还没有将整个字符串读完,写端又写入了数据,会导致数据混乱。
2. 深入理解匿名管道
匿名管道的五个特点:
1.只能单向通信的信道
2.面向字节流
3.只能在父子进程间通信
4.管道自带同步机制,原子性写入
5.管道也是文件,管道的生命周期随进程
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {0};
if(pipe(pipefd) != 0)
{
perror("pipe");
return 1;
}
if(fork() == 0)
{
// 子进程:负责写数据
close(pipefd[0]);
int count = 0;
char c = 'a'; // 要写入管道的字符
while(1)
{
write(pipefd[1], &c, 1);
count++;
printf("%d\n", count);
// 注意:当管道写满后,write 会阻塞,count 增长会停在这里
}
exit(0);
}
// 父进程:负责读数据
close(pipefd[1]);
while(1)
{
sleep(5); // 每隔5秒读一次,相当于故意不及时消费,让管道写满
// 尝试从管道中一次性读出最多4KB数据
char buffer[4*1024+1] = {0};
ssize_t s = read(pipefd[0], buffer, sizeof(buffer)-1);
buffer[s] = 0;
printf("father take: %s\n", buffer);
}
return 0;
}
管道的大小为64KB
管道的读写规则:
当没有数据可读时:
阻塞模式:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
非阻塞模式:read调用返回-1,errno值为EAGAIN。
当管道满的时候:
阻塞模式:write调用阻塞,直到有进程读走数据
非阻塞模式:调用返回-1,errno值为EAGAIN。
1.2 命名管道(Named Pipe / FIFO)
无亲缘关系
进程也可使用- 为了解决匿名管道只能在父子进程间通信的缺陷,引入了命名管道。
- 其性质除了能让任意进程间通信外,与匿名管道基本一致,即创建一个文件一个进程
- 往文件中写数据,一个进程读数据,且不让文件内容刷新到磁盘上,从而实现任意进程间的通信。
1. 创建命令行形式的管道
2. 使用代码创建FIFO
comm.h
#include<string.h>
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#define MY_FIFO "./fifo"
server.cpp
#include "comm.hpp"
int main() {
umask(0);
if (mkfifo(MY_FIFO, 0666) < 0) {
std::cerr << "mkfifo error: " << strerror(errno) << std::endl;
return EXIT_FAILURE;
}
int fd = open(MY_FIFO, O_RDONLY);
if (fd < 0) {
std::cerr << "open error: " << strerror(errno) << std::endl;
return EXIT_FAILURE;
}
while (true) {
char buffer[64] = {0};
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = '\0';
std::cout << "client -> " << buffer << std::endl;
}
else if (s == 0) {
std::cout << "client quit..." << std::endl;
break;
}
else {
std::cerr << "read error: " << strerror(errno) << std::endl;
break;
}
}
close(fd);
return 0;
}
client.cpp
#include "comm.hpp"
int main() {
int fd = open(MY_FIFO, O_WRONLY);
if (fd < 0) {
std::cerr << "open error: " << strerror(errno) << std::endl;
return EXIT_FAILURE;
}
while (true) {
std::cout << "请输入-> ";
std::cout.flush();
std::string input;
if (!std::getline(std::cin, input)) {
// 输入流结束或者错误
std::cerr << "输入结束或错误" << std::endl;
break;
}
if (!input.empty() && input.back() == '\n') {
input.pop_back();
}
// 写入管道
ssize_t written = write(fd, input.c_str(), input.size());
if (written < 0) {
std::cerr << "write error: " << strerror(errno) << std::endl;
break;
}
}
close(fd);
return 0;
}
总结:
- 使用
mkfifo
创建了一个有名管道文件./fifo - 服务端以只读方式打开 FIFO,不断读取数据并显示
- 客户端以只写方式打开 FIFO,获取用户输入并写入
- 是典型的单向通信:客户端写->服务端读
- 使用简单、适合单机进程间通信
IPC
1.3 为什么说管道(pipe/FIFO)的本质是文件?
1. 管道的实现方式
- 无论是匿名管道(pipe)还是有名管道(FIFO):
- 在内核里都会分配一个内核缓冲区(通常是环形队列,用于数据缓存)。
- 这个缓冲区是一个特殊的文件类型:
- 在 Unix/Linux 一切皆文件的设计里,
- 管道在内核里被实现为一个 inode 节点,类型是
FIFO special file
。
2. 本质是文件
- 管道是文件系统中的一种特殊文件(
S_IFIFO
)。 - 和普通文件不同:
- 普通文件数据最终落在磁盘。
- 管道的数据只存在于内核内存缓冲区。
- 用户态进程操作管道时,也要通过:
- 打开管道(获得文件描述符)
- 调用
read
/write
(文件操作接口)
所以从进程角度看,管道也是用文件描述符访问的,只是读写的数据不进磁盘,而是保存在内核缓冲区。
3. 区别
项目 | 管道(pipe/FIFO) | 普通文件 |
---|---|---|
是否驻留磁盘 | 数据只在内核内存缓冲区 | 数据最终写到磁盘 |
文件类型 | FIFO special file | regular file |
读写方式 | 顺序访问,先进先出 | 随机访问 |
接口 | 都通过文件描述符,read /write | 都通过文件描述符,read /write |
4. 为什么需要“文件”抽象?
- 因为 Unix 设计哲学“一切皆文件”:
- 网络 socket → 文件
- 设备 → 文件
- 管道 → 文件
- 所以内核统一用文件描述符、统一的
read
/write
接口让用户访问。
管道的本质是内核中的特殊文件(类型是 FIFO special file),
它用内核缓冲区存数据,但进程通过文件描述符来操作,保持了和普通文件相同的接口。
2. 内存映射文件(Memory Mapped File)
特点:
- 通常使用
mmap()
系统调用将文件映射到内存。 - 不同进程访问的是同一份物理内存,实现共享数据。
- 进程对文件内容的修改会反映到内存映射区,最终可以同步回文件。
优点:
- 无需频繁调用
read
/write
,通过内存操作即可读写数据,效率高。 - 对大文件访问更方便,可随机定位读写,而不需全部加载到内存。
缺点:
- 对并发写入仍需自行实现同步机制(如信号量或互斥锁),避免数据冲突。
- 文件必须存在于文件系统中,依赖文件系统的支持。
适用场景:
- 多进程需要共享大量只读数据。
- 数据库缓存、大文件操作或日志共享等需要高效访问的场景。
2.1 内存的映射
- 虚拟地址空间:每个进程拥有独立的虚拟地址空间,内存映射将文件映射到这块空间的某个区域。
- 物理内存页:映射的文件内容实际上被加载到物理内存的页(page)中,进程通过虚拟地址访问这些物理页。
- 映射关系:内核维护虚拟地址到物理页的映射关系,访问虚拟地址即访问对应物理页数据。
- 同步机制:修改内存映射区的数据可以同步回磁盘文件(如果使用
MAP_SHARED
标志)。
内存映射文件是一种将文件内容直接映射到进程虚拟内存空间的进程间通信(IPC)机制。不同进程可以将同一个文件映射到各自的地址空间,从而通过对内存的读写实现数据交换。
2.2 内存映射文件的映射过程详解
1. 基本流程
- 文件数据存储在磁盘上,文件系统管理这些数据。
- 当进程调用
mmap()
映射文件时:- 操作系统不会立即将整个文件加载到内存。
- 仅在访问时(缺页异常发生)才将对应文件内容加载到物理内存页中。
- 物理内存页存储了文件的实际数据。
- 操作系统在进程的虚拟地址空间中建立映射,使该虚拟地址指向对应的物理内存页。
- 多个进程映射同一文件时,它们的虚拟地址空间中对应的地址指向同一物理内存页,实现数据共享。
2. 关系图示
磁盘文件内容
│
(按需加载)
物理内存页(含文件数据)
│
(映射)
[进程A虚拟内存地址] [进程B虚拟内存地址]
3. 关键点总结
- 物理内存页是映射的中间媒介,文件内容被缓存于物理内存页。
- 进程只能通过虚拟地址访问内存,虚拟地址通过页表映射到物理页。
- 同一物理页可被多个进程映射,实现数据共享。
- 修改映射区域时(使用
MAP_SHARED
),修改的物理内存页最终会同步回磁盘文件。
4. 额外说明
- 映射文件时,物理内存页的加载是“按需”的,不会一次性全部加载。
- 这种机制结合了虚拟内存和磁盘文件系统的优势,实现高效且安全的数据共享。
总结一句话:
内存映射文件的核心机制是“先将文件内容按需加载到物理内存页,再通过虚拟内存地址映射让进程访问这些物理页”。
2.3 关键代码
a. 映射文件到内存
void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
b.读写数据
char* data = (char*)addr;
char c = data[10]; // 直接读取内存中第10个字节
data[20] = 'A'; // 直接修改第20个字节
addr
是进程虚拟地址空间中的地址,指向映射区域的起始位置。
该映射区域对应着磁盘上文件的一部分,通过内核页表将虚拟地址映射到存放文件内容的物理内存页。
进程通过addr
访问内存时,实际上访问的是文件对应的物理内存页中的数据
2.4 通信示例
进程a
int fd = open("/tmp/ipc_file", O_RDWR | O_CREAT, 0666);
ftruncate(fd, 4096); // 设置文件大小
void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
进程b
int fd = open("/tmp/ipc_file", O_RDWR);
void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
然后进程 A 写
addr[0] = 'H'
;,进程 B 读取addr[0]
就能读到'H'
2.5 与管道的对比
1. 内存映射文件(Memory Mapped File)
- 本质:把文件内容映射到进程的虚拟地址空间。
- 多个进程可通过映射同一个文件的物理页,直接读写共享数据。
- 常用
mmap()
实现,必须配合MAP_SHARED
才能实现进程间共享。 - 文件存在于磁盘,修改时先更新物理内存页,随后通过内核同步回磁盘。
- 内存映射避免了用户态和内核态之间的多次拷贝,是高性能的原因
2. 管道(pipe / FIFO)
- 本质:内核缓冲区(先进先出,FIFO 队列)。
- 写端进程向管道写数据,读端进程从管道读数据。
- 匿名管道只能在有亲缘关系(父子或兄弟)的进程间通信。
- 有名管道(FIFO 文件)可用于无亲缘关系的进程间通信。
3. 对比总结
特性 | 内存映射文件(mmap) | 管道(pipe / FIFO) |
---|---|---|
通信方式 | 映射同一物理内存页,直接读写 | 内核维护缓冲区,数据写入/读出 |
是否依赖文件 | 需要一个实际存在的文件 | 匿名管道不需要,有名管道需要特殊设备文件 |
数据访问方式 | 随机访问:任意偏移读写 | 顺序访问:先进先出(FIFO) |
是否需要拷贝 | 零拷贝,直接映射 | 内核缓冲区需要拷贝 |
通信效率 | 高效(特别适合大数据量或随机读写) | 一般(适合小数据量、流式通信) |
通信方向 | 双向(读写都可以) | 半双工(pipe)或全双工(两个 pipe) |
适用场景 | 大量数据共享、状态共享、随机访问 | 流式消息、命令行通信、数据流传输 |
4. 举例
-
内存映射文件
多个进程都 mmap/tmp/datafile
文件后,进程 A 改addr[10] = 'x'
,进程 B 马上能读到'x'
。 -
管道
写端进程write
"hello",读端进程read
后得到 “hello”。读过的数据被消费掉,不能随机访问。
mmap
适合高效共享大块数据、随机访问;
管道适合小数据量、流式传输、命令和事件通知。
3. 共享内存(Shared Memory)
共享内存是一种最快速的进程间通信方式,允许多个进程将同一块物理内存区域映射到各自的虚拟地址空间,直接读写同一份数据。
特点:
- 是直接共享:数据不需要拷贝,所有进程访问的是同一块内存。
- 通信效率极高,尤其适合大数据量或高频率通信。
- 本身不包含同步机制,需要额外配合信号量、互斥锁等,保证访问的安全性。
优点:
- 性能最佳:没有内核和用户空间之间的数据拷贝。
- 适合高并发、大数据传输。
缺点:
- 编程复杂:必须正确处理并发读写问题。
- 不同进程需要先通过其他方式(如信号量或消息队列)来同步和协商。
适用场景:
- 视频处理、缓存系统、图像共享等需要大量快速数据交换的场合。
3.1 共享内存与内存映射文件的区别与联系
- 概念回顾
项目 | 共享内存(Shared Memory) | 内存映射文件(Memory Mapped File) |
---|---|---|
本质 | 在内核中创建一块共享内存段,多个进程映射访问 | 把文件内容映射到进程虚拟内存空间 |
是否依赖文件 | 不需要文件(只依赖内核内存) | 必须有文件(可在磁盘上创建或使用现有文件) |
通信方式 | 多进程映射同一共享内存段,直接读写内存 | 多进程 mmap 同一个文件,虚拟地址映射到同一物理页 |
同步需求 | 自身不提供同步,需要信号量/互斥锁等 | 同样需要同步机制 |
数据持久化 | 内存段不写回磁盘,进程退出/重启后数据丢失 | 修改的数据可同步回磁盘文件,实现持久化 |
- 对比总结
特性 | 共享内存 | 内存映射文件 |
---|---|---|
通信效率 | 极高:直接操作内存,无内核拷贝 | 同样极高,进程直接访问内存 |
是否持久化 | 不持久化,断电或删除即失效 | 可持久化,进程退出后数据保留到文件 |
是否需要文件 | 不需要 | 必须依赖文件 |
适合数据大小 | 大量数据、高频访问 | 大量数据、高频访问 |
适用场景 | 临时状态共享、缓存、中间结果 | 数据共享 + 落盘,如日志、数据库缓存、配置等 |
内核支持 | System V / POSIX | POSIX mmap() |
管理复杂度 | 程序需显式创建/删除共享内存段,清理更复杂 | 使用更自然,只需 open 文件+ mmap |
内存映射文件的优势:
- 结合了共享内存和普通文件的优点
- 高效:和共享内存一样,多进程共享同一物理内存页,不需要频繁拷贝。
- 可持久化:修改的数据最终写回文件,可在进程退出后保留。
- 操作简单:直接
open + mmap
;同时文件可被不同进程通过路径访问。
为什么说 mmap 更通用?
- 共享内存适合短生命周期、临时状态(如缓存、进程间快速交换数据)。
- mmap 既能支持短时共享,也能方便地和磁盘文件结合:
- 持久化数据
- 实现大文件随机访问
- 多进程共享只读大文件(如数据库索引、图形资源)
举个例子:
- 共享内存:
多进程同时写入内存中一段 4KB 空间,用来传输图片帧、音频流。 - 内存映射文件:
多进程 mmap 同一个日志文件,进程 A 写日志,进程 B 实时读取分析。
mmap
同时拥有共享内存的高速零拷贝和文件的持久化特性,
因此更适合大数据量、高并发、需要落盘的多进程共享场景。
3.2 共享内存的代码实现
这里以 System V 共享内存 API 为例(POSIX shm_open 也很类似):
1. 关键函数
函数 | 功能说明 |
---|---|
shmget | 创建/获取一个共享内存段 |
shmat | 将共享内存段映射到进程虚拟地址空间 |
shmdt | 解除映射(从进程虚拟地址空间分离) |
shmctl | 控制共享内存段(删除、设置权限等) |
2. 代码示例
comm.h
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#include<string.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096
server.c
#include"comm.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
printf("key-> %x\n", key);
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666); // 创建全新共享内存
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid-> %d\n", shmid);
char* mem = (char*)shmat(shmid, NULL, 0);
// 通信逻辑
while(1)
{
printf("%s\n", mem);// 打印mem内存中的内容
sleep(1);
}
shmdt(mem);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
clinet.c
#include"comm.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT);
if(shmid < 0)
{
perror("shmget");
return 1;
}
// 挂接
char* mem = (char*)shmat(shmid, NULL, 0);
// 通信逻辑
char c = 'A';
while(c <= 'Z')
{
mem[c - 'A'] = c;
c++;
mem[c - 'A'] = 0;
sleep(2);
}
// 去关联
shmdt(mem);
//该共享内存不由client创建,所以不用它删除
return 0;
}
4. 消息队列(Message Queue)
消息队列是一种以消息为单位的进程间通信(IPC)机制。它由操作系统内核维护一个先进先出(FIFO)的消息链表,进程之间通过发送和接收消息来交换数据。
特点:
- 进程之间互相独立,通过写入和读取消息队列来通信。
- 支持异步通信:发送者写完消息就继续执行,无需等待接收者立即处理。
- 消息通常带有类型,接收者可以按类型接收所需消息。
优点:
- 发送方与接收方解耦,不必同时运行。
- 避免直接共享内存,结构简单。
缺点:
- 内核要维护消息队列结构,性能比共享内存略低。
- 队列容量有限,可能出现阻塞或丢弃消息。
适用场景:
- 多个生产者/消费者模型。
- 按消息内容和类型进行分类处理。
4.1 消息队列的数据结构存储和管道文件的存储方式对比
1. 消息队列存储方式
- 内核消息队列(System V、POSIX):
- 数据存在内核内存中
- 不存在于磁盘,不持久化
- 通过系统调用接口操作
- 用户态中间件消息队列(Kafka、RabbitMQ等):
- 数据先在进程的内存缓冲区,为保证可靠性,会持久化写入磁盘文件
- 消息存储机制复杂,通常是顺序写磁盘,结合内存缓存
- 支持持久化和恢复,重启不会丢失消息
2. 关键区别总结
方面 | 管道 | 内核消息队列 | 用户态消息队列 |
---|---|---|---|
数据存储位置 | 内核内存 pipe buffer | 内核内存 | 用户进程内存 + 磁盘文件 |
文件系统中的文件 | 命名管道有特殊文件节点(无数据) | 无对应文件 | 无对应文件(一般用配置和日志文件) |
持久化支持 | 否 | 否 | 通常支持持久化 |
典型用途 | 进程间简单流式通信 | 轻量级进程间消息传递 | 分布式系统高可靠消息传递 |
3. 结论
消息队列和管道的“数据结构存储”本质上都依赖内核内存,但用户态中间件消息队列通常额外持久化到磁盘以保证可靠性。管道文件在文件系统中的存在只是作为入口,不存储数据。
4.2 System V消息队列的创建使用和命令行监测
1. 相关函数
操作 | 函数/命令 | 说明 |
---|---|---|
创建消息队列 | msgget | 生成或打开消息队列 |
发送消息 | msgsnd | 发送消息 |
接收消息 | msgrcv | 接收消息 |
删除消息队列 | msgctl + IPC_RMID | 删除消息队列 |
查看消息队列 | ipcs -q | 查看所有消息队列 |
查看详情 | ipcs -q -i msqid | 查看指定消息队列信息 |
删除队列 | ipcrm -q msqid | 删除指定消息队列 |
2. 创建消息队列
key_t key = ftok("msgqueuefile", 65); // 生成key,文件和id自定义
if (key == -1) {
perror("ftok");
exit(1);
}
int msgid = msgget(key, 0666 | IPC_CREAT); // 创建消息队列
if (msgid == -1) {
perror("msgget");
exit(1);
}
printf("消息队列已创建,msgid=%d\n", msgid);
3. 发送消息
struct msgbuf msg;
msg.mtype = 1; // 消息类型
strcpy(msg.mtext, "Hello System V message queue!");
if (msgsnd(msgid, &msg, strlen(msg.mtext)+1, 0) == -1) {
perror("msgsnd");
exit(1);
}
printf("消息发送成功:%s\n", msg.mtext);
4. 接收消息
struct msgbuf rcvmsg;
if (msgrcv(msgid, &rcvmsg, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
exit(1);
}
printf("收到消息:%s\n", rcvmsg.mtext);
5.删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(1);
}
printf("消息队列已删除\n")
5. 套接字(Socket)
- 本地套接字(Unix Domain Socket):同一台机器上的进程通信
- 网络套接字(TCP/UDP Socket):跨主机通信
- 支持全双工通信
5.1 网络套接字和无名管道的对比
1. 相似点
特性 | 网络套接字 | 无名管道(pipe) |
---|---|---|
通信机制 | 读写缓冲区 | 读写缓冲区 |
通信方式 | 双向(TCP套接字)或单向(UDP) | 单向(普通pipe)或双向(socketpair) |
进程间通信 | 允许跨主机、跨网络通信 | 只能在同一台主机的父子或兄弟进程间 |
面向流(TCP) | 数据流传输,顺序可靠 | 字节流,无消息边界 |
内核缓冲区管理 | 内核维护缓冲区,应用读写操作 | 内核维护缓冲区,应用读写操作 |
API接口相似 | read(), write(), send(), recv() | read(), write() |
2. 关键区别
方面 | 网络套接字 | 无名管道 |
---|---|---|
通信范围 | 跨机器网络通信 | 局部进程间通信 |
协议复杂性 | 依赖协议栈(TCP/IP),支持复杂的连接管理 | 仅内核缓冲区,无协议复杂性 |
连接管理 | TCP需要建立连接(三次握手) | 无需连接,管道已连接 |
可靠性 | TCP保证可靠传输,UDP不保证 | 可靠、顺序字节流 |
地址机制 | 有IP地址+端口,标识通信端点 | 由父子进程共享文件描述符,没地址 |
数据边界 | UDP是消息边界明确的报文 | 无消息边界,仅字节流 |
3. 本质
- 无名管道是基于内核缓冲区的内存环形队列,两个相关进程共享这块缓冲区的读写权限,数据单向传递。
- 网络套接字(尤其是TCP套接字)虽然也用缓冲区存储数据,但它背后有完整的协议栈处理,负责数据包拆分、重组、确认、重传等复杂逻辑,支持跨网络通信。
4. 总结
网络套接字的底层数据收发,和无名管道都依赖内核缓冲区的读写机制,但套接字加入了复杂的网络协议支持,能跨主机通信,而管道只能用于本地简单进程间通信。
6. 信号量(Semaphore)
信号量是一种用于进程或线程同步的工具,本质上是一个计数器,用来控制对共享资源的并发访问,不直接传递数据。
特点:
- 信号量的值通常表示可用资源数量。
- 包括:
- 二进制信号量(取值 0 或 1,相当于互斥锁)。
- 计数型信号量(值大于等于 0,可控制多个资源)。
作用:
- 同步:协调多个进程或线程对共享资源的有序访问。
- 互斥:防止多个进程或线程同时访问关键区域(临界区),避免冲突。
优点:
- 功能灵活,既可用作互斥,也可用作资源计数。
- 是构建复杂并发控制(如生产者-消费者模型)的基础。
缺点:
- 不传输实际数据,只能实现同步或互斥。
- 编程时需谨慎管理,否则易导致死锁或资源竞争。
适用场景:
- 与共享内存或文件配合使用,实现多进程/多线程安全访问。
7. 信号(Signal)
信号(Signal) 是一种最简单、最古老的进程间通信(IPC)机制,主要用来通知进程发生了特定事件或中断进程的正常执行流程。
1. 特点
- 异步通知:操作系统或其他进程可以随时向目标进程发送信号,不需要目标进程主动等待。
- 信息量有限:一个信号通常只携带信号编号,不包含具体数据。
- 中断执行:接收到信号时,如果注册了信号处理函数,会中断当前执行,转而执行处理函数。
2. 常用场景
- 通知进程需要执行特定操作:
SIGINT
:中断进程(通常来自 Ctrl+C)SIGTERM
:请求优雅终止进程SIGHUP
:让进程重新加载配置文件
- 子进程状态变化:
SIGCHLD
:子进程退出或停止
- 定时器:
SIGALRM
:定时器超时通知
- 用户自定义信号:
SIGUSR1
、SIGUSR2
3. 优点
- 轻量级:系统调用简单,占用资源少。
- 异步:可以在任何时刻打断目标进程。
4. 缺点
- 信息量非常有限:无法直接传递复杂数据,只能携带信号编号。
- 可读性差:逻辑靠信号编号和处理函数约定,维护复杂。
- 非实时:信号可能被合并或丢弃(同一信号多次发送只记录一次)。
5. 关键系统调用
函数 / 命令 | 说明 |
---|---|
kill(pid, sig) | 向指定进程发送信号 |
signal(sig, func) | 注册信号处理函数(简单用法) |
sigaction | 更强大、可移植的信号处理接口 |
raise(sig) | 当前进程向自己发送信号 |
kill -s SIGTERM pid (命令行) | shell 中向进程发送信号 |
总结表
# | 类别 | 是否需要亲缘关系 | 是否支持跨主机 | 典型特点 |
---|---|---|---|---|
1 | 管道(无名/命名) | 无名需要 | 否 | 简单,单向,顺序传输 |
2 | 消息队列 | 否 | 否 | 灵活,随机访问 |
3 | 共享内存 | 否 | 否 | 速度快,需要同步机制 |
4 | 信号量 | 否 | 否 | 用于同步,不传输数据 |
5 | 信号 | 否 | 否 | 通知进程事件 |
6 | 套接字 | 否 | 是 | 支持跨主机,强大灵活,全双工通信 |
7 | 内存映射文件 | 否 | 否 | 文件到内存映射,读写高效 |
📌 注:除了这些,还有 futex、eventfd、条件变量、互斥锁、RPC 等更细分的方式,但常用的就是上面 7 大类。