进程间通信————system V 共享内存
1.进程通信的本质
使不同进程能够访问同一份共享资源,通过对这份资源的读写操作来实现数据交换或同步协作。
2.共享内存原理
共享内存的原理与动态库相似,其过程是操作系统先在物理内存中创建一份空间,再通过页表将该物理空间映射到进程地址空间的共享区,最后向应用层返回该映射的起始地址,用户便可直接访问。其他进程也能通过同样的方式,将这份内存通过页表映射到自己进程地址空间的共享区,进而获得起始地址并返回给应用层。从此往后两个进程就可以通过各自的列表访问同一块物理内存。
注意:以上的操作都不是进程直接做的,是由操作系统直接操作的。
进程间通信的底层实现依赖操作系统的核心管理能力,尤其是共享内存这类直接操作物理内存的机制,完全是由操作系统主导的。
因为后续可能有其他进程也想进行共享内存进行通信,所以操作系统还需要管理物理内存,而这一管理逻辑始终遵循 “先描述、再组织” 的方式:通过内核结构体(如专门定义的共享内存描述符)对每块共享内存的关键属性(如物理地址、大小、权限、引用关系等)进行精确刻画,以此建立对资源的 “认知”;进而,借助链表、数组或哈希表等数据结构将这些描述结构体有序组织,形成可高效检索、遍历与维护的管理体系。
3.总结步骤
3.1 创建与访问步骤
- 操作系统先在物理内存中开辟一块专用空间,作为进程间共享的基础资源;
- 借助页表机制,将这块物理空间映射到参与通信的进程各自地址空间的共享区域,建立虚拟地址与物理地址的关联;
- 向应用层返回映射后的虚拟地址起始位置,进程即可通过该地址直接读写共享内存,实现数据交互。
3.2 内存释放逻辑(去关联过程)
- 当进程不再使用时,先解除自身地址空间与共享内存的映射关系(断开页表关联);
- 待所有关联进程均完成解除映射后,操作系统最终释放对应的物理内存资源。
4.相关接口
4.1 ftok 生成共享内存键值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
- pathname:已存在的文件 / 目录路径(进程可访问,用于生成唯一标识)。
- proj_id:8 位非 0 整数(1-255),相同路径 + 不同proj_id生成不同key。
返回值:
- 成功返回key_t键值。
- 失败返回-1(设errno)。
4.2 shmget 创建 / 获取共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
- key:ftok生成的键值,标识共享内存。
- size:共享内存大小(创建时需指定,获取时设为 0)。
- shmflg:标志位(组合使用):
- IPC_CREAT:不存在则创建,存在则获取。
- IPC_CREAT | IPC_EXCL:确保创建新段(已存在则报错)。
- 权限标志(如0666):指定读写权限。
返回值:
- 成功返回shmid(共享内存标识符)。
- 失败返回-1(设errno)。
4.3 shmat 映射共享内存到进程
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:shmget返回的共享内存标识符。
- shmaddr:映射的起始地址(NULL表示由系统自动分配,推荐)。
- shmflg:连接方式(0为默认读写;SHM_RDONLY为只读)。
返回值:
- 成功返回进程内的共享内存起始地址。
- 失败返回(void *)-1。
4.4 shmdt 解除映射
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:shmaddr为shmat返回的共享内存起始地址。
返回值:
- 成功返回0。
- 失败返回-1(设errno)。
4.5 shmctl 控制 / 释放共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:共享内存标识符。
- cmd:控制命令:
- IPC_RMID:删除共享内存(仅所有者或 root 可执行),需所有进程解除映射后才释放物理内存。
- IPC_STAT:获取共享内存状态(存到buf中)。
- buf:状态结构体指针(IPC_RMID时设为NULL)。
返回值:
- 成功返回0。
- 失败返回-1(设errno)。
5.代码示例
5.1 创建一个共享内存支持两个进程进行通信。进程A 向共享内存当中写 “I am process A”,进程B 从共享内存当中读出内容并打印到标准输出。
正确分工:
- 服务端(接收端)进程负责创建和销毁共享内存
- 客户端(发送端)进程只需连接到存在的共享内存
com.hpp
// com.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>// 定义共享内存相关常量
#define pathname "/practice/lesson-test/pro_communication/sharedmem"
#define proj_id 0x6666
#define size 4096// 错误码枚举
enum
{FIFO_ERROR = 1,UNLINK_ERROR,OPEN_ERROR,READ_ERROR,FTOK_ERROR,SHMGET_ERROR
};// 获取共享内存键值
key_t getKey()
{int key = ftok(pathname, proj_id);if (key == -1){perror("ftok");exit(FTOK_ERROR);}return key;
}// 辅助函数:获取共享内存标识符
int getshmidHelper(int shmflg)
{key_t key = getKey();int shmid = shmget(key, size, shmflg);if (shmid == -1){perror("shmget");exit(SHMGET_ERROR);}return shmid;
}// 创建共享内存(如果不存在)
int creatshmflg()
{return getshmidHelper(IPC_CREAT | IPC_EXCL | 0666);
}// 获取已存在的共享内存
int getshmflg()
{return getshmidHelper(IPC_CREAT);
}
processA.cpp
// processA.cpp
#include "com.hpp"int main()
{// 获取共享内存标识符int shmid = getshmflg();// 将共享内存映射到当前进程的地址空间char *shmaddr = (char *)shmat(shmid, nullptr, 0);// 循环发送消息while (true){// 从标准输入读取消息并写入共享内存cout << "A sends a message to B:";fgets(shmaddr, size, stdin);}// 解除共享内存映射shmdt(shmaddr);return 0;
}
processB.cpp
// processB.cpp
#include "com.hpp"int main()
{// 创建共享内存(如果不存在)int shmid = creatshmflg();// 将共享内存映射到当前进程的地址空间char *shmaddr = (char *)shmat(shmid, nullptr, 0);// 循环接收消息while (true){// 从共享内存读取消息并输出cout << "B received a message from A:" << shmaddr;sleep(1);}// 解除共享内存映射shmdt(shmaddr);// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);return 0;
}
5.2 获取共享内存的属性
5.2.1 与共享内存相关的结构体
struct shmid_ds 用于存储共享内存段的详细属性信息。
struct ipc_perm 用于描述 IPC 资源(包括共享内存、消息队列、信号量)的权限和所有权。
struct shmid_ds {struct ipc_perm shm_perm; /* 共享内存的所有权和权限信息 */size_t shm_segsz; /* 共享内存段的大小(单位:字节) */time_t shm_atime; /* 最后一次关联的时间 */time_t shm_dtime; /* 最后一次调用 shmdt() 分离共享内存的时间(UNIX 时间戳) */time_t shm_ctime; /* 最后一次修改共享内存属性的时间(如权限变更、大小调整等) */pid_t shm_cpid; /* 创建该共享内存段的进程 PID(调用 shmget 创建的进程) */pid_t shm_lpid; /* 最后一次执行关联或去关联进程 PID(最近操作的进程) */shmatt_t shm_nattch; /* 当前附加到该共享内存段的进程数量(引用计数,0 表示无进程使用) */... /* 系统特定的扩展字段(不同内核版本可能有差异) */
};
struct ipc_perm {key_t __key; /* 创建共享内存时使用的 key 值(由 ftok指定) */uid_t uid; /* 共享内存当前所有者的有效用户 ID(可通过 IPC_SET 修改) */gid_t gid; /* 共享内存当前所有者的有效组 ID(可通过 IPC_SET 修改) */uid_t cuid; /* 共享内存创建者的有效用户 ID(创建后不可修改) */gid_ cgid; /* 共享内存创建者的有效组 ID(创建后不可修改) */unsigned short mode; /* 权限位 */unsigned short __seq; /* 系统内部序列号(用于防止 IPC 资源重复,内核自动维护) */
};
5.2.2 代码实现
结合5.1的代码将processB.cc代码修改即可
#include "com.hpp" int main()
{int shmid = creatshmflg();char *shmaddr = (char *)shmat(shmid, nullptr, 0);// 定义struct shmid_ds结构体变量,用于存储共享内存的状态信息// 该结构体由内核维护,包含共享内存的大小、连接数、权限等属性struct shmid_ds shmds;// 循环读取并打印共享内存中的数据,同时获取共享内存状态信息while (true){// 从共享内存中读取进程A发送的消息并打印cout << "B received a message from A:" << shmaddr;// 休眠1秒,避免频繁打印sleep(1);// 通过shmctl系统调用获取共享内存的状态信息,存储到shmds中// IPC_STAT命令表示:获取共享内存的属性,并存入第三个参数指向的结构体shmctl(shmid, IPC_STAT, &shmds);// 打印共享内存的大小(字节数),shm_segsz是结构体中记录共享内存大小的成员cout << "shm size:" << shmds.shm_segsz << endl;// 打印当前连接到该共享内存的进程数(nattch = number of attach)cout << "shm nattch:" << shmds.shm_nattch << endl;// 以十六进制形式打印共享内存的大小(与上面的十进制对应,方便对比)printf("0x%x\n", shmds.shm_segsz);// 打印共享内存的权限模式(类似文件权限,如0666)cout << "shm mode:" << shmds.shm_perm.mode << endl;}shmdt(shmaddr);shmctl(shmid, IPC_RMID, nullptr);return 0;
}
6.相关命令
6.1 查看系统中共享内存信息
ipcs -m
输出信息说明:
key
:共享内存的键值,用于唯一标识共享内存段shmid
:共享内存的唯一 ID,操作共享内存时需使用该 IDowner
:共享内存的所有者(用户)perms
:共享内存的访问权限(类似文件权限,如 644、755 等)bytes
:共享内存段的大小(单位:字节)nattch
:当前连接到该共享内存段的进程数status
:共享内存的状态(如是否被标记为删除)
6.2 删除共享内存标识
ipcrm -m [shmid] # [shmid] 替换为实际的共享内存ID
具体来说,它会标记对应的共享内存段为待删除状态,当所有关联到该共享内存段的进程都通过 shmdt 系统调用完成拆离后,系统会真正释放该共享内存段所占用的资源(包括内存空间和相关内核数据结构)。
7.共享内存的特性
- 生命周期:共享内存的生命周期是随内核的,用户主动关闭或者内核重启,共享内存才会释放。
- 同步与互斥:无内置保护机制,需用户自行实现同步(如结合信号量、管道等),否则可能出现数据不一致问题。
- 通信效率:是所有进程间通信方式中速度最快的,因为数据直接在共享内存中读写,无需内核中转;仅需一次映射(shmat),之后操作无系统调用开销(拷贝次数极少)。
- 数据管理:共享内存中的数据完全由用户进程维护,操作系统不参与数据的读写或格式处理。
8.扩展:解决共享内存没有同步互斥的保护机制的问题
在共享内存的通信中,由于其本身没有同步互斥机制,可能出现 “读方没准备好就读” 或 “写方没写完就被读” 的问题。而命名管道(FIFO)可以通过 “信号传递” 的方式,为共享内存提供简单的同步机制,原理如下:
命名管道的特性:命名管道是一种半双工的通信方式,支持进程间的阻塞读写 —— 当管道中没有数据时,读操作会阻塞;当管道满时,写操作会阻塞。
com.hpp
// com.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h> // 共享内存相关配置
#define pathname "/practice/lesson-test/pro_communication/sharedmem" // ftok生成key的路径
#define proj_id 0x6666 // ftok的项目ID
#define size 4096 // 共享内存大小(4KB,页大小整数倍)
// 命名管道相关配置
#define FIFO_FILE "./myfifo" // 命名管道文件路径
#define MODE 0666 // 权限(所有者、组、其他用户均有读写权限)
using namespace std;// 错误码枚举(便于定位错误类型)
enum
{FIFO_ERROR = 1, // 创建管道失败UNLINK_ERROR, // 删除管道失败OPEN_ERROR, // 打开文件失败READ_ERROR, // 读操作失败FTOK_ERROR, // 生成key失败SHMGET_ERROR // 获取共享内存失败
};// 初始化类:负责命名管道的创建与销毁
class Init
{
public:// 构造函数:程序启动时创建命名管道Init(){int ret = mkfifo(FIFO_FILE, MODE);if (ret == -1){perror("mkfifo"); exit(FIFO_ERROR); }}// 析构函数:程序结束时删除命名管道~Init(){int m = unlink(FIFO_FILE);if (m == -1){perror("unlink");exit(UNLINK_ERROR);}}
};// 生成共享内存的唯一标识key
key_t getKey()
{int key = ftok(pathname, proj_id);if (key == -1){perror("ftok");exit(FTOK_ERROR);}return key;
}// 辅助函数:通过shmget获取共享内存ID(根据传入的标志位)
int getshmidHelper(int shmflg)
{key_t key = getKey(); int shmid = shmget(key, size, shmflg);if (shmid == -1){perror("shmget");exit(SHMGET_ERROR);}return shmid;
}// 创建新的共享内存(若已存在则报错)
int creatshmflg()
{return getshmidHelper(IPC_CREAT | IPC_EXCL | 0666);
}// 获取已存在的共享内存(若不存在则创建)
int getshmflg()
{return getshmidHelper(IPC_CREAT);
}
processA.cpp
// processA.cpp
#include "com.hpp"int main()
{// 获取共享内存ID(若不存在则创建)int shmid = getshmflg();// 将共享内存映射到当前进程地址空间(系统自动分配地址,读写权限)char *shmaddr = (char *)shmat(shmid, nullptr, 0);// 打开命名管道(只写模式),用于通知进程B有新消息int fd = open(FIFO_FILE, O_WRONLY);if (fd == -1){perror("open");exit(OPEN_ERROR);}// 向共享内存写入消息while (true){cout << "A sends a message to B:"; // 提示输入// 从标准输入读取字符串,写入共享内存fgets(shmaddr, size, stdin);// 向管道写入一个字符(如'c'),通知B已写入新消息(同步机制)write(fd, "c", 1);}// 解除共享内存映射shmdt(shmaddr);// 关闭管道文件描述符close(fd);return 0;
}
processB.cpp
// processB.cpp
#include "com.hpp"int main()
{Init init; // 创建Init对象,构造函数中创建命名管道,析构时删除// 创建共享内存(若已存在则报错,确保是新创建的共享内存)int shmid = creatshmflg();// 将共享内存映射到当前进程地址空间(系统自动分配地址,读写权限)char *shmaddr = (char *)shmat(shmid, nullptr, 0);// 打开命名管道(只读模式),用于接收进程A的消息通知int fd = open(FIFO_FILE, O_RDONLY);if (fd == -1){perror("open");exit(OPEN_ERROR);}// 循环接收并打印消息while (true){char c; // 用于接收管道中的通知字符// 从管道读取通知(阻塞等待,直到A写入数据)int n = read(fd, &c, sizeof(c));if (n == 0) // 管道另一端关闭(A退出){break;}else if (n < 0) // 读操作出错{break;}// 从共享内存读取A发送的消息并打印cout << "B received a message from A:" << shmaddr;sleep(1); // 休眠1秒,避免频繁打印}// 解除共享内存映射shmdt(shmaddr);// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);// 关闭管道文件描述符close(fd);return 0;
}