每一个不曾起舞的日子,都是对生命的辜负。
共享内存
- 一.共享内存的原理
- 二.共享内存你的概念
- 2.1 接口认识
- 2.2演示生成key的唯一性
- 2.3 再谈key
- 三.共享资源的查看
- 3.1 如何查看IPC资源
- 3.2 IPC资源的特征
- 3.3 进程之间通过共享内存进行关联
- 四.共享内存的特点
- 五.共享内存的内核结构
- 六.共享内存函数的总结
共享内存是为通信而诞生的。除了上一节中讲到的公共文件的方案,还有什么其他方案呢?----以共享内存的方式
一.共享内存的原理
在之前学过的进程地址空间的基础上,我们知道,进程之间具有独立性,因为每个进程的内核数据结构的数据以及页表的映射都是独立的。而对于共享内存,我们同样了解,这是为了让进程之间能够进行通信的公共空间,接下来就通过进程地址空间的结构去了解共享空间的位置及原理:
OS为了让两个毫不相关的进程之间进行通信,进行了三个工作:
- 在对应的内存当中让用户帮OS申请一块空间(通过指定的调用接口)
- 将创建好的内存映射进进程的地址空间(用户就可以通过访问起始地址的方式来进行对申请的这块内存空间的访问)
- 未来不想通信:
- 取消进程和内存的映射关系
- 释放内存
因此,我们把申请的这块空间称之为共享内存,将映射关系称之为进程和共享内存进行挂接。将取消进程和内存的映射关系称之为去关联,释放内存释放的就是共享内存。
理解:
- 进程间通信,是专门设计的,用来IPC的,和malloc/new不是一个东西。
- 共享内存是一种通信方式,所有想通信的进程,都可以用。
- OS中一定会存在着很多共享内存。
二.共享内存你的概念
通过让不同的进程,看到同一个内存块的方式,叫做共享内存。
2.1 接口认识
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);// size:共享内存的大小
对于shmflg,常见的有这么两种选择:
IPC_CREAT
:如果不存在共享文件则创建,存在则获取IPC_EXCL
:- 1.无法单独使用,单独使用没有意义,需要结合IPC_CREAT
- 2.
IPC_CREAT|IPC_EXCL
:如果不存在,就创建,如果已存在,就出错返回。即在用户的角度,如果创建成功,一定是一个新的shm!
shmget返回值: 记住他是一个标识符就够用了,得到的是共享内存的标识符。(和文件fd没有任何关系)
key: 是什么不重要,最重要的是其具备的唯一性。
而获取key值,则通过一个新的接口:ftok
,ftok通过指定的字符串数据*pathname
以及char类型的proj_id
数据进行一系列的算法整合返回了具有唯一性的Key:
key_t ftok(char *pathname, char proj_id);
由于创建的key值有可能已经被别人使用了,因此有失败的可能性。创建Key值如果失败,则返回-1。
2.2演示生成key的唯一性
makefile
.PHONY:all
all:shm_client shm_server
shm_client:shm_client.cc
g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shm_client shm_server
comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include<iostream>
#include<cerrno>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."//当前路径
#define PROJ_ID 0x66
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)
{
// cin, cout, cerr ->stdin, stdout, stderr->0, 1, 2;标准错误stderr向2打印。
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);//终止进程
}
return k;
}
#endif
shm_server.cc
#include"comm.hpp"
int main()
{
key_t k = getKey();
printf("0x%x\n", k);
return 0;
}
shm_client.cc
#include"comm.hpp"
int main()
{
key_t k = getKey();
printf("0x%x\n", k);
return 0;
}
通过make后执行发现,两个程序的k值是一样的,这就证明了ftok指定参数的返回值是唯一的。(k实际上就是32位的一个整数)
2.3 再谈key
OS中一定存在多个共享内存,因为彼此之间可能都需要通信,因此也就都需要申请一块空间。而OS申请的共享空间,也一定和进程一样需要被管理,既然需要管理,那么一定也是先描述再组织的方式,即共享内存 = 物理内存块+共享内存的相关属性 。
之前谈到过,key是什么不重要,能进行唯一性的标识最重要,因此创建共享内存的时候,是如何保证共享内存在系统中是唯一的呢?当然是通过key来确定的,只要一个进程也看到了同一个key,就能够访问这个共享内存。那么key在哪里,实际上这就和PCB一样,key就在内核中的属性集合里,即:
struct shm{
key_t key;
//...
}
即:key是通过shmget这样的系统调用,设置进入共享内存属性中,用来表示该共享内存在内核中的唯一性!
shmid和key就好比fd和inode。为什么有了key还需要shmid呢?通过key和shmid的区分,能够面向系统层面和用户层面,这样能够更好的进行解耦,以免内核中的变化影响到用户级。
三.共享资源的查看
共享(IPC)
3.1 如何查看IPC资源
ipcs -m/q/s
查看:
3.2 IPC资源的特征
我们发现,当第一次执行成功之后,再次调用不会成功,这是因为共享内存并不像管道一样进程结束之后自动释放内存,共享内存的声明周期是随着OS的,不是随着进程的!因此这就需要我们主动的去释放这块空间,即通过指令:ipcrm -m 加上这块共享内存shmid的值
,虽然key也是唯一的,但key是系统层面的,shmid才是对于我们用户层面的。
对于释放共享内存,除了上述的手动命令,其也有自己的接口能够进行共享内存物理空间的释放,即:
#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//cmd代表控制的种类,即内置的有多种选择。
//返回值:失败则返回-1
void delShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << " : " << strerror(errno) << std::endl;
}
}
以下代码复制SSH渠道:执行while :; do ipcs -m; sleep 1; echo"--------------------";done
,并在左侧执行shm_server,观察现象:
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include<iostream>
#include<cerrno>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#define PATHNAME "."//当前路径
#define PROJ_ID 0x66
#define MAX_SIZE 4096 //单位是字节
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);//可以获取同样的key
if(k < 0)
{
// cin, cout, cerr ->stdin, stdout, stderr->0, 1, 2;标准错误stderr向2打印。
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);//终止进程
}
return k;
}
int getShmHelper(key_t k, int flags)
{
//k是要shmget,设置进入共享内存属性中的!用来表示
//该共享内存,在内核中的唯一性!!
//shmid VS key
//fd VS inode
int shmid = shmget(k, MAX_SIZE, flags);
if(shmid < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
//失败就终止,没有共享内存了
exit(2);
}
return shmid;
}
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT);
}
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL);
}
void delShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << " : " << strerror(errno) << std::endl;
}
}
#endif
#include"comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k); // key
int shmid = createShm(k);
printf("shmid: %d\n", shmid); // shmid
sleep(5);//5s之后就会被删除
delShm(shmid);
return 0;
}
发现,进程执行之前没有对应的信息,执行之后信息出现5s,最终被释放。再次执行也不会出错。
进程之间进行关联
上述都没有提到进程进行关联的问题,有几个进程能够进行关联在上述动图右侧nattch可以看到,明显看到上面的nattch值为0,那么下面就来进行挂接一下:
此时就又有一个新的接口:
#include<sys/types.h>
#include<sys/shm.h>
//参数1:指定的共享内存,参数二:地址空间,一般设置为nullptr,参数三:读写权限,一般设置为0就可以了,默认就可以读写。
//返回值:共享内存空间的起始地址,就等价于malloc的返回值
void *shmat(int shmid, const void* shmaddr, int shmflg);
因此就可以在comm.hpp中加上这个功能:
void* attchShm(int shmid)
{
void* mem = shmat(shmid, nullptr, 0);//64系统:指针占8字节
if((long long)mem == -1L)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(3);
}
return mem;
}
但此时挂接就需要进行读写,因此还需要在createshm中添加选项:
接下来看看运行结果:
可以发现的是,由于我们新增了0600即拥有者的读写权限,perm也就显示了600,此外nattch的链接数量也变成了1,这说明有一个进程和这个共享内存关联起来了,而我们所演示的就是我自己的进程与共享内存进行了关联。运行之后同样可以释放。但上面直接释放过于粗暴,因为我们之前将进程和共享内存进行了关联,所以我们需要在释放之前将这个关联去掉,否则就有可能出问题。去关联并不是删掉共享内存,而是回收对应的页表。为了去关联,就又引出了一个接口:
// 参数就是在shmat时设定的返回值,对于返回值:成功就是0,失败就是-1.
int shmdt(const void* shmaddr);
因此,添加了这段代码后,就比上述现象在结束之前多了一个nattch变为0的过程。
3.3 进程之间通过共享内存进行关联
上述我们已经实现了shm_server与共享内存的关联,如果想让两个进程之间进行通信,那就需要另一个shm_client也与同一个共享内存进行关联:
但对于这段代码,不需要释放共享内存,因为在shm_server.cc中已经实现。这段接入之后,nattch的关联数就会变成2。
在之前的学习中,我们通过管道采用char buffer[1024]缓冲区的方式进行通信,现在有了共享内存就可以通过共享内存将两个进程连接起来。
shm_server.cc
#include"comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k); // key
int shmid = createShm(k);
printf("shmid: %d\n", shmid); // shmid
sleep(5);//5s之后就会被删除
char* start = (char*)attchShm(shmid);//挂接成功
printf("attach success, addresss start: %p\n", start);
//使用
while(true)
{
//char buffer[]; read(pipefd, buffer)
printf("client say: %s\n", start);
sleep(1);
}
//去关联:让进程和共享内存丧失关联性
detachShm(start);
sleep(5);
delShm(shmid);
return 0;
}
shm_client.cc
#include"comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = getShm(k);
printf("shmid: %d\n", shmid);
sleep(5);
//与共享内存产生关联
char* start = (char*)attchShm(shmid);
printf("attach success, address start: %p\n", start);
const char* message = "hello server,我是另一个进程,正在和你通信";
pid_t id = getpid();
int cnt = 1;
char buffer[1024];
while(true)
{
sleep(1);
snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
// snprintf(buffer, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt);
// memcpy(start, buffer, strlen(buffer)+1);
// //pid, count, message
}
//去关联:让进程和共享内存丧失关联性
detachShm(start);
return 0;
}
执行:(记得手动ipcrm -m)
四.共享内存的特点
共享内存的优点:
所有进程间通信,速度是最快的!因为其共享内存在进程地址空间能够大大减少数据的拷贝次数。(即本来用buffer,现在没有必要)
综合考虑管道和共享内存,考虑键盘输入和显示器输入,共享内存共有几次数据拷贝,即同一段代码,通过管道和共享内存,分别进行了几次拷贝?
对于管道来说,通过的是如下步骤:
将键盘输入的数据放到自己指定的缓冲区buffer中为第一次,将buffer中的数据拷贝到管道中是第二次,将管道中的数据拷贝到另一个进程的缓冲区中为第三次,将缓冲区的数据打印在显示器中为第四次,此外还有输入输出流stdin和stdout,所以为4+2次。
对于共享内存来说,没有中间的buffer,因此也就是2+2次。
共享内存的缺点:
共享内存不会进行同步和互斥的操作,没有对数据做任何的保护。也就是说,共享内存并不像管道一样,管道是当读写都打开时,如果不读,写满就会不写了,如果写端不写,读端就不会继续读了并且阻塞在那里,而共享内存没有做这样的保护。那么如何保护?今后将会学到信号量和互斥锁的方式对管道进行保护。
五.共享内存的内核结构
共享内存数据结构:
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 */
};
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 */
}
共享内存的大小
共享内存的大小,一般建议是4KB的整数倍,因为系统分配共享内存是以4KB为单位的! — 内存划分内存块的基本单位。
否则内核会给你向上取整。但我们能够使用的仍是我们指定的大小。
六.共享内存函数的总结
上面在演示的时候,已经逐步的介绍了有关共享内存函数的功能,我们在这里总结一下:
shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
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,表示连接操作用来只读共享内存
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:指向一个保存着共享内存的模式状态和访问权限的数据结构,一般设定为nullptr即可
返回值:成功返回0;失败返回-1
此外,关于最终的代码展示在如下链接:共享内存代码