
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

进程间通信简介
Linux的进程间通信(IPC, Inter-Process Communication)是多进程协同工作的核心机制。本文将深入探讨IPC的原理、实现和应用,帮助读者建立完整的知识体系,掌握高性能系统设计的关键技能。
文章目录
- 进程间通信简介
- 进程间通信思维导图
- [1 ~> IPC概述](#1 ~> IPC概述)
-
- [1.1 为什么需要进程间通信?](#1.1 为什么需要进程间通信?)
- [1.2 IPC的发展历程](#1.2 IPC的发展历程)
-
- [1.2.1 阶段一:管道](#1.2.1 阶段一:管道)
- [1.2.2 阶段二:System V IPC](#1.2.2 阶段二:System V IPC)
- [1.2.3 阶段三:POSIX IPC](#1.2.3 阶段三:POSIX IPC)
- [1.3 IPC分类体系](#1.3 IPC分类体系)
- [2 ~> 管道通信机制](#2 ~> 管道通信机制)
-
- [2.1 管道的本质](#2.1 管道的本质)
- [2.2 匿名管道](#2.2 匿名管道)
-
- [2.2.1 创建匿名管道](#2.2.1 创建匿名管道)
- [2.2.2 基础示例:从键盘到屏幕](#2.2.2 基础示例:从键盘到屏幕)
- [2.2.3 深度理解:fork共享管道](#2.2.3 深度理解:fork共享管道)
- [2.2.4 站在文件描述符角度理解管道](#2.2.4 站在文件描述符角度理解管道)
-
- [2.2.4.1 Linux一切皆文件的思想](#2.2.4.1 Linux一切皆文件的思想)
- [2.2.4.2 文件描述符布局](#2.2.4.2 文件描述符布局)
- [2.2.5 站在内核角度理解管道](#2.2.5 站在内核角度理解管道)
-
- [2.2.5.1 管道的内核实现](#2.2.5.1 管道的内核实现)
- [2.2.5.2 数据流向](#2.2.5.2 数据流向)
- [2.3 管道读写规则](#2.3 管道读写规则)
-
- [2.3.1 阻塞模式(默认)](#2.3.1 阻塞模式(默认))
- [2.3.2 非阻塞模式(O_NONBLOCK)](#2.3.2 非阻塞模式(O_NONBLOCK))
- [2.3.3 原子性保证](#2.3.3 原子性保证)
- [2.4 管道的特点](#2.4 管道的特点)
-
- [2.4.1 只能用于有亲缘关系的进程](#2.4.1 只能用于有亲缘关系的进程)
- [2.4.2 提供流式服务](#2.4.2 提供流式服务)
- [2.4.3 生命周期随进程](#2.4.3 生命周期随进程)
- [2.4.4 内核同步与互斥](#2.4.4 内核同步与互斥)
- [2.4.5 半双工通信](#2.4.5 半双工通信)
- [2.5 实践:进程池实现](#2.5 实践:进程池实现)
- [2.6 命名管道](#2.6 命名管道)
-
- [2.6.1 创建命名管道](#2.6.1 创建命名管道)
- [2.6.2 命名管道的打开规则](#2.6.2 命名管道的打开规则)
-
- [2.6.2.1 为读而打开](#2.6.2.1 为读而打开)
- [2.6.2.2 为写而打开](#2.6.2.2 为写而打开)
- [2.6.3 实践:Server-Client通信](#2.6.3 实践:Server-Client通信)
-
- [2.6.3.1 服务端(server.c)](#2.6.3.1 服务端(server.c))
- [2.6.3.2 客户端(client.c)](#2.6.3.2 客户端(client.c))
- [2.6.3.3 编译运行](#2.6.3.3 编译运行)
- [3 ~> 共享内存](#3 ~> 共享内存)
-
- [3.1 共享内存概述](#3.1 共享内存概述)
- [3.2 共享内存示意图](#3.2 共享内存示意图)
- [3.3 共享内存数据结构](#3.3 共享内存数据结构)
-
- [3.3.1 内核管理共享内存的数据结构](#3.3.1 内核管理共享内存的数据结构)
- [3.4 共享内存API详解](#3.4 共享内存API详解)
-
- [3.4.1 创建共享内存:shmget()](#3.4.1 创建共享内存:shmget())
-
- [3.4.1.1 参数说明](#3.4.1.1 参数说明)
- [3.4.1.2 返回值](#3.4.1.2 返回值)
- [3.4.2 附加共享内存:shmat()](#3.4.2 附加共享内存:shmat())
-
- [3.4.2.1 参数说明](#3.4.2.1 参数说明)
- [3.4.2.2 返回值](#3.4.2.2 返回值)
- [3.4.3 分离共享内存:shmdt()](#3.4.3 分离共享内存:shmdt())
-
- [3.4.3.1 参数说明](#3.4.3.1 参数说明)
- [3.4.3.2 注意](#3.4.3.2 注意)
- [3.4.4 控制共享内存:shmctl()](#3.4.4 控制共享内存:shmctl())
-
- [3.4.4.1 cmd命令](#3.4.4.1 cmd命令)
- [3.5 实践:共享内存通信](#3.5 实践:共享内存通信)
-
- [3.5.1 公共头文件(comm.h)](#3.5.1 公共头文件(comm.h))
- [3.5.2 公共实现(comm.c)](#3.5.2 公共实现(comm.c))
- [3.5.3 服务端(server.c)](#3.5.3 服务端(server.c))
- [3.5.4 客户端(client.c)](#3.5.4 客户端(client.c))
- [3.5.5 编译运行](#3.5.5 编译运行)
-
- [3.5.5.1 Makefile](#3.5.5.1 Makefile)
- [3.5.5.2 运行示例](#3.5.5.2 运行示例)
- [3.6 共享内存的问题与解决方案](#3.6 共享内存的问题与解决方案)
-
- [3.6.1 共享内存的问题](#3.6.1 共享内存的问题)
-
- [3.6.1.1 缺乏同步机制](#3.6.1.1 缺乏同步机制)
- [3.6.1.2 缺乏访问控制](#3.6.1.2 缺乏访问控制)
- [3.6.2 解决方案:使用信号量同步](#3.6.2 解决方案:使用信号量同步)
-
- [3.6.2.1 思路](#3.6.2.1 思路)
- [3.6.2.2 示例框架](#3.6.2.2 示例框架)
- [3.7 共享内存管理命令](#3.7 共享内存管理命令)
-
- [3.7.1 查看共享内存](#3.7.1 查看共享内存)
- [3.7.2 删除共享内存](#3.7.2 删除共享内存)
- [4 ~> 消息队列](#4 ~> 消息队列)
-
- [4.1 消息队列概述](#4.1 消息队列概述)
-
- [4.1.1 核心特点](#4.1.1 核心特点)
- [4.2 消息队列的特性](#4.2 消息队列的特性)
-
- [4.2.1 类型化消息](#4.2.1 类型化消息)
- [4.2.2 生命周期随内核](#4.2.2 生命周期随内核)
- [4.2.3 链表结构](#4.2.3 链表结构)
- [4.3 消息队列API](#4.3 消息队列API)
-
- [4.3.1 创建消息队列](#4.3.1 创建消息队列)
- [4.3.2 发送消息](#4.3.2 发送消息)
- [4.3.3 接收消息](#4.3.3 接收消息)
- [4.3.4 控制消息队列](#4.3.4 控制消息队列)
- [4.4 消息结构](#4.4 消息结构)
- [5 ~> 信号量](#5 ~> 信号量)
-
- [5.1 信号量概述](#5.1 信号量概述)
- [5.2 并发编程核心概念](#5.2 并发编程核心概念)
-
- [5.2.1 基本概念](#5.2.1 基本概念)
- [5.2.2 保护的本质](#5.2.2 保护的本质)
- [5.3 信号量的本质](#5.3 信号量的本质)
-
- [5.3.1 理解角度](#5.3.1 理解角度)
- [5.4 信号量的操作](#5.4 信号量的操作)
-
- [5.4.1 P操作(申请资源):](#5.4.1 P操作(申请资源):)
- [5.4.2 V操作(释放资源):](#5.4.2 V操作(释放资源):)
- [5.5 电影院类比](#5.5 电影院类比)
- [5.6 System V信号量API](#5.6 System V信号量API)
-
- [5.6.1 创建信号量集](#5.6.1 创建信号量集)
- [5.6.2 操作信号量](#5.6.2 操作信号量)
- [5.6.3 控制信号量](#5.6.3 控制信号量)
- [6 ~> 内核管理机制](#6 ~> 内核管理机制)
-
- [6.1 IPC资源组织](#6.1 IPC资源组织)
-
- [6.1.1 核心结构](#6.1.1 核心结构)
- [6.2 多态实现](#6.2 多态实现)
-
- [6.2.1 示例(简化版)](#6.2.1 示例(简化版))
- [6.3 资源生命周期](#6.3 资源生命周期)
-
- [6.3.1 System V IPC资源的生命周期](#6.3.1 System V IPC资源的生命周期)
- [6.3.2 管理命令](#6.3.2 管理命令)
- [7 ~> 实践案例](#7 ~> 实践案例)
-
- [7.1 案例1:生产者-消费者模型](#7.1 案例1:生产者-消费者模型)
-
- [7.1.1 场景](#7.1.1 场景)
- [7.1.2 实现要点](#7.1.2 实现要点)
- [7.2 案例2:分布式日志系统](#7.2 案例2:分布式日志系统)
-
- [7.2.1 场景](#7.2.1 场景)
- [7.2.2 实现要点](#7.2.2 实现要点)
- [7.3 案例3:高性能计算任务分发](#7.3 案例3:高性能计算任务分发)
-
- [7.3.1 场景](#7.3.1 场景)
- [7.3.2 实现要点](#7.3.2 实现要点)
- [8 ~> 总结与展望](#8 ~> 总结与展望)
-
- [8.1 IPC机制对比](#8.1 IPC机制对比)
- [8.2 选择建议](#8.2 选择建议)
-
- [8.2.1 选择匿名管道](#8.2.1 选择匿名管道)
- [8.2.2 选择命名管道](#8.2.2 选择命名管道)
- [8.2.3 选择共享内存](#8.2.3 选择共享内存)
- [8.2.4 选择消息队列](#8.2.4 选择消息队列)
- [8.3 实践建议](#8.3 实践建议)
-
- [8.3.1 优先使用标准接口](#8.3.1 优先使用标准接口)
- [8.3.2 注意资源管理](#8.3.2 注意资源管理)
- [8.3.3 处理同步问题](#8.3.3 处理同步问题)
- [8.3.4 错误处理](#8.3.4 错误处理)
- [8.3.5 性能优化](#8.3.5 性能优化)
- [8.4 展望](#8.4 展望)
-
- [8.4.1 新型IPC机制](#8.4.1 新型IPC机制)
- [8.4.2 用户态驱动](#8.4.2 用户态驱动)
- [8.4.3 容器化环境](#8.4.3 容器化环境)
- [8.4.4 分布式系统](#8.4.4 分布式系统)
- 附录:常用命令参考
- 结语
- 结尾
进程间通信思维导图


1 ~> IPC概述
1.1 为什么需要进程间通信?
在现代操作系统中,进程是程序执行的基本单位,每个进程拥有独立的地址空间。这种设计带来了隔离性和安全性,但也带来了一个问题:进程之间如何交换信息和协同工作?
进程间通信的四大目的:
- 数据传输: 一个进程需要将数据发送给另一个进程
- 资源共享: 多个进程之间共享同样的资源
- 通知事件: 一个进程向其他进程发送消息,通知某种事件发生
- 进程控制: 某个进程完全控制另一个进程的执行(如调试器)
1.2 IPC的发展历程
Linux系统中的IPC机制经历了三个主要发展阶段:
1.2.1 阶段一:管道
- 最古老的IPC形式
- 简单但功能强大
- 适用于亲缘关系进程
1.2.2 阶段二:System V IPC
- System V消息队列
- System V共享内存
- System V信号量
- 生命周期随内核
1.2.3 阶段三:POSIX IPC
- 标准化的IPC接口
- 跨平台兼容性
- 扩展了互斥量、条件变量、读写锁等
1.3 IPC分类体系
bash
Linux IPC
├── 管道
│ ├── 匿名管道 (pipe)
│ └── 命名管道 (FIFO)
├── System V IPC
│ ├── 消息队列
│ ├── 共享内存
│ └── 信号量
└── POSIX IPC
├── 消息队列
├── 共享内存
├── 信号量
├── 互斥量
├── 条件变量
└── 读写锁
2 ~> 管道通信机制
2.1 管道的本质
管道是Unix中最古老的进程间通信形式,其本质是内核缓冲区。从一个进程连接到另一个进程的数据流被称为"管道"。
核心特点:
- 面向字节流
- 半双工通信(单向)
- 只能用于有亲缘关系的进程
2.2 匿名管道
2.2.1 创建匿名管道
c
#include <unistd.h>
int pipe(int fd[2]);
参数说明:
fd:文件描述符数组
fd[0]:读端fd[1]:写端
返回值:
- 成功:返回0
- 失败:返回-1
2.2.2 基础示例:从键盘到屏幕
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int fds[2];
char buf[100];
int len;
// 创建管道
if (pipe(fds) == -1) {
perror("make pipe");
exit(1);
}
// 从标准输入读取数据
while (fgets(buf, 100, stdin)) {
len = strlen(buf);
// 写入管道
if (write(fds[1], buf, len) != len) {
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// 从管道读取
if ((len = read(fds[0], buf, 100)) == -1) {
perror("read from pipe");
break;
}
// 写入标准输出
if (write(1, buf, len) != len) {
perror("write to stdout");
break;
}
}
return 0;
}
2.2.3 深度理解:fork共享管道
关键原理:
当调用fork()创建子进程后,父进程和子进程都继承了管道的文件描述符,从而实现了通信。
c
int main()
{
int pipefd[2];
pid_t pid;
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程:关闭写端,只读
close(pipefd[1]);
char buf[100];
read(pipefd[0], buf, 100);
printf("Child received: %s\n", buf);
close(pipefd[0]);
} else {
// 父进程:关闭读端,只写
close(pipefd[0]);
write(pipefd[1], "Hello from parent!", 18);
close(pipefd[1]);
wait(NULL);
}
return 0;
}
2.2.4 站在文件描述符角度理解管道
2.2.4.1 Linux一切皆文件的思想
管道的使用和文件完全一致:
read()读取管道write()写入管道close()关闭管道- 可以使用
select()/poll()进行I/O多路复用
2.2.4.2 文件描述符布局
bash
进程A 管道缓冲区 进程B
fd[1](写端) → [ 内核缓冲区 ] ← fd[0](读端)
2.2.5 站在内核角度理解管道
2.2.5.1 管道的内核实现
在内核中,管道通过pipe_inode_info结构管理:
- 一个循环缓冲区
- 一个读指针
- 一个写指针
- 同步机制(互斥锁)
2.2.5.2 数据流向
bash
用户空间 → 系统调用 → 内核缓冲区 → 系统调用 → 用户空间
2.3 管道读写规则
2.3.1 阻塞模式(默认)
| 情况 | 读操作 | 写操作 |
|---|---|---|
| 没有数据可读 | 阻塞等待 | - |
| 管道已满 | - | 阻塞等待 |
| 所有写端关闭 | 返回 0(EOF) | - |
| 所有读端关闭 | - | 产生 SIGPIPE 信号 |
2.3.2 非阻塞模式(O_NONBLOCK)
| 情况 | 读操作 | 写操作 |
|---|---|---|
| 没有数据可读 | 返回 -1,errno=EAGAIN | - |
| 管道已满 | - | 返回 -1,errno=EAGAIN |
2.3.3 原子性保证
写入数据量 ≤ PIPE_BUF:
- Linux保证写入的原子性
写入数据量 > PIPE_BUF:
- 不保证原子性
- 可能与其他进程的数据交错
PIPE_BUF大小:
bash
$ getconf PIPE_BUF
4096 # 通常为4KB
2.4 管道的特点
2.4.1 只能用于有亲缘关系的进程
- 由进程创建,然后fork共享
2.4.2 提供流式服务
- 面向字节流
- 无消息边界
2.4.3 生命周期随进程
- 进程退出,管道释放
2.4.4 内核同步与互斥
- 内核自动处理同步问题
2.4.5 半双工通信
- 数据单向流动
- 双向通信需要两个管道
2.5 实践:进程池实现
进程池是管道应用的经典场景,通过预创建多个工作进程,提高任务处理效率。
2.5.1 通道封装(Channel.hpp)
cpp
#ifndef __CHANNEL_HPP__
#define __CHANNEL_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
class Channel {
public:
Channel(int wfd, pid_t who)
: _wfd(wfd), _who(who) {
_name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
}
std::string Name() {
return _name;
}
void Send(int cmd) {
::write(_wfd, &cmd, sizeof(cmd));
}
void Close() {
::close(_wfd);
}
pid_t Id() {
return _who;
}
int wFd() {
return _wfd;
}
~Channel() {}
private:
int _wfd;
std::string _name;
pid_t _who;
};
#endif
2.5.2 进程池实现(ProcessPool.hpp)
cpp
#ifndef __PROCESSPOOL_HPP__
#define __PROCESSPOOL_HPP__
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include <cassert>
#include <functional>
#include "Channel.hpp"
using work_t = std::function<void()>;
enum StatusCode {
OK = 0,
UsageError,
PipeError,
ForkError
};
class ProcessPool {
public:
ProcessPool(int n, work_t w)
: processnum(n), work(w) {}
int InitProcessPool() {
for (int i = 0; i < processnum; i++) {
// 1. 创建管道
int pipefd[2] = {0};
if (pipe(pipefd) < 0)
return PipeError;
// 2. 创建进程
pid_t id = fork();
if (id < 0)
return ForkError;
// 3. 子进程
if (id == 0) {
// 关闭历史写端
for (auto &c : channels) {
c.Close();
}
::close(pipefd[1]); // 关闭写端
// 重定向标准输入
dup2(pipefd[0], 0);
// 执行工作函数
work();
::exit(0);
}
// 4. 父进程
::close(pipefd[0]); // 关闭读端
channels.emplace_back(pipefd[1], id);
}
return OK;
}
void DispatchTask() {
int who = 0;
int num = 20;
while (num--) {
// 选择任务
int task = SelectTask();
// 轮询选择子进程
Channel &curr = channels[who++];
who %= channels.size();
std::cout << "send " << task << " to " << curr.Name()
<< ", 任务还剩: " << num << std::endl;
// 派发任务
curr.Send(task);
sleep(1);
}
}
void CleanProcessPool() {
for (auto &c : channels) {
c.Close();
pid_t rid = ::waitpid(c.Id(), nullptr, 0);
if (rid > 0) {
std::cout << "child " << rid << " wait ... success" << std::endl;
}
}
}
void DebugPrint() {
for (auto &c : channels) {
std::cout << c.Name() << std::endl;
}
}
private:
std::vector<Channel> channels;
int processnum;
work_t work;
int SelectTask() {
return rand() % 4;
}
};
#endif
2.5.3 任务管理(Task.hpp)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <ctime>
#include <unistd.h>
using task_t = std::function<void()>;
class TaskManager {
public:
TaskManager() {
srand(time(nullptr));
tasks.push_back([]() {
std::cout << "sub process[" << getpid() << "] 执行访问数据库的任务" << std::endl;
});
tasks.push_back([]() {
std::cout << "sub process[" << getpid() << "] 执行URL解析" << std::endl;
});
tasks.push_back([]() {
std::cout << "sub process[" << getpid() << "] 执行加密任务" << std::endl;
});
tasks.push_back([]() {
std::cout << "sub process[" << getpid() << "] 执行数据持久化任务" << std::endl;
});
}
int SelectTask() {
return rand() % tasks.size();
}
void Execute(unsigned long number) {
if (number < tasks.size()) {
tasks[number]();
}
}
private:
std::vector<task_t> tasks;
};
TaskManager tm;
void Worker() {
while (true) {
int cmd = 0;
int n = ::read(0, &cmd, sizeof(cmd));
if (n == sizeof(cmd)) {
tm.Execute(cmd);
} else if (n == 0) {
std::cout << "pid: " << getpid() << " quit..." << std::endl;
break;
}
}
}
2.5.4 主程序(Main.cc)
cpp
#include "ProcessPool.hpp"
#include "Task.hpp"
void Usage(std::string proc) {
std::cout << "Usage: " << proc << " process-num" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
Usage(argv[0]);
return UsageError;
}
int num = std::stoi(argv[1]);
ProcessPool *pp = new ProcessPool(num, Worker);
// 1. 初始化进程池
pp->InitProcessPool();
// 2. 派发任务
pp->DispatchTask();
// 3. 退出进程池
pp->CleanProcessPool();
delete pp;
return 0;
}
2.5.5 编译运行
Makefile:
makefile
BIN=processpool
CC=g++
FLAGS=-c -Wall -std=c++11
LDFLAGS=-o
SRC=$(wildcard *.cc)
OBJ=$(SRC:.cc=.o)
$(BIN):$(OBJ)
$(CC) $(LDFLAGS) $@ $^
%.o:%.cc
$(CC) $(FLAGS) $<
.PHONY:clean
clean:
rm -f $(BIN) $(OBJ)
运行示例:
bash
$ make
$ ./processpool 5
send 2 to Channel-5-12345, 任务还剩: 19
send 1 to Channel-6-12346, 任务还剩: 18
...
2.6 命名管道
匿名管道的限制在于只能用于有亲缘关系的进程。命名管道(FIFO) 突破了这个限制,允许无亲缘关系的进程进行通信。
2.6.1 创建命名管道
命令行方式(注意文件类型是p,表示pipe,管道):
bash
$ mkfifo filename
$ ls -l filename
prw-r--r-- 1 user user 0 Jan 1 00:00 filename
程序方式:
c
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
2.6.2 命名管道的打开规则
2.6.2.1 为读而打开
- 阻塞模式:等待直到有进程为写而打开
- 非阻塞模式:立即返回成功
2.6.2.2 为写而打开
- 阻塞模式:等待直到有进程为读而打开
- 非阻塞模式:立即返回失败(errno=ENXIO)
2.6.3 实践:Server-Client通信
2.6.3.1 服务端(server.c)
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
umask(0);
// 创建命名管道
if (mkfifo("mypipe", 0644) < 0) {
ERR_EXIT("mkfifo");
}
// 以只读方式打开
int rfd = open("mypipe", O_RDONLY);
if (rfd < 0) {
ERR_EXIT("open");
}
char buf[1024];
while (1) {
buf[0] = 0;
printf("Please wait...\n");
ssize_t s = read(rfd, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s - 1] = 0; // 去掉换行符
printf("client say# %s\n", buf);
} else if (s == 0) {
printf("client quit, exit now!\n");
break;
} else {
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
2.6.3.2 客户端(client.c)
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
// 以只写方式打开
int wfd = open("mypipe", O_WRONLY);
if (wfd < 0) {
ERR_EXIT("open");
}
char buf[1024];
while (1) {
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s] = 0;
write(wfd, buf, strlen(buf));
} else if (s <= 0) {
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}
2.6.3.3 编译运行
bash
# 终端1:启动服务端
$ gcc server.c -o server
$ ./server
Please wait...
# 终端2:启动客户端
$ gcc client.c -o client
$ ./client
Please Enter# Hello World!
# 终端1显示:
client say# Hello World!
3 ~> 共享内存
3.1 共享内存概述
共享内存是最快的IPC形式!
一旦共享内存映射到进程的地址空间,这些进程间数据传递不再涉及到内核。换句话说,进程不再通过执行进入内核的系统调用来传递彼此的数据。
为什么最快?
避免了数据在用户空间和内核空间之间的拷贝
直接在内存中访问共享数据
不需要系统调用的开销
3.2 共享内存示意图
bash
进程A的地址空间 共享物理内存 进程B的地址空间
+----------------+ +----------------+ +----------------+
| 用户数据 | | | | 用户数据 |
+----------------+ | | +----------------+
| | | | | |
| [映射区] | <----> | 共享内存 | <----> | [映射区] |
| | | | | |
+----------------+ +----------------+ +----------------+
3.3 共享内存数据结构
3.3.1 内核管理共享内存的数据结构
c
struct shmid_ds {
struct ipc_perm shm_perm; // 权限和所有者信息
size_t shm_segsz; // 共享内存段大小
time_t shm_atime; // 最后附加时间
time_t shm_dtime; // 最后分离时间
time_t shm_ctime; // 最后改变时间
pid_t shm_cpid; // 创建者PID
pid_t shm_lpid; // 最后操作PID
shmatt_t shm_nattch; // 当前附加数
// ...
};
3.4 共享内存API详解
3.4.1 创建共享内存:shmget()
c
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
3.4.1.1 参数说明
key:共享内存段的标识符
- 通常通过
ftok()函数生成
size:共享内存大小(字节)
- 建议为页面大小(4096)的整数倍
shmflg:标志位 IPC_CREAT:不存在则创建IPC_EXCL:与IPC_CREAT一起使用,已存在则报错- 权限位:如
0666
3.4.1.2 返回值
- 成功:返回共享内存标识符(shmid)
- 失败:返回-1
3.4.2 附加共享内存:shmat()
c
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
3.4.2.1 参数说明
shmid:共享内存标识符
shmaddr:指定连接地址
NULL:由内核自动选择(推荐)非NULL:使用指定地址
shmflg:标志位
SHM_RND:地址向下调整为SHMLBA的整数倍SHM_RDONLY:只读附加
3.4.2.2 返回值
- 成功:返回指向共享内存的指针
- 失败:返回-1
3.4.3 分离共享内存:shmdt()
c
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
3.4.3.1 参数说明
shmaddr:由shmat()返回的指针
3.4.3.2 注意
- 分离不等于删除
- 只是取消当前进程与共享内存的关联
3.4.4 控制共享内存:shmctl()
c
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3.4.4.1 cmd命令
IPC_STAT:获取共享内存状态IPC_SET:设置共享内存状态IPC_RMID:删除共享内存段
3.5 实践:共享内存通信
3.5.1 公共头文件(comm.h)
c
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif
3.5.2 公共实现(comm.c)
c
#include "comm.h"
#include <sys/ipc.h>
static int commShm(int size, int flags) {
// 生成key值
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0) {
perror("ftok");
return -1;
}
// 创建/获取共享内存
int shmid = shmget(key, size, flags);
if (shmid < 0) {
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid) {
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size) {
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(int size) {
return commShm(size, IPC_CREAT);
}
3.5.3 服务端(server.c)
c
#include "comm.h"
#include <unistd.h>
int main() {
// 1. 创建共享内存
int shmid = createShm(4096);
// 2. 附加共享内存
char *addr = shmat(shmid, NULL, 0);
sleep(2);
// 3. 读取共享内存
int i = 0;
while (i++ < 26) {
printf("client# %s\n", addr);
sleep(1);
}
// 4. 分离共享内存
shmdt(addr);
sleep(2);
// 5. 删除共享内存
destroyShm(shmid);
return 0;
}
3.5.4 客户端(client.c)
c
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main() {
// 1. 获取共享内存
int shmid = getShm(4096);
sleep(1);
// 2. 附加共享内存
char *addr = shmat(shmid, NULL, 0);
sleep(2);
// 3. 写入共享内存
int i = 0;
while (i < 26) {
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
// 4. 分离共享内存
shmdt(addr);
sleep(2);
return 0;
}
3.5.5 编译运行
3.5.5.1 Makefile
makefile
.PHONY:all
all:server client
client:client.c comm.c
gcc -o $@ $^
server:server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f server client
3.5.5.2 运行示例
bash
# 终端1:启动服务端
$ make
$ ./server
client# A
client# AB
client# ABC
...
# 终端2:启动客户端
$ ./client
3.6 共享内存的问题与解决方案
3.6.1 共享内存的问题
3.6.1.1 缺乏同步机制
- 没有内置的同步与互斥
- 可能导致数据竞争
3.6.1.2 缺乏访问控制
- 所有附加进程都可以随意访问
- 需要自行设计访问协议
3.6.2 解决方案:使用信号量同步
3.6.2.1 思路
- 使用信号量实现对共享内存的互斥访问
- 结合管道实现进程间同步
3.6.2.2 示例框架
c
// 服务端
int fd = OpenFIFO(FIFO_NAME, O_RDONLY);
while (true) {
Wait(fd); // 等待信号
printf("%s\n", shmaddr);
if (strcmp(shmaddr, "quit") == 0)
break;
}
// 客户端
int fd = OpenFIFO(FIFO_NAME, O_WRONLY);
while (true) {
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if (s > 0) {
shmaddr[s - 1] = 0;
Signal(fd); // 发送信号
if (strcmp(shmaddr, "quit") == 0)
break;
}
}
3.7 共享内存管理命令
3.7.1 查看共享内存
bash
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66026a25 688145 root 666 4096 0
3.7.2 删除共享内存
bash
$ ipcrm -m 688145
4 ~> 消息队列
4.1 消息队列概述
消息队列提供了一个从一个进程向另一个进程发送一块数据的方法。每个数据块都被认为有一个类型,接收者进程可以根据类型选择性地接收数据。
4.1.1 核心特点
- 面向消息(非流式)
- 支持消息类型优先级
- 可以实现复杂的通信模式
4.2 消息队列的特性
4.2.1 类型化消息
- 每个消息有一个类型字段
- 接收者可以按类型接收
4.2.2 生命周期随内核
- 必须手动删除
- 否则不会自动清除
4.2.3 链表结构
- 消息按接收顺序排列
- 支持优先级队列
4.3 消息队列API
4.3.1 创建消息队列
bash
int msgget(key_t key, int msgflg);
4.3.2 发送消息
bash
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
4.3.3 接收消息
bash
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
4.3.4 控制消息队列
bash
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
4.4 消息结构
c
struct msgbuf {
long mtype; // 消息类型(必须>0)
char mtext[1]; // 消息数据(变长)
};
5 ~> 信号量
5.1 信号量概述
信号量主要用于同步和互斥。理解信号量,需要先理解并发编程的核心概念。
5.2 并发编程核心概念
5.2.1 基本概念
- 共享资源:多个执行流(进程)都能看到的同一份资源
- 临界资源:被保护起来的共享资源
- 临界区:访问临界资源的代码段
- 互斥:任何时刻只允许一个执行流访问资源
- 同步:多个执行流访问资源时具有一定的顺序性
5.2.2 保护的本质
保护共享资源,本质是保护访问共享资源的代码(临界区)。
5.3 信号量的本质
信号量是一个计数器(本质)!
5.3.1 理解角度
- 特性方面:计数器
- 作用方面:保护临界区
- 本质方面:资源的预订机制
5.4 信号量的操作
5.4.1 P操作(申请资源):
- 计数器减1
- 如果计数器
≥0,继续执行 - 如果计数器
<0,阻塞等待
5.4.2 V操作(释放资源):
- 计数器加1
- 如果计数器
≤0,唤醒等待的进程
5.5 电影院类比
场景:
电影院有100个座位
信号量初始值为100
P操作(购票):
bash
观众1: P() → 100-1=99 → 有票,进入
观众2: P() → 99-1=98 → 有票,进入
...
观众101: P() → 0-1=-1 → 无票,等待
V操作(退票):
bash
有人退票: V() → -1+1=0 → 唤醒等待的观众101
5.6 System V信号量API
5.6.1 创建信号量集
bash
int semget(key_t key, int nsems, int semflg);
5.6.2 操作信号量
bash
int semop(int semid, struct sembuf *sops, unsigned nsops);
5.6.3 控制信号量
bash
int semctl(int semid, int semnum, int cmd, ...);
6 ~> 内核管理机制
6.1 IPC资源组织
Linux内核通过统一的数据结构管理IPC资源------(如下)
6.1.1 核心结构
c
struct ipc_ids {
int in_use;
unsigned short seq;
unsigned short seq_max;
struct rw_semaphore rw_mutex;
struct idr ipcs_idr;
};
6.2 多态实现
Linux内核使用函数指针实现了IPC的多态------
6.2.1 示例(简化版)
c
struct ipc_ops {
int (*getnew)(struct ipc_namespace *, struct ipc_params *);
int (*associate)(struct kern_ipc_perm *, int);
int (*more_checks)(struct kern_ipc_perm *, struct ipc_params *);
};
struct ipc_namespace {
struct ipc_ids ids[3]; // 消息队列、信号量、共享内存
};
6.3 资源生命周期
6.3.1 System V IPC资源的生命周期
- 创建后持续存在
- 直到显式删除或系统重启
- 不随进程退出而自动释放
6.3.2 管理命令
bash
# 查看所有IPC资源
$ ipcs
# 删除共享内存
$ ipcrm -m shmid
# 删除消息队列
$ ipcrm -q msqid
# 删除信号量集
$ ipcrm -s semid
7 ~> 实践案例
7.1 案例1:生产者-消费者模型
7.1.1 场景
- 多个生产者进程生产数据
- 多个消费者进程消费数据
- 使用共享内存+信号量实现
7.1.2 实现要点
- 共享内存作为缓冲区
- 信号量实现同步和互斥
- 循环队列管理数据
7.2 案例2:分布式日志系统
7.2.1 场景
- 多个应用进程写入日志
- 独立的日志收集进程读取日志
- 使用命名管道实现
7.2.2 实现要点
- 多个生产者写入同一个命名管道
- 单个消费者读取命名管道
- 缓冲机制防止数据丢失
7.3 案例3:高性能计算任务分发
7.3.1 场景
- 主进程分配计算任务
- 多个Worker进程执行计算
- 使用匿名管道实现
7.3.2 实现要点
- 进程池预创建
- 任务队列管理
- 结果收集机制
8 ~> 总结与展望
8.1 IPC机制对比
| 表格机制 | 速度 | 复杂度 | 适用场景 |
|---|---|---|---|
| 匿名管道 | 中低 | 低 | 亲缘进程通信 |
| 命名管道 | 中低 | 低 | 无亲缘进程通信 |
| 共享内存 | 最快 | 高 | 大数据量交换 |
| 消息队列 | 中 | 中 | 类型化消息传递 |
| 信号量 | - | 中 | 同步与互斥 |
8.2 选择建议
8.2.1 选择匿名管道
- 简单的数据传输
- 父子进程通信
- 单向数据流
8.2.2 选择命名管道
- 简单的跨进程通信
- 不关心速度
- 希望像文件一样操作
8.2.3 选择共享内存
- 大数据量传输
- 要求高性能
- 能自行处理同步
8.2.4 选择消息队列
- 需要消息类型
- 异步通信
- 解耦生产者和消费者
8.3 实践建议
8.3.1 优先使用标准接口
- POSIX IPC比System V IPC更标准
- 更好的跨平台兼容性
8.3.2 注意资源管理
- System V IPC需要手动释放
- 避免资源泄漏
8.3.3 处理同步问题
- 共享内存必须配合同步机制
- 信号量、互斥锁、条件变量选择合适的工具
8.3.4 错误处理
- 检查所有系统调用的返回值
- 处理EAGAIN、EINTR等特殊情况
8.3.5 性能优化
- 批量操作减少系统调用
- 合理设置缓冲区大小
- 使用零拷贝技术
8.4 展望
随着Linux系统的发展,IPC机制也在不断演进。
8.4.1 新型IPC机制
- Unix Domain Socket:高性能本地通信
- memfd:内存文件描述符
- io_uring:异步I/O框架
8.4.2 用户态驱动
- SPDK、DPDK等用户态驱动
- 减少内核开销
8.4.3 容器化环境
- 容器间通信需求
- Namespaces和Cgroups的影响
8.4.4 分布式系统
- 跨主机IPC
- RDMA、共享远程内存
附录:常用命令参考
管道相关
bash
# 查看管道大小
$ ulimit -p
# 创建命名管道
$ mkfifo /tmp/mypipe
# 删除命名管道
$ rm /tmp/mypipe
共享内存相关
bash
# 查看共享内存
$ ipcs -m
# 查看共享内存限制
$ cat /proc/sys/kernel/shmmax
$ cat /proc/sys/kernel/shmall
# 删除共享内存
$ ipcrm -m <shmid>
消息队列相关
bash
# 查看消息队列
$ ipcs -q
# 查看消息队列限制
$ cat /proc/sys/kernel/msgmax
$ cat /proc/sys/kernel/msgmnb
# 删除消息队列
$ ipcrm -q <msqid>
信号量相关
bash
# 查看信号量集
$ ipcs -s
# 删除信号量集
$ ipcrm -s <semid>
# 查看信号量限制
$ cat /proc/sys/kernel/sem
结语
Linux进程间通信是多进程协同工作的基石。掌握IPC机制,不仅能够帮助我们编写高效的多进程程序,更能深入理解操作系统的核心原理。
从简单的管道到复杂的共享内存,从同步互斥到资源管理,每一个机制都蕴含着深刻的设计思想。希望本文能够帮助读者建立完整的IPC知识体系,在实践中灵活运用各种通信机制。
注意
- 没有最好的IPC机制,只有最合适的IPC机制
- 性能、复杂度、可维护性需要权衡
- 理解原理比记住API更重要
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
