之前的文章中我们讲述了匿名管道与命名管道相关的知识点,在本文中我们将继续讲述一种进程间通信的方式:共享内存。
systemV共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
由于进程是相互独立的,那么进程间通信就跟我们之前所述的一样,需要让我们看到同一份的资源,这样才能够进行通信。
共享内存原理
我们知道创建一个进程就会产生一个PCB,地址空间以及页表结构,并且需要哪个将自己的数据映射到OS的物理内存中的特定区域,为了让不同的进程看到同一份资源,在物理内存中开辟一段空间,并将这段地址映射到进程A和进程B的共享区中,然后进程将映射的虚拟地址返回给用户,这样我们就完成了让不同的进程看到了同一份资源 -- 共享内存。当我们不需要使用共享内存的时候,就可以将共享内存的映射关系去掉 ,然后释放内存块。在宏观上控制共享内存,可以分为三个步骤:1、创建;2、关联或者取消关联;3、释放共享内存;
共享内存函数
shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字, 这个key我们通过使用ftok函数来获得key_t ftok(const char *pathname, int proj_id);
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1// IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL:创建一个共享内存,如果共享内存不存在就创建,如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!
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:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
命令 说明 IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值 IPC_SET 把进程有足够权限的前提下,把共享内存的当前管来之设置为shmid_ds数据结构中给出的值 IPC_RMID 删除共享内存段
简单的代码例子
系统中可以使用shm进行通信 -> 在任何一个时刻,可能有多个共享内存在被用来进行通信 -> 系统中一定会存在大量的shm同时存在 -> OS要对这些shm进行管理,使用之间学习的先描述再组织的方式将其管理起来。所以共享内存不仅仅只是在内存中开辟空间,系统为了管理shm,构建对应的描述共享内存的结构体对象 -> 共享内存 = 共享内存的内核数据结构 + 真正开辟的空间。
创建key
//comm.hpp
#define PATHNAME "."
#define PROJID 0x6666
const int gsize = 4096; //暂时
key_t getKey(){
key_t k = ftok(PATHNAME, PROJID);
if(k == -1){
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x){
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
//server.cc client.cc
// 1. 创建key
key_t k = getKey();
cout << "server key: " << toHex(k) << endl;
使用ftok函数对key的值进行创建,当不同的进程拥有了同样的pathname以及proj_id,那么就可以获得同一份共享内存的key值就可以找到同一份资源。
创建共享内存
在使用共享内存的时候一定是一个进程先创建,另一个进程获取。在创建的时候最好获得一个全新的。因此我们对shmget的flag选项设置成IPC_CREAT | IPC_EXCL
//comm.hpp
static int createShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, gsize, flag);
if(shmid == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t k, int size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666); // 这里需要给共享内存添加权限,不然无法正常访问
}
int getShm(key_t k, int size)
{
return createShmHelper(k, size, IPC_CREAT);
}
//server.cc client.cc
// 2. 创建共享内存
int shmid = createShm(k, gsize);
cout << "server shmid: " << shmid << endl;
运行结束时服务端与客户端退出了,但是共享内存并没有清理,再次运行就会发现出现了如下的情况,通过指令 ipcs -m 就可以查看对应的共享内存信息。
我们可以使用 ipcrm -m + [对应的shmid值] 进行删除,key值我们可以将其类比做文件的inode编号,shmid可以类比做文件的fd。对shm的未来的所有操作,在用户层都用shmid。
同样还有可以使用上述的shmctl函数进行删除操作,下面的图中就可以看到自动进行删除。
关联与去关联
现在我们已经有了共享内存,但还并不能使用,需要将我们的进程与共享内存进行关联才能正常使用。共享内存的关联函数shmat与malloc函数的使用方法相似。
// comm.hpp
char* attachShm(int shmid){
char *start = (char*)shmat(shmid, nullptr, 0);
return start;
}
void detachShm(char *start){
int n = shmdt(start);
assert(n != -1);
(void)n;
}
在ipcs -m 中还可以查看到 nattch 这个就可以看到对应的连接数,我们通过阅读上述的代码得知:先是无连接,然后是sever关联,再是cilent关联,最后是去关联。
删除共享内存
在使用完成共享内存之后就可以将共享内存删除。
void delShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
}
通信
在上述的工作都完成了之后就可以尝试开始进程间的通信:
在客户端依次写入大写的英文字母,在服务端对共享内存进行读取,从上面的图片中就可以看出。
一些其他的注意点
共享内存的大小
在创建共享内存的时候我们当时暂时设置了一个4KB的大小,当我们对其进行修改变成4097字节时可以发现查找共享内存是确实显示的是4097的大小,但当查看shmget函数的使用却发现:
它申请的内存是PAGE_SIZE的整数倍为单位的,而PAGE_SIZE的大小正好是4KB。由于申请了4097的空间大于4KB,操作系统在我们申请的时候会在底层给我们8KB,但是用户能够使用的只有4097大小的空间。
共享内存的速度
在通信的时候,没有使用任何的接口,因此一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了。因为共享内存的这种特性,可以让进程通信的时候减少拷贝次数,所以共享内存是所有进程间通信速度最快的。正常来说如果把外设到内存中的数据交互看做一次拷贝的话,那么通过管道通信的方式就需要至少四次拷贝,外设与内存需要两次,进程与管道之间也需要两次;而如果使用共享内存的话就只需要外设与内存间的两次拷贝,由于共享内存是物理地址通过页表映射到虚拟地址空间中的,就减少了拷贝的次数。
当我们只启动server时,已经创建了共享内存,但是此时若client并没有启动,server端就会无脑的进行读取,打印出来的全是空值,共享内存没有任何的保护机制(同步互斥)