前言:在现代操作系统中,每个进程都拥有独立的地址空间,进程间通信(IPC)使不同进程能够交换数据、协调工作和共享资源。
什么是进程间通信,为何要又进程间通信?
进程的设计初衷是具有独立性,一个进程崩溃不会影响其他进程,并能精确控制每个进程的资源使用,所以不同进程是相互隔离的
而隔离会带来进程无法直接访问彼此的内存、无法直接调用其他进程的函数等情况。
所以我们需要一种安全、可控的通信机制来解决上述产生的情况,这种通信机制就是进程间通信。
进程间通信的机制有多种,本篇只对system V共享内存做介绍。
一、system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,绕过了内核数据拷贝,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
它允许不相关的进程直接访问同一块物理内存,实现近乎内存访问速度的通信。
1.1 共享内存示意图

如上图所示,由用户通过系统调用让OS去创建或获取共享内存,映射到自己的地址空间中,于是进程A、进程B就能通过该内存实现通信了。
共享内存通信的特点
- 生命周期随内核
- 共享内存是IPC中最快的(利用指针访问内存,减少了xi't)
- 共享内存没有同步、互斥机制,来对多个进程的访问进行协同
1.2 基本操作
shmget函数
功能:用来创建共享内存
原型如下
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字,可由ftok()生成
size:共享内存⼤⼩,建议是4096字节的整数倍
shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,
出错返回。
返回值:成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1
ftok函数
key_t ftok(const char *pathname, int proj_id);
参数说明
pathname 必须是已存在的文件/目录路径,进程需对该文件有访问权限(至少可执行/读权限)
proj_id 非0的8位整数(通常取 1-255,因为仅低 8 位有效),用于区分同一文件生成的不同IPC键值
返回值
成功:返回一个非负的 key_t 类型键值
失败:返回 -1,并设置 errno 标识错误原因
shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1
说明:
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
SHMLBA(Shared Memory Low Boundary Address)是System V共享内存机制中的一个对齐常量,定义了共享内存段在进程虚拟地址空间中映射时的最小对齐边界。
shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:⽤于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

删除共享内存段时,当所有进程分离后才真正销毁。
1.3 完整通信流程
使用共享内存进行通信的时候,必须是一个进程去创建,另一个进程去获取。

如上图所示,基本通信步骤如下
步骤 1:创建共享内存段
-
进程1调用
shmget(IPC_CREAT | 0666)系统调用 -
内核检查是否已存在对应 key 的共享内存段
-
若不存在,则创建新段,并返回一个唯一的 shmid(共享内存标识符)
-
若已存在,则返回现有段的 shmid
步骤 2:附加到共享内存
-
进程1调用
shmat(shmid),将共享内存段映射到自己的虚拟地址空间 -
内核分配一段虚拟内存,并建立与物理共享内存的映射关系
-
返回一个指向共享内存的指针 ptr1,进程1可通过 ptr1 直接读写数据
步骤 3:写入数据
-
进程1通过指针 ptr1 向共享内存中写入数据
-
数据直接写入物理内存页,无需经过内核缓冲区
-
此时共享内存中的数据对其他进程可见(但需先附加)
步骤 4:使用者获取共享内存
-
进程2调用
shmget(key),使用相同的 key 获取共享内存段 -
内核返回与进程1相同的 shmid(因为是同一个共享内存段)
-
进程2获得访问权限,准备附加
步骤 5:使用者附加到共享内存
-
进程2调用
shmat(shmid),将共享内存段映射到自己的虚拟地址空间 -
内核建立映射关系,返回指针 ptr2
-
此时 ptr1 和 ptr2 指向同一块物理内存
步骤 6:读取数据
-
进程2通过指针 ptr2 从共享内存中读取数据
-
由于共享内存是物理内存,读取操作直接访问数据,无拷贝开销
-
数据与进程1写入的内容完全一致
步骤 7:分离共享内存
-
进程1调用
shmdt(ptr1),解除与共享内存的映射 -
进程2调用
shmdt(ptr2),解除映射 -
分离后,进程无法再访问共享内存,但内存段仍存在(除非被删除)
步骤 8:销毁共享内存
-
进程1调用
shmctl(shmid, IPC_RMID, NULL),标记共享内存段为“待删除” -
当所有进程都分离后,内核真正释放物理内存
-
共享内存段从系统中移除
1.3 共享内存数据结构
struct shmid_ds {
struct kern_ipc_perm shm_perm; // IPC权限
struct file *shm_file; // 关联的tmpfs文件
size_t shm_nattch; // 当前附加进程数
size_t shm_segsz; // 段大小
time64_t shm_atim; // 最后附加时间
time64_t shm_dtim; // 最后分离时间
time64_t shm_ctim; // 最后修改时间
pid_t shm_cprid; // 创建者PID
pid_t shm_lprid; // 最后操作者PID
...
};
在内核中,OS要对共享内存内存进行管理,初始映射时不复制物理页,首次写入时才分配,也就是写时拷贝,系统内存压力大时,可交换到swap(若标记为可交换)。
1.4 使用用例
shm.hpp
#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>
#include <string>
const std::string g_pathname = ".";
const int g_proj_id = 0x66;
const int g_default_size = 4096; // 共享内存的大小必须是4kb的整数倍 4kb对齐(内核层面)
class SharedMemory
{
public:
SharedMemory(int size = g_default_size) : _size(size), _startptr(nullptr), _windex(0), _rindex(0), _num(nullptr), _data_start(nullptr)
{
}
~SharedMemory()
{
}
// key vs shmid
// 1.key: 只有内核使用,用来标识shm的唯一性
// 2.shmid: 给用户使用,用来进行访问shm
bool Create()
{
return CreateHelper(IPC_CREAT | IPC_EXCL | 0666);
}
bool Get()
{
return CreateHelper(IPC_CREAT);
}
bool Attach()
{
_startptr = shmat(_shmid, nullptr, 0);
if ((long long)_startptr == -1)
{
perror("shmat error");
return false;
}
_num = (int *)_startptr;
_data_start = _startptr + sizeof(int);
Setzero();
return true;
}
bool Detach()
{
int n = shmdt(_startptr);
if (n < 0)
{
perror("shmdt error");
return false;
}
return true;
}
bool RemoveShm()
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0)
{
perror("shmctl error");
return false;
}
std::cout << "删除共享内存成功" << std::endl;
return true;
}
void AddChar(char ch)
{
if (*_num == _size)
return;
((char *)_data_start)[_windex++] = ch;
_windex %= _size;
(*_num)++;
}
void Setzero()
{
*_num = 0;
}
void PopAddr(char* ch)
{
if (*_num == 0)
return;
*ch = ((char *)_data_start)[_rindex++];
_rindex %= _size;
(*_num)--;
}
void Know()
{
struct shmid_ds ds;
shmctl(_shmid, IPC_STAT, &ds);
ds.shm_atime;
ds.shm_nattch;
ds.shm_perm.__key;
ds.shm_cpid;
}
// void AddInt(){}
// void AddString(){}
private:
key_t _key;
int _size;
int _shmid;
void *_startptr;
int _windex;
int _rindex;
int *_num;
void *_data_start;
bool CreateHelper(int flags)
{
_key = ftok(g_pathname.c_str(), g_proj_id);
if (_key < 0)
{
perror("ftok error");
exit(1);
}
printf("创建键值成功 key = %#x\n", _key);
_shmid = shmget(_key, _size, flags);
if (_shmid < 0)
{
perror("shmget error");
exit(2);
}
printf("创建共享内存成功 shmid = %d\n", _shmid);
return true;
}
};
server.cpp
#include "Shm.hpp"
#include <unistd.h>
int main()
{
SharedMemory shm;
shm.Create();
// 挂载
shm.Attach();
sleep(2);
// 使用
// while(true)
// {
// }
// 去关联,移除
shm.Detach();
shm.RemoveShm();
return 0;
}
client.cpp
#include "Shm.hpp"
int main()
{
SharedMemory shm;
shm.Get();
shm.Attach();
shm.Know();
shm.Detach();
return 0;
}
该示例展示了如何利用共享内存实现服务端和客户端的通信,需要注意的是,我们把共享内存前4个字节定义为一个计数器,用于标记共享内存中有多少数据,其他就只是对共享内存基本接口的封装了。
2216

被折叠的 条评论
为什么被折叠?



