【Linux】进程间通信
一、进程间通信基础认知
1.1 IPC的核心目的
- 数据传输:一个进程将数据发送给另一个进程(如客户端给服务器发请求)。
- 资源共享:多个进程共享同一份资源(如共享配置文件、硬件设备)。
- 事件通知:进程间传递状态信息(如子进程结束后通知父进程回收资源)。
- 进程控制:一个进程控制另一个进程的执行(如调试器控制被调试程序)。
1.2 IPC的发展与分类
发展历程
- 早期:管道(Unix最古老的IPC形式)
- 中期:System V IPC(包含共享内存、消息队列、信号量)
- 现代:POSIX IPC(兼容多系统,功能更完善)
常见分类
| 类别 | 具体实现 | 适用场景 |
|---|---|---|
| 管道 | 匿名管道(pipe)、命名管道(FIFO) | 亲缘进程/非亲缘进程的流式数据传输 |
| System V IPC | 共享内存、消息队列、信号量 | 高并发数据共享、同步互斥 |
| POSIX IPC | 消息队列、共享内存、互斥量、条件变量 | 跨平台、线程/进程通用同步通信 |
二、管道通信:最基础的IPC方式
管道是基于"文件"的流式通信机制,遵循"Linux一切皆文件"思想,本质是内核中的一块缓冲区,通过文件描述符操作。
2.1 匿名管道(pipe):亲缘进程专用
匿名管道只能在具有共同祖先的进程(如父子、兄弟进程)间使用,由pipe函数创建,自动分配读写文件描述符。
2.1.1 核心函数与原理
c
#include <unistd.h>
int pipe(int fd[2]); // 创建匿名管道
- 参数
fd:文件描述符数组,fd[0]为读端,fd[1]为写端。 - 返回值:成功返回0,失败返回-1。
原理:
- 父进程调用
pipe创建管道,得到两个文件描述符。 - 父进程
fork创建子进程,子进程继承父进程的文件描述符。 - 关闭无用端:父进程关闭读端(
fd[0]),子进程关闭写端(fd[1]),形成单向通信通道。
2.1.2 实战案例:父子进程管道通信
c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main() {
int pipefd[2];
// 1. 创建管道
if (pipe(pipefd) == -1) ERR_EXIT("pipe error");
pid_t pid = fork();
if (pid == -1) ERR_EXIT("fork error");
if (pid == 0) { // 子进程:写端
close(pipefd[0]); // 关闭读端
write(pipefd[1], "hello from child", 16);
close(pipefd[1]); // 关闭写端
exit(EXIT_SUCCESS);
}
// 父进程:读端
close(pipefd[1]); // 关闭写端
char buf[1024] = {0};
read(pipefd[0], buf, sizeof(buf));
printf("parent recv: %s\n", buf);
close(pipefd[0]); // 关闭读端
waitpid(pid, NULL, 0); // 回收子进程
return 0;
}
2.1.3 管道读写规则
- 无数据可读时:阻塞模式下
read阻塞,非阻塞模式下read返回-1(errno=EAGAIN)。 - 管道满时:阻塞模式下
write阻塞,非阻塞模式下write返回-1(errno=EAGAIN)。 - 所有写端关闭:
read返回0(类似文件结束)。 - 所有读端关闭:
write触发SIGPIPE信号,进程默认退出。 - 写入数据≤
PIPE_BUF(默认4096字节):保证原子性;超过则不保证。
2.1.4 匿名管道特点
- 半双工:数据只能单向流动,双向通信需创建两个管道。
- 亲缘进程专用:依赖文件描述符继承,非亲缘进程无法获取。
- 生命周期随进程:进程退出后管道自动释放。
- 流式服务:无消息边界,需手动处理数据拆分。
2.2 命名管道(FIFO):非亲缘进程通信
匿名管道的限制是只能用于亲缘进程,命名管道(FIFO)通过文件系统中的特殊文件标识,突破了这一限制,支持任意进程间通信。
2.2.1 核心函数与创建方式
命令行创建:
bash
mkfifo fifo_name # 创建名为fifo_name的命名管道
代码创建:
c
#include <sys/stat.h>
#include <sys/types.h>
int mkfifo(const char *filename, mode_t mode);
filename:FIFO文件名(路径)。mode:权限(如0644),与文件权限一致。- 返回值:成功返回0,失败返回-1。
2.2.2 实战案例1:文件拷贝工具
通过命名管道实现两个非亲缘进程间的文件拷贝,分为读文件端和写文件端。
写端(read_file.c):
c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main() {
// 创建命名管道
mkfifo("file_pipe", 0644);
// 打开源文件(只读)
int infd = open("source.txt", O_RDONLY);
if (infd == -1) ERR_EXIT("open source.txt");
// 打开命名管道(只写)
int outfd = open("file_pipe", O_WRONLY);
if (outfd == -1) ERR_EXIT("open file_pipe");
char buf[1024];
int n;
// 读源文件,写入管道
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
close(infd);
close(outfd);
unlink("file_pipe"); // 删除FIFO文件
return 0;
}
读端(write_file.c):
c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main() {
// 打开目标文件(只写、创建、清空)
int outfd = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1) ERR_EXIT("open dest.txt");
// 打开命名管道(只读)
int infd = open("file_pipe", O_RDONLY);
if (infd == -1) ERR_EXIT("open file_pipe");
char buf[1024];
int n;
// 读管道,写入目标文件
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}
Makefile:
makefile
.PHONY: all clean
all: read_file write_file
read_file: read_file.c
gcc -o $@ $^
write_file: write_file.c
gcc -o $@ $^
clean:
rm -f read_file write_file dest.txt file_pipe
运行方式:
bash
# 终端1:运行写端(读源文件,写管道)
./read_file
# 终端2:运行读端(读管道,写目标文件)
./write_file
2.2.3 实战案例2:Server-Client通信
通过命名管道实现客户端与服务器的双向通信,服务器持续监听,客户端发送消息。
服务器(server.c):
c
#include <stdio.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)
#define FIFO_NAME "server_fifo"
int main() {
umask(0); // 清除权限掩码
// 创建命名管道
if (mkfifo(FIFO_NAME, 0644) < 0) ERR_EXIT("mkfifo");
// 打开管道(只读,阻塞等待客户端连接)
int rfd = open(FIFO_NAME, O_RDONLY);
if (rfd < 0) ERR_EXIT("open");
char buf[1024];
while (1) {
memset(buf, 0, sizeof(buf));
printf("Waiting for client...\n");
// 读客户端消息
ssize_t s = read(rfd, buf, sizeof(buf) - 1);
if (s > 0) {
printf("Client say: %s\n", buf);
} else if (s == 0) {
printf("Client quit, server exit.\n");
break;
} else {
ERR_EXIT("read");
}
}
close(rfd);
unlink(FIFO_NAME);
return 0;
}
客户端(client.c):
c
#include <stdio.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)
#define FIFO_NAME "server_fifo"
int main() {
// 打开管道(只写,连接服务器)
int wfd = open(FIFO_NAME, O_WRONLY);
if (wfd < 0) ERR_EXIT("open");
char buf[1024];
while (1) {
memset(buf, 0, sizeof(buf));
printf("Please enter message: ");
fflush(stdout);
// 读键盘输入
ssize_t s = read(0, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s - 1] = 0; // 去掉换行符
write(wfd, buf, strlen(buf));
// 输入"quit"退出
if (strcmp(buf, "quit") == 0) break;
}
}
close(wfd);
return 0;
}
运行结果:
bash
# 终端1:服务器
./server
Waiting for client...
Client say: hello server
Waiting for client...
Client say: quit
Client quit, server exit.
# 终端2:客户端
./client
Please enter message: hello server
Please enter message: quit
2.2.4 命名管道与匿名管道的区别
- 创建方式:匿名管道用
pipe,命名管道用mkfifo或命令行。 - 通信范围:匿名管道仅限亲缘进程,命名管道支持任意进程。
- 打开方式:匿名管道创建后自动打开,命名管道需用
open打开。 - 其他特性:读写规则、半双工特性完全一致。
三、进程池实现:基于管道的高效任务分发
在高并发场景中,频繁创建和销毁进程会消耗大量资源,进程池通过预先创建多个子进程,循环处理任务,提升效率。下面基于管道实现一个简单的进程池。
3.1 核心组件设计
- TaskManager:任务管理器,存储可执行任务,随机选择任务。
- Channel:通信信道,封装管道写端和子进程ID,用于父进程向子进程发送任务。
- ProcessPool:进程池核心,负责创建子进程、派发任务、回收资源。
3.2 完整代码实现
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)); // 初始化随机数种子
// 注册4种任务
tasks.emplace_back([]() {
std::cout << "子进程[" << getpid() << "] 执行数据库访问任务\n" << std::endl;
});
tasks.emplace_back([]() {
std::cout << "子进程[" << getpid() << "] 执行URL解析任务\n" << std::endl;
});
tasks.emplace_back([]() {
std::cout << "子进程[" << getpid() << "] 执行数据加密任务\n" << std::endl;
});
tasks.emplace_back([]() {
std::cout << "子进程[" << getpid() << "] 执行数据持久化任务\n" << std::endl;
});
}
// 随机选择一个任务编号
int SelectTask() { return rand() % tasks.size(); }
// 执行指定编号的任务
void Execute(int task_id) {
if (task_id >= 0 && task_id < tasks.size()) {
tasks[task_id]();
}
}
private:
std::vector<task_t> tasks;
};
// 全局任务管理器实例
TaskManager tm;
// 子进程工作函数(循环读取任务并执行)
void Worker() {
while (true) {
int task_id = 0;
// 从管道读任务编号(管道读端已重定向到标准输入0)
ssize_t n = read(0, &task_id, sizeof(task_id));
if (n == sizeof(task_id)) {
tm.Execute(task_id); // 执行任务
} else if (n == 0) {
std::cout << "子进程[" << getpid() << "] 收到退出信号,退出\n" << std::endl;
break;
}
}
}
Channel.hpp(通信信道)
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
class Channel {
public:
// 构造:初始化管道写端和子进程ID
Channel(int wfd, pid_t child_pid) : _wfd(wfd), _child_pid(child_pid) {
_name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(child_pid);
}
std::string Name() const { return _name; } // 获取信道名称
pid_t ChildPid() const { return _child_pid; } // 获取子进程ID
int WriteFd() const { return _wfd; } // 获取管道写端
// 发送任务编号给子进程
void SendTask(int task_id) {
write(_wfd, &task_id, sizeof(task_id));
}
// 关闭管道写端
void Close() {
if (_wfd != -1) {
close(_wfd);
_wfd = -1;
}
}
private:
int _wfd; // 管道写端
std::string _name; // 信道名称(用于调试)
pid_t _child_pid; // 对应子进程ID
};
ProcessPool.hpp(进程池)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>
#include "Channel.hpp"
#include "Task.hpp"
using work_func = std::function<void()>;
// 错误码定义
enum ErrorCode {
OK = 0,
USAGE_ERROR = 1,
PIPE_ERROR = 2,
FORK_ERROR = 3
};
class ProcessPool {
public:
// 构造:初始化进程数和工作函数
ProcessPool(int proc_num, work_func work) : _proc_num(proc_num), _work(work) {}
// 初始化进程池:创建子进程和通信管道
int Init() {
for (int i = 0; i < _proc_num; ++i) {
int pipefd[2];
// 创建管道
if (pipe(pipefd) < 0) {
perror("pipe create failed");
return PIPE_ERROR;
}
// 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return FORK_ERROR;
} else if (pid == 0) {
// 子进程:关闭写端,重定向读端到标准输入
close(pipefd[1]);
dup2(pipefd[0], 0); // 后续用read(0, ...)读任务
_work(); // 执行工作函数(Worker)
exit(0); // 子进程执行完任务退出
} else {
// 父进程:关闭读端,保存写端到Channel
close(pipefd[0]);
_channels.emplace_back(pipefd[1], pid);
std::cout << "创建子进程[" << pid << "],信道:" << _channels.back().Name() << std::endl;
}
}
return OK;
}
// 派发任务:循环向子进程发送任务
void DispatchTasks(int task_count = 20) {
int curr_child = 0;
while (task_count-- > 0) {
int task_id = tm.SelectTask(); // 随机选择任务
Channel& curr_chan = _channels[curr_child]; // 轮询选择子进程
// 发送任务
std::cout << "========================" << std::endl;
std::cout << "发送任务" << task_id << "到" << curr_chan.Name()
<< ",剩余任务:" << task_count << std::endl;
std::cout << "========================" << std::endl;
curr_chan.SendTask(task_id);
sleep(1); // 间隔1秒发送下一个任务
curr_child = (curr_child + 1) % _proc_num; // 轮询切换子进程
}
}
// 清理进程池:关闭管道,回收子进程
void Cleanup() {
// 关闭所有管道写端(子进程read返回0,触发退出)
for (auto& chan : _channels) {
chan.Close();
std::cout << "关闭信道:" << chan.Name() << std::endl;
}
// 回收所有子进程
for (auto& chan : _channels) {
pid_t rid = waitpid(chan.ChildPid(), nullptr, 0);
if (rid > 0) {
std::cout << "回收子进程[" << rid << "]成功" << std::endl;
}
}
}
private:
std::vector<Channel> _channels; // 通信信道列表
int _proc_num; // 子进程数量
work_func _work; // 子进程工作函数
};
main.cc(主程序)
cpp
#include "ProcessPool.hpp"
#include <string>
// 用法提示
void Usage(const std::string& proc_name) {
std::cout << "Usage: " << proc_name << " [子进程数量]" << std::endl;
}
int main(int argc, char* argv[]) {
// 检查参数
if (argc != 2) {
Usage(argv[0]);
return USAGE_ERROR;
}
int proc_num = std::stoi(argv[1]);
if (proc_num <= 0) {
std::cout << "子进程数量必须为正整数" << std::endl;
return USAGE_ERROR;
}
// 创建进程池:proc_num个子进程,工作函数为Worker
ProcessPool pool(proc_num, Worker);
// 初始化进程池
if (pool.Init() != OK) {
return -1;
}
// 派发20个任务
pool.DispatchTasks(20);
// 清理进程池
pool.Cleanup();
return 0;
}
Makefile
makefile
BIN = process_pool
CC = g++
FLAGS = -std=c++11 -Wall -g
SRC = $(wildcard *.cc)
OBJ = $(SRC:.cc=.o)
$(BIN): $(OBJ)
$(CC) $(FLAGS) -o $@ $^
%.o: %.cc
$(CC) $(FLAGS) -c $<
.PHONY: clean test
clean:
rm -f $(BIN) $(OBJ)
test:
@echo "源文件:$(SRC)"
@echo "目标文件:$(OBJ)"
3.3 运行结果与说明
bash
# 启动进程池(创建3个子进程)
./process_pool 3
# 输出示例
创建子进程[1234],信道:Channel-3-1234
创建子进程[1235],信道:Channel-4-1235
创建子进程[1236],信道:Channel-5-1236
========================
发送任务2到Channel-3-1234,剩余任务:19
========================
子进程[1234] 执行数据加密任务
...
核心流程:
- 父进程创建指定数量的子进程,每个子进程对应一个管道。
- 子进程将管道读端重定向到标准输入,循环等待任务。
- 父进程通过管道写端向子进程发送任务编号。
- 子进程接收任务编号,执行对应的任务。
- 任务派发完成后,父进程关闭管道写端,子进程退出并被回收。
四、共享内存:最快的IPC机制
共享内存是所有IPC中速度最快的方式,它直接将内核中的一块内存映射到多个进程的地址空间,进程间通信无需经过内核转发,直接操作内存即可。
4.1 核心原理
- 内核创建一块共享内存区域。
- 多个进程通过系统调用将该区域映射到自己的虚拟地址空间。
- 进程通过虚拟地址直接读写共享内存,数据传递无需内核参与。
- 通信完成后,进程将共享内存与自身地址空间脱离,内核释放共享内存。
4.2 核心函数
共享内存的操作主要通过4个函数实现,头文件为<sys/shm.h>。
| 函数 | 功能 | 原型 |
|---|---|---|
| shmget | 创建/获取共享内存 | int shmget(key_t key, size_t size, int shmflg); |
| shmat | 映射共享内存到进程地址空间 | void *shmat(int shmid, const void *shmaddr, int shmflg); |
| shmdt | 脱离共享内存 | int shmdt(const void *shmaddr); |
| shmctl | 控制共享内存(删除、查询等) | int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
关键参数说明
key:共享内存标识(通过ftok函数生成,确保多个进程获取同一共享内存)。size:共享内存大小(建议为4096的整数倍,即内存页大小)。shmflg:权限标志(如IPC_CREAT|0666创建共享内存,IPC_EXCL确保创建全新内存)。cmd:控制命令(IPC_RMID删除共享内存)。
4.3 实战案例1:基础共享内存通信
实现服务器与客户端的共享内存通信,服务器创建共享内存并读取数据,客户端写入数据。
comm.h(公共头文件)
c
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATHNAME "." // ftok的路径
#define PROJ_ID 0x6666 // ftok的项目ID
#define SHM_SIZE 4096 // 共享内存大小
// 创建共享内存
int createShm(int size);
// 获取共享内存(已存在时)
int getShm(int size);
// 删除共享内存
int destroyShm(int shmid);
#endif
comm.c(公共实现)
c
#include "comm.h"
#include <perror.h>
#include <stdlib.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 createShm(int size) {
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
// 获取已存在的共享内存
int getShm(int size) {
return commShm(size, IPC_CREAT);
}
// 删除共享内存
int destroyShm(int shmid) {
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl");
return -1;
}
return 0;
}
server.c(服务器)
c
#include "comm.h"
#include <stdio.h>
#include <unistd.h>
int main() {
// 1. 创建共享内存
int shmid = createShm(SHM_SIZE);
if (shmid < 0) return -1;
// 2. 映射共享内存到地址空间
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) {
perror("shmat");
return -1;
}
// 3. 读取共享内存数据
int i = 0;
while (i++ < 26) {
printf("客户端数据:%s\n", shmaddr);
sleep(1);
}
// 4. 脱离共享内存
shmdt(shmaddr);
sleep(2);
// 5. 删除共享内存
destroyShm(shmid);
return 0;
}
client.c(客户端)
c
#include "comm.h"
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
// 1. 获取共享内存
int shmid = getShm(SHM_SIZE);
if (shmid < 0) return -1;
// 2. 映射共享内存到地址空间
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) {
perror("shmat");
return -1;
}
// 3. 写入数据到共享内存
int i = 0;
while (i < 26) {
shmaddr[i] = 'A' + i;
i++;
shmaddr[i] = '\0'; // 字符串结束符
sleep(1);
}
// 4. 脱离共享内存
shmdt(shmaddr);
sleep(2);
return 0;
}
Makefile
makefile
.PHONY: all clean
all: server client
server: server.c comm.c
gcc -o $@ $^
client: client.c comm.c
gcc -o $@ $^
clean:
rm -f server client
运行结果
bash
# 终端1:服务器
./server
客户端数据:A
客户端数据:AB
...
客户端数据:ABCDEFGHIJKLMNOPQRSTUVWXYZ
# 终端2:客户端
./client
4.4 实战案例2:带访问控制的共享内存
共享内存本身不提供同步机制,多个进程同时读写会导致数据混乱。下面通过管道实现简单的访问控制,确保读写顺序。
Comm.hpp(增强版公共头文件)
cpp
#pragma once
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <ctime>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
// 日志级别
enum LogLevel { DEBUG, NOTICE, WARNING, ERROR };
const string LogMsg[] = {"DEBUG", "NOTICE", "WARNING", "ERROR"};
// 日志打印函数
ostream &Log(const string &msg, LogLevel level) {
cout << "[" << (unsigned)time(nullptr) << "] [" << LogMsg[level] << "] " << msg;
return cout;
}
// 配置参数
#define PATH_NAME "./"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define FIFO_NAME "./shm_fifo"
// 管道操作封装
class Fifo {
public:
Fifo() {
umask(0);
// 创建管道
if (mkfifo(FIFO_NAME, 0666) < 0) {
perror("mkfifo");
exit(1);
}
Log("管道创建成功", NOTICE) << endl;
}
~Fifo() {
unlink(FIFO_NAME);
Log("管道删除成功", NOTICE) << endl;
}
};
// 管道读写封装(用于同步)
int OpenFifo(const string &path, int flags) {
int fd = open(path.c_str(), flags);
assert(fd >= 0);
return fd;
}
void CloseFifo(int fd) { close(fd); }
// 等待信号(读管道)
void Wait(int fd) {
Log("等待客户端数据...", NOTICE) << endl;
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(temp));
assert(s == sizeof(temp));
}
// 发送信号(写管道)
void Signal(int fd) {
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(temp));
assert(s == sizeof(temp));
Log("通知服务器读取数据...", NOTICE) << endl;
}
// key转换为十六进制字符串(调试用)
string KeyToHex(key_t key) {
char buf[32];
snprintf(buf, sizeof(buf), "0x%x", key);
return buf;
}
ShmServer.cc(带访问控制的服务器)
cpp
#include "Comm.hpp"
Fifo fifo; // 全局对象,自动创建/删除管道
int main() {
// 1. 生成key
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("生成key成功", DEBUG) << " key: " << KeyToHex(key) << endl;
// 2. 创建共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
Log("创建共享内存成功", DEBUG) << " shmid: " << shmid << endl;
// 3. 映射共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
assert(shmaddr != (void *)-1);
Log("映射共享内存成功", DEBUG) << " shmid: " << shmid << endl;
// 4. 打开管道(读端,等待客户端信号)
int fifo_fd = OpenFifo(FIFO_NAME, O_RDONLY);
// 5. 循环读取数据
while (true) {
Wait(fifo_fd); // 等待客户端通知
if (strcmp(shmaddr, "quit") == 0) break;
Log("读取到数据", NOTICE) << ": " << shmaddr << endl;
}
// 6. 资源清理
CloseFifo(fifo_fd);
shmdt(shmaddr);
Log("脱离共享内存成功", DEBUG) << " shmid: " << shmid << endl;
shmctl(shmid, IPC_RMID, nullptr);
Log("删除共享内存成功", DEBUG) << " shmid: " << shmid << endl;
return 0;
}
ShmClient.cc(带访问控制的客户端)
cpp
#include "Comm.hpp"
int main() {
// 1. 生成key
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0) {
Log("生成key失败", ERROR) << " key: " << KeyToHex(key) << endl;
exit(1);
}
Log("生成key成功", DEBUG) << " key: " << KeyToHex(key) << endl;
// 2. 获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid < 0) {
Log("获取共享内存失败", ERROR) << " shmid: " << shmid << endl;
exit(1);
}
Log("获取共享内存成功", DEBUG) << " shmid: " << shmid << endl;
// 3. 映射共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == (void *)-1) {
Log("映射共享内存失败", ERROR) << " shmid: " << shmid << endl;
exit(1);
}
Log("映射共享内存成功", DEBUG) << " shmid: " << shmid << endl;
// 4. 打开管道(写端,发送信号)
int fifo_fd = OpenFifo(FIFO_NAME, O_WRONLY);
// 5. 循环写入数据
while (true) {
cout << "请输入消息(输入quit退出):";
fflush(stdout);
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if (s > 0) {
shmaddr[s - 1] = '\0'; // 去掉换行符
Signal(fifo_fd); // 通知服务器读取
if (strcmp(shmaddr, "quit") == 0) break;
}
}
// 6. 资源清理
CloseFifo(fifo_fd);
shmdt(shmaddr);
Log("脱离共享内存成功", DEBUG) << " shmid: " << shmid << endl;
return 0;
}
Makefile
makefile
.PHONY: all clean
all: ShmServer ShmClient
ShmServer: ShmServer.cc
g++ -std=c++11 -o $@ $^
ShmClient: ShmClient.cc
g++ -std=c++11 -o $@ $^
clean:
rm -f ShmServer ShmClient
4.5 共享内存特点
- 速度最快:数据直接在进程间传递,无需内核转发。
- 无同步机制:需手动实现同步(如管道、信号量),否则会出现数据竞争。
- 生命周期随内核:创建后需手动删除(
shmctl(IPC_RMID)),否则重启系统才释放。 - 容量限制:共享内存大小受内核参数限制(可通过
/proc/sys/kernel/shmmax调整)。
五、System V消息队列与信号量
5.1 消息队列
消息队列是内核中的一个消息链表,进程可向队列中添加消息,或从队列中读取消息,支持按消息类型读取。
核心特点
- 消息有类型和数据,支持按类型筛选消息。
- 生命周期随内核,需手动删除。
- 自带同步机制:队列空时读阻塞,队列满时写阻塞。
- 速度慢于共享内存,适合数据量小、需按类型通信的场景。
核心函数
msgget:创建/获取消息队列。msgsnd:发送消息。msgrcv:接收消息。msgctl:控制消息队列(删除、查询等)。
5.2 信号量
信号量不是用于传递数据,而是用于同步和互斥,本质是一个计数器,用于控制进程对临界资源的访问。
核心概念
- 临界资源:一次只能被一个进程访问的资源(如共享内存、硬件设备)。
- 临界区:访问临界资源的代码段。
- 互斥:任意时刻只允许一个进程进入临界区。
- 同步:进程访问临界资源时遵循一定的顺序。
核心操作
- P操作:申请资源,计数器减1,若计数器<0则阻塞。
- V操作:释放资源,计数器加1,若计数器≤0则唤醒一个阻塞进程。
核心特点
- 生命周期随内核,需手动删除。
- 用于保护临界资源,解决同步互斥问题。
- 常与共享内存配合使用,确保共享内存的安全访问。
六、内核如何管理IPC资源
Linux内核通过专门的数据结构管理System V IPC资源(共享内存、消息队列、信号量),核心结构如下:
6.1 核心数据结构
struct ipc_ids:管理所有IPC资源的全局结构,包含资源计数器、最大ID、互斥锁等。struct kern_ipc_perm:每个IPC资源的权限结构,包含所有者ID、组ID、权限等。- 专用结构:共享内存(
struct shmid_ds)、消息队列(struct msg_queue)、信号量(struct sem_array),分别存储对应资源的详细信息。
6.2 资源标识机制
- 每个IPC资源通过
key(键值)唯一标识,进程通过ftok函数生成key,确保访问同一资源。 - 内核为每个IPC资源分配一个
id(标识码),进程通过id操作资源。
6.3 生命周期管理
- 共享内存、消息队列、信号量的生命周期随内核,进程退出后资源不会自动释放。
- 需通过
shmctl(IPC_RMID)、msgctl(IPC_RMID)、semctl(IPC_RMID)手动删除资源。 - 可通过
ipcs命令查看系统中的IPC资源,ipcrm命令手动删除资源。
七、总结
Linux进程间通信机制各有优劣,选择时需根据场景权衡:
- 管道(匿名/命名):简单易用,适合流式数据传输,无需复杂配置。
- 共享内存:速度最快,适合大数据量通信,需配合同步机制使用。
- 消息队列:支持按类型通信,适合小数据量、需异步通信的场景。
- 信号量:不传递数据,专门用于同步互斥,保护临界资源。
实战开发中,管道常用于父子进程通信,共享内存+信号量常用于高并发大数据量场景,命名管道用于非亲缘进程通信。