《Linux系统编程》System V IPC 进阶:消息队列与信号量从入门到内核

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux操作系统从入门到实践》《Qt从入门到实践》

《算法题讲解指南》--优选算法

《算法题讲解指南》--递归、搜索与回溯算法

《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一. System V 消息队列:结构化的跨进程通信](#一. System V 消息队列:结构化的跨进程通信)

[1.1 核心原理与特性](#1.1 核心原理与特性)

[1.2 核心 API 详解](#1.2 核心 API 详解)

[1.2.1 数据结构(内核管理结构体)](#1.2.1 数据结构(内核管理结构体))

[1.2.2 核心 API 使用](#1.2.2 核心 API 使用)

[1.3 实战案例:消息队列实现 C/S 通信](#1.3 实战案例:消息队列实现 C/S 通信)

[1.3.1 公共头文件(comm.hpp)](#1.3.1 公共头文件(comm.hpp))

[1.3.2 服务端(server.cc)](#1.3.2 服务端(server.cc))

[1.3.3 客户端(client.cc)](#1.3.3 客户端(client.cc))

[1.3.4 编译与运行](#1.3.4 编译与运行)

[1.4 消息队列避坑指南](#1.4 消息队列避坑指南)

[二. System V 信号量:同步与互斥的核心工具](#二. System V 信号量:同步与互斥的核心工具)

[2.1 核心概念铺垫](#2.1 核心概念铺垫)

[2.2 核心原理与特性](#2.2 核心原理与特性)

[2.2.1 底层实现逻辑](#2.2.1 底层实现逻辑)

[2.2.2 核心特性](#2.2.2 核心特性)

为什么要有信号量?

[2.3 核心 API 详解](#2.3 核心 API 详解)

[2.3.1 semget:创建 / 获取信号量集](#2.3.1 semget:创建 / 获取信号量集)

[2.3.2 semop:执行 P/V 操作(核心 API)](#2.3.2 semop:执行 P/V 操作(核心 API))

[2.3.3 semctl:控制信号量集](#2.3.3 semctl:控制信号量集)

[2.3.4 union semun 结构体(需手动定义)](#2.3.4 union semun 结构体(需手动定义))

[2.4 实战案例:信号量保护共享内存](#2.4 实战案例:信号量保护共享内存)

[2.4.1 公共头文件(sem_comm.h)](#2.4.1 公共头文件(sem_comm.h))

[2.4.2 服务端(server.cc)](#2.4.2 服务端(server.cc))

[2.4.3 客户端(client.cc)](#2.4.3 客户端(client.cc))

[2.4.4 编译与运行](#2.4.4 编译与运行)

[2.5 信号量避坑指南](#2.5 信号量避坑指南)

[三. 内核如何管理 System V IPC 资源](#三. 内核如何管理 System V IPC 资源)

[3.1 核心管理结构](#3.1 核心管理结构)

[3.1.1 struct ipc_ids(全局管理结构体)](#3.1.1 struct ipc_ids(全局管理结构体))

[3.1.2 struct kern_ipc_perm(权限控制结构体)](#3.1.2 struct kern_ipc_perm(权限控制结构体))

[3.2 内核管理流程图(重点)](#3.2 内核管理流程图(重点))

[3.4 结论和总结](#3.4 结论和总结)

结束语


前言

在 Linux 进程间通信(IPC)体系中,System V IPC 家族除了高效的共享内存,还包含消息队列和信号量两大核心组件。消息队列负责结构化数据的传输,信号量专注于进程间的同步与互斥,二者与共享内存配合使用,可以构建出稳定、安全的跨进程通信方案。本文将从原理、API 使用、实战案例到内核管理机制,全面拆解这两种 IPC 技术,带你深入理解 System V IPC 的完整生态。

一. System V 消息队列:结构化的跨进程通信

消息队列 是一种 "基于消息的 IPC 机制" ,核心是内核 维护的一个链表结构的消息队列,进程可向队列中添加带类型的消息,也可按类型读取消息,实现结构化、异步的数据传输。

1.1 核心原理与特性

消息队列的本质是内核中的链表 ,每个消息队列由唯一的 key标识,每个消息包含三部分:

  • 消息类型(正整数,用于接收方筛选消息);
  • 消息长度(消息正文的字节数);
  • 消息正文(实际传输的数据)。

进程通过 msgsnd 向队列发送消息链表尾插入 ),通过msgrcv 从队列读取消息按类型从链表头或指定位置提取),内核自动管理消息的存储和同步。

1.2 核心 API 详解

System V 消息队列通过ftok、msgget、msgsnd、msgrcv、msgctl五个 API 实现完整操作,与共享内存的 API 设计逻辑一致,降低学习成本。

1.2.1 数据结构(内核管理结构体)

内核通过 struct msg_queue管理消息队列属性(和我们查指令查出来的不同,这个是内核里的),核心字段如下:

cpp 复制代码
struct msg_queue 
{
    struct kern_ipc_perm q_perm;  // 权限控制结构体(含key、uid、gid等)
    int q_id;                     // 消息队列ID
    time_t q_stime;               // 最后一次msgsnd时间
    time_t q_rtime;               // 最后一次msgrcv时间
    time_t q_ctime;               // 最后一次属性修改时间
    unsigned long q_cbytes;       // 队列中消息总字节数
    unsigned long q_qnum;         // 队列中消息总数
    unsigned long q_qbytes;       // 队列最大允许字节数(默认16384)
    pid_t q_lspid;                // 最后一次发送消息的进程PID
    pid_t q_lrpid;                // 最后一次接收消息的进程PID
    struct list_head q_messages;   // 消息链表头
    struct list_head q_receivers;  // 等待接收消息的进程链表
    struct list_head q_senders;    // 等待发送消息的进程链表
};

1.2.2 核心 API 使用

(1)ftok:生成唯一 Key(与共享内存通用)

cpp 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
  • 作用 :将**"文件路径 + 项目 ID"** 转换为唯一 key,作为消息队列的全局标识;
  • 注意:路径必须是存在的文件,proj_id为非 0 整数(如0x6666)。

(2)msgget:创建 / 获取消息队列

cpp 复制代码
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

参数

  • **key:**ftok 生成的 Key;
  • msgflg: 权限标志,常用组合:
    • **IPC_CREAT:**不存在则创建,存在则获取(一般用于获取消息队列);
    • **IPC_CREAT | IPC_EXCL:**不存在则创建,存在则报错(一般用于创建消息队列);
  • 权限位(如0666):控制进程对队列的访问权限;
  • 返回值:成功返回消息队列 ID(msgid),失败返回 - 1。

(3)msgsnd:发送消息

cpp 复制代码
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数

  • **msqid:**msgget返回的消息队列 ID;
  • msgp: 指向消息结构 体的指针 (需自定义,首字段必须是long mtype消息类型);
  • msgsz: 消息正文长度(不含消息类型字段);
  • **msgflg:**发送标志(0阻塞,IPC_NOWAIT非阻塞);
  • 返回值:成功返回 0,失败返回 - 1。

(5)msgctl:控制消息队列(核心功能:删除)

cpp 复制代码
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数

  • cmd: 控制命令(核心为 IPC_RMID,删除消息队列);
  • buf: 存储队列属性的结构体指针(IPC_RMID 时可设为 NULL);
  • 返回值:成功返回 0,失败返回 - 1。

1.3 实战案例:消息队列实现 C/S 通信

通过**"服务端 + 客户端"**演示消息队列的使用,服务端接收客户端消息并回复,客户端发送消息并接收回复。

1.3.1 公共头文件(comm.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/msg.h>

// 错误处理宏
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66;          // int proj_id
const std::string pathname = "."; // const char *pathname
const int gmode = 0666;           // 权限位

struct my_msgbuf
{
    long mtype;       // 消息类型(服务端→客户端:100,客户端→服务端:200)
    char mtext[1024]; // 消息正文
};

#define CREATER "creater"
#define USER "user"

// 消息队列Msg结构体
class Msg
{
public:
    Msg(const std::string &pathname, int projid, const std::string &usertype)
        : _msgid(-1), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key == -1)
        {
            ERR_EXIT("ftok");
        }

        printf("msg key:0x%x\n", _key);

        // 创建/获取信号量
        if (usertype == USER)
        {
            Get();
        }
        else if (usertype == CREATER)
        {
            Creat();
        }
    }

    // 发送消息
    void SendMsg(long type, const char *text)
    {
        struct my_msgbuf msg;
        msg.mtype  =type;
        strncpy(msg.mtext, text, 1023);
        int n = msgsnd(_msgid, &msg, strlen(msg.mtext), 0);
        if (n < 0)
        {
            ERR_EXIT("msgsnd");
        }
    }

    // 接收消息
    void RecvMsg(long type, char *text)
    {
        struct my_msgbuf msg;
        msg.mtype = type;
        ssize_t n = msgrcv(_msgid, &msg, 1024, type, 0);
        if (n < 0)
        {
            ERR_EXIT("msgrcv");
        }
        // 结尾手动加\0
        msg.mtext[n] = 0;
        strncpy(text, msg.mtext, 1023);
    }

    ~Msg()
    {
        if (_usertype == CREATER)
        {
            Destroy();
            printf("服务端:消息队列已删除\n");
        }
        else
        {
            printf("客户端:退出通信\n");
        }
    }

private:
    // 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
    // 创建全新消息队列
    void Creat()
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
        printf("服务端: 创建消息队列成功, msgid=%d\n", _msgid);
    }

    // 获取已存在的消息队列
    void Get()
    {
        CreatHelper(IPC_CREAT);
        printf("客户端: 获取消息队列成功, msgid=%d\n", _msgid);
    }

    // 内部通用函数:创建/获取消息队列
    void CreatHelper(int flg)
    {
        _msgid = msgget(_key, flg);
        if (_msgid == -1)
        {
            ERR_EXIT("msgget");
        }
    }

    // 删除消息队列
    void Destroy()
    {
        int n = msgctl(_msgid, IPC_RMID, nullptr);
        if (n < 0)
        {
            ERR_EXIT("msgctl");
        }
    }

private:
    key_t _key;
    int _msgid;
    std::string _usertype;
};

1.3.2 服务端(server.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1. 创建消息队列(通信发起者)
    Msg msg(pathname, projid, CREATER);

    // 2. 循环接收客户端消息(类型200)并回复消息给客户端(类型100)
    char buffer[1024];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer));
        // 接收客户端消息
        msg.RecvMsg(200, buffer);
        printf("服务端收到:%s\n", buffer);

        // 回复客户端
        if (strcmp(buffer, "quit") == 0)
        {
            msg.SendMsg(100, "客户端退出,服务端即将关闭");
            break;
        }
        msg.SendMsg(100, "已收到你的消息");
        sleep(1);
    }

    // 3. 删除消息队列
    // 进程结束自动调用析构函数
    return 0;
}

1.3.3 客户端(client.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 1. 获取消息队列
    Msg msg(pathname, projid, USER);

    // 2. 循环发送消息(类型200)并接收回复(类型100)
    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        printf("客户端请输入:");
        fflush(stdout);
        fgets(buffer, 1024, stdin);
        buffer[strlen(buffer) - 1] = '\0'; // 去除换行符

        // 发送消息
        msg.SendMsg(200, buffer);
        if (strcmp(buffer, "quit") == 0) 
        {
            break;
        }

        // 接收回复
        memset(buffer, 0, sizeof(buffer));
        msg.RecvMsg(100, buffer);
        printf("服务端回复:%s\n", buffer);
    }
    return 0;
}

1.3.4 编译与运行

Makefile:

cpp 复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -f client server

1.4 消息队列避坑指南

  • 消息结构体必须以 long mtype 开头:这是内核规定的格式,否则 msgsnd/msgrcv 会报错;
  • 消息类型必须为正整数: mtype 不能为 0 或负数,否则发送失败;
  • 消息长度不含 mtype 字段:msgsndmsgsz 参数是消息正文长度不是整个结构体长度
  • 必须手动删除队列 :消息队列不会随进程退出而释放 ,需用 msgctl(IPC_RMID) 删除,残留队列可通过 ipcs -q 查看ipcrm -q msgid 删除

二. System V 信号量:同步与互斥的核心工具

信号量并非用于数据传输 ,而是用于保护临界资源 ,实现进程间的同步与互斥 。它本质是一个 "内核维护的计数器" ,通过 P/V 操作(申请 / 释放资源)控制进程对临界资源的访问。

2.1 核心概念铺垫

在学习信号量前,需明确三个关键概念:

  • 临界资源:多个进程共享的资源(如共享内存、文件),一次仅允许一个进程访问;
  • 临界区:进程中访问临界资源的代码段;
  • 同步与互斥
    • 互斥: 任意时刻仅允许一个进程进入临界区(如多个进程写共享内存);
    • 同步: 多个进程访问临界资源时需遵循特定顺序(如 "生产者先写,消费者再读")。

信号量的核心是 "通过计数器控制资源访问权限",计数器值代表 "可用资源数量":

  • P 操作 (申请资源):计数器 - 1 ,若计数器 < 0,进程阻塞
  • V 操作 (释放资源):计数器 + 1 ,若计数器 ≤ 0,唤醒一个阻塞进程

2.2 核心原理与特性

2.2.1 底层实现逻辑

System V 信号量是一个 "信号量集" (可包含多个信号量),内核通过struct sem_array 管理信号量集属性,每个信号量通过struct sem描述:

cpp 复制代码
struct sem_array 
{
    struct kern_ipc_perm  sem_perm;  //IPC权限信息(所有者、权限掩码、key等,参考ipc.h)
    int                   sem_id;     //信号量集合在内核的标识符semid
    time_t                sem_otime;  //最后一次执行semop()操作的时间戳
    time_t                sem_ctime;  //最后一次修改信号量集合属性的时间(semctl/创建/删除)
    struct sem            *sem_base;   //指向信号量数组首元素的指针,数组内是单个struct sem
    struct sem_queue      *sem_pending;//阻塞等待该信号量集合的进程等待队列头(挂起的阻塞操作) 
    struct sem_queue      **sem_pending_last; //等待队列尾指针,用于快速尾插新阻塞进程
    struct sem_undo       *undo;       //关联该信号量集的sem_undo链表,实现进程退出自动撤销信号量操作
    unsigned long         sem_nsems; //当前信号量集合内包含的信号量个数
};
cpp 复制代码
struct sem 
{
    int semval;         // 信号量计数器值
    pid_t sempid;       // 最后一次操作该信号量的进程PID
    unsigned short semncnt; // 等待计数器>0的进程数
    unsigned short semzcnt; // 等待计数器=0的进程数
};

2.2.2 核心特性

为什么要有信号量?

2.3 核心 API 详解

System V 信号量的 API 与消息队列、共享内存风格一致,核心包括semget、semop、semctl

2.3.1 semget:创建 / 获取信号量集

cpp 复制代码
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

参数

  • nsems:信号量集 中的信号量个数(创建时必须指定,获取时可设为 0);
  • 其他参数与上面的msgget 一致;
  • 返回值:成功返回信号量集 ID(semid),失败返回 - 1。

2.3.2 semop:执行 P/V 操作(核心 API)

cpp 复制代码
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

参数

  • sops: 指向 struct sembuf 数组的指针,每个元素描述一个信号量的操作;
  • nsops: sops数组的长度(操作的信号量个数);
  • struct sembuf结构体:
cpp 复制代码
struct sembuf 
{
    unsigned short sem_num; // 信号量集中的信号量下标(从0开始)
    short sem_op;           // 操作类型(-1=P操作,+1=V操作)
    short sem_flg;          // 操作标志(0=阻塞,IPC_NOWAIT=非阻塞)
};

2.3.3 semctl:控制信号量集

cpp 复制代码
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

参数

  • semnum: 信号量集中的信号量下标(操作单个信号量时指定);
  • **cmd:**控制命令(核心命令如下);
  • 可变参数: 根据cmd不同,可传入union semun结构体 (用于设置信号量初始值);

核心命令说明

命令 描述
IPC_RMID 删除信号量集(忽略其他参数)
SETVAL 设置单个信号量的初始值(需传入 union semun
GETVAL 获取单个信号量的当前值
SETALL 设置信号量集中所有信号量的初始值
GETALL 获取信号量集中所有信号量的当前值

2.3.4 union semun 结构体(需手动定义)

cpp 复制代码
union semun 
{
    int val;                // 用于SETVAL/GETVAL
    struct semid_ds *buf;   // 用于IPC_STAT/IPC_SET
    unsigned short *array;  // 用于SETALL/GETALL
    struct seminfo *__buf;  // 用于IPC_INFO
};

2.4 实战案例:信号量保护共享内存

结合共享内存和信号量,实现**"多进程安全写共享内存"**------ 通过信号量的互斥机制,确保同一时刻仅一个进程写入共享内存,避免数据混乱。

2.4.1 公共头文件(sem_comm.h)

cpp 复制代码
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/sem.h>

// 错误处理宏
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66;          // int proj_id
const std::string pathname = "."; // const char *pathname
const int gmode = 0666;           // 权限位

#define SEM_NUM 1 // 信号量集中的信号量个数

// 定义semun联合体,用于semctl系统调用中的可变参数
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

#define CREATER "creater"
#define USER "user"

// 信号量sem结构体
class Sem
{
public:
    Sem(const std::string &pathname, int projid, const std::string &usertype)
        : _semid(-1), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key == -1)
        {
            ERR_EXIT("ftok");
        }
        printf("sem key:0x%x\n", _key);

        // 创建/获取信号量
        if (usertype == USER)
        {
            Get();
        }
        else if (usertype == CREATER)
        {
            Creat();
            // void InitSem(int semid, int semnum, int val)
            // semnum:信号量集中的信号量下标(操作单个信号量时指定)
            // val:初始化信号量值为 1(互斥场景初始值设为 1(二元信号量))
            InitSem(_semid, 0, 1);
        }
    }

    // P操作
    void P()
    {
        struct sembuf sb;
        //初始化struct sembuf sb
        sb.sem_num = 0; //下标为0的信号量
        sb.sem_op = -1; // P操作:计数器-1,此时联合体semun中的val减为0,其他进程无法再进入
        sb.sem_flg = 0; // 阻塞模式
        int n = semop(_semid, &sb, 1);
        if(n == -1)
        {
            ERR_EXIT("semop P");
        }
    }

    // V操作
    void V()
    {
        struct sembuf sb;
        //初始化struct sembuf sb
        sb.sem_num = 0; //下标为0的信号量
        sb.sem_op = 1; // V操作:计数器+1,此时联合体semun中的val恢复成1,使其他进程可以进入
        sb.sem_flg = 0; // 阻塞模式
        int n = semop(_semid, &sb, 1);
        if(n == -1)
        {
            ERR_EXIT("semop V");
        }
    }

    ~Sem()
    {
        if (_usertype == CREATER)
        {
            Destroy();
        }
    }

private:
    // 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
    // 创建信号量集
    void Creat()
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    // 获取信号量集
    void Get()
    {
        CreatHelper(IPC_CREAT);
    }

    // 信号量集通用函数
    void CreatHelper(int flg)
    {
        _semid = semget(_key, 1, flg);
        if (_semid == -1)
        {
            ERR_EXIT("semget");
        }
    }

    // 初始化信号量
    void InitSem(int semid, int semnum, int val)
    {
        // 创建联合体semun对象,对val进行初始化
        union semun un;
        un.val = val;
        int n = semctl(semid, semnum, SETVAL, un);
        if (n < 0)
        {
            ERR_EXIT("semctl SETVAL");
        }
    }

    // 删除信号量集
    void Destroy()
    {
        int n = semctl(_semid, 0, IPC_RMID);
        if (n < 0)
        {
            ERR_EXIT("semctl IPC_RMID");
        }
    }

private:
    int _semid;
    key_t _key;
    std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};

//复用共享内存Shm结构体
class Shm
{
public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(-1), _size(4096), _start_mem(nullptr), _usertype(usertype)
    {
        // 获取唯一 Key
        _key = ftok(pathname.c_str(), projid);
        if (_key == -1)
        {
            ERR_EXIT("ftok");
        }
        printf("key:0x%x\n", _key);

        // 创建/获取共享内存
        if (usertype == USER)
        {
            Get();
        }
        else if (usertype == CREATER)
        {
            Creat();
        }

        // 进程连接共享内存
        Attach();
    }

    void *GetVirtualAddr()
    {
        printf("VirtualAddr:%p\n", _start_mem);
        return _start_mem;
    }

    // 获取指定共享内存的相关属性
    void Attr()
    {
        struct shmid_ds ds;                    // 描述共享内存的数据结构
        int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数
        printf("shsm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
        printf("uid: %d\n", ds.shm_perm.uid);
    }

    // 析构函数
    ~Shm()
    {
        Detach();
        if (_usertype == CREATER)
        {
            Destroy();
        }
    }

private:
    // 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
    // 创建共享内存
    void Creat()
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    // 获取共享内存
    void Get()
    {
        CreatHelper(IPC_CREAT);
    }

    void CreatHelper(int flg)
    {
        _shmid = shmget(_key, _size, flg);
        if (_shmid == -1)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid:%d\n", _shmid);
    }

    // 共享内存映射挂载
    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if (_start_mem == (void *)(-1))
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    // 去关联
    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success\n");
        }
        else
        {
            ERR_EXIT("shmdt");
        }
    }

    // 删除共享内存
    void Destroy()
    {
        if (_shmid == -1)
            return;
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        if (n < 0)
        {
            ERR_EXIT("shmctl");
        }
        else
        {
            printf("shmctl delete shm:%d success\n", _shmid);
        }
    }

private:
    int _shmid;
    int _size;
    void *_start_mem; // 共享内存在地址空间的起始地址
    key_t _key;
    std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};

2.4.2 服务端(server.cc

cpp 复制代码
#include "sem_comm.hpp"

int main()
{
    // 1. 创建信号量集(1个信号量)并初始化(初始值1,互斥)
    Sem sem(pathname, projid, CREATER);

    // 2. 创建共享内存
    Shm shm(pathname, projid, CREATER);
    char *shmaddr = (char *)shm.GetVirtualAddr();

    // 3. 循环写入共享内存(P/V操作保护临界区)
    for (int i = 0; i < 4; i++)
    {
        sem.P(); // 申请资源(进入临界区)
        snprintf(shmaddr, 1024, "进程[%d]写入数据:%d", getpid(), i);
        printf("进程[%d]写入:%s\n", getpid(), shmaddr);
        sleep(3);
        sem.V(); // 释放资源(退出临界区)
    }
    sleep(2);

    // 4. 清理资源
    // 进程结束自动调用sem和shm的析构函数
    return 0;
}

2.4.3 客户端(client.cc

cpp 复制代码
#include "sem_comm.hpp"

int main()
{
    // 1. 获取信号量集
    Sem sem(pathname, projid, USER);

    // 2. 获取共享内存
    Shm shm(pathname, projid, USER);
    char *shmaddr = (char *)shm.GetVirtualAddr();

    // 3. 循环读取共享内存(P/V操作保护临界区)
    for (int i = 0; i < 4; i++)
    {
        sem.P(); // 申请资源(进入临界区)
        printf("进程[%d]读取:%s\n", getpid(), shmaddr);
        // sleep(3);
        sem.V(); // 释放资源(退出临界区)
    }

    // 4. 清理资源
    //进程结束自动调用sem和shm的析构函数
    return 0;
}

2.4.4 编译与运行

Makefile:

bash 复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -f client server
  • 关键:信号量初始值为 1,确保同一时刻仅一个进程进入临界区(读写共享内存),避免数据混乱。

2.5 信号量避坑指南

  • 信号量初始值设置互斥场景 初始值设为 1(二元信号量 ),同步场景 按资源数量设置(如 2 个资源初始值设为 2:多元信号量);
  • P/V 操作成对出现 :进入临界区前执行 P 操作,退出后执行 V 操作,避免死锁
  • 信号量集删除时机 :需等待所有进程 完成操作后再删除,否则正在操作的进程会报错;
  • 避免信号量滥用 :信号量仅用于同步互斥 ,不用于数据传输,不要试图通过信号量传递信息

三. 内核如何管理 System V IPC 资源

System V IPC(共享内存、消息队列、信号量)的内核管理逻辑一致 ,核心通过 struct ipc_idsstruct kern_ipc_perm 实现全局管理,这是理解 System V IPC 本质的关键。

3.1 核心管理结构

我们这里仅展示两个具有共性的管理结构体(共享内存,消息队列,信号量都有),剩下这些结构之间的关联的具体流程看后面的部分!

3.1.1 struct ipc_ids(全局管理结构体)

内核 维护三个全局 ipc_ids 结构体 ,分别管理共享内存消息队列信号量(有三个静态全局变量):

cpp 复制代码
struct ipc_ids 
{
    int in_use;                // 当前使用的IPC资源个数
    int max_id;                // 最大的IPC资源ID
    unsigned short seq;        // 序列号(用于生成唯一ID)
    unsigned short seq_max;    // 序列号最大值
    struct mutex mutex;        // 保护该结构体的互斥锁
    struct ipc_id_ary nullentry; // 空条目
    struct ipc_id_ary *entries; // 指向IPC资源数组的指针
};
  • 作用:记录系统中所有该类型 IPC 资源的元数据,实现资源的创建、查找、删除。

3.1.2 struct kern_ipc_perm(权限控制结构体)

所有 System V IPC 资源都包含 struct kern_ipc_perm 字段,用于权限控制和唯一标识

cpp 复制代码
struct kern_ipc_perm 
{
    spinlock_t lock;           // 自旋锁
    int deleted;              // 资源是否被标记删除
    key_t key;                // 资源的唯一Key
    uid_t uid;                // 创建者用户ID
    gid_t gid;                // 创建者组ID
    uid_t cuid;               // 最后修改者用户ID
    gid_t cgid;               // 最后修改者组ID
    mode_t mode;              // 访问权限(如0666)
    unsigned long seq;        // 序列号
    void *security;           // 安全相关指针
};
  • 核心key 字段是 IPC 资源的全局唯一标识mode 字段控制进程对资源的访问权限

3.2 内核管理流程图(重点)

3.4 结论和总结

  • System V IPC 资源的生命周期随内核 ,本质是内核维护结构体和数据结构
  • key 是资源的全局唯一标识msgid 是进程访问资源的句柄
  • 权限控制 通过 kern_ipc_perm.mode 实现,与文件权限规则一致。

System V 消息队列、信号量 核心要点总结:

  • 消息队列:用于结构化、异步跨进程通信,支持按类型筛选消息,适合数据传输场景;
  • 信号量:用于同步与互斥,通过 P/V 操作保护临界资源,常与共享内存配合使用;
  • 内核管理 :System V IPC 通过ipc_idskern_ipc_perm 实现全局管理生命周期随内核 ,需手动删除
  • 选型建议
    • 需结构化数据传输→消息队列;
    • 需同步互斥→信号量;
    • 需高效大数据传输→共享内存 + 信号量。

结束语

至此,我们已经完整走完了 System V IPC 的三大组件:消息队列、共享内存和信号量。消息队列提供结构化的数据传递,共享内存以绕开内核的机制实现最高效的通信,而信号量则通过 P/V 操作解决了共享内存的同步问题。同时,我们也深入内核,看到了 ipc_idskern_ipc_perm 等管理结构,理解了操作系统"先描述,再组织"的管理思路。System V IPC 是 Linux 传统 IPC 的核心,掌握这三种技术,足以应对绝大多数跨进程通信场景,也为后续学习网络编程和多线程同步打下坚实基础。

相关推荐
代码熬夜敲Q1 小时前
网络工程相关
linux·服务器·网络
我先去打把游戏先2 小时前
VMware NAT 模式 Ubuntu 虚拟机「宿主机能上网、虚拟机 ping 不通外网 + apt 更新卡死」全故障复盘
linux·运维·vscode·单片机·嵌入式硬件·ubuntu·keil5
开压路机2 小时前
基础IO
linux·运维·服务器
JeJe同学2 小时前
LabelImg 标签字体大小修改教程
linux·人工智能·python
小鸡毛程序员2 小时前
从零搭建 Linux 开发服务器:VMware NAT 静态网络 + Docker + MySQL + Redis + 云服务器迁移
linux·服务器·网络
Cx330❀3 小时前
【Linux网络】一文吃透 TCP Socket 编程
linux·运维·服务器·开发语言·网络·tcp/ip
zizle_lin3 小时前
WSL初始化Ubuntu的使用
linux·运维·ubuntu·wsl
c++之路3 小时前
Linux 下 C++ 开发环境搭建
linux·运维·c++
溜达的大象3 小时前
Ubuntu服务器之间校时
linux·ubuntu·excel