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



注意:上述的映射、页表操作,都是由操作系统(内核)来做,而不是用户态进程做!
为什么?
- 用户态没权限直接操作页表(这是 MMU 和硬件特权模式约束)。
- 只有内核有权限分配物理页、修改页表、维护引用计数。
- 如果用户态可以随意改,那整个系统就失去内存隔离了,安全性就没有了。
所以,用户态只能发起系统调用(shmget
、shmat
),具体"找页、改表、挂映射"由内核执行 。那么当系统中存在多个共享内存,OS 要不要进行管理呢?回答:要的!这就又是我们老生常谈的 "先描述,再组织"(内核结构体描述共享内存)。
深入理解 "挂接"
挂接 = 页表映射 = 在自己的地址空间里开一扇门,让我们能访问内核里那块共享仓库。 挂接是 Linux 传统的说法,和文件系统的"挂载"类似,本质都是:把已有的物理资源,通过操作系统管理,暴露给一个命名空间/可访问空间。
- 文件系统挂载:把设备/分区挂到某个目录。
- 共享内存挂接:把物理页挂到某个虚拟地址段。
比喻: System V 共享内存就是一块 公共仓库 (物理页帧),这块仓库在内核里用
shmid_ds
管理,内核负责"保管"。单纯shmget
就是创建了这块仓库,但谁也没把仓库门对接到自己家里。shmat
(attach )就像是在你的家(进程的虚拟地址空间)里开个门,和这块仓库打通,让你能直接看到和访问这块公共物理页。所以:挂接 = 虚拟地址空间里挂一块区域指向同一块物理页帧,进程间通信的前提是让不同的进程先看到同一份资源!
不挂能不能用? 不能!
共享内存只是一块物理页帧,没挂接到进程页表前,进程看不到。
只有挂接后,CPU 访问你的虚拟地址才会翻译到这块物理内存。
挂接后要不要解绑? 要!
- 用完后要
shmdt
(detach)把这块映射从页表里去掉,虚拟内存就能被释放。- 但物理页帧还在(因为其他进程可能还挂着)。
- 当最后一个挂接者
shmdt
后,如果之前用IPC_RMID
标记删除了,就会真正释放物理页。
2. 系统调用
1. ftok
------ 生成 IPC key(为后面的 shmget
铺垫)
1. 作用
ftok
的作用是 生成一个 System V IPC 的 key(键值) ,本质是一个将路径作为的字符串和 proj_id 这个整数结合的一个算法。和这个 key 用来在:shmget
(共享内存)参数里作为唯一标识。它不是随机生成的!而是用 pathname
和 proj_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_CREAT ,IPC_EXCL ,0666 |
注意: System V 共享内存段在物理页帧分配时总是 按页对齐(通常是 4KB) ,但 shmget
的 size
是逻辑大小,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. 参数详解
shmid
:共享内存段标识符(由shmget
返回)。cmd
:IPC_RMID
:删除,从系统中标记删除该段。IPC_STAT
:获取状态,用于接收共享内存的状态信息(输出)。IPC_SET
:设置状态,用于向内核提交新状态(输入)。SHM_LOCK
:锁定共享内存段(防止换出到交换空间)。SHM_UNLOCK
:解除锁定。
buf
:如果是IPC_STAT
或IPC_SET
,就需要传状态结构体指针;IPC_RMID
时可传NULL
(nullptr
)。
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. 查看系统现有的共享内存
bashipcs -m # 查看系统现有的共享内存
2. 删除指定系统的共享内存
bashipcrm -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
。-
EINVAL
:shmid
不存在或非法。 -
EACCES
:权限不足(比如只读段却尝试写挂接)。 -
ENOMEM
:找不到可用虚拟地址(尤其是自己指定shmaddr
时更容易出现)。
-
6. shmdt
------ 脱挂,释放虚拟空间的映射
1. 作用
从当前进程的页表中卸载共享内存段的映射区域,并把内核引用计数 -1,不会释放物理页帧,物理段释放要靠 shmctl(IPC_RMID)
和引用计数归零一起决定。
shmdt
负责 "关门走人",shmctl(IPC_RMID)
负责 "拆掉仓库"。
对比 shmdt
shmctl
主要功能 脱挂(取消挂接) 控制(管理)共享内存段 作用对象 当前进程的虚拟地址空间 内核中整个共享内存段 是否影响物理内存 ❌ 不会直接删除物理页帧 ✅ 可以通过 IPC_RMID
标记删除物理页帧引用计数影响 调用后, shmid_ds.shm_nattch
-1IPC_RMID
后,等引用数归零才真正释放调用场景 挂接用完后必须调用,释放虚拟空间 要永久回收内存段时必须调用 是否必须 一般必须(防止虚拟内存泄漏) 必须(否则段会一直挂在内核 IPC 表) 错误示例 不脱挂会浪费虚拟地址空间 不标记删除会造成内核残留,需手动 ipcrm
2. 函数原型
c++
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
3. 参数详解
const void *shmaddr
: 由shmat
返回的指针,表示要脱挂的共享内存区域的起始虚拟地址。- 注意 :这个地址必须是之前
shmat
成功挂接时返回的那个指针。不允许随便传地址!
4. 返回值
- 成功:返回 0。
- 失败:返回 -1。常见错误:
EINVAL
:找不到对应挂接(shmaddr
非法)。ENOMEM
:有些实现里如果内部释放失败(比较罕见)。
3. System V 共享内存的生命周期随内核
结论:System V 共享内存段 = 不手动删就不回收,引用计数归零 + IPC_RMID 才是唯一的释放条件!IPC_RMID
+ shm_nattch == 0
→ 真正释放物理页。 如果没 IPC_RMID
,引用计数再归零也不删,物理页照样挂着!
System V 共享内存的生命周期和两个东西密切相关:
- 共享内存 段本身 (
shmid_ds
描述的物理页)。 - 挂接(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 共享内存通信完整步骤
-
ftok
(可选,但推荐): 生成一个 key_t,作为 IPC 对象的唯一标识。本质上就是用路径名+id 生成一个整数 key。 -
shmget
: 内核分配一块 物理内存页帧 ,挂到内核的共享内存表(shmid_ds
)。得到一个shmid
(共享内存段标识符)。 -
shmat
挂接(attach): 把这块共享内存段映射到调用进程的 虚拟地址空间,更新页表。返回一个指针,后续直接对这块物理页读写。 -
各个进程的读写操作:......(省略)。
-
shmdt
: 用完后,进程要执行shmdt
脱挂,把这块内存区域从自己的页表取消映射。 -
shmctl
: 用IPC_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
。常用的判断写法:
cppif (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;
}