【Linux】进程间通信

【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。

原理

  1. 父进程调用pipe创建管道,得到两个文件描述符。
  2. 父进程fork创建子进程,子进程继承父进程的文件描述符。
  3. 关闭无用端:父进程关闭读端(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] 执行数据加密任务
...

核心流程

  1. 父进程创建指定数量的子进程,每个子进程对应一个管道。
  2. 子进程将管道读端重定向到标准输入,循环等待任务。
  3. 父进程通过管道写端向子进程发送任务编号。
  4. 子进程接收任务编号,执行对应的任务。
  5. 任务派发完成后,父进程关闭管道写端,子进程退出并被回收。

四、共享内存:最快的IPC机制

共享内存是所有IPC中速度最快的方式,它直接将内核中的一块内存映射到多个进程的地址空间,进程间通信无需经过内核转发,直接操作内存即可。

4.1 核心原理

  1. 内核创建一块共享内存区域。
  2. 多个进程通过系统调用将该区域映射到自己的虚拟地址空间。
  3. 进程通过虚拟地址直接读写共享内存,数据传递无需内核参与。
  4. 通信完成后,进程将共享内存与自身地址空间脱离,内核释放共享内存。

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进程间通信机制各有优劣,选择时需根据场景权衡:

  • 管道(匿名/命名):简单易用,适合流式数据传输,无需复杂配置。
  • 共享内存:速度最快,适合大数据量通信,需配合同步机制使用。
  • 消息队列:支持按类型通信,适合小数据量、需异步通信的场景。
  • 信号量:不传递数据,专门用于同步互斥,保护临界资源。

实战开发中,管道常用于父子进程通信,共享内存+信号量常用于高并发大数据量场景,命名管道用于非亲缘进程通信。

相关推荐
Altair12311 小时前
实验6 基于端口和域名的虚拟主机
linux·运维·服务器·云计算
爱和冰阔落1 小时前
【Linux工具链】编译效率革命:条件编译优化+动静态库管理+Makefile自动化,解决多场景开发痛点
linux·运维·自动化
2501_941111511 小时前
Python多线程与多进程:如何选择?(GIL全局解释器锁详解)
jvm·数据库·python
wa的一声哭了1 小时前
WeBASE管理平台部署-WeBASE-Web
linux·前端·网络·arm开发·spring boot·架构·区块链
2501_941111332 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
♡喜欢做梦2 小时前
MyBatis操作数据库(入门)
java·数据库·mybatis
Shylock_Mister2 小时前
ARM与x86交叉编译实战排错指南
linux·c语言·arm开发
敲上瘾2 小时前
MySQL事务全攻略:ACID特性、隔离级别与并发控制
linux·数据库·mysql·安全
懒羊羊不懒@2 小时前
【MySQL | 基础】事务
数据库·mysql