034 进程间通信 —— System V 共享内存

进程间通信 ------ System V 共享内存

传统的 System V IPC 主要包括三种:共享内存、消息队列、信号量,我们后面主要涉及其 System v 共享内存,消息队列和信号量仅作为了解。 现代开发和教学中,共享内存 作为重点和常用技术,而 消息队列信号量 相对被弱化,主要有以下原因(了解,信息来自网络):

  • 共享内存的独特优势(不可替代性):

  • 极致性能(说白了就是快): 它是所有 IPC 方式中 速度最快 的。一旦建立映射,数据交换直接在内存中进行,没有内核到用户空间的数据拷贝开销

  • 灵活性: 共享内存本身只是提供了一块共享区域,进程可以在上面构建任何复杂的数据结构和通信协议。消息队列则限制了消息的结构和大小。

  • 消息队列的局限性(逐渐被替代):

  • 性能瓶颈: 每次发送和接收消息都涉及 系统调用数据在内核与用户空间之间的拷贝。对于大量或高频小消息的开销非常明显。

  • 灵活性限制: 消息有最大长度限制,且通常是 FIFO 的,虽然支持优先级,但模型相对固定。

  • 注: 老方案,API 麻烦,扩展性差,写多进程服务时不如直接用 socket。大厂一般直接上 RabbitMQ、Kafka(用户态 + 网络),或者自己封装 Pipe/Socket。

  • 信号量的现状:

  • 同步需求永存: 只要存在并发访问共享资源(尤其是共享内存!),就需要同步机制。信号量的核心功能(互斥、同步)仍然是必需的。

  • 注: 单独用很少,一般是搭配共享内存或者消息队列,用于加锁。现代 C++ 里更常用 POSIX Mutex、pthread_mutex、futex。所以学信号量只需要理解一下"需要同步",但实战更偏向 POSIX Mutex 或用户态自旋锁。

1. System V 共享内存的直接原理

System V 共享内存 就是一块 物理内存区域 ,由内核在物理内存中分配,多个进程通过 shmget 等系统调用 映射到各自的虚拟地址空间 ,从而实现 零拷贝的高效通信 。即:System V 共享内存 = 一块物理内存 + 多个进程虚拟映射 + 自己实现同步控制 。本质是 内核帮我们做了虚拟地址和物理页的映射表维护,保证多进程读写指向同一物理地址。

共享区映射到同一物理内存的过程:

  1. 首先,一个进程(或内核初始化时)通过系统调用 shmget 创建获取 一个共享内存段。这个段存在于物理内存中,由一个唯一的标识符 shmid 标识。
  2. 接下来,任何 想要使用这个共享内存段的进程(包括创建者),都需要在自己的地址空间中 附加(Attach,更多情况我们更喜欢称挂接) 它。这是通过系统调用 shmat 完成的。
  3. shmat 在调用进程的虚拟地址空间中动态分配一块映射区(通常位于 mmap 动态映射区域,常见于堆和栈之间),并在页表中建立映射,使这块虚拟地址指向共享内存段实际占用的物理页帧,而不再指向进程私有页(注意 通常位置处于进程地址空间的共享区,但并非强制!)。
  4. 多个进程执行 shmat 后: 它们各自的虚拟地址空间中,被 shmat 返回的那个地址(或指定的地址)所对应的页表项,都指向了 同一块物理内存区域
  5. 结果: 当一个进程通过它附加的虚拟地址写入数据时,数据直接写入了这块共享物理内存。另一个进程通过它自己附加的(不同的)虚拟地址读取时,直接从这块共享物理内存读取数据。不同的虚拟地址,通过各自的页表,映射到相同的物理地址。 这就是"让不同进程看到同一份资源"的本质。

下面找了几个较为形象的图:

注意:上述的映射、页表操作,都是由操作系统(内核)来做,而不是用户态进程做!

为什么?

  • 用户态没权限直接操作页表(这是 MMU 和硬件特权模式约束)。
  • 只有内核有权限分配物理页、修改页表、维护引用计数。
  • 如果用户态可以随意改,那整个系统就失去内存隔离了,安全性就没有了。

所以,用户态只能发起系统调用(shmgetshmat),具体"找页、改表、挂映射"由内核执行 。那么当系统中存在多个共享内存,OS 要不要进行管理呢?回答:要的!这就又是我们老生常谈的 "先描述,再组织"(内核结构体描述共享内存)。

深入理解 "挂接"

挂接 = 页表映射 = 在自己的地址空间里开一扇门,让我们能访问内核里那块共享仓库。 挂接是 Linux 传统的说法,和文件系统的"挂载"类似,本质都是:把已有的物理资源,通过操作系统管理,暴露给一个命名空间/可访问空间

  • 文件系统挂载:把设备/分区挂到某个目录。
  • 共享内存挂接:把物理页挂到某个虚拟地址段。

比喻: System V 共享内存就是一块 公共仓库 (物理页帧),这块仓库在内核里用 shmid_ds 管理,内核负责"保管"。单纯 shmget 就是创建了这块仓库,但谁也没把仓库门对接到自己家里。shmatattach )就像是在你的家(进程的虚拟地址空间)里开个门,和这块仓库打通,让你能直接看到和访问这块公共物理页。所以:挂接 = 虚拟地址空间里挂一块区域指向同一块物理页帧,进程间通信的前提是让不同的进程先看到同一份资源!


不挂能不能用? 不能!

  • 共享内存只是一块物理页帧,没挂接到进程页表前,进程看不到。

  • 只有挂接后,CPU 访问你的虚拟地址才会翻译到这块物理内存。


挂接后要不要解绑? 要!

  • 用完后要 shmdt(detach)把这块映射从页表里去掉,虚拟内存就能被释放。
  • 但物理页帧还在(因为其他进程可能还挂着)。
  • 当最后一个挂接者 shmdt 后,如果之前用 IPC_RMID 标记删除了,就会真正释放物理页。

2. 系统调用

1. ftok ------ 生成 IPC key(为后面的 shmget 铺垫)

1. 作用

ftok 的作用是 生成一个 System V IPC 的 key(键值) ,本质是一个将路径作为的字符串和 proj_id 这个整数结合的一个算法。和这个 key 用来在:shmget(共享内存)参数里作为唯一标识。它不是随机生成的!而是用 pathnameproj_id 生成的一个整数 key_t。在前面的有名管道中,我们使用路径作为唯一标识(路径本身就具有唯一性),这里使用一个 key 值作为唯一性也是异曲同工之妙。注:ftok 只是为了生成 key,和数据本身没关系!

2. ftok 函数原型
cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);			// 有效路径和任意整数
3. 怎么生成?

内部算法 大概 是:把 inode 的低字节、设备号等信息混进去,经过算法组合,从而形成唯一的哈希值 key。所以:

  • 只要文件不变,同一个 proj_id 得到的 key 是一样的。
  • 这样保证不同进程用相同文件和 proj_id,就能找到同一块 IPC 资源。
4. 返回值
  • 成功:返回 key (key_t),是个整数。
  • 失败:返回 -1,设置 errno
    • EACCES:路径不可访问。
    • ENOENT:文件不存在。
    • ENOTDIR:路径中的目录不存在。
5. 示例
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <cerrno>
#include <cstdio>
using namespace std;

int main()
{
    key_t key = ftok("./tmp.txt", 65);  // 文件必须存在
    if (key == -1)
    {
        perror("ftok");
        return 1;
    }
    
    cout << "IPC Key: " << key << endl;
    return 0;
}

2. shmget ------ 创建/获取共享内存

1. 作用
  • 创建一个新的共享内存段。
  • 或者 获取已存在的共享内存段。

关键靠 key 做唯一标识。

2. shmget 函数原型
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
3. 参数详解
参数 含义
key IPC 键值 (由 ftok 生成),用于唯一标识,可以自定义,但是不保证一定有效,可能存在冲突!
size 共享内存段大小 (单位字节),如果是获取已存在的,则忽略,注意 页对齐 行为!通常分配 4KB 的整数倍(4096 的整数倍)
shmflg 权限标志 + 控制标志 ,典型:IPC_CREATIPC_EXCL0666

注意: System V 共享内存段在物理页帧分配时总是 按页对齐(通常是 4KB) ,但 shmgetsize 是逻辑大小,ipcs -m 里显示也是逻辑大小,实际物理内存占用是向上对齐的页大小倍数,使用时应按逻辑大小访问,超出即是"越界访问",可能存在 未定义行为/段错误

例如:shmget(4100) 表示:"我承诺只用前 4100 字节(0~4099 合法)",OS 回应:" 我实际给你 8192 字节(4KB * 2),但超出的部分你别碰 "(非越界访问指 0 ≦ 有效值 < 用户分配值的大小)。永远记住:"能" 做不代表 "应该" 做。在系统编程中,自律比能力更重要。不立即爆炸,但终将毁灭!

4. 常用的 shmflg
  • 0666:权限位,表示其他进程是否可读/写(和 open 的权限一样)。
  • IPC_CREAT:如果不存在则创建,存在就获取返回。
  • IPC_EXCL:和 IPC_CREAT 一起用时,要求"仅当不存在时才创建",否则出错。
  • IPC_EXCL:不单独使用!

举例:

  • 只想创建(如果存在则失败):IPC_CREAT | IPC_EXCL | 0666
  • 想获取(或必要时创建):IPC_CREAT | 0666
5. 返回值
  • 成功:返回共享内存标识符(shmid),唯一 int ID。
  • 失败:返回 -1,设置 errno
6. 示例
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <cerrno>
#include <cstdio>
using namespace std;

int main()
{
    key_t key = ftok("/tmp.txt", 65);                       // 生成唯一 key
    int shmid = shmget(key, 4096, 0666 | IPC_CREAT);        // 创建或获取 4KB 共享内存

    if (shmid == -1)
    {
        perror("shmget");
        return 1;
    }
    
    cout << "shmid: " << shmid << endl;
    return 0;
}

3. 二者关系总结

函数 作用 关键点
ftok 生成 key 依赖文件 inode 和 proj_id,不保证全局唯一,但同参数一致性好
shmget 创建/获取共享内存 需要 key、大小和标志,内核内部管理分配和引用计数

小坑提醒:

  • ftok 不是必须的!我们完全可以自己写 key_t,只要和另一端一致就行(很多老项目直接用固定数值)。
  • 不同机器,ftok 生成同一文件的 key 可能不一样(因为 inode 和设备号可能不同),所以跨机集群时要注意!
  • shmget 只分配描述符,不分配虚拟地址;只有 shmat 才会把它挂到进程地址空间。

ftok 是为了在多人协作或多程序协作时,保证只要文件和 proj_id 一致,生成的 key 就一致,从而多个进程可以用相同 key 访问同一个 IPC 对象。


4. shmctl ------ 删除(控制/管理)共享内存

1. shmctl 函数原型
c++ 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);		 // 控制共享内存
2. 参数详解
  1. shmid:共享内存段标识符(由 shmget 返回)。
  2. cmd
    • IPC_RMID:删除,从系统中标记删除该段。
    • IPC_STAT:获取状态,用于接收共享内存的状态信息(输出)。
    • IPC_SET:设置状态,用于向内核提交新状态(输入)。
    • SHM_LOCK:锁定共享内存段(防止换出到交换空间)。
    • SHM_UNLOCK:解除锁定。
  3. buf:如果是 IPC_STATIPC_SET,就需要传状态结构体指针;IPC_RMID 时可传 NULLnullptr)。
3. 返回值
  • 成功:返回 0。
  • 失败 :返回 -1,同时设置 errno。常见错误:
    • EINVAL:无效的 shmid
    • EACCES:没有权限。
    • EIDRM:段已被删除。
shmid_ds 结构体:

这是 shmctl 操作的核心数据结构,用来描述共享内存段的元数据。

具体含义:

c++ 复制代码
struct shmid_ds
{
struct ipc_perm shm_perm;   // 权限信息(UID, GID, mode)
size_t shm_segsz;           // 段大小(字节)
time_t shm_atime;           // 上次 attach 时间
time_t shm_dtime;           // 上次 detach 时间
time_t shm_ctime;           // 创建或上次修改时间
pid_t shm_cpid;             // 创建该段的进程 PID
pid_t shm_lpid;             // 最后一次操作该段的进程 PID
shmatt_t shm_nattch;        // 当前 attach 的进程数量(引用计数)
...
};
c++ 复制代码
struct ipc_perm
{
key_t          __key;    	// 传递给 shmget(2) 的键值
uid_t          uid;      	// 共享内存拥有者的有效用户ID
gid_t          gid;      	// 共享内存拥有者的有效组ID
uid_t          cuid;     	// 创建该共享内存的进程的有效用户ID
gid_t          cgid;     	// 创建该共享内存的进程的有效组ID
unsigned short mode;     	// 权限标志位,包含 SHM_DEST 和 SHM_LOCKED 等标志
unsigned short __seq;    	// 序列号,用于生成唯一标识
};

如何查看哪个进程还挂着共享内存?

查看 shm_nattch(引用计数),只要 > 0,物理页就不释放!


场景示例:

c++ 复制代码
struct shmid_ds buf;
shmctl(shmid, IPC_STAT, &buf);

// 查看大小、创建者 PID、引用数
printf("Size: %zu\n", buf.shm_segsz);
printf("Creator PID: %d\n", buf.shm_cpid);
printf("Nattach: %ld\n", buf.shm_nattch);
c++ 复制代码
shmctl(shmid, IPC_RMID, NULL);  // 标记删除
// 标记删除 ≠ 立刻删除,等引用数归 0 后才真正释放。
1. 查看系统现有的共享内存
bash 复制代码
ipcs -m    # 查看系统现有的共享内存
2. 删除指定系统的共享内存
bash 复制代码
ipcrm -m <shmid>    # 删除指定共享内存段

5. shmat ------ "挂接内存"

1. 函数原型
c++ 复制代码
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
2. 作用

进程各自的虚拟地址空间是隔离的,但 shmat 把同一个物理内存页挂接到多个进程的页表里,所以多个进程访问同一块物理页,实现了多进程 "看到同一份资源" ,到此,多进程间才具备的通信的能力。底层做的是页表映射和引用计数,返回值是可直接访问的指针,真正实现多进程间的零拷贝数据共享(拷贝少 → 速度快)。

3. 参数详解
参数 作用 解释
int shmid 共享内存段标识符 来自 shmget 的返回值
const void *shmaddr 希望映射到进程虚拟空间的 首选地址 通常写 NULL/nullptr(让内核自己找个合适的可用地址)
int shmflg 标志位 主要用于指定映射权限或对齐
  • shmaddr

    • 如果是 NULL,表示"由内核自动分配虚拟地址",这是 最常用也最安全的写法

    • 如果指定了具体地址:必须是页对齐地址,shmflg 可以带 SHM_RND 表示"按 4KB 对齐"。

  • shmflg 的典型值:

    • 0:最常用,表示默认读写。
    • SHM_RDONLY:以只读方式挂接(这个进程只能读,别的进程可以写)。
    • SHM_RND:如果指定了 shmaddr,且希望地址自动按 4KB 页对齐,就要加这个。
4. 返回值
  • 成功 :返回共享内存段在当前进程虚拟地址空间中的起始地址(void *)。

  • 失败: 返回 (void *) -1,并设置 errno

    • EINVALshmid 不存在或非法。

    • EACCES:权限不足(比如只读段却尝试写挂接)。

    • ENOMEM:找不到可用虚拟地址(尤其是自己指定 shmaddr 时更容易出现)。


6. shmdt ------ 脱挂,释放虚拟空间的映射

1. 作用

从当前进程的页表中卸载共享内存段的映射区域,并把内核引用计数 -1,不会释放物理页帧,物理段释放要靠 shmctl(IPC_RMID) 和引用计数归零一起决定。

shmdt 负责 "关门走人",shmctl(IPC_RMID) 负责 "拆掉仓库"。

对比 shmdt shmctl
主要功能 脱挂(取消挂接) 控制(管理)共享内存段
作用对象 当前进程的虚拟地址空间 内核中整个共享内存段
是否影响物理内存 ❌ 不会直接删除物理页帧 ✅ 可以通过 IPC_RMID 标记删除物理页帧
引用计数影响 调用后,shmid_ds.shm_nattch -1 IPC_RMID 后,等引用数归零才真正释放
调用场景 挂接用完后必须调用,释放虚拟空间 要永久回收内存段时必须调用
是否必须 一般必须(防止虚拟内存泄漏) 必须(否则段会一直挂在内核 IPC 表)
错误示例 不脱挂会浪费虚拟地址空间 不标记删除会造成内核残留,需手动 ipcrm
2. 函数原型
c++ 复制代码
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
3. 参数详解
  • const void *shmaddrshmat 返回的指针,表示要脱挂的共享内存区域的起始虚拟地址。
  • 注意 :这个地址必须是之前 shmat 成功挂接时返回的那个指针。不允许随便传地址!
4. 返回值
  • 成功:返回 0。
  • 失败:返回 -1。常见错误:
    • EINVAL:找不到对应挂接(shmaddr 非法)。
    • ENOMEM:有些实现里如果内部释放失败(比较罕见)。

3. System V 共享内存的生命周期随内核

结论:System V 共享内存段 = 不手动删就不回收,引用计数归零 + IPC_RMID 才是唯一的释放条件!IPC_RMID + shm_nattch == 0 → 真正释放物理页。 如果没 IPC_RMID,引用计数再归零也不删,物理页照样挂着!

System V 共享内存的生命周期和两个东西密切相关:

  1. 共享内存 段本身shmid_ds 描述的物理页)。
  2. 挂接(attach)的 引用计数shm_nattch)。

他们俩共同决定:什么时候物理页还存在,什么时候物理页真正被回收!

注意:System V 共享内存不会自动回收,必须手动使用 shmctl / ipcrm内核 会一直保留这块物理页帧,只要系统 重启前 ,这段共享内存都在 /proc/sysvipc/shm 里挂着!所以 System V IPC 的最终清理手段 = 重启!

System V IPC 是 Linux 内核维护的全局资源,而 Xshell 是 一个远程终端工具 ,本质就是个 SSH 客户端。它 不会"托管"共享内存段 ,只是帮助登录远程 Linux 主机。关掉 Xshell,只是 SSH 断了,跟远程机器上的进程、内核资源没有必然关系。所以,关掉 Xshell 对共享内存没有任何直接影响!

4. 示例 Demo

1. System V 共享内存通信完整步骤

  1. ftok (可选,但推荐): 生成一个 key_t,作为 IPC 对象的唯一标识。本质上就是用路径名+id 生成一个整数 key。

  2. shmget 内核分配一块 物理内存页帧 ,挂到内核的共享内存表(shmid_ds)。得到一个 shmid(共享内存段标识符)。

  3. shmat 挂接(attach): 把这块共享内存段映射到调用进程的 虚拟地址空间,更新页表。返回一个指针,后续直接对这块物理页读写。

  4. 各个进程的读写操作:......(省略)。

  5. shmdt 用完后,进程要执行 shmdt 脱挂,把这块内存区域从自己的页表取消映射。

  6. shmctlIPC_RMID 命令显式标记这块共享内存段为"待删除"。当挂接计数归零时,内核真正回收物理页。

2. System V 共享内存的服务端-客户端模型 Demo

fgets 函数

1. 作用 / 功能

C 标准库提供的一个输入函数,用于 从指定的文件流中读取一行字符串,可安全限制最大读取长度,避免缓冲区溢出。常用于读取带空格的一整行输入(包括换行符),常用于:

  • stdin 获取用户输入。
  • 从文件中按行读取文本。
2. 函数原型
cpp 复制代码
#include <cstdio>   		// C++ 推荐使用
#include <stdio.h>			// 或者在纯 C 里用
char *fgets(char *str, int n, FILE *stream);
3.参数详解
参数名 类型 说明
char *str 输入输出参数 指向用于存放读取字符串的缓冲区的指针。读取到的字符串(包括换行符)会存到这里。必须有足够空间。
int n 输入参数 要读取的最大字符数(包含结尾的 \0),所以实际最多读取 n - 1 个字符。
FILE *stream 输入参数 文件流指针,比如 stdin(标准输入)或用 fopen 打开的文件指针。
4. 返回值
  • 返回值类型: char *
  • 成功时: 返回传入的缓冲区 str 指针。
  • 失败时: 如果发生错误或遇到文件结尾(EOF)且未读取到任何字符,则返回 NULL

常用的判断写法:

cpp 复制代码
if (fgets(buffer, size, stdin) != NULL)
{
   // 成功
}
else
{
   // 读取失败或 EOF
}
5. 代码示例
cpp 复制代码
#include <cstdio>                       // 引入 fgets 需要的头文件
#include <cstdlib>                      // exit() 用于异常退出

int main()
{
   const int SIZE = 100;               // 定义缓冲区大小
   char buffer[SIZE];                  // 创建缓冲区

   printf("请输入一行文字(最多 %d 个字符):\n", SIZE - 1);

   if (fgets(buffer, SIZE, stdin) != NULL)      // 调用 fgets 从 stdin 读取
   {
       printf("您输入的是:%s", buffer);         // fgets 会保留换行符
   }
   else
   {
       perror("读取失败");                       // 读取出错或 EOF
       exit(1);
   }

   return 0;
}
1. Log.hpp 文件(之前写的日志插件)
c++ 复制代码
#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>                                      // exit, perror
#include <unistd.h>                                      // read, write, close
#include <sys/types.h>                                   // open, close, read, write, lseek
#include <sys/stat.h>                                    // mkdir
#include <fcntl.h>                                       // open, O_RDONLY, O_WRONLY, O_CREAT, O_APPEND
#include <errno.h>                                       // errno
#include <sys/time.h>                                    // gettimeofday, struct timeval
#include <ctime>                                         // localtime_r, struct tm
using namespace std;

// 管道错误码
enum FIFO_ERROR_CODE
{
    FIFO_CREATE_ERR = 1,                                 // 这是创建管道文件失败的错误码
    FIFO_DELETE_ERR = 2,                                 // 这是删除管道文件失败的错误码
    FIFO_OPEN_ERR                                        // 这是打开管道文件失败的错误码(枚举会自动赋值为3)
};

// 日志等级
enum Log_Level
{
    Fatal,                                               // 最严重级别
    Error,                                               // 严重错误
    Warning,                                             // 警告
    Debug,                                               // 调试信息
    Info                                                 // 普通信息
};

class Log
{
    int  enable = 1;                                     // 是否启用日志
    int  classification = 1;                             // 是否分类
    string log_path = "./log.txt";                       // 日志存放路径
    int  console_out = 1;                                // 是否输出到终端

    // 日志等级转换成字符串
    string level_to_string(int level)
    {
        switch (level)
        {
            case Fatal:
                return "Fatal";
            case Error:
                return "Error";
            case Warning:
                return "Warning";
            case Debug:
                return "Debug";
            case Info:
                return "Info";
            default:
                return "None";
        }
    }

    // 获取当前计算机的时间,返回格式:YYYY-MM-DD HH:MM:SS.UUUUUU (含微秒)
    string get_current_time()
    {
        struct timeval tv;                               // timeval:包含秒和微秒
        gettimeofday(&tv, nullptr);                      // 系统调用:获取当前时间(精确到微秒)

        struct tm t;                                     // tm:分解时间,转格式(年、月、日、时、分、秒)
        localtime_r(&tv.tv_sec, &t);                     // 把秒转换成年月日时分秒(本地时区)

        char buffer[64];                                 // 定义字符数组作为格式化输出的缓冲区

        snprintf(buffer, sizeof(buffer),
                "%04d-%02d-%02d %02d:%02d:%02d.%06ld",
                t.tm_year + 1900,                        // 年:tm_year 从 1900 开始计数
                t.tm_mon + 1,                            // 月:tm_mon 从 0 开始,0 表示 1 月
                t.tm_mday,                               // 日
                t.tm_hour,                               // 时
                t.tm_min,                                // 分
                t.tm_sec,                                // 秒
                tv.tv_usec);                             // 微秒部分,取自 gettimeofday

        return string(buffer);                           // 转换成 string 返回
    }

public:
    Log() = default;                                     // 使用默认构造
    Log(int enable, int classification, string log_path, int console_out)
        : enable(enable),
        classification(classification),
        log_path(log_path),
        console_out(console_out)
    {

    }
    
    // 重载函数调用运算符
    void operator()(int level, const string& content)
    {
        if (enable == 0)
        {
            return;                                      // 日志未启用
        }

        string level_str = "[" + level_to_string(level) + "] ";
        string log_message;

        if (classification == 1)
        {
            log_message = level_str + "[" + get_current_time() + "] " + content + "\n";
        }
        else if (classification == 0)
        {
            log_message = "[" + get_current_time() + "] " + content + "\n";
        }
        else
        {
            printf("传入的分类参数错误!\n");              // 分类未启用
            return;
        }

        if (console_out == 1)
        {
            cout << log_message;
        }

        log_to_file(level, log_message);
    }

private:
    // 文件路径的后缀处理函数:当按照日志等级分类存储并且文件路径是 "./log.txt" 这种有文件扩展名时的处理方法
    string Suffix_processing(int level, string log_path)
    {
        string Path;
        if (log_path.back() == '/')                      // 如果是一个目录的路径,比如 "./log/",则最终文件名为 "log_等级名.txt"
        {
            Path = log_path + "log_" + level_to_string(level) + ".txt";
        }
        else                                             // 如果是一个文件路径,比如 "./log.txt",则最终文件名为 "log_等级名.txt"
        {
            size_t pos = log_path.find_last_of('.');     // 从后往前找到第一个 '.' 的位置,即最后一次出现的 '.' 的位置
            if (pos != string::npos)
            {
                string left = log_path.substr(0, pos);   // 去掉后缀,即我所需要的有效的前部分路径
                string right = log_path.substr(pos);     // 保留后缀,即有效的文件扩展名
                Path = left + "_" + level_to_string(level) + right;         // 组合成新的文件名
            }
            else                                         // 如果没有文件扩展名(比如 "./log"),则直接在文件名后面加上 "_等级名.txt"
            {
                Path = log_path + "_" + level_to_string(level) + ".txt";
            }
        }

        return Path;
    }

    // 核心写文件函数
    void log_to_file(int level, const string& log_content)
    {
        string Path;

        if (classification == 1)
        {
            Path = Suffix_processing(level, log_path);   // 按照日志等级分类存储
        }
        else if (classification == 0)
        {
            Path = log_path;                             // 不分类直接使用传入的 log_path
        }

        // 追加写入,文件不存在则创建,权限 0644
        int fd = open(Path.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (fd < 0)
        {
            perror("");
            exit(FIFO_OPEN_ERR);
        }

        write(fd, log_content.c_str(), log_content.size());
        close(fd);
    }
};
2. comm.hpp 文件
c++ 复制代码
#pragma once

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Log.hpp"
using namespace std;

const char* pathname = "/home/hcc";
const int project_id = 0x1234;
const int shm_size = 4096;
#define FIFO_FILE "./myfifo"
#define MODE 0664
Log log(1, 0, "./log.txt", 1);

// 获取共享内存的key
key_t get_key()
{
    key_t key = ftok(pathname, project_id);

    if(key == -1)
    {
        cout << "共享内存的key创建失败!" << endl;
        exit(1);
    }

    return key;
}

// 创建共享内存
int create_shm(int flag)
{
    int shm_id = shmget(get_key(), shm_size, flag);

    if(shm_id == -1)
    {
        cout << "共享内存创建失败!" << endl;
        exit(1);
    }
    
    return shm_id;
}

// 只想创建共享内存,如果存在则报错
int create_shm_only()
{
    return create_shm(IPC_CREAT | IPC_EXCL | 0666);
}

// 获取共享内存,如果不存在则创建
int get_shm()
{
    return create_shm(IPC_CREAT | 0666);
}
3. processA.cc 文件
c++ 复制代码
#include "comm.hpp"

int main()
{
    int shmid = create_shm_only();                            // 创建共享内存(只创建)
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);         // 挂接共享内存

    int fd = open(FIFO_FILE, O_RDONLY);                       // 打开管道文件
    if(fd < 0)
    {
        perror("打开文件失败!");
        log(Error, "打开文件失败!");
        exit(FIFO_OPEN_ERR);
    }

    struct shmid_ds shmds;                                    // 共享内存信息结构体

    while(true)
    {
        char c;                                               // 读取字符
        ssize_t s = read(fd, &c, 1);                          // 读取管道文件
        if(s < 0 || s == 0)
        {
            break;
        }

        cout << "客户说:" << shmaddr << endl;
        sleep(1);                                             // 模拟业务处理时间,延迟1秒

        shmctl(shmid, IPC_STAT, &shmds);                      // 获取共享内存信息
        cout << "段大小: " << shmds.shm_segsz << endl;
        cout << "附着进程数: " << shmds.shm_nattch << endl;
        printf("传递给进程的键值: 0x%x\n",  shmds.shm_perm.__key);
        cout << "访问模式: " << shmds.shm_perm.mode << endl;
    }

    shmdt(shmaddr);                                           // 脱挂共享内存
    shmctl(shmid, IPC_RMID, nullptr);                         // 删除共享内存
    close(fd);                                                // 关闭文件描述符

    return 0;
}
4. processB.cc 文件
c++ 复制代码
#include "comm.hpp"

int main()
{
    int shmid = get_shm();                                    // 获取共享内存ID
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);          // 挂接共享内存

    int fd = open(FIFO_FILE, O_WRONLY);                       // 打开FIFO文件
    if(fd < 0)
    {
        perror("打开文件失败!");
        log(Error, "打开文件失败!");
        exit(FIFO_OPEN_ERR);
    }

    while(true)
    {
        cout << "请输入要发送的消息:";
        fgets(shmaddr, 4096, stdin);                          // 从标准输入读取消息
        write(fd, "c", 1);                                    // 发送消息
    }

    shmdt(shmaddr);                                           // 脱挂共享内存
    close(fd);                                                // 关闭FIFO文件

    return 0;
}
5. 运行示例

System V 共享内存 Demo

相关推荐
奇妙-24 分钟前
创龙3576ububuntu系统设置静态IP方法
linux
Jayyih1 小时前
嵌入式系统学习Day23(进程)
linux·运维·服务器
Johny_Zhao2 小时前
Conda、Anaconda、Miniconda对比分析
linux·网络安全·信息安全·kubernetes·云计算·conda·shell·containerd·anaconda·yum源·系统运维·miniconda
大数据小墨2 小时前
在Arch Linux上设置SDDM自动登录Hyprland
linux
shylyly_2 小时前
Linux->多线程3
java·linux·开发语言·阻塞队列·生产者消费者模型
小王努力学编程2 小时前
从零开始的 Docker 之旅
linux·运维·服务器·docker·容器·容器编排·镜像制作
望获linux2 小时前
【实时Linux实战系列】基于实时Linux的音频实时监控系统
大数据·linux·服务器·网络·数据库·操作系统·嵌入式软件
轻松Ai享生活3 小时前
Linux Swap 详解 (1)
linux
東雪蓮☆4 小时前
深入理解 iptables:Linux 防火墙从入门到精通
linux·运维·网络
努力学习的小廉4 小时前
深入了解linux系统—— 线程互斥
linux·运维·服务器