目录
[System V IPC 整体框架](#System V IPC 整体框架)
[System V 共享内存的定义](#System V 共享内存的定义)
[System V 共享内存的特点](#System V 共享内存的特点)
[System V 共享内存原理](#System V 共享内存原理)
[System V 共享内存函数](#System V 共享内存函数)
[size 参数的隐藏规则](#size 参数的隐藏规则)
[ftok 的意义](#ftok 的意义)
[System V 共享内存的命令行操作](#System V 共享内存的命令行操作)
[System V 共享内存的内核数据结构](#System V 共享内存的内核数据结构)
[基于System V 共享内存的代码示例](#基于System V 共享内存的代码示例)
System V IPC 整体框架
System V 是一种进程间通信的标准,诞生于早期的 UNIX System V 系统,它为不同进程提供了一套统一的通信机制,主要包含三类核心组件:
System V 共享内存 System V 消息队列 System V 信号量
这三类机制常被结合使用,构成了一套完整的基于System V 标准进程间通信方案。
System V 共享内存的定义
System V 共享内存:允许多个进程可以将同一块物理内存映射到自己的虚拟地址空间,进程之间可以通过访问自己的虚拟地址,直接读写共享的物理内存,从而实现高效的进程间通信。
System V 共享内存的特点
System V 共享内存的生命周期随内核,不随进程,需要手动删除。
优点:System V 共享内存是System V IPC 中效率最高的通信方式。
原因:进程无需通过内核中转,直接通过自身的虚拟地址读写共享内存,避免了用户态与内核态之间的数据拷贝。
例如:对于管道,进程要实现进程间通信,需要先将用户态数据拷贝到内核态缓冲区,再把内核态缓冲区的数据,读取到用户态,多次数据拷贝与系统调用会带来较大开销,降低通信效率。
缺点:System V 共享内存不提供读写保护或同步通知机制。(解决方案:共享内存必须配合信号量、互斥锁等同步机制使用,才能保证多进程访问时的数据一致性)
造成的影响:缺乏保护机制会直接导致数据一致性问题。
例如:进程 A 正在向共享内存写入数据时,进程 B 无法感知当前共享内存的读写状态。若此时进程 B 也执行写入操作,就可能覆盖进程 A 正在写入的数据;若进程 B 执行读取操作,则可能读到进程 A 未写完的、不完整的数据,最终导致数据不一致。
System V 共享内存原理
分配共享内存之前

分配共享内存之后

System V 共享内存函数
shmget

参数
key:共享内存的系统级标示符
size:共享内存的大小
shmflg:常见的两种用法
IPC_CREAT | IPC_EXCL | mode:mode 八进制数字,通常为0666,代表共享内存被创建出来的权限。若共享内存不存在,则创建共享内存并返回;若共享内存已经存在,出错返回。
0:若共享内存不存在,则出错返回;若共享内存已经存在,获取共享内存,并返回,获取的权限为共享内存创建的权限。
注意:IPC_EXCL 单独使用没有任何意义。
返回值
成功:返回一个非负整数,代表共享内存的用户级标示符
失败:返回 -1
注意:key值是内核层面使用的,shmid是用户层面使用的。
size 参数的隐藏规则
共享内存的实际分配大小,会被内核向上对齐到通常以4KB为单位的大小。例如你申请4097字节,内核会分配4096 * 2 个字节,虽然OS给你多分配了4095字节,但是你只能使用这4097字节,不能越界访问。
当shmflg 包含 IPC_CREAT 时,size 才会生效;如果共享内存已经存在,size 必须小于等于已有共享内存的大小,但此时 size 的值并不代表获取共享内存的进程能访问的共享内存的大小,即使设置为 0,依旧可以使用整个共享内存,如果size大于共享内存的大小,则出错返回。
ftok

ftok 接受 pathname 和 proj_id 两个参数,内部通过某种算法运算,专门生成一个唯一的key,让不同进程能找到同一个共享内存。
参数
pathname:一个真实存在的普通文件路径,不推荐使用目录路径
proj_id:一个非 0 的数字,只用低8位(1 ~ 255)
返回值:
成功:返回key值
失败:返回-1
ftok 的意义
如果两个进程想用同一个共享内存,它们必须约定用同一个key。ftok 就是让不同进程能稳定生成相同且唯一 key 的标准方法。
shmat

功能:将共享内存映射到进程的虚拟地址空间的共享区
参数
shmid:共享内存的标示符
shmaddr:指定映射到进程的虚拟地址空间的共享区起始地址
NULL :表示让内核自动选择一个合适的虚拟地址。 ---- 推荐、新手用
某个具体地址:强制把共享内存映射到指定的虚拟地址 ---- 不推荐、高手才用
shmflg:shmaddr 为 NULL 时,shmflg的作用:控制本次映射的读写权限。
0:表示共享内存创建的权限。---- 常用
SHM_RDONLY:只读权限。 ---- 不常用
返回值:
成功:返回共享内存的起始虚拟地址。
失败:返回(void*) -1
shmdt

功能:将共享内存与当前进程脱离
参数
shmaddr:共享内存的起始虚拟地址,由shmat函数得到
返回值:
成功:返回0
失败:返回-1
注意:将共享内存与当前进程脱离不等于删除共享内存
shmctl

功能:控制共享内存,查看、修改、删除内核的共享内存。
参数
shmid:共享内存的标示符
cmd:将要采取的动作
buf:一个指针,指向保存着共享内存的内核数据结构
cmd:
-
IPC_STAT:第三个参数需要传递一个struct shmid_ds 类型的地址,将指定共享内存的内核数据结构拷贝到传递的参数中,获取共享内存的信息。
-
IPC_RMID:第三个参数无效,通常设为 NULL,删除共享内存。
-
IPC_SET:在进程有足够权限的前提下,将buf中的数据拷贝到指定共享内存的内核数据结构中,修改共享内存的信息。
共享内存删除规则:调用 IPC_RMID 并不会立刻删除,只有当使用该共享内存的所有进程都断开映射后,才真正删除。
注意:调用删除共享内存的进程不会阻塞,而是正常运行结束,因为共享内存为内核资源,操作系统会对该共享内存打上标签,只要该共享内存被打上标签,那么其他进程调用shmat对该共享内存进行映射,会直接失败返回。所以直到该共享内存的计数器为0时,OS自动释放共享资源。
返回值:
成功:返回0
失败:返回-1
System V 共享内存的命令行操作
ipcs -m ---- 查看共享内存
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
ipcrm -m shmid ---- 删除指定共享内存
System V 共享内存的内核数据结构
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
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 */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
cpp
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
基于System V 共享内存的代码示例
comm.hpp ---- 封装实现了共享内存的创建,建立映射,取消映射,删除。
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <cstdio>
#define PATHNAME "./comm.hpp"
#define PROJ_ID 0
#define SIZE 4096
#define CREATER "creater"
#define USER "user"
#define ERR_EXIT(ERROR_MSG) \
do \
{ \
perror(ERROR_MSG); \
exit(EXIT_FAILURE); \
} while (0)
class Shm
{
private:
void CreatOrGet()
{
_k = ftok(PATHNAME, PROJ_ID);
if (_k < 0)
{
ERR_EXIT("ftok error");
}
printf("ftok sucess,key:0x%x\n", _k);
umask(0);
if (_name == CREATER)
_shmid = shmget(_k, _size, IPC_CREAT | IPC_EXCL | 0666);
else if (_name == USER)
_shmid = shmget(_k, _size, 0);
if (_shmid < 0)
{
ERR_EXIT("shmget error");
}
printf("shmget sucess,shmid:%d\n", _shmid);
}
void Attach()
{
_start_address = shmat(_shmid, NULL, 0);
if (_start_address == (void *)-1)
{
ERR_EXIT("shmat error");
}
printf("shmat sucess, Virtul Address:%p\n", _start_address);
}
void Detach()
{
int n = shmdt(_start_address);
if (n < 0)
{
ERR_EXIT("shmdt error");
}
printf("shmdt sucess\n");
}
void Destory()
{
Detach();
if (_name == CREATER)
{
int n = shmctl(_shmid, IPC_RMID, NULL);
if (n < 0)
{
ERR_EXIT("delete error");
}
printf("delete sucess\n");
}
}
public:
Shm(std::string &name)
: _size(SIZE), _name(name), _start_address(nullptr)
{
CreatOrGet();
Attach();
}
int Shmid()
{
return _shmid;
}
int Key()
{
return _k;
}
void *VirAddress()
{
return _start_address;
}
size_t Size()
{
return _size;
}
~Shm()
{
Destory();
}
private:
int _shmid;
key_t _k;
size_t _size;
void *_start_address;
std::string _name;
};
进程可以通过下列方式进行访问共享内存 ---- 访问共享内存的方式与访问堆上申请的空间一摸一样,这里不再做演示,因为共享内存没有进行同步与互斥,没有保护共享内存,会带来并发问题,也就是上述共享内存特点中的缺点。
cpp
#include "comm.hpp"
int main()
{
std::string name = "creater";
Shm shm(name);
int* p = (int*)shm.VirAddress();
// 进行读写数据
return 0;
}