Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【Linux进程通信】四、System V IPC

【Linux进程通信】四、System V IPC

作者头像
利刃大大
发布于 2025-03-22 00:27:36
发布于 2025-03-22 00:27:36
5300
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 前言

​ 这里我们介绍的这种通信方式也就是 system V IPC 在我们后面的使用和日常见到的其实并不多,但是包括其中的共享内存、消息队列、信号量,我们如果了解共享内存其原理的话,能够更好的帮助我们了解之前我们学过的进程地址空间的概念!

​ 至于信号量,我们后面讲多线程的时候会再次讲,我们只引入一些概念如互斥等等,而消息队列我们就只说说其原理,不会细讲!

Ⅱ. 认识共享内存

一、共享内存的原理

​ 之前我们学过管道通信,分为匿名管道和命名管道,匿名管道通过父子进程的属性继承原理来完成父子进程看到同一份资源的目的,而命名管道则是通过路径与文件名来唯一标识管道文件,来让不同的进程之间进行通信!

​ 而共享内存也是一样,我们得让不同的进程看到同一份资源,但是这次我们不是使用继承还是文件名路径来标识,而是通过在内存中的一段空间:共享内存区中申请一段空间,并且进程可以通过获得一个唯一的标识 ID 来获得这段共享内存的位置,当多个进程同时获得这段共享内存的时候,我们就称它们通过共享内存看到了同一份资源!这里面有许多的细节,我们一一来解释!

​ 在 Linux 中,首先我们假设这里有两个进程分别被调度,那么它们就有各自对应的进程控制块 PCB 和地址空间 mm_struct 并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元 MMU 进行管理,由于两个进程拥有独立的数据结构,所以我们可以知道其是有独立性的!如下图所示:

​ 但是这里就要一个问题了!既然两个进程的之间的数据结构是相互独立的,我们学过进程地址空间也知道,如果它们指向了同一段物理空间,那么其中一方进行修改,是会发生写时拷贝的,并不会影响另一方,那么这样子我们如何进行通信呢 ❓❓❓

​ 所以为了让两个毫不相干的进程能看到同一份资源,操作系统会做以下几个工作:

  1. 在物理内存当中申请一段共享内存空间
  2. 将创建好的共享内存空间通过页表映射到进程的进程地址空间(这个过程叫做挂接)
  3. 不同的进程通过操作各自的进程地址空间中的该段共享内存空间的虚拟地址,来操作共享内存
  • 如果某个进程不想通信了,那么就将该进程与共享内存的映射取消掉(去关联),如果需要的话再将共享内存释放掉(看是否其他进程还在通信)

​ 那么就会有人问,调用 malloc 函数不也能在内存中开辟一段空间并且和进程之间映射起来吗 ❓❓❓

​ 答案肯定是不行的!因为我们 malloc 出来的空间,只是属于某个进程的,而进程之间具有独立性,所以其他进程是压根看不到这份资源的,就算看到了,那么也不能进行通信,因为存在写时拷贝,而共享内存是特殊的,是运行不同进程之间共同操作的一段空间!

二、linux中共享内存的数据结构

​ 在 linux 中,共享内存也是需要被管理的,就像我们的进程控制块、文件描述符等等都是遵循一个原则:先描述、再组织!

​ 在 Linux 内核中,每个共享内存都由一个名为 struct shmid_kernel 的结构体来管理 shmid 表示共享内存的 id,而且 Linux 限制了系统最大能创建的共享内存为 128 个。通过类型为 struct shmid_kernel 结构的数组来管理,其中 struct shmid_ds 结构体用于管理共享内存的属性信息,而 shm_segs数组 用于管理系统中所有的共享内存。

​ 另外 struct shmid_ds 结构体中存在另一个结构体 struct ipc_perm ,它存储确定执行 IPC 操作的权限所需的信息!

​ 这几个结构体如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct shmid_kernel
{ 
    struct shmid_ds u;
    
    /* the following are private */
    unsigned long shm_npages; /* size of segment (pages) */          // 表示共享内存使用了多少个内存页
    pte_t *shm_pages; /* array of ptrs to frames -> SHMMAX */        // 指向了共享内存映射的虚拟内存页表项数组
    struct vm_area_struct *attaches; /* descriptors for attaches */  // 
};

static struct shmid_kernel *shm_segs[SHMMNI]; // SHMMNI等于128

struct shmid_ds {
    struct ipc_perm shm_perm; /* operation perms */           // 该空间的所有权等属性,其中包括了用来唯一标识的key
    int shm_segsz; /* size of segment (bytes) */              // 共享空间的大小(字节为单位)
    __kernel_time_t shm_atime; /* last attach time */         // 最后关联时间
    __kernel_time_t shm_dtime; /* last detach time */         // 最后去除关联时间
    __kernel_time_t shm_ctime; /* last change time */         // 属性最后修改时间
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */         // 创建该空间的进程pid
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */   // 最后一个关联或者挂接该空间的进程pid
    unsigned short shm_nattch; /* no. of current attaches */  // 当前空间的挂接数
    ......
};

struct ipc_perm {
    key_t __key; /* Key supplied to shmget(2) */         // 这个就是用来标识shmid唯一性的key
    uid_t uid;   /* Effective UID of owner */
    gid_t gid;	 /* Effevtive GID of owner */
    uid_t cuid;	 /* Effective UID of creator */
    gid_t cgid;  /* Effective GID of creator */
    unsigned short mode;  /* Permissions + SHM_DEST and SHM_LOCKED flags */
    unsigned short __seq;  /* Sequence number */
}

​ 在我们讲接口之前看到这些可能会疑惑,但是在后面讲完接口调用的时候,这里对我们来说比较重要的就是这个 shmid_ds 以及 ipc_perm 中的 __key

三、理解共享内存的概念

​ 从上面的讲解我们可以得到:通过让不同的进程,看到同一个内存块且进行通信的方式,叫做共享内存

​ 下面我们提出几个概念,为了后面我们引出共享内存申请、调用等等的内容做铺垫:

  • 进程间通信,是程序员们专门设计来实现 IPC 的,这和 malloc 虽然理论和思想上差不多,但是却有完全不一样的作用,进程间通信主要是为了解决进程间共享的内存问题
  • 共享内存是一种通信方式,所有想通信的进程都可以使用
  • 操作系统中可能会同时存在很多的共享内存,就像我们以前每家每户都有一部专属电话来沟通一样。

​ 那么第一个概念其实就是想说,既然专门设计了进程间通信,那么肯定底层会有对应的接口供我们使用!

​ 而后面两个概念就是为了帮助我们理解这些接口参数的概念!下面我们来介绍一下这些接口!

Ⅲ. 共享内存的使用

首先我们得知道共享内存的一些特性:

  • 共享内存是以 覆盖写 的方式
  • 共享内存的 声明周期随操作系统,而不是随进程(也就是说不需要使用的时候需要手动释放,不然会造成内存耗尽问题,如何释放我们下面会说)

​ 接下来我们先来看看在 linux 下如何查看对应的 共享内存空间消息队列 以及 信号量 !方便我们调用接口的时候查看!

⭐ 共享内存的查看 – ipcs指令

ipcs 指令的作用是报告进程间通信设施的状态,包括共享内存、消息队列以及信号量等等

​ 下面我们来看一下 ipcs 的指令选项,这里只使用几个比较常见的,其他的选项可以参考下面 ipcs -help 中的!

① 列表说明
  • key :用来传递给 shmid 的编号
  • msqid :消息队列的编号
  • shmid :共享内存段的编号
  • semid :信号量数组的编号
  • owner :创建该空间的用户
  • perms :权限
  • nattch :挂接数,也就是连接到共享内存的进程个数
  • status :共享内存段的状态(是否有进程关联等等)
  • bytes :创建的大小
  • used-bytes :消息队列已使用的大小
  • nsems :对应信号量数组中信号量的个数
② 查看帮助:ipcs -help
③ 三类资源查看方式
  • ipcs -m :单独查看共享内存段
  • ipcs -q :单独查看消息队列
  • ipcs -s : 单独查看信号量数组
  • ipcs -aipcs : 查看所有的资源(设施)
④ 资源选项和输出选项可以搭配使用

​ 这里以 -c 显示创建者和拥有者为例:ipcs -cipcs -c -s,其它选项如 -t-p-l-u-b 也是同理的!

⑤ 通过选项 -i 打印资源的详细信息

​ 使用 -i 选项的时候要配合 semid 或者 shmid 一起使用,而不能一起单独使用!

⭐ 共享内存段的删除 – ipcrm指令

​ 本来这个共享内存段的删除内容是要在后面说的,这个顺序会比较合理一点,但是还是觉得不卖关子,我们先把删除解决了,并且要提一下为什么要进行删除操作!

​ 之前我们说过一个进程如果不想使用该共享内存段了,那么就得将该进程与该共享内存的映射去掉,也就是去关联,这是为了防止我们后面做了不当的操作影响到该共享内存中的其它进程通信

​ 除此之外,共享内存段的生命周期是随操作系统的,而不是随进程的(System V 版本的通信生命周期都是随操作系统的),也就是说就算没有进程指向该共享内存段,这个内存段也是会存在的,那么如果我们不手动对其释放,那么就要等到操作系统关闭的时候才能关闭,而共享内存段是占有内存大小的,所以这极有可能成为内存耗尽的隐患!所以当我们不想使用该段空间的时候,我们就得手动将其释放掉,而这就要借助到 ipcrm 指令了!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ipcrm -m shmid编号

注意共享内存只有在当前映射链接数为0时才会被删除释放!

​ 那么这里可能会有人问,为什么不是删除对应的 key 编号而是 shmid 编号呢 ❓❓❓

​ 这里我们就得搞清楚 keyshmid 的关系:

key 只是用来在 OS 层面上唯一标识的,不能用来管理共享内存的; shmidOS 给用户用来标识共享内存段的 id,用来在用户层进行共享内存段的管理的!

​ 因为我们是用户,所以我们是在用户层进行操作,只能通过 shmid 编号来进行删除!

🐇 补充: ipcrm -a 表示释放所有进程间通信的资源

一、shmget – 获取共享内存段标识符

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

// 作用:得到一个共享内存标识符,或者创建一个共享内存对象并返回共享内存标识符
// 返回值:获取成功则返回一个非负数,即共享内存标识符,取决于shmflg的参数(不同操作系统返回值不同);获取失败则返回-1,并且设置错误码errno

我们来单独看看这个函数的参数:

  • key:一个需要我们传递的用来保证共享内存的唯一性的(一般我们用 ftok 函数来获取,下面会讲)
  • size:要创建的这段共享内存的大小(以字节为单位),设为 0 时表示只获取共享内存
  • shmflg:表示获取共享内存的时候的选项标志位(与 open 等函数接口参数类似,其实就是宏,并且可以用按位或连接不同的选项)
    • 0:表示只取共享内存标识符,若不存在则会报错
    • IPC_CREAT:表示如果存在与 key 相等的共享内存空间则直接返回其内存标识符,若不存在则创建一个共享内存并返回其标识符
    • IPC_CREAT | IPC_EXCL:表示如果存在与 key 相等的共享内存空间则报错,若不存在则创建一个共享内存并返回其标识符(IPC_EXCL单独使用的时候没有意义
    • 访问权限 :注意这里我们 一般都是要或上这个访问权限的,就是这段共享内存的权限,和文件权限是一样的!如 06660664 等等
💥参数的细节

​ 这里的参数其实有多个细节,我们先来谈一下,后面就不会再细谈了!

​ 先来谈谈这里的 size,一般来说,共享内存的大小我们都是建议是 4KB 的整数倍因为系统分配共享内存是以 4KB 为单位的(这其实是内存划分内存块的基本单位 page,这个我们后面会讲!)

那么如果我们将其大小设为 4099 个字节而不是 4096 个字节会发生什么呢 ❓❓❓

内核在分配大小的时候会进行以 4KB 为单位向上取整,也就是 8192 字节即 8KB,但是我们会发现创建出来的共享内存打印大小的时候还是 4099 字节啊,这和我们想的不符合啊,为什么不是 8192 字节呢 ❓❓❓

其实就是因为 4099 字节只是我们申请的在共享内存段中可使用的大小,但是实际上 OS 还是会为我们在内存中申请 8192 字节大小的内存空间,但是我们只能使用 4099 字节大小的空间


​ 再来谈一谈这个 key,首先我们这个 shmget 函数就是为了得到一个唯一标识的共享内存段标识符,但是我们怎么保证它就是唯一的呢 ❓❓❓

​ 其实就是通过这个 keykey 是多少不重要,最重要的是 key 要能进行唯一性标识,而这个 key 是通过我们下面会讲的 ftok 函数得到的,操作系统会将文件路径与项目标识符转化为一个 System V IPCkey

二、ftok – 获取唯一标识符key

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

// 作用:这个函数会根据传的路径名和id值,通过算法形成一个key值
// 返回值:成功的话返回这个得到的key值(key_t其实就是int),失败的话返回-1,并设置错误码errno
// 参数:
//    pathname:自定义路径,但是必须是一个存在的路径,不一定是当前文件的路径
//    proj_id:自定义id,但是必须是非0

​ 这样子只要我们让想要通信的进程,通过相同的 pathnameproj_id 传给 ftok 函数,那么其返回的必定是一个相同的 key,因为底层使用的算法是不变的!再使用 shmget 函数获取共享内存段标识符,这样子就能让不同的进程看到同一份资源!

注意:key_t 类型其实就是 int 类型!


​ 那么此时会有问题,既然 key 是一个唯一用来标识这个共享内存的位置的,那么为什么还要用 shmid ❓❓❓

​ 我们可以先举个例子,我们一般在学校里用的是学号,在公司里用的是工号等等,为什么不直接使用我们的身份证呢,因为这样子的话即使我们的身份证被修改了,我们在学校、公司等场合用的也不是身份证,这种现象就叫做 低耦合,也是一种常见的编码思维!

​ 我们的 key 是在内核层用来标定唯一性的,而 shmid 是在用户层来标定唯一性的,哪一天我们的操作系统可能改了,那我们用户层很多标识符就都得改了,但是如果每个软件都有各自的 shmid 的话而不是以统一的 key 为标识的话,这样子一来耦合性就降低了很多,当内核层出现问题时用户层也不必大费周章

三、shmctl – 完成对共享内存的控制

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/type.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

// 作用:对共享内存进行控制,如删除共享内存、得到或修改共享内存的状态
// 返回值:执行成功则返回0,失败返回-1,并且设置错误码errno

其中参数为:

  • shmid :要控制的共享内存的 shmid
  • cmd :要进行控制的选项,这里介绍三个,如下:
    • IPC_STAT:得到共享内存的状态,把共享内存的 shmid_ds 结构复制到 buf
    • IPC_SET:改变共享内存的状态,把 buf 指向的 shmid_ds 结构中的 uidgidmode 复制到共享内存中的 shmid_ds 结构体内
    • IPC_RMID:删除该共享内存
  • buf :是一个 struct shmid_ds* 也就是我们上面讲过的结构体用于管理共享内存的属性信息,如果我们想要获取这个共享内存的属性等则可以创建一个该类型的结构体传递过去若不想获取任何内容的话可以直接设为 NULL

💘注意: 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为 0 时,表示没有进程访问了,共享内存才会真正被删除

四、shmat – 关联共享内存(attach)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

// 作用:把共享内存区对象映射到调用进程的地址空间
// 返回值:成功的话则返回关联好的共享内存的地址,失败的话返回-1,并设置错误码errno

其中参数为:

  • shmid :要关联的共享内存的 shmid
  • shmaddr :关联共享内存挂接到指定的位置。一般我们不用关心,传 NULL,交给 OS 自己决定一个合适的地址位置即可
  • shmflg :关联共享内存的方式,若指定 SHM_RDONLY 则表示只读方式关联,否则以读写方式关联一般传 0 即可

​ 💘注意:fork 后子进程继承已关联(attach)的共享内存地址。但是 exec 后该子进程与已连接的共享内存地址自动去关联(detach)。进程结束后,已关联的共享内存地址也会自动去关联(detach)。

五、shmdt – 去关联(detach)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

// 作用:与对应shmaddr位置处的共享内存去关联
// 返回值:成功去关联则返回0,失败则返回-1,并且设置错误码errno
// 参数:shmaddr表示关联的共享内存的起始地址

💘注意:去关联不等同于删除共享内存!

⚜ 函数的使用与通信测试代码

​ 有了上面这些函数与知识铺垫,我们大概理清一下步骤:

  • 对于 server
    1. 首先创建一个共享内存段并获得其标识符
    2. 通过标识符与共享内存段关联
    3. 进行通信
    4. 去关联
    5. 删除该共享内存段
  • 对于 client
    1. 获得共享内存段标识符
    2. 通过标识符与共享内存段关联
    3. 进行通信
    4. 去关联

​ 现在我们可以把上述内容实现包装到一个 .hpp 文件中,然后 serverclient 分别去调用即可,下面给出整体的代码:(具体现象自行上机观察)

comm.hpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
using namespace std;

// 设置两个常量pathname和proj_id,用来获取key值
const char* PATHNAME = ".";
const int PROJ_ID = 0x66;

const int MAX_SIZE = 4096; // 共享内存的大小

// 调用ftok函数获得key
key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if(k == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k;
}

// 创建或者获取共享内存标识符的函数
int getShmHelper(key_t k, int flags)
{
    int shmid = shmget(k, MAX_SIZE, flags);
    if(shmid == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

// 为client提供获取共享内存标识符的函数
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}

// 为server提供创建共享内存标识符的函数
int createShm(key_t k)
{
    // 注意这里是创建shm,所以必须也得给共享内存带上访问权限
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}

// 关联共享内存
void* attachShm(int shmid)
{
    // 注意当前是64位机,所以指针大小为8字节,所以不能强转为int
    void* start = shmat(shmid, nullptr, 0);
    if((long long)start == -1L)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(4);
    }
    return start;
}

// 去关联
void detachShm(const void* shmaddr)
{
    if(shmdt(shmaddr) == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(5);
    }
}

// 删除共享内存
void delShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(3);
    }
}

#endif

server.cpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    printf("0x%x\n", k);

    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);

    // 关联该共享内存
    char* start = (char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);
    sleep(5);

    // 使用
    while(true)
    {
        sleep(1);
        // 不需要读取到数组中,因为可以直接通过共享内存地址start打印出来
        printf("client say: %s\n", start);

        // 调用shmctl获取共享内存的属性
        struct shmid_ds ds;
        shmctl(shmid, IPC_STAT, &ds);
        printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x\n",\
                ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
    }

    // 去关联
    detachShm(start);
    sleep(5);

    // 一般谁创建shm,谁删除shm!
    delShm(shmid);
    return 0;
}

client.cpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);

    int shmid = getShm(k);
    printf("shmid: %d\n", shmid);

    // 关联该共享内存
    char* start = (char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);
    sleep(3);

    // 使用
    pid_t id = getpid();
    int cnt = 1;
    const char* str = "i am client! i am talking with you!";
    while(true)
    {
        sleep(1);
        // 不需要先加载到字符数组中,直接可以通过共享内存地址写入即可
        snprintf(start, MAX_SIZE, "%s : [pid: %d][cnt: %d]", str, id, cnt++);
    }

    // 去关联
    detachShm(start);
    sleep(3);
    return 0;
}

makefile

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.PHONY : all
all : shm_client shm_server

shm_client : client.cpp
	g++ -o $@ $^ -std=c++11
shm_server : server.cpp
	g++ -o $@ $^ -std=c++11

.PHONY : clean
clean : 
	rm -f shm_client shm_server

Ⅳ. 共享内存的特点

​ 1、共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用(比如 writeread 等)来传递彼此的数据。

​ 2、System V IPC生命周期是随内核的!(就算创建 System V 资源的进程退出了,但是它申请的资源还存在)只能通过 OS 重启,或者程序员手动释放来清理资源。

​ 3、当 client 端没有写入,甚至没有启动的时候,server 端不会像管道一样等待 client 端写入!因为 共享内存不提供任何同步或互斥机制,需要程序员自行保证数据的安全!所以不太安全!

Ⅴ. 拓展知识(为多进程铺垫)

​ 1、由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥

​ 2、系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源

​ 3、在进程中涉及到互斥资源(临界资源)的程序段叫临界区。(其余的就是非临界区)

​ 4、要么不做,要么就做完,我们称这种情况为:原子性

​ 5、所有的进程在访问共享资源之前,都必须先申请 sem 信号量,查看是否共享资源还有位置(就像我们去买电影票一样,必须看看还有没有座位),而在这之前所有进程必须先得看到同一个信号量,而信号量本身也是一个共享资源,那么信号量必须保证自己的安全性(不然随便进程的 ++ 还是 -- 操作可能就让信号量乱了,这样子自己都不安全,怎么去管理其它资源?)。当进程申请完 sem 信号量之后,对应的 sem--,然后就要去到这段共享资源中查找空闲的子资源(相对于电影票是有的,但是我们得选空的座位),并且占位;之后释放的时候对应的 sem++,也就是该位置腾了出来!

Ⅶ. system V 消息队列

System V IPC 之消息队列

一、消息队列的原理与特点

​ 消息队列和共享内存、信号量一样,同属 System V IPC 通信机制。消息队列是一系列连续排列的消息,保存在内核中,通过消息队列的引用标识符来访问。使用消息队列的好处是对每个消息指定了特定消息类型,接收消息的进程可以请求接收下一条消息,也可以请求接收下一条特定类型的消息

​ 消息队列的结构如下:

整个消息队列具有以下两种类型的数据结构:

  • msqid_ds:标识整个消息队列的基本情况,主要包括整个消息队列的权限,包括拥有者和操作权限等信息,另外还包括两个重要的指针分别指向消息队列中的第一个消息和最后一个消息。
  • msg:整个消息队列的主体,一个消息队列有若干个消息,每个消息数据结构的基本属性包括消息类型、消息大小、消息内容指针和下一个消息数据结构位置。

​ 由上图可以看出,消息队列实为一个链表,虽然绝大多数教科书说消息队列按 FIFO 的方式读取,但据笔者来看,完全可以根据消息的类型来访问,并不一定要按 FIFO 的方式, 只是在组织形式上为 FIFO 的方式。在 Linux 操作系统中,对消息队列进行了以下规定:

  • 默认情况下,整个系统中最多允许有 16 个消息队列。
  • 每个消息队列最大为 16384 字节。
  • 消息队列中的每个消息最大为 8192 字节。

msqid_ds 的数据结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// come from /usr/include/msg.h
struct msqid_ds
{
    struct ipc_perm msq_perm; // 对应于该消息队列的ipc_perm结构指针,就是权限
    struct msg *msg_first;    // msg结构指针(msg结构用于表示一个消息),此指针指向消息队列中的第一个消息
    struct msg *msg_last;     // msg结构指针,指向消息队列中的最后一个消息
    ulong msg_ctypes;         // 记录消息队列中当前的总字节数
    ulong msg_qnum;           // 记录消息队列中当前的总消息数
    ulong msg_qbytes;         // 记录消息队列中最大可容纳的字节数
    pid_t msg_lspid;          // 最近一个执行 msgsnd 函数的进程的 PID
    pid_t msg_lrpid;          // 最近一个执行 msgrcv 函数的进程的 PID
    time_t msg_stime;         // 最近一次执行 msgsnd 函数的时间
    time_t msg_rtime;         // 最近一次执行 msgrcv 函数的时间
    time_t msg_ctime;         // 最近一次改变该消息队列的时间
}

ipc_perm 结构体定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// come from /usr/include/bits/ipc.h
/* Data structure used to pass permission information to IPC operations. */
struct ipc_perm
{
    __key_t __key;               // IPC对象的key值,用于唯一标识该IPC对象
    __uid_t uid;                 // 拥有者ID
    __gid_t gid;                 // 拥有者组ID
    __uid_t cuid;                // 创建者ID
    __gid_t cgid;                // 创建者组ID
    unsigned short int mode;     // 读写权限
    unsigned short int __pad1;   // 填充字段,保证ipc_perm结构体的大小为32字节
    unsigned short int __seq;    // IPC对象的序列号,用于区分不同的IPC对象
    unsigned short int __pad2;   // 填充字段,保证ipc_perm结构体的大小为32字节
    unsigned long int __unused1; // 未使用的字段,保证ipc_perm结构体的大小为64字节
    unsigned long int __unused2; // 未使用的字段,保证ipc_perm结构体的大小为64字节
};

​ 在 Linux 系统中,IPC 对象包括消息队列、共享内存和信号量等,它们用于实现进程间的通信和同步。ipc_perm 结构体用于描述这些 IPC 对象的权限信息,包括拥有者、读写权限等。这些信息对于进程间的安全和正确性非常重要,因此 ipc_perm 结构体在 Linux 系统中发挥着重要的作用。

struct meg 结构体的定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// come from /usr/src/kernels/'uname –r'/include/linux/msg.h
/* one msg_msg structure for each message */
struct msg_msg 
{
    struct list_head m_list; // 用于将消息加入到消息队列中的链表中,实现FIFO的队列访问方式。
    long m_type;             // 消息的类型,用于区分不同的消息。
    int m_ts;                // 消息大小,以字节为单位。
    struct msg_msgseg* next; // 下一个消息位置
    void *security;          // 真正消息位置
    /* the actual message follows immediately */
};

​ 消息队列所传递的消息由两部分组成:即消息的类型和所传递的数据。一般用一个结构体来表示。通常消息类型用一个正的长整数表示,而数据则根据需要设定。比如设定一个传递 1024 个字节长度的字符串数据的消息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct msgbuf
{
    long msgtype;
    char msgtext[1024];
}

​ 传递消息时将所传递的数据内容写入 msgtext 中,然后把这个结构体发送到消息队列中即可!

​ 下面是消息队列的一些特点:

1、消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法!

2、每个数据块都被认为是有一个类型 type,接收者进程接收的数据块可以有不同的类型值!

3、IPC 资源必须删除,因为不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核,参考共享内存!

二、消息队列的接口

​ 消息队列的接口和共享内存的接口都是大同小异的!下面只写出几个常见的接口,具体的用法可自行查看 man 手册:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int msgget(key_t key, int msgflg); // 消息队列的创建与打开
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 向消息队列中发送消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 从消息队列中接收消息
int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 消息队列的控制

Ⅷ. system V 信号量

System V IPC 之信号量

一、信号量的分类

在学习 IPC 信号量之前,让我们先来了解一下 Linux 提供两类信号量:

  • 内核信号量,由内核控制路径使用。
  • 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量和 System V 信号量。

💘 POSIX 信号量与 System V 信号量的区别如下:

  1. POSIX 来说,信号量是个非负整数,常用于线程间同步。而 System V 信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为 System V IPC 服务的,信号量只不过是它的一部分,常用于进程间同步
  2. POSIX 信号量的引用头文件是 <semaphore.h>,而 System V 信号量的引用头文件是 <sys/sem.h>
  3. 从使用的角度,System V 信号量的使用比较复杂,而 POSIX 信号量使用起来相对简单。

下面我们来介绍 System V 信号量编程的基本内容。

二、System V IPC 信号量

信号量是一种用于对多个进程访问共享资源进行控制的机制。共享资源通常可以分为两大类:

  • 互斥共享资源,即任一时刻只允许一个进程访问该资源
  • 同步共享资源,即同一时刻允许多个进程访问该资源

​ 信号量是为了 解决互斥共享资源的同步问题而引入的机制。信号量的实质是整数计数器,其中记录了可供访问的共享资源的单元个数。本文接下来提到的信号量都特指 System V IPC 信号量。

​ 当有进程要求使用某一资源时,系统首先要检测该资源的信号量,如果该资源的信号量的值大于 0,则进程可以使用这一资源,同时信号量的值减 1。进程对资源访问结束时,信号量的值加 1。如果该资源信号量的值等于 0,则进程休眠,直至信号量的值大于 0 时进程被唤醒,访问该资源。

​ 信号量中一种常见的形式是 双态信号量,即要么不做,要么做完。双态信号量对应于只有一个可供访问单元的互斥共享资源,它的初始值被设置为 1,任一时刻至多只允许一个进程对资源进行访问。

信号量用于实现对任意资源的锁定机制。它可以用来同步对任何共享资源的访问。

三、System V IPC 信号量的数据结构

System V 子系统提供的信号量机制是比较复杂的。我们不能单独定义一个信号量,而只能定义一个信号量集,其中包括一组信号量,同一信号量集中的信号量可以使用同一 ID 引用。每个信号量集都有一个与其相对应的结构,其中包含了信号量集的各种信息,该结构的声明如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// come from /usr/include/linux/sem.h
struct semid_ds
{
    struct ipc_perm sem_perm; 			 // 对应于该信号量集的ipc_perm结构,也就是权限
    struct sem *sem_base;     			 // sem结构指针,指向信号量集中第一个信号量的sem结构
    ushort sem_nsems;         			 // 信号量集中信号量的个数
    time_t sem_otime;         			 // 最近一次调用semop函数的时间
    time_t sem_ctime;         			 // 最近一次改变该信号量集的时间
    struct sem_queue *sem_pending;       // 阻塞在该信号量集上的进程队列
 	struct sem_queue **sem_pending_last; // 指向最后一个阻塞在该信号量集上的进程的指针
 	struct sem_undo *undo;               // 用于实现撤销操作的undo队列
 	unsigned short sem_nsems;            // 当前信号量集中已经分配的信号量的数量
};

sem 结构记录了一个信号量的信息,其声明如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct sem
{
    ushort semval;  // 信号量的值 
    pid_t sempid;   // 最后一次返回该信号量的进程ID
    ushort semncnt; // 等待可利用资源出现的进程数
    ushort semzcnt; // 等待全部资源可被独占的进程数
};

四、与信号量相关的函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int semget(key_t key, int nsems, int semflg); // 信号量集的创建与打开
int semop(int semid, struct sembuf *sops, size_t nsops); // 对信号量集的操作
int semctl(int semid, int semnum, int cmd, union semun arg); // 信号量的控制

// 用于表示Linux系统中对信号量进行操作时的参数
struct sembuf
{
    short sem_num;  // 要操作的信号量在信号量集里的编号
    short sem_op;   // 进行的操作,可以为负数、零或正数,表示减少、不变或增加信号量的值
    short sem_flag; // 操作的标志,可以为IPC_NOWAIT(非阻塞)或0(阻塞)
};

// 用于在Linux系统中进行信号量操作时的参数传递
union semun
{
    int val;              // 用于设置信号量的初始值,或者用于获取信号量的当前值
    struct semid_ds *buf; // 指向semid_ds结构体的指针,用于获取或设置信号量集的属性
    ushort *array;        // 指向ushort类型数组的指针,用于获取或设置信号量集中多个信号量的值
};

Ⅸ. system V IPC 通信的共同特点

​ 观察仔细的话我们可以发现,system V IPC 的不同方式的接口相似度是非常高的,无论是 shmid_dsmsgid_ds 还是 semid_ds,它们这些结构体的其中第一个字段都是 struct ipc_perm 结构体类型,并且它们的属性内容都是相同的,都存放着 _keyuidgid 等等属性!

​ 其实在 linux 中实现的逻辑大概是这样子的:

​ 这其实就对比于 c++ 中的多态属性!但是当时 c++ 还没诞生,其实 c语言 中早就有面向对象的思想了!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Linux】system V消息队列,信号量
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
用户11029103
2025/03/19
600
【Linux】system V消息队列,信号量
进程间通讯(六).semaphore and shared(3)
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
franket
2021/09/15
6360
linux网络编程之System V 共享内存 和 系列函数
s1mba
2017/12/28
1.1K0
【Linux】 IPC 进程间通信(三)(消息队列 & 信号量)
🔥 消息队列(Message Queue) 是一种进程间通信(IPC)机制,它允许不同进程或线程之间通过发送和接收消息来交换数据。
IsLand1314
2024/11/19
3190
【Linux】 IPC 进程间通信(三)(消息队列 & 信号量)
进程通信(三)共享内存
共享内存是操作系统直接在物理内存上开辟一段空间作为进程间通信的缓冲区域, 与管道、消息队列等其他进程通信方式相比较,共享内存拥有更高的效率,原因是共享内存的设计是基于物理内存的地址直接进行操作的,这样相比其他方式的IPC省去了重重的系统调用,因此在很大程度上提高了其效率。
lexingsen
2022/02/24
1.2K0
进程通信(三)共享内存
Linux进程间通信之System V
对于进程间通信,想必管道大家再熟悉不过了,对于管道这种通信方式,其实是对底层代码的一种复用,linux工程师借助类似文件缓冲区的内存空间实现了管道,其实也算偷了一个小懒,随着linux的发展,linux正式推出了System V来专门进行进程间通信,它和管道的本质都是一样的,都是让不同的进程看到同一份资源。
咬咬
2024/06/12
1310
Linux进程间通信之System V
System V通信
之前已经讲了通过管道来进行进程间通信,匿名管道是通过子进程继承父进程的文件描述符表来使两个进程看到同一份匿名管道文件实现的,有名管道是通过文件名作为唯一标识来使两个毫不相干的进程看到同一份资源。管道通信是基于文件系统的通信方式。而System V是操作系统提供的聚焦于本地通信的通信方式,本文介绍System V主要是介绍共享内存这种通信方式。
始终学不会
2023/10/17
1570
System V通信
【在Linux世界中追寻伟大的One Piece】System V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
枫叶丹
2024/10/03
1020
【在Linux世界中追寻伟大的One Piece】System V共享内存
【Linux】进程间通信详解
进程间通信(Interprocess communication,简称IPC)就是让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。
诺诺的包包
2023/10/15
4700
【Linux】进程间通信详解
【Linux】进程间通信——System V共享内存
  System V是一种在Linux系统中用于进程间通信(IPC)的机制。它提供了几种不同的通信方式,包括共享内存、消息队列和信号量。以下是关于Linux进程间通信System V共享内存的详细解释:
大耳朵土土垚
2024/12/09
1420
【Linux】进程间通信——System V共享内存
【高级编程】linux进程间通信总结
1. 概览 本文记录经典的IPC:pipes, FIFOs, message queues, semaphores, and shared memory。 2. PIPES 管道是UNIX系统IPC的最古老形式,并且所有的UNIX系统都提供此通信机制。但管道有两个局限性: 历史上,它们是半双工的,现在某些系统提供全双工管道。 它们只能在共有祖先的进程间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父进程与子进程之间就可以使用管道通讯。 管道由pipe创建。 #include <unistd
程序员互动联盟
2018/03/13
2K0
【高级编程】linux进程间通信总结
Linux之进程间通信——system V(共享内存、消息队列、信号量等)
本文介绍了另一种进程间通信——system V,主要介绍了共享内存,消息队列、信号量,当然消息队列了信号量并非重点,简单了解即可。
摘星
2023/10/15
5770
Linux之进程间通信——system V(共享内存、消息队列、信号量等)
15(进程间通信)
管道是Unix系统IPC最古老的方式。管道有下列两种局限性: (1) 历史上,它们是半双工的(即数据只能在一个方向上流动)。 (2) 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程就可以应用该管道
提莫队长
2019/02/21
5800
【Linux】SystemV IPC
那么我们知道,进程间通信的本质就是先让不同的进程看到同一份资源。我们以前学的管道都是基于文件的,那么我们还有其它方案进行进程间通信吗?有的,那么我们下面学习的共享内存就是由操作系统帮我们在地址空间中进行通信。
YoungMLet
2024/03/01
1710
【Linux】SystemV IPC
【Linux】进程间通信(命名管道、共享内存、消息队列、信号量)
命名管道是通过文件路径让不同进程看到同一份资源。 命名管道可以让两个毫不相干的进程进行进程间通信。
秦jh
2024/10/29
2380
【Linux】进程间通信(命名管道、共享内存、消息队列、信号量)
System V IPC 共享内存详解
​ 这里我们介绍的这种通信方式也就是 system V IPC 在我们后面的使用和日常见到的其实并不多,但是包括其中的共享内存、消息队列、信号量,我们如果了解共享内存其原理的话,能够更好的帮助我们了解之前我们学过的进程地址空间的概念!
利刃大大
2023/04/12
1K0
System V IPC 共享内存详解
【Linux】system V进程间通信——共享内存、消息队列、信号量
进程具有独立性:内核数据结构包括对应的代码、数据与页表都是独立的。OS系统为了让进程间进行通信:1.申请一块空间 2.将创建好的内存映射进进程的地址空间。共享内存让不同的进程看到同一份的资源就是在物理内存上申请一块内存空间,如何将创建好的内存分别与各个进程的页表之间建立映射,然后在虚拟地址空间中将虚拟地址填充到各自页表的对应位置,建立起物理地址与虚拟地址的联系。
平凡的人1
2023/10/15
3720
【Linux】system V进程间通信——共享内存、消息队列、信号量
深入探索进程间通信:System V IPC的机制与应用
在Linux系统下,System V指的是一套由AT&T开发的UNIX操作系统版本及其相关的进程间通信(IPC)机制。
绝活蛋炒饭
2024/12/16
1730
深入探索进程间通信:System V IPC的机制与应用
进程间通信—管道,共享内存,消息队列,信号量
在操作系统中进程具有独立性,那么进程之间进行通信必然成本不低。那么进程间通信方式有哪些呢?
梨_萍
2023/06/01
2K0
进程间通信—管道,共享内存,消息队列,信号量
Linux进程通信
管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,是只存在于内存中的文件,本质是内核的一块缓冲。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。管道是单向的、先进先出的、无结构的、固定大小字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。
Marigold
2022/06/17
1.9K0
Linux进程通信
推荐阅读
相关推荐
【Linux】system V消息队列,信号量
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验