System V 共享内存(System V Shared Memory, SHM)是一种进程间通信(IPC)机制,允许多个进程共享一块内存区域,从而实现高效的数据交换。共享内存是最快的 IPC 方式之一,因为数据直接在内存中读写,而无需经过内核进行复制。
1. system V共享内存
1.1 共享内存的基本概念
System V 共享内存基于键(key)标识,并使用 shmget 创建或获取共享内存段。创建的共享内存可以被多个进程映射到自己的地址空间,实现数据共享。
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说就是进程不再通过执行 进入内核的系统调用 来传递彼此的数据。
1.2 共享内存函数
在 System V 共享内存(SHM)中,主要涉及以下几个系统调用:
1.2.1 shmget
- 获取共享内存标识符
int shmget(key_t key, size_t size, int shmflg);
参数:
key:共享内存的键值,可使用 ftok 生成,也可直接指定一个整数。
size:共享内存的大小(单位:字节)。如果 shmget 需要创建新的共享内存区域,则必须指定 size,否则可以为 0。
shmflg:权限标志,通常设置为 0666 | IPC_CREAT,表示创建一个可读写的共享内存:
0666:文件权限,表示所有用户可读写。
IPC_CREAT:如果共享内存不存在,则创建新共享内存。
IPC_EXCL:与 IPC_CREAT 组合使用,确保共享内存不存在,否则 shmget 失败并返回 -1。
返回值:
成功:返回共享内存 ID(shmid)。
失败:返回 -1
,可使用 perror("shmget")
查看错误原因。
示例:
key_t key = ftok("shmfile", 65); // 生成 key
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
1.2.2 shmat
- 将共享内存映射到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存 ID,由 shmget 返回。
shmaddr:建议映射地址,一般设为 NULL 让系统自动分配合适的地址。
shmflg:访问模式:
0:可读可写。
SHM_RDONLY:只读模式。
返回值:
成功:返回共享内存地址(指向 void*
)。
失败:返回 (void*) -1
,可用 perror("shmat")
检查错误原因。
示例:
char *data = (char *) shmat(shmid, NULL, 0);
if (data == (char *) -1) {
perror("shmat");
exit(EXIT_FAILURE);
}
1.2.3 shmdt
- 解除共享内存映射
int shmdt(const void *shmaddr);
参数:
shmaddr:共享内存的起始地址,即 shmat 返回的指针。
返回值:
成功:返回 0
。
失败:返回 -1
,可用 perror("shmdt")
查看错误原因。
示例:
if (shmdt(data) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除。
共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统
1.2.4 shmctl
- 控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存 ID,由 shmget 返回。
cmd:控制命令
buf:shmid_ds 结构体指针,用于存储共享内存信息或修改参数。
cmd控制命令:
返回值:
成功:返回 0
。
失败:返回 -1
,可用 perror("shmctl")
查看错误原因。
示例(删除共享内存):
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(EXIT_FAILURE);
}
1.2.5 ftok
- 生成唯一键值(可选)
key_t ftok(const char *pathname, int proj_id);
参数:
pathname:一个有效路径(如 "/tmp/shmfile"),该文件必须存在,并且pathname必须是时C风格的字符串。
proj_id:项目 ID,范围 0~255,通常设定为固定值。
返回值:
成功:返回 key_t
类型的键值。
失败:返回 -1
,可用 perror("ftok")
查看错误原因。
示例:
key_t key = ftok("/tmp/shmfile", 65);
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
共享内存的使用步骤
- 创建或获取共享内存:使用
shmget()
创建或获取共享内存 ID。 - 映射到进程地址空间:使用
shmat()
获取共享内存指针。 - 进程间通信:使用共享内存进行数据读写。
- 解除映射:使用
shmdt()
解除共享内存映射。 - 删除共享内存(可选):使用
shmctl()
删除共享内存,防止残留。
共享内存是将同一块物理内存映射到各个进程虚拟地址空间,可以直接通过虚拟地址访问,相较于其它方式少了两步内核态与用户态之间的数据拷贝因此速度最快
2. IPC命令
2.1 ipcs
- 显示 IPC 资源信息
ipcs
命令用于查看当前系统中的 IPC 资源,包括共享内存、消息队列和信号量。
基本语法
ipcs [选项]
常用选项
-m:显示共享内存(shm)。
-q:显示消息队列(msg)。
-s:显示信号量(sem)。
-a:显示所有 IPC 资源(默认)。
-c:显示创建者信息。
-p:显示进程信息(如创建者和最后访问进程)。
-t:显示时间信息(创建、上次连接、最近访问)。
显示共享内存信息
显示消息队列信息
2.2 ipcrm
- 删除 IPC 资源
ipcrm
命令用于删除共享内存、消息队列和信号量。
基本语法
ipcrm [资源类型] [ID]
常用选项
//必须明确指定要删除的资源类型
-m shmid:删除共享内存(shmid 为 ipcs -m 查询到的 ID)。
-q msqid:删除消息队列。
-s semid:删除信号量。
--all=shm:删除所有共享内存。
--all=msg:删除所有消息队列。
--all=sem:删除所有信号量。
共享内存的生命周期遵循引用计数规则,ipcrm -m ID
只是标记删除(类似shmctl(IPC_RMID)
)
共享内存只有在当前映射连接数为0时才会被删除释放
3. system V消息队列
System V 消息队列(System V Message Queue)也是一种进程间通信(IPC)机制,允许不同进程通过消息传递的方式进行数据交换。它与管道(Pipe)相比,具有更灵活的特性,例如消息可以有类型,消息队列不会因进程终止而消失,适用于复杂的进程通信需求。
3.1 基本概念
消息队列是内核提供的一种先进先出的消息存储结构,不同进程可以向队列中写入消息或从队列中读取消息。每条消息都有一个类型标识,进程可以按顺序读取消息,也可以根据类型选择性读取。
3.2 相关系统调用
System V 消息队列主要通过 msgget
、msgsnd
、msgrcv
和 msgctl
进行操作。
四个函数都包含以下头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
(1)创建或获取消息队列 - msgget
int msgget(key_t key, int msgflg);
key_t key:消息队列的唯一标识,进程可以通过相同的 key 访问同一个消息队列。
int msgflg:消息队列的标志位,控制创建和权限设置。
IPC_CREAT:如果消息队列不存在,则创建它;如果已存在,则直接返回消息队列 ID。
IPC_EXCL(与 IPC_CREAT 组合使用):如果队列已存在,则返回 -1 并设置 errno 为 EEXIST,防止已有队列被重复打开。
权限位(0666、0600 等):类似文件权限,控制谁可以访问消息队列。
成功:返回消息队列的 ID(非负整数)。
失败:返回 -1,并设置 errno 说明错误原因。
(2)发送消息 - msgsnd
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
int msqid:消息队列的 ID,由 msgget 获取。
const void *msgp:指向要发送的消息结构体的指针。
size_t msgsz:消息正文(不包括消息类型)的大小,单位是字节。
int msgflg:消息发送选项:
IPC_NOWAIT:如果队列已满,则立即返回 -1,不阻塞进程。
0(默认):如果队列已满,则阻塞进程,直到有空间可用。
消息结构通常定义如下:
struct msgbuf {
long mtype; // 消息类型,必须大于 0
char mtext[1]; // 消息正文
};
(3)接收消息 - msgrcv
int msgrcv(int msqid, void *msgp, size_t msgsz, long mtype, int msgflg);
msgp:指向存放接收消息的缓冲区的指针,通常是一个自定义结构体,结构体的前部分必须包含一个 long 类型的 mtype 成员。
msgsz:要接收的消息正文部分的大小(不包括 mtype)。
mtype:
大于 0:接收队列中消息类型等于 mtype 的第一条消息。
等于 0:接收队列中的第一条消息(无论其类型为何)。
小于 0:接收队列中消息类型值最小且不超过 |mtype| 的消息。
msgflg:
IPC_NOWAIT:如果队列为空,立即返回 -1,并设置 errno 为 ENOMSG。
MSG_NOERROR:如果消息正文长度大于 msgsz,则截断多余部分(否则会出错)。
返回值:
成功时,返回接收到的消息正文长度(不含 mtype
)。
失败时,返回 -1
,并设置 errno
以指示错误。
(4)控制消息队列 - msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd:要执行的操作,常见的有:
IPC_RMID:删除消息队列。
IPC_STAT:获取消息队列的状态信息。
IPC_SET:修改消息队列的状态信息。
struct msqid_ds *buf
:用于存储或修改消息队列的属性。
struct msqid_ds {
struct ipc_perm msg_perm; // 访问权限
time_t msg_stime; // 最后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后修改时间
unsigned long msg_cbytes; // 当前消息队列字节数
msgqnum_t msg_qnum; // 当前消息数量
msglen_t msg_qbytes; // 队列的最大字节数
pid_t msg_lspid; // 最后发送消息的进程 ID
pid_t msg_lrpid; // 最后接收消息的进程 ID
};
4. system V信号量
4.1 并发编程概念铺垫
多个执行流(进程)能看到的同一份公共资源称为共享资源。
被保护起来的资源叫做临界资源。
保护的方式常见的有互斥与同步。任何时刻,只允许一个执行流访问资源,称为互斥。多个执行流访问临界资源时,具有一定的顺序性,称为同步。
系统中某些资源一次只允许一个进程使用,这样的资源称为临界资源或互斥资源。
在进程中涉及到互斥资源的程序段叫做临界区。代码可以分为访问临界资源的代码(临界区)和不访问临界资源的代码(非临界区)。
所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护。
锁本身也是共享的,因此必须保证锁的安全性。申请锁时必须是原子的,即要么成功申请,要么完全失败,不会出现中间状态。原子性的特点是要么做,要么不做。
在 IPC 资源管理方面,System V IPC 资源不会自动清除,必须手动删除,否则会一直存在,除非系统重启,因为 System V IPC 资源的生命周期随内核。
从理解角度来看,信号量是一个计数器,用来表明临界资源中,资源的多少。
从作用方面来看,信号量用于保护临界区,确保多个进程或线程访问共享资源时不发生冲突。
从本质方面来看,申请信号量的本质是对资源的预定机制。
在操作方面,申请资源时,计数器减一(P 操作),释放资源时,计数器加一(V 操作)。
- P 操作(等待):如果信号量值大于 0,则减 1;如果等于 0,则阻塞进程。
- V 操作(释放):将信号量值加 1,并唤醒等待的进程(如果有)。
信号量只有1和0两态的信号量,叫做二元信号量,这也是互斥。有多个信号量时叫多元信号量。
4.2 信号量
System V 信号量是一种进程间通信(IPC)机制,主要用于进程间同步。与互斥锁不同,信号量不仅可以用于互斥访问共享资源,还可以用于控制多个进程对资源的访问数量。
想申请临界资源,先申请信号量,对信号量计数器做--操作,申请成功访问临界资源,否则进程阻塞挂起。
信号量和通信的关系:
1. 先访问信号量P,进程得看到同一个信号量
2. 不是传递数据才叫做进程间通信(IPC),通知、同步、互斥也算
4.3 System V 信号量相关 API
System V 信号量主要通过 semget semctl semop 进行操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
(1)创建或获取信号量集合:semget
int semget(key_t key, int nsems, int semflg);
nsems:信号量集中的信号量个数,仅在创建新信号量时有效:
若 semget 用于创建新信号量集,则 nsems 需要指定信号量的数量。
若 semget 仅用于获取已有信号量集,则 nsems 通常设为 0(忽略该参数)。
semflg:标志位,控制信号量的创建和访问权限:
IPC_CREAT:如果不存在,则创建新的信号量集。
IPC_EXCL:与 IPC_CREAT 组合使用,确保信号量集不存在,否则返回错误。
0666(或其他权限位):指定读写权限,如 0666 表示所有用户可读写。
返回值:
成功:返回信号量集的标识符(非负整数)。
失败:返回 -1
,并设置 errno
指定错误原因。
(2)对信号量进行控制:semctl
int semctl(int semid, int semnum, int cmd, ...);
semnum:指定操作的信号量编号(从 0 开始)。
如果 cmd 作用于整个信号量集(如 IPC_RMID 删除信号量),该值可以忽略。
若对单个信号量执行操作(如 SETVAL 设值),则 semnum 指定目标信号量。
cmd:操作命令,控制 semctl 的行为,常见命令包括:
SETVAL:设置信号量的值(需要提供 union semun)。
GETVAL:获取信号量的值(返回信号量值)。
IPC_RMID:删除信号量集(释放系统资源)。
SETALL:设置整个信号量集的值(需要提供 union semun)。
GETALL:获取整个信号量集的值。
IPC_STAT:获取信号量信息。
IPC_SET:设置信号量权限。
semctl
需要一个可选的第四个参数,通常是 union semun
,其定义如下:
union semun {
int val; // 用于 SETVAL 设置单个信号量值
struct semid_ds *buf; // 用于 IPC_STAT/IPC_SET 获取/设置信号量属性
unsigned short *array; // 用于 GETALL/SETALL 读取/设置整个信号量集
};
返回值:
成功:
SETVAL
、SETALL
、IPC_SET
、IPC_RMID
成功时返回 0
。
GETVAL
、GETALL
、IPC_STAT
成功时返回对应的值或填充 buf
结构体。
失败:返回 -1
,并设置 errno
指定错误原因。
(3)对信号量进行操作:semop
int semop(int semid, struct sembuf *sops, unsigned nsops);
sops:指向 sembuf 结构体数组的指针,每个 sembuf 结构体表示对一个信号量的操作。
nsops:sops 数组中的操作数量,即一次操作多少个信号量。
返回值
成功:返回 0
。
失败:返回 -1
,并设置 errno
,常见错误如下:
struct sembuf {
unsigned short sem_num; // 信号量编号
short sem_op; // 操作值,-1 表示 P 操作,+1 表示 V 操作
short sem_flg; // 操作标志,如 SEM_UNDO
};
sem_op 取值含义:
-1 (P 操作):等待信号量变为正数,然后执行 semval--(即占用资源)。
+1 (V 操作):释放信号量,执行 semval++(即释放资源)。
0:等待 semval == 0,用于同步(常用于进程等待)。
sem_flg 可选标志:
IPC_NOWAIT:如果操作不能立即执行,则立即返回(不会阻塞)。
SEM_UNDO:进程退出时自动撤销对该信号量的操作(避免死锁)。
示例
P操作(获取资源)
void P(int semid) {
struct sembuf sop = {
.sem_num = 0, // 信号量编号
.sem_op = -1, // 操作值(获取资源)
.sem_flg = 0 // 通常为0或IPC_NOWAIT
};
if (semop(semid, &sop, 1) == -1) {
perror("semop P failed");
exit(EXIT_FAILURE);
}
}
V操作(释放资源)
void V(int semid) {
struct sembuf sop = {
.sem_num = 0, // 信号量编号
.sem_op = 1, // 操作值(释放资源)
.sem_flg = 0 // 通常为0
};
if (semop(semid, &sop, 1) == -1) {
perror("semop V failed");
exit(EXIT_FAILURE);
}
}
5. 共享内存实现通信
shm.hpp
#pragma once
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
const std::string pathname = ".";
const int projid = 0x66;
const int gsize = 4096;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0) \
class Shm
{
private:
void CreatHelper(int flg)
{
//打印共享内存键值
printf("key: 0x%x\n", _key);
//获取共享内存标识符并打印
_shmid = shmget(_key, _size, flg);
if(_shmid < 0) ERR_EXIT("shmget");
printf("shmid: %d\n", _shmid);
}
void Create()
{
CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
}
void Open()
{
CreatHelper(IPC_CREAT);
}
void Attr()
{
_start_mem = shmat(_shmid, nullptr, 0);
if((long long)_start_mem < 0) ERR_EXIT("shmat");
printf("attach success\n");
}
void Death()
{
int n = shmdt(_start_mem);
if(n < 0) ERR_EXIT("shmdt");
printf("deattach success");
}
void Destory()
{
Death();
if (_usertype == CREATER)
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n > 0)
printf("shmctl delete shm: %d success\n", _shmid);
else
ERR_EXIT("shmctl");
}
}
public:
Shm(const std::string &pathname, int projid, std::string usertype)
: _pathname(pathname)
, _usertype(usertype)
, _size(gsize)
, _start_mem(nullptr)
{
_key = ftok(pathname.c_str(), projid);
if(_usertype == CREATER)
{
Create();
}else if(_usertype == USER)
{
Open();
}
Attr();
}
int Size()
{
return _size;
}
void *VirtualAddr()
{
printf("VirtualAddr: %p\n", _start_mem);
return _start_mem;
}
void Attr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
printf("segse: %ld\n", ds.shm_segsz);
printf("atime: %ld\n", ds.shm_atime);
}
~Shm()
{
Destory();
}
private:
key_t _key;
int _size;
int _shmid;
std::string _pathname;
std::string _usertype;
void *_start_mem;
};
Makefile
.PHONY:all
all:client server
client:: client.cc
g++ -o $@ $^ -std=c++11
server:: server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm client server
client.cc
#include "shm.hpp"
int main()
{
Shm shm(pathname, projid, USER);
char *mem = (char*)shm.VirtualAddr();
for(char c = 'A'; c <= 'Z'; c++)
{
mem[c-'A'] = c;
sleep(1);
}
return 0;
}
server.cc
#include "shm.hpp"
int main()
{
Shm shm(pathname, projid, CREATER);
char *mem = (char*)shm.VirtualAddr();
while (true)
{
printf("%s\n", mem);
sleep(1);
}
return 0;
}
./server先运行,等到client运行后开始输出ABC...
6. 借助管道实现控制版的共享内存
fifo.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
#define PATH "."
#define FIFONAME "fifo"
class NameFifo
{
public:
NameFifo(const std::string &path, const std::string &name)
: _path(path), _name(name)
{
_fifoname = _path + "/" + _name;
int n = mkfifo(_fifoname.c_str(), 0666);
if (n == -1)
{
std::cerr << "mkfifo error" << std::endl;
}
std::cout << "mkfifo success" << std::endl;
}
~NameFifo()
{
int n = unlink(_fifoname.c_str());
if (n == 0)
{
std::cout << "remove FIFO_FILE success" << std::endl;
}
else
{
std::cerr << "remove FIFO_FILE failed" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _fifoname;
};
class Fileoper
{
public:
Fileoper(const std::string &path, const std::string &name)
: _path(path), _name(name), _fd(-1)
{
_fifoname = _path + "/" + _name;
}
void OpenForRead()
{
// 打开,write方法中没有执行open的时候,就要在open内部进行阻塞
_fd = open(_fifoname.c_str(), O_RDONLY);
if (_fd < 0)
{
std::cerr << "open error" << std::endl;
return;
}
std::cout << "open success" << std::endl;
}
void OpenForWrite()
{
_fd = open(_fifoname.c_str(), O_WRONLY);
if (_fd < 0)
{
std::cerr << "open fifo cerr" << std::endl;
return;
}
std::cout << "open fifo success" << std::endl;
}
void WakeUp()
{
char c = 'c';
int n = write(_fd, &c, 1);
printf("尝试唤醒: %d\n", n);
}
bool Wait()
{
char c;
int num = read(_fd, &c, 1);
if (num > 0)
{
printf("醒来: %d\n", num);
return true;
}
else
return false;
}
void Close()
{
if (_fd > 0)
close(_fd);
}
~Fileoper() {}
private:
std::string _path;
std::string _name;
std::string _fifoname;
int _fd;
};
client.cc
#include "shm.hpp"
#include "Fifo.hpp"
int main()
{
Fileoper writefifo(PATH, FIFONAME);
writefifo.OpenForWrite();
Shm shm(pathname, projid, USER);
char *mem = (char*)shm.VirtualAddr();
int index = 0;
for(char c = 'A'; c <= 'Z'; c++, index += 2)
{
sleep(1);
mem[index] = c;
mem[index + 1] = c;
sleep(1);
mem[index + 2] = 0;
writefifo.WakeUp();
}
writefifo.Close();
return 0;
}
server.cc
#include "shm.hpp"
#include "Fifo.hpp"
int main()
{
Shm shm(pathname, projid, CREATER);
// 创建管道文件
NameFifo fifo(PATH, FIFONAME);
// 文件操作
Fileoper readfile(PATH, FIFONAME);
readfile.OpenForRead();
char *mem = (char *)shm.VirtualAddr();
while (true)
{
if(readfile.Wait())
{
printf("%s\n", mem);
sleep(1);
}
else break;
}
readfile.Close();
return 0;
}