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. 结果: 当一个进程通过它附加的虚拟地址写入数据时,数据直接写入了这块共享物理内存。另一个进程通过它自己附加的(不同的)虚拟地址读取时,直接从这块共享物理内存读取数据。不同的虚拟地址,通过各自的页表,映射到相同的物理地址。 这就是“让不同进程看到同一份资源”的本质。

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

image-20250702193137249

image-20250702193250253

image-20250702193332460

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

为什么?

  • 用户态没权限直接操作页表(这是 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 函数原型

1
2
3
4
#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. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

image-20250702210024126


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

1. 作用

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

关键靠 key 做唯一标识。

2. shmget 函数原型

1
2
3
4
#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. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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 函数原型

1
2
3
4
#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 操作的核心数据结构,用来描述共享内存段的元数据。

image-20250703094226102

具体含义:

1
2
3
4
5
6
7
8
9
10
11
12
>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 的进程数量(引用计数)
>...
>};
1
2
3
4
5
6
7
8
9
10
>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,物理页就不释放!


场景示例:

1
2
3
4
5
6
7
>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);
1
2
>shmctl(shmid, IPC_RMID, NULL);  // 标记删除
>// 标记删除 ≠ 立刻删除,等引用数归 0 后才真正释放。

1. 查看系统现有的共享内存

1
>ipcs -m    # 查看系统现有的共享内存

2. 删除指定系统的共享内存

1
>ipcrm -m <shmid>    # 删除指定共享内存段

5. shmat —— “挂接内存”

1. 函数原型

1
2
3
4
#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. 函数原型

1
2
3
4
#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 命令显式标记这块共享内存段为“待删除”。当挂接计数归零时,内核真正回收物理页。

image-20250707210158998

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

fgets 函数

1. 作用 / 功能

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

  • stdin 获取用户输入。
  • 从文件中按行读取文本。

2. 函数原型

1
2
3
>#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

常用的判断写法:

1
2
3
4
5
6
7
8
>if (fgets(buffer, size, stdin) != NULL)
>{
// 成功
>}
>else
>{
// 读取失败或 EOF
>}

5. 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>#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 文件(之前写的日志插件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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