Linux下 进程间通信详解(二)System V IPC

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】


进程间通信相关内容:

  1. Linux下 进程间通信详解(一)管道、进程池详解
  2. Linux下 进程间通信详解(二)System V IPC

文章目录

  • [1. System V共享内存](#1. System V共享内存)
    • [1.1 共享内存的原理](#1.1 共享内存的原理)
    • [1.2 进一步理解](#1.2 进一步理解)
    • [1.3 共享内存函数](#1.3 共享内存函数)
      • [1. shmget函数](#1. shmget函数)
      • [2. ftok](#2. ftok)
      • [3. ipcs -m/ipcrm -m](#3. ipcs -m/ipcrm -m)
      • [4. shmctl 函数](#4. shmctl 函数)
      • [5. shmat 函数](#5. shmat 函数)
      • [6. shmdt 函数](#6. shmdt 函数)
    • [1.4 共享内存数据结构和特性](#1.4 共享内存数据结构和特性)
      • [1. 共享内存数据结构](#1. 共享内存数据结构)
      • [2. 共享内存特性](#2. 共享内存特性)
    • [1.5 测试代码](#1.5 测试代码)
  • [2. System V 消息队列(选学了解即可)](#2. System V 消息队列(选学了解即可))
    • [2.1 原理与特点](#2.1 原理与特点)
    • [2.2 消息队列结构](#2.2 消息队列结构)
    • [2.3 接口](#2.3 接口)
      • [1. msgget](#1. msgget)
      • [2. msgctl](#2. msgctl)
      • [3. msgsnd](#3. msgsnd)
      • [4. msgrcv](#4. msgrcv)
      • [4. ipcs -q](#4. ipcs -q)
  • [3. System V 信号量(选学了解即可)](#3. System V 信号量(选学了解即可))
    • [3.1 并发编程,概念铺垫](#3.1 并发编程,概念铺垫)
    • [3.2 信号量](#3.2 信号量)
    • [3.3 信号量结构体](#3.3 信号量结构体)
    • [3.4 接口](#3.4 接口)
      • [1. semget](#1. semget)
      • [2. semctl](#2. semctl)
      • [3. semop](#3. semop)
      • [4. ipcs -s](#4. ipcs -s)
  • [4. IPC在内核中的数据结构设计(区分用户级 和 内核级数据结构)](#4. IPC在内核中的数据结构设计(区分用户级 和 内核级数据结构))
  • [5. 共享内存在内核中的原理(重点!!讲透)](#5. 共享内存在内核中的原理(重点!!讲透))

1. System V共享内存

1.1 共享内存的原理

每个进程都有属于自己的虚拟地址空间,由页表映射内存中的物理地址,代码和数据都在具体的物理内存中!只需要两个进程 的虚拟地址 通过 页表 映射到同一块物理空间,即 让不同的进程,把同一个内存块,映射到自己的虚拟地址空间,每一个进程得到自己的虚拟地址空间的起始地址!

1.2 进一步理解

  1. 映射到进程的虚拟地址空间中的 共享区!
  1. 共享内存原理,其实就是是一个简化版本的动态库映射!
  2. 共享内存是可以在同一个进程存在多份的,所以内核需要通过结构体对其进行管理!所以 共享内存 = 共享内存管理结构体 + 共享内存本身!
  3. 使用共享内存的步骤:a. 创建 b. 关联挂接(页表映射)c. 使用 d. 去关联 f. 释放共享内存

所以 共享内存是什么?

采用多个进程,使用虚拟地址空间映射的方式,让不同的进程看到同一个内存块!

1.3 共享内存函数

1. shmget函数

c 复制代码
功能:⽤来创建共享内存
头文件:
#include<sys/shm.h>
原型
int shmget(key_t key, size_t size, int shmflg);
  • 参数
  1. key:这个共享内存段名字( key_t 本质上是一个至少 32 位的整数类型)
  2. size:共享内存⼤⼩( 最好是4kb整数倍!因为如果你申请4097byte 它实际会给你申请8kb 但在bytes上依然是4097bytes 造成资源浪费! )
  3. shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
    • 取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
    • 取值为IPC_CREAT | IPC_EXCL: 共享内存不存在,创建并返回;共享内存已存在,出错返回。
    • 同时可以带上权限,例如:IPC_CREAT | IPC_EXCL|0666否则当用shmat挂接时会因为无权限而挂接失败!!
  • 返回值:
    • 成功:返回一个非负整数,即 共享内存标识符 (shmid),后续操作(如 shmat)都需要用到它。
    • 失败:返回 -1,并将错误原因存入 errno 中(如 EEXIST 已存在、ENOMEM 内存不足等)。

key 如果由OS自己创建 那么假设进程A创建好了是无法传给进程B的(能传递不就已经实现进程间通信了吗!)
所以 需要由用户自己来创建key(理论上随便取),因为key是自己取的 所以理论上进程 A B都知道 那个具体的共享内存


key vs shmid:

  1. key只在内存中,标识共享内存的唯一性!用户使用共享内存的时候,不使用这个key
  2. shmid 只在用户当中使用,在自己代码中,使用shmid来访问共享内存!

相当于fd 和 inode之间的关系!

对比维度 key (全局"名字") shmid (本地"句柄")
核心作用 用于发现或创建资源。不同进程通过相同的 key 访问同一块共享内存。 用于操作和释放资源。后续所有的挂接、分离和控制操作都必须使用它。
所属层级 用户层标识符(进程间约定的公共标识)。 内核层标识符(内核分配的操作句柄)。
生成方式 由用户指定,或者用ftok 由操作系统内核在 shmget() 调用成功后自动分配并返回。
生命周期 静态约定,长期有效(随内核生命周期存在)。 动态生成,随 shmget() 调用获得,在进程内有效。
经典类比 类似文件系统中的文件路径名(如 /tmp/my_shm)或 inode 号。 类似文件系统中的文件描述符 (fd)。

2. ftok

ftok 是 Linux/Unix 系统编程中用于生成 System V IPC 键值(key_t)的核心函数。它的主要作用是将一个已存在的路径名和一个项目标识符(整数)组合,生成一个系统范围内唯一的键值(key)

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  1. pathname (文件路径)
  • 必须指向一个已经存在且可访问 的文件或目录(例如 /tmp/myfile 或当前目录 .)。
  • 内核会通过这个路径获取文件的元数据(主要是设备号 st_dev ( 表示该文件所在的底层存储设备(如某块硬盘或某个分区)的编号 )和索引节点号 st_ino( 每个文件在创建时,文件系统都会为其分配一个唯一的 inode 编号)。
  1. proj_id (项目标识符)
  • 用户自定义的整数,通常取值范围是 1~255(实际只取低 8 位)。
  • 它的作用是让同一个文件也能生成多个不同的 IPC 键值(比如用同一个文件,proj_id=1 生成共享内存的 key,proj_id=2 生成消息队列的 key)。
  1. 返回值
  • 成功时:返回生成的 key_t 键值
  • 失败时:返回 -1

键值生成原理(了解即可):

ftok 并不是简单地返回一个随机数,而是通过组合以下信息计算出来的:

  • 文件所在设备的设备号(st_dev 的低 8 位)
  • 文件的索引节点号(st_ino 的低 16 位)
  • 项目标识符(proj_id 的低 8 位)

计算公式为:

key = (st_dev & 0xff) << 16 | (st_ino & 0xffff) | (proj_id & 0xff) << 24


3. ipcs -m/ipcrm -m

ipcs -m 是 Linux 系统中用于查看当前系统中所有共享内存段(Shared Memory Segments)详细信息的命令。

  • key:共享内存的全局键值(即通过 ftok() 生成或硬编码的 key_t 值)。
  • shmid:共享内存段的唯一标识符(内核分配的索引)。
  • owner:创建该共享内存的用户(对应 shm_perm 中的所有者信息)。
  • perms:访问权限(如 600、666,对应shm_perm.mode)。
  • bytes:共享内存段的大小(对应 shm_segsz)。
  • nattch:当前挂接(attach)到该共享内存的进程数量(对应 shm_nattch)。
  • status:当前状态(例如 dest表示已被标记删除,但还在等待最后一个进程分离)。

ipcrm -m 是 Linux/Unix 系统中用于 删除指定共享内存段(Shared Memory Segment) 的命令。

bash 复制代码
ipcrm -m <shmid>

⚠️:指令的执行本质也是用户代码在执行!


除了使用数字 ID,ipcrm 还支持通过大写的 -M 参数,直接使用 ftok() 生成的十六进制键值(Key)来删除共享内存:

bash 复制代码
ipcrm -M 0x0399ef4c

4. shmctl 函数

shmctl 是 Linux/Unix 系统编程中用于控制和管理 System V 共享内存段的核心函数。

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid :共享内存的标识符(由 shmget 函数返回的 ID)。
  • cmd:控制命令,决定了你要对共享内存执行什么操作(见下文)。
  • buf :指向 struct shmid_ds 结构体的指针。根据 cmd 的不同,它可能作为输入缓冲区(写入属性),也可能作为输出缓冲区(读取属性),或者在某些命令下直接设为 NULL

cmd 参数是 shmctl 的灵魂,最常用的有以下三个:

  • IPC_STAT(查询状态) : 将共享内存当前的属性(如大小、权限、创建时间等)读取到 buf 指向的 struct shmid_ds 结构中。这相当于内核版的 ipcs -m -i <shmid>
  • IPC_SET(修改属性) : 将 buf 中修改后的属性(如所有者 uid、所属组 gid、访问权限 mode)设置到共享内存中。执行此操作需要足够的权限(通常是属主或 root)。
  • IPC_RMID(删除共享内存) : 标记该共享内存段为"待删除"状态。一旦执行,系统将不再允许新的进程挂接(attach)该内存。当所有已经挂接的进程都分离(detach)后,内核会真正释放这块物理内存。执行此命令时,buf参数通常设为 NULL

  • 成功 :返回 0
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括 EACCES(无权限)、EINVAL(无效的 shmid 或 cmd)、EPERM(权限不足以执行删除或修改操作)等。

利用chmctl查询状态:

cpp 复制代码
// 1. 准备一个结构体变量
struct shmid_ds buf;

// 2. 获取共享内存信息(假设 shmid 已经获取到了)
shmctl(shmid, IPC_STAT, &buf);

// 3. 打印出关键信息
printf("大小: %d 字节\n", buf.shm_segsz);
printf("权限: %o\n", buf.shm_perm.mode);
printf("有几个进程在用: %d\n", buf.shm_nattch);

5. shmat 函数

shmat 是 Linux/Unix 系统编程中用于将共享内存段附加(Attach)到调用进程地址空间 的核心函数。

如果说 shmget 是在内核中"申请了一块地",那么 shmat 就是"在你的进程里修一条路,让你能直接走到这块地上"。

c 复制代码
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid :共享内存的标识符(由 shmget 函数返回的 ID)。
  • shmaddr :期望附加到的虚拟内存地址。
    • NULL(最常用):让操作系统内核自动选择一个合适的、不会冲突的地址。
    • 传非 NULL :尝试附加到指定的地址。(不建议!
  • shmflg :附加标志位,控制访问权限。
    • 0(最常用) :以默认的读写模式附加。
    • SHM_RDONLY :以只读模式附加。

  • 成功 :返回附加好的共享内存段在进程地址空间中的起始地址 (类似于 malloc 返回的指针)。
  • 失败 :返回 (void *)-1,并将错误原因存入全局变量 errno 中。
    常见的错误包括 EACCES(无权限)、EINVAL(无效的 shmid 或 shmaddr)、ENOMEM(核心内存不足)等。

6. shmdt 函数

shmdtLinux/Unix 系统编程中用于将共享内存段从当前进程地址空间中分离(Detach)的函数。

如果说 shmat 是"修一条路让进程能访问共享内存",那么 shmdt 就是 "把这条路拆掉"。调用成功后,当前进程将无法再通过该指针访问这块共享内存。

c 复制代码
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
  • shmaddr :必须传入之前调用 shmat() 函数成功返回的共享内存起始地址。

  • 成功 :返回 0
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。最常见的错误是 EINVAL,表示传入的 shmaddr 不是有效的共享内存段起始地址。

1.4 共享内存数据结构和特性

1. 共享内存数据结构

cpp 复制代码
struct shmid_ds
 {
    struct ipc_perm shm_perm; /* 操作权限 */
    int shm_segsz; /* 共享内存段的大小(以字节为单位) */
    __kernel_time_t shm_atime; /* 最后一次挂接(attach)的时间 */
    __kernel_time_t shm_dtime; /* 最后一次分离(detach)的时间 */
    __kernel_time_t shm_ctime; /* 最后一次状态变更的时间 */
    __kernel_ipc_pid_t shm_cpid; /* 创建者的进程 ID (PID) */
    __kernel_ipc_pid_t shm_lpid; /* 最后一次操作者的进程 ID (PID) */
    unsigned short shm_nattch; /* 当前挂接(attach)的进程数量 */
    unsigned short shm_unused; /* 兼容性保留字段 */
    void *shm_unused2; /* 同上 - 供 DIPC 使用 */
    void *shm_unused3; /* 未使用字段 */
};

cpp 复制代码
struct ipc_perm
 {
    key_t          __key;       /* 提供给 shmget(2) 的键值 */
    uid_t          uid;         /* 所有者的有效用户 ID (UID) */
    gid_t          gid;         /* 所有者的有效组 ID (GID) */
    uid_t          cuid;        /* 创建者的有效用户 ID (UID) */
    gid_t          cgid;        /* 创建者的有效组 ID (GID) */
    unsigned short mode;        /* 权限位 + SHM_DEST 和 SHM_LOCKED 标志 */
    unsigned short __seq;       /* 序列号 */
};

2. 共享内存特性

  1. 共享内存(包括 system IPC),它的生命周期随内核!即用户如果不主动删除ipsc资源会和你的OS一样,一直存在直到你重启系统。
  2. 当以 IPC_CREAT | IPC_EXCL第二次创建共享内存会失败 本质是key发生了冲突!
  3. 访问共享内存,不需要系统调用,因为shm已经映射到了进程的用户。当写端拷贝数据的shm,其他端能立马看到!所以共享内存是所有进程间通信方式速度最快的:因为
    a. 拷贝次数少(管道至少要两次 用户内存到管道来回!而共享内存直接可以写入直接把其当作用户内存写,然后读的时候只需标准输出拷贝一次!) b.直接映射,不需要系统调用!
  4. 缺点:没有资源的保护机制,即没有同步或者互斥。

1.5 测试代码

下面是根据共享内存特性完成的进程间通信代码:

Shm.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <sys/shm.h>

const int gsize = 128; // 4096的整数倍!

// 截止到现在,我们有没有通信???没有!
// 让不同的进程,看到同一份资源!

// 用户指明的,本质: 等价于命名管道哪里的文件路径!!
// PATHNAME,PROJ_ID: 我们两个就能看到同一份资源!!
#define PATHNAME "/tmp"
#define PROJ_ID 0x66

class Shm
{
public:
    Shm() : _shmid(-1), _size(gsize), _start_addr(nullptr)
    {
    }

    void Delete()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        (void)n;
    }
    void Attach()
    {
        _start_addr = shmat(_shmid, nullptr, 0);
        if ((long long int)_start_addr == -1)
            exit(3);
    }
    void PrintAttr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if(n < 0)
        {
            perror("shmctl");
            exit(4);
        }
        printf("key: 0x%x\n", ds.shm_perm.__key);
        printf("shm_nattch: %ld\n", ds.shm_nattch);
        printf("shm_segsz: 0x%lx\n", ds.shm_segsz);
    }
    void Detach()
    {
        int n = shmdt(_start_addr);
        (void)n;
    }

    void Get()
    {
        GetHelper(IPC_CREAT);
    }
    void Create()
    {
        GetHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    void *Addr()
    {
        return _start_addr;
    }
    int Size()
    {
        return _size;  
    }

    ~Shm() {}

private:
    key_t GetKey()
    {
        return ftok(PATHNAME, PROJ_ID);
    }
    void GetHelper(int shmflg)
    {
        // 1. 构建键值
        key_t k = GetKey();
        if (k < 0)
        {
            std::cerr << "GetKey error";
            exit(1);
        }
        // 2. 创建新的共享内存
        _shmid = shmget(k, _size, shmflg);
        if (_shmid < 0)
        {
            perror("shmget");
            exit(2);
        }
        printf("key=0x%x, _shmid = %d\n", k, _shmid);
    }

private:
    int _shmid;
    int _size;
    void *_start_addr;
};

Server.cc

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

int main()
{
    printf("aaaaaaaaaaaaa\n");
    printf("aaaaaaaaaaaaa\n");
    printf("aaaaaaaaaaaaa\n");
    printf("aaaaaaaaaaaaa\n");
    printf("aaaaaaaaaaaaa\n");
    printf("aaaaaaaaaaaaa\n");
    // 生命周期的代码级管理!
    Shm sharedmem;
    sharedmem.Create();
    sharedmem.Attach();

    sleep(2);

    sharedmem.PrintAttr();

    char *shm_start = (char *)sharedmem.Addr();
    int size = sharedmem.Size();

    while (true)
    {
        // 本质就是读取共享内存!
        // 临界区代码!
        for (int i = 0; i < size; i++)
        {
            std::cout << shm_start[i] << ' ';
        }
        std::cout << std::endl;
        sleep(1);
    }

    sharedmem.Detach();
    sharedmem.Delete();
    return 0;
}

Client.cc

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

int main()
{
    // 不需要创建内核级共享内存,当然也不需要删除
    Shm sharedmem;
    sharedmem.Get();
    sharedmem.Attach();

    sleep(2);
    
    sharedmem.PrintAttr();

    char *shm_start = (char *)sharedmem.Addr();
    int size = sharedmem.Size();
    int index = 0;
    while (true)
    {
        std::cout << "Please Enter@ ";
        std::cin >> *shm_start;
        // *(shm_start + index) = ch;
        // shm_start[index++] = ch;
        shm_start++;

        index %= size;
        // sleep(1);
    }

    sharedmem.Detach();
    return 0;
}

效果:

2. System V 消息队列(选学了解即可)

2.1 原理与特点

管道是通过让不同进程看到同一个文件缓冲区,共享内促是看到同一个内存块,而消息队列就是看到同一个队列!

  • 消息队列提供了一个从一个进程向另外一个进程发送有类型块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 消息队列也有管道一样的不足,就是每个消息的最大长度是有上限的(MSGMAX)
  • 每个消息队列的总的字节数也是有上限的(MSGMNB),系统上消息队列的总数也有上限(MSGMNI)的

特点:允许不同的进程,向内核中发送带类型(因为消息队列是将信息以数据块的形式链接到消息队列中,可以双向通信,但其中的内容有别人的数据,也会有自己的数据,所以我们需要通过不同的类型来判断队列中的数据块是别人传的还是自己传的)的数据块!

2.2 消息队列结构

cpp 复制代码
struct msqid_ds
{
    struct ipc_perm msg_perm;       /* 权限信息 */
    struct msg msg_first;           /* 队列中的第一条消息,未使用 */
    struct msg msg_last;            /* 队列中的最后一条消息,未使用 */
    __kernel_time_t msg_stime;      /* 最后一次发送消息的时间 */
    __kernel_time_t msg_rtime;      /* 最后一次接收消息的时间 */
    __kernel_time_t msg_ctime;      /* 最后一次修改时间 */
    unsigned long msg_lcbytes;      /* 32位系统复用字段,无实际意义 */
    unsigned long msg_lqbytes;      /* 同上,无实际意义 */
    unsigned short msg_cbytes;      /* 队列中当前字节总数 */
    unsigned short msg_qnum;        /* 队列中当前消息总数 */
    unsigned short msg_qbytes;      /* 队列允许的最大字节数 */
    __kernel_ipc_pid_t msg_lspid;   /* 最后一次发送消息的进程ID */
    __kernel_ipc_pid_t msg_lrpid;   /* 最后一次接收消息的进程ID */
};

cpp 复制代码
struct ipc_perm
 {
    key_t __key;          /* 提供给 xxxget(2) 函数的键值 */
    uid_t uid;            /* 所有者的有效用户ID */
    gid_t gid;            /* 所有者的有效组ID */
    uid_t cuid;           /* 创建者的有效用户ID */
    gid_t cgid;           /* 创建者的有效组ID */
    unsigned short mode;  /* 访问权限 */
    unsigned short __seq; /* 序列号 */
};

我们发现 消息队列无论在数据结构上,还是在后面介绍的接口和返回值,以及底层原理上都十分相似具有一定共性 ,这个就是System V标准!

2.3 接口

1. msgget

msgget 是 Linux/Unix 系统编程中用于创建一个新的消息队列,或获取一个已存在的消息队列标识符的系统调用函数。

如果说共享内存是"修一条路让进程直接访问同一块内存",那么消息队列就像是"在邮局设立了一个专属信箱"。msgget 的作用就是去邮局申请或认领这个信箱

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
  • key :消息队列的全局键值(通常由 ftok() 函数生成成!)。
  • msgflg :控制消息队列的创建方式和访问权限。常见的标志位包括:
    • IPC_CREAT:如果该键值对应的消息队列不存在,则创建它;如果已存在,则直接打开。
    • IPC_EXCL :必须与 IPC_CREAT 结合使用(如 IPC_CREAT | IPC_EXCL)。如果队列已存在,则直接报错返回 -1。这常用于确保当前进程是队列的唯一创建者。
    • 权限控制符 :如0666,用于设置队列的读写权限。

  • 成功 :返回一个非负整数,即消息队列标识符(msqid)。后续发送和接收消息都需要用到这个 ID。
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括:
    • EEXIST:队列已存在,且同时使用了 IPC_CREATIPC_EXCL
    • ENOENT:队列不存在,且未指定 IPC_CREAT 标志。
    • EACCES:队列存在,但当前进程没有请求的访问权限。
c 复制代码
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    key_t key;
    int msgid;

    // 1. 生成全局唯一的键值
    key = ftok("/tmp/my_msg_queue", 'A');
    if (key == -1) {
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }

    // 2. 创建或获取消息队列(如果不存在则创建,权限为 0666)
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget failed");
        exit(EXIT_FAILURE);
    }

    printf("成功获取消息队列,ID: %d\n", msgid);
    // 接下来可以使用 msgsnd() 发送消息,或使用 msgrcv() 接收消息
    return 0;
}

2. msgctl

msgctl 是 Linux/Unix 系统编程中用于控制和管理 System V 消息队列的系统调用函数。

如果说 msgget 是"去邮局申请或认领信箱",msgsndmsgrcv 是"寄信和收信",那么 msgctl 就是 "邮局的后台管理接口" 。它负责查询信箱的状态、修改信箱的属性,或者在不需要时直接拆除信箱。

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msqid :消息队列的标识符(由 msgget 函数返回的 ID)。
  • cmd :控制命令,决定了你要对消息队列执行什么操作。最常用的有以下三个:
    • IPC_STAT(查询状态) :读取消息队列当前的属性信息(如权限、当前消息数量、队列字节数等),并将其存储到 buf 指向的 struct msqid_ds 结构体中。
    • IPC_SET(修改属性) :将 buf 中修改后的属性(如所有者 uid、所属组 gid、访问权限 mode 或最大字节数 msg_qbytes)设置到消息队列中。执行此操作需要足够的权限(通常是属主或 root)。
    • IPC_RMID(删除队列) :从系统内核中彻底移除该消息队列,并销毁与之关联的数据结构。执行此命令时,buf 参数通常设为 NULL
  • buf :指向 struct msqid_ds 结构体的指针。在执行 IPC_STAT 时作为输出缓冲区,在执行 IPC_SET 时作为输入缓冲区。

  • 成功 :返回 0
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括:
    • EACCES:执行 IPC_STAT 时,调用进程没有读取权限。
    • EPERM:执行 IPC_SETIPC_RMID 时,调用进程没有足够的权限(例如不是创建者且不是 root)。
    • EINVAL:传入的 msqidcmd 参数无效。
c 复制代码
// 假设 msqid 是之前通过 msgget 获取的消息队列 ID

// 1. 删除消息队列(彻底销毁)
if (msgctl(msqid, IPC_RMID, NULL) == -1) 
{
    perror("msgctl IPC_RMID failed");
    exit(EXIT_FAILURE);
}
printf("消息队列 %d 已成功删除\n", msqid);

3. msgsnd

msgsnd 是 Linux/Unix 系统编程中用于向 System V 消息队列发送(写入)消息的系统调用函数。

如果说 msgget 是"去邮局申请信箱",那么 msgsnd 就是 "把写好的信件投递到信箱中"

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid :消息队列的标识符(由 msgget 返回的 ID)。
  • msgp :指向消息缓冲区的指针。这个缓冲区必须是一个用户自定义的结构体,且第一个字段必须是 long 类型的消息类型(mtype ,后面跟着消息的实际数据(mtext)。
c 复制代码
struct msgbuf {
long mtype;       /* 消息类型,必须 > 0 */
char mtext[100];  /* 消息文本(实际数据) */
};
  • msgsz :要发送的消息数据的实际大小(字节数)。注意:这个大小仅指 mtext 的长度,不包含 mtype 占用的字节数。
  • msgflg :控制发送行为的标志位。
    • 0(阻塞模式):如果消息队列已满(达到系统限制或字节数上限),调用进程将挂起等待,直到队列有空间容纳该消息。
    • IPC_NOWAIT(非阻塞模式) :如果消息队列已满,函数不会等待,而是立即返回 -1,并将 errno 设置为 EAGAIN

  • 成功 :返回 0
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括:
    • EAGAIN:队列已满,且指定了 IPC_NOWAIT 标志。
    • EACCES:调用进程没有对该消息队列的写入权限。
    • EIDRM:在等待发送的过程中,该消息队列被其他进程删除了。
    • EINTR:在阻塞等待队列空间时,进程被系统信号中断。
    • EINVAL:参数无效(例如 msqid 无效,或 mtype 的值小于 1)。
c 复制代码
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

// 1. 定义消息结构体
struct my_msg {
    long mtype;
    char mtext[100];
};

int main() 
{
    int msgid = msgget(0x1234, IPC_CREAT | 0666); // 假设已获取队列ID
    struct my_msg msg;

    // 2. 组装消息
    msg.mtype = 1;  // 设置消息类型(必须大于0)
    strcpy(msg.mtext, "Hello, Message Queue!");

    // 3. 发送消息
    // 注意:msgsz 只传数据部分的长度,即 sizeof(msg.mtext)
    if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
        perror("msgsnd failed");
        return -1;
    }

    printf("消息发送成功!\n");
    return 0;
}

msgsnd 是消息队列生产者的核心接口。与共享内存不同,调用 msgsnd 时,数据会被 从用户空间拷贝到内核空间的消息队列中。

4. msgrcv

msgrcv 是 Linux/Unix 系统编程中用于从 System V 消息队列中接收(读取)消息的系统调用函数。

如果说 msgsnd 是"把信件投递到信箱",那么 msgrcv 就是 "从信箱中取信" 。它最强大的特性在于支持按消息类型(msgtyp)选择性接收,而不必像管道那样只能严格按照先进先出(FIFO)的顺序读取。

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msqid :消息队列的标识符(由 msgget 返回的 ID)。
  • msgp :指向接收缓冲区的指针。与发送时一样,这个结构体的第一个字段必须是 long mtype,用于存放接收到的消息类型,后面跟着用于存放实际数据的 mtext
  • msgsz :接收缓冲区中 mtext 部分的最大容量(字节数)。
  • msgtyp消息类型过滤条件 (这是 msgrcv 的灵魂参数):
    • msgtyp == 0 :接收队列中第一条(最早发送的)消息(标准的 FIFO 模式)。
    • msgtyp > 0 :接收队列中第一条类型等于 msgtyp 的消息。
    • msgtyp < 0 :接收队列中类型值小于或等于 msgtyp 绝对值的消息中,类型值最小的那一条。
  • msgflg :控制接收行为的标志位。
    • 0(阻塞模式):如果队列中没有符合条件的消息,调用进程将挂起等待,直到有消息到达。
    • IPC_NOWAIT(非阻塞模式) :如果队列中没有符合条件的消息,函数立即返回 -1,并将 errno 设置为 ENOMSG
    • MSG_NOERROR :如果接收到的消息长度大于 msgsz,则只拷贝 msgsz 字节的数据,多余的部分会被直接丢弃 ,且不会报错。如果不加此标志,消息过长会返回 E2BIG 错误。

  • 成功 :返回实际拷贝到 mtext 中的字节数
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括:
    • E2BIG:消息长度大于 msgsz,且未指定 MSG_NOERROR 标志。
    • ENOMSG:指定了 IPC_NOWAIT,且队列中没有符合条件的消息。
    • EIDRM:在阻塞等待消息时,该消息队列被其他进程删除了。
    • EINTR:在阻塞等待消息时,进程被系统信号中断。
c 复制代码
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

// 1. 定义消息结构体(与发送端保持一致)
struct my_msg 
{
    long mtype;
    char mtext[100];
};

int main() 
{
    int msgid = msgget(0x1234, 0666); // 假设已获取已存在的队列ID
    struct my_msg buffer;

    // 2. 接收类型为 1 的消息
    // 注意:msgsz 只传数据部分的长度
    ssize_t len = msgrcv(msgid, &buffer, sizeof(buffer.mtext), 1, 0);
    
    if (len == -1)
 {
        perror("msgrcv failed");
        return -1;
    }

    printf("接收到消息,类型: %ld, 内容: %s\n", buffer.mtype, buffer.mtext);
    return 0;
}

msgrcv 是消息队列消费者的核心接口。与 msgsnd 类似,它也会触发系统调用,将数据从内核空间拷贝回你的用户空间缓冲区 。同时,一旦消息被 msgrcv 成功接收,该消息就会从内核的消息队列中被彻底删除

4. ipcs -q

ipcs -q 是 Linux 系统中专门用于查看当前所有 System V 消息队列(Message Queues)状态的命令。

3. System V 信号量(选学了解即可)

3.1 并发编程,概念铺垫

  • 多个执行流(进程),能看到的同一份公共资源:共享资源
  • 被保护起来的共享资源叫做临界资源
  • 保护的方式常见:互斥与同步
  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫临界区
    你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)
  • 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护
  • 原子性:代码执行的时候,要么做完要么不做 无法被打断

3.2 信号量

所谓信号量,本质就是个 计数器!用于描述临界资源中,资源数量的多少。

比如电影院 有100张票(有限资源,初始值100的信号量),买票(申请信号量,本质即对资源的预定机制!),在电影院申请座位资源的访问的时候: 只要买到票,座位哪怕你不坐,座位也被你提前预定了!

说人话就是: 申请资源首先申请信号量,申请成功你就可以访问资源,申请失败就会被阻塞!


细节1: 每一个进程都要申请信号量,也就是每一个进程,要看到同一个信号量?

所以说, 信号量本身就是共享资源,那么信号量 负责保护进程间通信的资源,那么谁来保护信号量呢?

其实 信号量的申请(-- p操纵)和 信号量的释放(++ v操作)他们都是原子的!!


细节2: 如果是超级VIP放映厅,一共只有一个座位呢?

那么次数 设sem=1 也就是说一次只能放一个人进来看电影,这种就叫做 互斥 !而这种信号量叫 二元信号量


细节3: ++、--操作是原子的吗?

其实以--为例子 对于汇编是很多行代码 第一步要将值导入到CPU内部,然后在CPU内作减法,然后再写回内存(至少3行!); 所以在语言层面上,识别是否是原子型:如果代码是一行代码,那么就是原子的!!!


细节4: 你怎么保证多个进程,看到的是同一个sem?【必须看到同一个信号量

如果要进行共享资源的保护,你就必须先让不同的进程,看到同一份资源(计数器资源!)
这就是为什么信号量 会被归入进程的IPC!

3.3 信号量结构体

cpp 复制代码
// semid_ds 结构体定义在 <sys/sem.h> 中
struct semid_ds 
{
    struct ipc_perm sem_perm;  /* 所有者权限和访问权限 */
    time_t sem_otime;          /* 最后一次执行 semop 操作的时间 */
    time_t sem_ctime;          /* 最后一次修改该结构体的时间 */
    unsigned long sem_nsems;   /* 信号量集合中的信号量数量 */
};

// ipc_perm 结构体定义
// 高亮字段可通过 IPC_SET 进行设置
struct ipc_perm 
{
    key_t __key;          /* 提供给 semget(2) 的键值 */
    uid_t uid;            /* 所有者的有效用户ID */
    gid_t gid;            /* 所有者的有效组ID */
    uid_t cuid;           /* 创建者的有效用户ID */
    gid_t cgid;           /* 创建者的有效组ID */
    unsigned short mode;  /* 访问权限 */
    unsigned short __seq; /* 序列号 */
};

3.4 接口

1. semget

semget 是 Unix/Linux 系统中用于创建或访问 System V 信号量集的核心系统调用,其本质是为进程间同步与互斥提供"资源计数器"的入口。

该函数的原型为:

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
  • key :用于唯一标识一个信号量集的键值。通常使用 ftok() 函数生成。
  • nsems :指定要创建的信号量集中包含的信号量个数。在访问已存在的信号量集时,此参数可设为 0
  • semflg :控制创建或访问行为的标志位。常用组合包括:
    • IPC_CREAT:如果指定的 key 不存在,则创建新的信号量集。
    • IPC_EXCL:与 IPC_CREAT 一起使用时,如果信号量集已存在,则返回错误(EEXIST)。
    • 权限位:如 0666,用于设置信号量集的访问权限。

当调用 semget 成功创建一个新的信号量集时,内核会初始化一个 semid_ds 结构体,其中包含 信号量的数量(也是个数字不过被OS保护起来了!!)、操作时间戳和权限信息 。这个结构体是后续所有信号量操作(如 semop 进行 P/V 操作,semctl 进行控制或删除)的基础。

2. semctl

semctl 是 Unix/Linux 系统中用于控制和管理 System V 信号量集的系统调用函数。

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • semid :信号量集的标识符(由 semget 函数返回的 ID)。

  • semnum :信号量在集合中的索引(下标)。因为 semget 可以一次创建多个信号量(信号量集),所以需要通过下标指定操作哪一个(从 0 开始)。如果只操作整个集合,通常填 0

  • cmd :控制命令,决定了你要执行什么操作。最常用的有以下三个:

    • SETVAL(初始化值) :将指定的信号量初始化为一个已知的值。这在信号量第一次使用前必须执行,否则其值可能是未定义的垃圾值。
    • IPC_RMID(删除集合):从系统内核中彻底移除该信号量集,并释放相关内存。执行此命令时,不需要第四个参数。
    • GETVAL(获取值):返回指定信号量的当前值。
  • ...(可选参数) :这是一个可变参数。当 cmdSETVALGETALLSETALL 等需要传递复杂数据时,需要传入一个 union semun 联合体。

    c 复制代码
    union semun
     {
       int val;                  
       struct semid_ds *buf;     
       unsigned short *array;    
     };  

  • 成功 :返回值取决于 cmd。对于 GETVAL 等命令返回相应的值;对于 SETVALIPC_RMID 等命令返回 0
  • 失败 :返回 -1,并将错误原因存入全局变量 errno 中。常见的错误包括:
    • EACCES:执行 GETVAL 等读取操作时,调用进程没有读取权限。
    • EPERM:执行 IPC_RMID
      时,调用进程没有足够的权限(例如不是创建者且不是 root)。
    • EINVAL:传入的 semidsemnumcmd 参数无效。
c 复制代码
#include <sys/sem.h>
#include <stdio.h>

// 自定义 union semun
union semun 
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main()
 {
    int semid = semget(0x1234, 1, IPC_CREAT | 0666); // 假设已获取信号量集ID
    union semun arg;

    // 1. 初始化信号量(将 0 号信号量的值设为 1)
    arg.val = 1; 
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL failed");
        return -1;
    }
    printf("信号量初始化成功!\n");

    // 2. 删除信号量集(彻底销毁,防止资源泄漏)
    if (semctl(semid, 0, IPC_RMID) == -1)
     {
        perror("semctl IPC_RMID failed");
        return -1;
    }
    printf("信号量集 %d 已成功删除\n", semid);

    return 0;
}

3. semop

semop 是 Linux/Unix 系统中用于对 System V 信号量集执行原子操作的系统调用函数。

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
  • semid :信号量集的标识符(由 semget 返回的 ID)。
  • sops :指向 sembuf 结构体数组的指针。每个结构体代表一个对特定信号量的操作。
  • nsops :数组中操作的数量。semop 保证数组中的所有操作要么全部成功执行,要么全都不执行(即原子性)。

这是 semop 操作的灵魂,它定义了具体的操作行为:

c 复制代码
struct sembuf 
{
    unsigned short sem_num;  // 信号量在集合中的索引(从0开始)
    short          sem_op;   // 操作类型(核心参数)
    short          sem_flg;  // 操作标志
};

sem_op 的三种经典取值:

  • 负数(P操作/申请资源) :将信号量的值减去 sem_op 的绝对值。如果当前信号量的值小于该绝对值,进程将阻塞等待,直到资源可用。
  • 正数(V操作/释放资源) :将信号量的值加上 sem_op 的值。这通常用于释放资源,且从不强制进程等待
  • 零(等待归零):如果信号量当前值为 0,操作立即完成;否则进程将阻塞,直到信号量的值变为 0。

sem_flg 的常用标志:

  • 0(默认):阻塞模式。条件不满足时,进程会挂起等待。
  • IPC_NOWAIT :非阻塞模式。如果操作不能立即完成,函数会立即返回 -1,并将 errno 设为 EAGAIN
  • SEM_UNDO防死锁神器。内核会记录该进程对信号量的修改。如果进程异常崩溃退出,内核会自动撤销(Undo)这些修改,防止资源被永久锁定。

返回值与错误处理:

  • 成功 :返回 0
  • 失败 :返回 -1,并设置 errno
c 复制代码
#include <sys/sem.h>
#include <stdio.h>

// 封装 P 操作(申请资源)
int sem_p(int semid) 
{
    struct sembuf sb = {0, -1, SEM_UNDO}; // 对第0个信号量减1,带UNDO保护
    if (semop(semid, &sb, 1) == -1) 
    {
        perror("semop P failed");
        return -1;
    }
    return 0;
}

// 封装 V 操作(释放资源)
int sem_v(int semid)
 {
    struct sembuf sb = {0, 1, SEM_UNDO};  // 对第0个信号量加1,带UNDO保护
    if (semop(semid, &sb, 1) == -1)
     {
        perror("semop V failed");
        return -1;
    }
    return 0;
}

int main() 
{
    int semid = semget(0x1234, 1, IPC_CREAT | 0666);
    
    // 假设已通过 semctl 将信号量初始化为 1
    sem_p(semid); // 进入临界区
    printf("正在访问共享资源...\n");
    sem_v(semid); // 离开临界区
    
    return 0;
}

4. ipcs -s

ipcs -s 是 Linux 系统中专门用于查看当前所有 System V 信号量集状态的命令。

4. IPC在内核中的数据结构设计(区分用户级 和 内核级数据结构)

参考图:
⚠️: 为什么我们前文看到的数据结构和我们下图的数据结构不同,因为我们看到的如shmid_ds这个结构叫用户级数据结构,里面的有效成员是OS选择性的把内核中的shmid_kernel里面的用户需要的成员拷贝了一部分!

消息队列的实际的内核数据结构:


信号量的实际的内核数据结构:


共享内存的实际内核数据结构:


内核中实际的ipc_perm


而用于管理ipc_perm的是 ipc_id_ary的结构体:


至此我们可以用数组对ipc_perm进行管理了!但是我们怎么知道 这个ipc_perm 是 消息队列还是共享内存或信号量??

所以内核里面还有个 ipc_ids的结构体!

里面的entries指针指向 ipc_id_ary

这个结构体会定义对象!以消息队列为例子 消息队列自己会有个static struct ipc_ids msg_ids;其它同理:

毕竟不同的IPC,其对应的系统调用底层不同!所以系统一共存在三个柔性数组 分别管理 消息队列、共享内存和信号量!!


所以我们 如 int msgget(key_t key , int msgflg)的返回值,我们就可以把它当作 数组下标(现实还有一些别的复杂操作 但我们不要管!):


System V 的三大 IPC 机制(共享内存、消息队列、信号量 ; IPC资源 = 内核数据结构 + 资源本身)在设计上呈现出高度的同构性。它们不仅遵循统一的 key 值寻址规范,其底层数据结构也都内嵌了 ipc_perm 这一公共属性。基于这种设计,操作系统内核在管理这些 IPC 对象时,展现出了精妙的架构思想:

1. 统一的元数据管理与冲突检测

内核将包含 ipc_perm 的结构体地址进行集中式管理(详情看 2)。由于 ipc_perm 中封装了 key 值,这种统一的管理方式使得内核在创建新的 IPC 对象时,能够高效地遍历并校验 key 的唯一性,从而避免资源冲突。

2. 类似面向对象的多态实现

从数据结构设计的角度来看,ipc_perm 扮演了类似 C++ 中"基类"的角色,所以OS可以定义一个如下结构体:

cpp 复制代码
struct ipc_id_ary
{
 int size;
 kern_ipc_perm *p[NUM];//柔性数组 结构体指针数组
}

这个p[NUM]指向的就是各个(不管是消息队列还是共享内存)ipc_perm ,也就是我们可以用一个结构体数组对其进行统一的管理!将来设置key的时候判断是否存在,我是不是只需要便利该数组 直接找对应的ipc_perm.key进行对比即可! 然后我将该数组强转成( 看上图 会解释为什么不是 msqid_ds )

如:(msg_queue*)p[0] -> XXX我是不是就可以访问详细队列的其他属性了!!!

所以具体的 IPC 对象(如 shmid_dsmsqid_ds 等)则相当于"子类"。内核通过一个统一的数组来管理这些包含公共属性的结构体。在访问时,内核只需对指针进行强制类型转换,即可获取特定对象的专属属性。这种"相同指针指向不同对象以获取差异化信息"的机制,本质上与 C++ 中的继承和多态思想如出一辙,是 C 语言环境下实现面向对象特性的经典方案。

3. 基于回绕机制的索引分配策略

数组下标按道理是从0开始的,只不过有个起始计数器的概念,实际上是通过线性递增且会回绕的数组下标来定位的(因为我们用的越久可能数组下标就会越大,所以使用的时候会一直递增,直到满的时候,即越界的时候,会再回绕,可以理解模运算 跟fd中的始终是最小的下标不一样!

5. 共享内存在内核中的原理(重点!!讲透)

创建共享内存时 会创建下面的对象:

那么是如何申请内存空间的呢? 会创建一个struct file 和管道一样 不需要真正存在文件 里面有个 *addr_space指针指向共享内存的内核缓冲区!

而进程的mm_struct结构体有个指针指向 vm_area_struct结构体(该结构体用于指向管理虚拟内存!)

其中 shmid_kernelvm_area_struct都有个对应的指针指向 file struct结构体:

同时在vm_area_struct中有个void * vm_private_data的指针 指向vm_area_struct

所以 当其他进程附加这段内核缓冲区到虚拟内存 不就当作共享内存了嘛!!!这个共享内存没有名字,叫匿名共享内存映射!!

而因为这段空间直接映射到虚拟内存 所以不就可以像数组一样 不借助系统调用直接利用虚拟地址访问了嘛!!!


如果未来不考虑进程间通信 我把从磁盘加载到内存中文件的 内核缓冲区 也映射到进程虚拟地址空间未来我也可以 像访问数组一样 直接编辑文件内容!!这种技术我们叫 mmap!!

而动态库就是这样被映射到进程的虚拟地址空间的!!

相关推荐
CHANG_THE_WORLD38 分钟前
12.总结:深入理解 Linux I/O 多路复用:select、poll、epoll 全解析
linux·运维·服务器
风曦Kisaki1 小时前
# Linux笔记:操作系统优化与资源管理
linux·运维·服务器·笔记
Mr.HeBoYan2 小时前
一次持续三天才出现的丢包故障——深入解析 DPDK Memory Ordering、rte_ring 与 CPU Memory Barrier (下)
linux·网络·算法·架构·dpdk
zhangrelay3 小时前
笔记本轻量高品质延寿工具完整分系统清单
运维·笔记·学习
Discipline~Hai4 小时前
ARM01-ARM体系架构
linux·c语言·arm开发·架构
RisunJan4 小时前
Linux命令-screen(终端复用器)
linux·运维
啊哦1115 小时前
安装VMware Workstation
linux·运维·服务器
持敬chijing5 小时前
Linux基础命令与目录命令
linux·运维·服务器
yunwei375 小时前
实证研究:AI Agent 规则需要上下文与分层强制执行
linux·安全·ai编程
小张成长计划..6 小时前
【Linux】12:进程状态
linux·运维·服务器