5. System V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
5.1 原理, 让不同进程看到同一份资源, 通过进程地址空间映射
申请内存的步骤
- 申请共享物理内存
- 挂接到进程地址空间
释放共享内存的步骤
- 让进程的地址空间和物理内存去关联
- 释放共享内存
5.2 共享内存函数
5.2.1 shmget函数
功能:
用来创建共享内存
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:
成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmflg常用的两个参数
- IPC_CREAT: 如果你申请的共享内存不存在,就创建, 存在, 就获取并返回
- IPC CREAT|IPC EXCL:如果你申请的共享内存不存在, 就创建, 存在, 就出错返回。这样能确保如果我们申请成
功了一个共享内存,这个共享内存一定是一个新的!key的话题
- key是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
- 第一个进程可以通过key创建共享内存. 第二个之后的进程, 只要拿着同一个key就可以和第一个进程看到同一个共享内存了
- 对于一个已经创建好的共享内存, key在描述那个共享内存的数据结构中
- 第一次创建的时候,必须有一个key了。
- key类似绝对路径, 特点是唯一
5.2.2 ftok函数
功能:
将路径名和项目标识符转换为System V IPC密钥
函数原型:
key_t ftok(const char *pathname, int proj_id);
参数:
pathname: 路径
proj_id: 项目id
返回值:
如果成功,则返回生成的key_t值。
如果失败,返回-1,errno表示stat(2)系统调用的错误。
描述:
ftok()函数使用给定路径名命名的文件的标识(必须引用一个现有的、可访问的文件)和proj_id的最低有效8位(必须是非零的)来生成一个密钥t类型的System V IPC密钥,适合与msgget(2)、senget(2)或shnget(2)一起使用。当使用proj_id的相同值时,对于命名相同文件的所有路径名,结果值是相同的。当(同时存在的)文件或项目id不同时,返回的值应该不同。
- 问题:为什么不让系统直接生成一个key,而需要我们手动生成呢?
因为系统直接生成的key不容易传给另一个我们想要通信的进程, 所以需要用户约定同一个key
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is %d", k);
return k;
}
int GetShareMem()
{
int shmid = shmget(GetKey(), SIZE, IPC_CREAT | IPC_EXCL);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat shmid sucess, shmid is %d", shmid);
return shmid;
}
- 问题:这里的
shmid
和key
的区别key: 只在操作系统内标定唯一性
shimid: 只在你的进程内,用来表示资源的唯一性!
5.2.3 查看和删除当前系统的共享内存
ipcs -m
共享内存的生命周期是随内核的
除非用户主动关闭, 或者内核重启, 否则共享内存会一直存在
ipcrm -m shmid
用来删除当前系统的共享内存
5.2.3 设置权限
只需要在shmget();
第三个参数 或 上权限的数字即可
int shmid = shmget(GetKey(), SIZE, IPC_CREAT | IPC_EXCL | 0666);
5.2.4shmat函数
功能:将共享内存段连接到进程地址空间
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY, 也可以是0, 表示共享内存段将被附加到由shmat()函数返回的地址处
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
公式:shmaddr - (shmaddr % SHMLBA)shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
5.2.5shmdt函数
功能:将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
5.2.6 shmctl函数
功能:用于控制共享内存
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
cmd的几个取值
5.2.7 一个保存着共享内存的模式状态和访问权限的数据结构
参数buf(5.2.6中的buf)是一个指向shmid结构体的指针,在<sys/shm.h>中定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
ipc_perm结构定义如下(突出显示的字段可以使用IPC_SET设置):
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t **uid**; /* Effective UID of owner */
gid_t **gid**; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short **mode**; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
5.2.8当前的代码
// common.hpp
#ifndef __COMMON__H__
#define __COMMON__H__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"
using namespace std;
const string PATH = "/home/lyf";
const int PROID = 0x1234;
// 共享内存的大小建议是4096的整数倍
// 当我们将SIZE改为4097时,OS实际给我们的是4096*2的大小, 但多出来的那4095byte我们并不能使用
// 这就造成了浪费
const int SIZE = 4096;
Log log;
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is 0x%x", k);
return k;
}
int ShareMemHelper(int flag)
{
int shmid = shmget(GetKey(), SIZE, flag);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat shmid sucess, shmid is %d", shmid);
return shmid;
}
// 创建共享内存
int CreatShareMem()
{
return ShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取共享内存的shimid
int GetShareMem()
{
return ShareMemHelper(IPC_CREAT);
}
#endif
// processA.cc
#include "common.hpp"
int main()
{
sleep(3);
// 申请共享物理内存
int shmid = CreatShareMem();
sleep(3);
// 挂接到进程地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(INFO, "Attach shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 让进程的地址空间和物理内存去关联
shmdt(shmaddr);
log(INFO, "Detatch shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 释放共享内存
shmctl(shmid, IPC_RMID, nullptr);
log(INFO, "Free shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
log(INFO, "Process quit");
return 0;
}
5.2.9 再加一个程序
// processB.cc
#include "common.hpp"
int main()
{
sleep(3);
// 获取共享物理内存
int shmid = GetShareMem();
sleep(3);
// 挂接到进程地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(INFO, "Attach shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 让进程的地址空间和物理内存去关联
shmdt(shmaddr);
log(INFO, "Detatch shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
log(INFO, "Process quit");
return 0;
}
5.2.10进程间通信
我们上面做的工作, 已经可以让不同的进程看到同一份资源了, 下面进行通信
// processA.cc
#include "common.hpp"
int main()
{
int shmid = CreatShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有人把数据写入到共享内存,其实我们立马能看到了
// 不需要经过系统调用,直接就能看到数据了!
// IPC code, 让A进程读
while(true) {
cout << "Process say@ " << shmaddr; // 直接访问共享内存
sleep(1);
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
// processB.cc
#include "common.hpp"
int main()
{
int shmid = GetShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// IPC code, 让B进程写
while(true) {
// 直接访问共享内存
// cout << "Please enter@ ";
// fgets(shmaddr, SIZE, stdin);
// 通过一个buffer来访问
char buffer[1024];
cout << "Please enter@ ";
fgets(buffer, sizeof(buffer), stdin);
// strlen+1, 让读取的时候读到\0
memcpy(shmaddr, buffer, strlen(buffer)+1);
}
shmdt(shmaddr);
return 0;
}
可以看到, 进程A在一直读取
5.3共享内存的特点
共享内存没有同步互斥之类的保护机制
共享内存是所有的进程间通信中,速度最快的
因为拷贝少, 对比管道, 管道当我们像管道写数据, 需要write(), 数据从用户级缓冲区拷贝到内核级缓冲区, 从管道中读数据, 需要read(), 数据再次拷贝, 从内核级缓冲区拷贝到用户级缓冲区
- 共享内存内部的数据,由用户自己维护
5.4添加管道
修改一下5.2.10中的代码, 不让processA一直刷屏, 引入了之前写的命名管道的代码
// common.hpp
#ifndef __COMMON__H__
#define __COMMON__H__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"
using namespace std;
const string PATH = "/home/lyf";
const int PROID = 0x1234;
// 共享内存的大小建议是4096的整数倍
// 当我们将SIZE改为4097时,OS实际给我们的是4096*2的大小, 但多出来的那4095byte我们并不能使用
// 这就造成了浪费
const int SIZE = 4096;
Log log;
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is 0x%x", k);
return k;
}
int ShareMemHelper(int flag)
{
int shmid = shmget(GetKey(), SIZE, flag);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat or Get shmid sucess, shmid is %d", shmid);
return shmid;
}
// 创建共享内存
int CreatShareMem()
{
return ShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取共享内存的shimid
int GetShareMem()
{
return ShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0666
const int N = 1024;
enum {
FIFO_CREAT_ERR = 1,
FIFO_DEL_ERR,
FIFO_OPEN_ERR,
FIFO_READ_ERR
};
// 用于处理server.cc部分代码的初始化和析构处理
struct Init
{
Init()
{
// 创建信道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1) {
perror("mkfifo");
exit(FIFO_CREAT_ERR);
}
}
~Init()
{
// 删除信道
int m = unlink(FIFO_FILE);
if (m == -1) {
perror("unlink");
exit(FIFO_DEL_ERR);
}
}
};
#endif
// processA.cc
#include "common.hpp"
int main()
{
Init init;
int shmid = CreatShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有人把数据写入到共享内存,其实我们立马能看到了
// 不需要经过系统调用,直接就能看到数据了!
// IPC code, 让A进程读
struct shmid_ds shmds;
// 打开信道
int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后, 自己才会打开文件, 向后执行, open 阻塞了!
if(fd == -1) {
log(FATAL, "open error, %s", strerror(errno));
exit(FIFO_OPEN_ERR);
}
while(true) {
char c;
ssize_t s = read(fd, &c, 1);
if(s == 0) break;
else if(s == -1) break;
cout << "Process say@ " << shmaddr; // 直接访问共享内存
// shmctl(shmid, IPC_STAT, &shmds);
// cout << "No. of current attaches" << shmds.shm_nattch<<endl;
// cout << "key is" << shmds.shm_perm.__key<<endl;
// sleep(1);
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
close(fd);
return 0;
}
// processB.cc
#include "common.hpp"
int main()
{
int shmid = GetShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// IPC code, 让B进程写
// 打开信道
int fd = open(FIFO_FILE, O_WRONLY);
if(fd == -1) {
log(FATAL, "open error, %s", strerror(errno));
exit(FIFO_OPEN_ERR);
}
while(true) {
// 直接访问共享内存
cout << "Please enter@ ";
fgets(shmaddr, SIZE, stdin);
write(fd, "c", 1); // 通知另一个进程
// 通过一个buffer来访问
// char buffer[1024];
// cout << "Please enter@ ";
// fgets(buffer, sizeof(buffer), stdin);
// // strlen+1, 让读取的时候读到\0
// memcpy(shmaddr, buffer, strlen(buffer)+1);
}
shmdt(shmaddr);
return 0;
}