【Linux:文件】进程间通信

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《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.5.1 通道封装(Channel.hpp)](#2.5.1 通道封装(Channel.hpp))
      • [2.5.2 进程池实现(ProcessPool.hpp)](#2.5.2 进程池实现(ProcessPool.hpp))
      • [2.5.3 任务管理(Task.hpp)](#2.5.3 任务管理(Task.hpp))
      • [2.5.4 主程序(Main.cc)](#2.5.4 主程序(Main.cc))
      • [2.5.5 编译运行](#2.5.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有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux:文件】库的制作与原理:动静态库

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
MoonOutCloudBack1 小时前
VeRL 框架中的奖励 (reward) 与奖励模型:从 PPO 配置到实现细节
人工智能·深度学习·语言模型·自然语言处理
alfred_torres1 小时前
MedIA 2025 | TopoTxR:拓扑学“外挂”加持,深度学习精准预测乳腺癌化疗响应
人工智能·深度学习·拓扑学
梦游钓鱼1 小时前
C++指针深度解析:核心概念与工业级实践
开发语言·c++
小雨中_1 小时前
3.1 RLHF:基于人类反馈的强化学习
人工智能·python·深度学习·算法·动态规划
小野嵌入式1 小时前
3小时精通嵌入式串口通信!从零玩转ESP32+Modbus+OTA(1)
c语言·单片机·嵌入式硬件·mcu·物联网
The森1 小时前
Linux IO模型纵深解析:文章索引
linux·运维·服务器
MaoziShan1 小时前
CMU Subword Modeling | 11 Rules of realization and rules of referral
人工智能·语言模型·自然语言处理
心本无晴.1 小时前
LangGraph 进阶指南:从状态机到生产级多智能体架构的全面进化
linux·windows·microsoft
coding随想1 小时前
揭秘V8引擎的类型混淆漏洞:安全开发的警示与启示
网络·安全