文章目录
- 引入
- 共享内存的原理
- 共享内存的相关接口
- shmget()
- shmat()
- shmdt()
- shmctl()
- 共享内存的简单使用
- 共享内存的特点
引入
进程间通信,顾名思义就是一个进程和另一个进程之间进行对话,以此完成数据传输、资源共享、通知事件或进程控制等。
众所周知,进程具有独立性,即使是父子进程也会彼此独立,互相看不到对方的任何信息。
而独立性是阻碍通信的,所以进程间通信要打破这种阻碍,打破进程独立性,也就是要让两个不想干的进程看到同一份资源。
在上一篇文章中我们分别介绍了匿名管道和命名管道两种通信方式,通过建立一个内存级文件,一个进程向该文件写内容,另一个进程从该文件中读内容,这样就完成了两个进程之间的通信。
既然可以建立一个内存级文件,能否省去文件,直接开辟一块内存,要通信的进程能同时使用这块内存呢?
这就是下面要介绍的共享内存(Shared Memory)。
共享内存的原理
在介绍共享内存之前先了解一下它的工作原理。
我们肯定了解C语言中有malloc
函数,可以开辟一块内存空间,这块空间能通过页表映射到进程地址空间的堆区。但是进程空间不止有堆区,还有什么栈区、初始化数据区、未初始化数据区等等…其中在堆区和栈区之间还有一个共享区。我们今天要讲的共享内存,就是要映射到共享区的。
我们同样可以使用类似于malloc
功能的接口来申请一块内存空间。然后将申请的这部分内存空间映射到进程地址空间的共享区,在另一个进程中我们也做相同的事情,这样两个进程就同时关联了同一块物理内存空间,一个进程向这段内存中写,另一个进程从这块内存读取,这样同样也能实现两个进程之间的通信。当两个进程之间不再需要通信时,我们先不急着释放这块内存,因为释放之后所有共享这块内存的进程都无法使用了,我们也不知道究竟有几个进程同时使用这块内存通信。所以首先取消内存和不再需要通信的进程之间的映射,也就是去关联。当这块内存真不需要的时候,再将其释放掉。
如此就是通过共享内存进行通信的简单原理:
共享内存的相关接口
有了原理层面的简单了解,就可以学习一下相关接口了。下面主要介绍四个接口s。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmget()
int shmget(key_t key, size_t size, int shmflg);
功能:用来创建共享内存
参数:key: 可以唯一标识共享内存段的key值
size: 共享内存块的大小,一般是4kb的整数倍
shmflag: 共享内存块的权限,用二进制数中的位进行标识
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
对于
key
值,是需要我们设计传过去的,它的类型是key_t
,实际是int。key值要保证唯一性,以此保证共享内存可以唯一标识,我们可以通过ftok()
函数进行获取,所以再简单介绍一下ftok()
函数:#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
很简单,参数为一个字符串和一个整数,字符串一般传我们要创建的共享内存块的路径,
proj_id
可以设一个随机数,ftok
就会生成一个key
并返回,如果出错了则返回-1。
size
就不多介绍了,还是需要注意size
虽然可以随便填,但一般是4kb的整数倍。
shmfalg
为创建文件的一些权限选项,其选项常用的主要就两个:IPC_CREAT
和IPC_EXCL
。IPC_CREAT
的作用是如果key
对应的共享内存块不存在,则创建一块,然后返回该内存块的shmid
;如果已经存在相应的共享内存块了,则直接返回对应的id
。而IPC_EXCL
通常要配合IPC_CREAT
使用,当要获取的共享内存块不存在时不起作用,当其已经存在时则会获取失败,保证要获取的共享内存块是新鲜的。除此之外还要加上要创建的共享内存块的权限,就跟用open
创建文件时的参数mode
一样,比如0x0666
是所有人可读可写,0x0600
是只有自己可读可写。如果忘记加权限则创建出来的共享内存是无法使用的。
shmat()
全称是shm attach,功能就是将进程与共享内存挂接。
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接到进程地址空间
参数:shmid: 共享内存的id,可以唯一标识共享内存
shmaddr: 指定链接到进程空间中的地址
shmflg: 常用的标识有两个,SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存的首地址,类似于malloc;失败返回-1
对于
shmid
,这个就是shmget
的返回值,和key
一样都能进行唯一性标识,区别就是key
是内核层的,shmid
是应用层的。进程要挂接共享内存时手里肯定得有id。
shmaddr
如果传参的话要传一个地址,意思是指定映射到进程地址空间的哪个位置。如果传nullptr
则会随机映射,一般都是传nullptr
。
shmflg
如果传了SHM_RDONLY
则进程只能从共享内存块中读取。SHM_RND
通常要配合shmaddr
使用,会使映射的位置不一定在shmaddr
处,会向下舍入到SHMLAB
的整数倍。用的也很少。如果shmflg
传个0过去,则表示可读可写。
shmdt()
全称是shm detach,功能就是取消进程与共享内存的挂接,也就是去关联。
int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离
参数:shmaddr: 由shmat所返回的指针,也就是共享内存的首地址,用法类似于free()
返回值:成功返回0;失败返回-1
该函数的功能仅仅是去关联,并不是删除共享内存段。
当进程不再需要通过该共享内存通信时应及时取消挂接。
shmctl()
全称是shm control,功能就是控制共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:用于控制共享内存
参数:shmid: 由shmget返回的共享内存标识码
cmd: 将要采取的动作,常用的有三个取值
buf: 输出型参数,指向一个存储共享内存块部分属性结构体,如果需要时会将shmid对应的共享内存块的信息拷贝到该结构体中,为输出型参数
cmd
的取值主要有IPC_STAT
,IPC_SET
和IPC_RMID
。
IPC_STAT
的功能是将共享内存内核数据结构中的属性拷贝到buf
中,用来开获取共享内存的状态信息,如果要使用这个选项,共享内存必须要可读。
IPC_SET
和上面的选项功能相反,是要把buf
中的信息写入到内核数据结构中,使用时要谨慎。只有创建或者拥有该共享内存块或被赋予权限的人才能进行此操作。
IPC_RMID
就是删除共享内存了,但是只有当没有进程挂接共享内存时该共享内存块才会真正删除。只有创建或者拥有该共享内存块或被赋予权限的人才能进行此操作。一般就是谁创建的谁删除。
buf
是struct shmid_ds
结构体类型指针,指向这样一个结构,man手册中给出了该结构的部分信息: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 */ ... };
其中还包含一个
struct ipc_perm shm_perm
结构体成员,其信息如下: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 */ };
共享内存的简单使用
下面举一个使用共享内存进行通信的简单例子。
为了便于使用,我们把一些公共信息比如key
和pathname
和共享内存的接口进行封装一下,放在一个头文件common.hpp
中:
// common.hpp
#pragma once
#include <iostream>
#include <cerrno> // 用于打印报错信息
#include <cstring> // 调用C的部分字符串相关的接口
#include <cstdlib>
#include <cstdio>
#include <sys/ipc.h> // 共享内存的相关接口
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." // 在当前目录下创建共享内存
#define PROJ_ID 0x66 // ftok()创建key时的参数,任意取值都行
#define MAX_SIZE 4096 // 共享内存的大小,建议4kb的整数倍
// 通过ftok()获取key
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);
if (k < 0)
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
return k;
}
// 辅助函数,结合后面两个函数看
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if (shmid < 0)
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
return shmid;
}
// 获取shm,如果不存在就创建
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT);
}
// 创建新的shm
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
// 挂接shm
void* attachShm(int shmid)
{
void* mem = shmat(shmid, nullptr, 0);
if (mem == (void*)(-1))
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
return mem;
}
// 去关联
void detachShm(void* mem)
{
if (shmdt(mem) == -1)
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
}
// 删除shm
void delShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << " : " << strerror(errno) << std::endl;
exit(-1);
}
}
在server.cpp
中创建共享内存,然后等待接受信息:
// server.cpp
#include "common.hpp"
#include <unistd.h> // 在该文件中会调用getpid()函数
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k); // key
int shmid = createShm(k);
printf("shmid: %d\n", shmid); // shmid
// 直接以char*类型使用共享内存段,也就是把共享内存段存储的数据看成字符串
// 后续使用就跟使用用malloc开辟的空间一样
char *start = (char *)attachShm(shmid);
printf("attach success, address start: %p\n", start);
struct shmid_ds ds; // 保存共享内存信息的结构体
shmctl(shmid, IPC_STAT, &ds); // 获取共享内存的部分属性信息并打印一下
printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x\n",
ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
// 使用
while (true)
{
printf("client say : %s\n", start);
sleep(1);
}
// 去关联
detachShm(start);
sleep(10);
delShm(shmid); // 删除共享内存
return 0;
}
在client.cpp
中向共享内存写入信息:
// clent.cpp
#include "common.hpp"
#include <unistd.h> // 在该文件中会调用getpid()函数
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k); // key
int shmid = getShm(k);
printf("shmid: %d\n", shmid); // shmid
char *start = (char*)attachShm(shmid);
printf("attach success, address start: %p\n", start);
const char* message = "hello server, 我是另一个进程,正在和你通信";
pid_t id = getpid();
int cnt = 1;
while(true)
{
snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
sleep(1);
}
detachShm(start);
return 0;
}
在client.cpp中一秒发送一条信息,在server.cpp中一秒打印一条信息,此时运行情况如下:
共享内存的特点
在结束掉上面两个进程之后再次运行server
:
此时会报错说文件已经存在了,也就是我们结束掉进程之后共享内存并没有自动销毁。
所以共享内存的生命周期是不随进程的,如果我们一直不释放,只有在关掉系统时消失,所以共享内存的生命周期是随OS的。
当然linux也有一些查看共享内存相关的命令,我们可以用ipcs -m
命令查看共享内存:
发现此时确实有一个共享内存,查看它的key和shmid信息也确实是我们刚刚创建的。其中几个属性还有owner,为创建共享内存的用户。perms就是permissions,权限的意思,正好也是我们创建共享内存时输入的权限0666。bytes就是共享内存的大小。nattach就是和共享内存挂接的进程的数量。
此时我们就可以用ipcrm -m $shmid
指令手动释放掉共享内存:
上面创建完共享内存之后的使用方式就跟使用malloc
开辟的空间似的。因为在进程的视角来看那就是自己空间的一部分,直接使用,不像管道似的还要先把要发送的数据存起来开然后向管道中写入,多了向管道中写入和从管道中读出两次拷贝。所以共享内存是所有进程间通信的方式中最快的。
我们还发现了,在运行client之前server端已经向显示器打印信息了,即使共享内存中没有信息。在client端关掉之后,server端就一直打印client最后发送的信息。所以共享内存是不会像管道一样能进行同步和互斥,也不会对数据有任何的保护。同步和互斥是通过信号量来完成的。