💘作者:泠沫
💘博客主页:泠沫的博客
💘专栏:Linux系统编程,文件认识与理解,Linux进程学习…
💘觉得博主写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!
🏠 共享内存原理
通过前面的学习我们知道,实现两个进程通信的最重要的前提就是让两个进程看到同一块内存空间。上一节提到的匿名管道和命名管道,本质上都是在内存上寻找一块空间使得两个进程都能看到。区别就是实现的方式不同,现在我们即将学习的共享内存其实本质上也是为了让两个进程看到同一块内存上的空间。
共享内存的实现主要是一下过程:
-
操作系统在内存上开辟一块空间,该空间开辟成功之后,对用户开放权限。
-
与malloc相似的是,操作系统在内存上开辟完这样一块空间之后,会给进程返回这块空间的起始地址,然后通过页表的映射,该进程的进程地址空间就会为这块空间进行编址。
-
然后再让想要与之通信的进程获得共享内存的起始地址,也通过页表映射到自己的进程地址空间之中。
-
如此,就实现了两个毫无关系的进程都与内存上一块共享内存建立起关联,虽然两个进程在各自的进程地址空间看到的共享内存的地址不同,但是通过页表映射其实指向的就是同一块空间。
-
最后这两个进程就可以各自访问这块空间,实现一个进程将数据交给另一个进程。
🏠 共享内存函数
在学习完共享内存的原理之后,我们来学习如何与共享内存有关的接口函数。
-
ftok
ftok,这个函数是用来生成一个特殊数字key的,这个数字可以用来唯一标识一个共享内存。这个函数内部是通过一些算法来将一个字符串和一个整数整合成一个特殊的整数,然后输出。第一个参数是一个文件路径+文件名,这个参数必须是现在已有的,可访问的路径文件。
第二个参数是一个整数,可以随便传。
-
shmget
shmget,这个系统调用接口的作用是在内存上开辟空间,作为共享内存。如果创建成功则返回共享内存的编号,创建失败则返回-1且错误码被设置。
第一个参数key是用来唯一标识一个共享内存的,这个数字是什么不重要,重要的是它能唯一标识一个共享内存,这个数字是通过ftok()函数生成的。未来这个key是会被写入进共享内存的内核数据结构的。
第二个参数是指明要创建的共享内存的大小,一般建议是4096的整数倍,因为操作系统申请内存的基本单位就是4096字节。
第三个参数是创建文件时的选项,一般有IPC_CREAT和IPC_EXCL。IPC_CREAT表示如果要申请的共享内存不存在,则创建并获取共享内存的id。如果存在,则直接获取共享内存的id。IPC_EXCL要和IPC_CREAT搭配使用。表示如果不存在就创建,存在就报错。这能帮助我们如果创建成功,那一定是一个新的共享内存。如果对于创建新的共享内存,我们还要在带上一个权限。最终第三个参数如下:IPC_CREAT | IPC_EXCL | 0600。
看到这里可能有些读者就疑惑了,前面说了key可以唯一标识一个共享内存,现在又说到了共享内存id,那这个共享内存id又是什么东西?
其实,不论是key还是shmid都可以唯一标识一个共享内存,只不过shmid是用户层,而key是系统层。这样有利于用户层和系统底层代码的解耦。
-
shmat
shmat是将创建好的共享内存与进程关联起来,也就是获取共享内存的物理地址,通过页表转化成进程虚拟地址。关联成功返回共享内存映射后的虚拟地址,失败返回(void *) -1且错误码被设置。第一个参数shmid就是之前创建共享内存成功后的返回值,这个shmid是用户用来唯一标识一个共享内存的数字。
第二个参数shmaddr为要附加的地址指针,如果为nullptr则由系统自动选择一个未使用的地址进行附加。一般传nullptr。
第三个参数shmflg是标志位,用于控制附加的方式和权限,它的两个可能取值是SHM_RND和SHM_RDONLY。一般传0。shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存 -
shmdt
shmdt是将进程与共享内存去关联。去关联成功返回0,失败返回-1且错误码被设置。参数shmaddr表示共享内存在该进程的虚拟地址。
-
shmctl
shmctl是用来控制共享内存的,其中就包括删除已有的共享内存。第一个参数shmid是用来唯一标识共享内存的编号。
第二个参数cmd表示将要采取的动作。常见的就是这三个:IPC_STAT,IPC_RMID,IPC_SET。
第三个参数buf是一个输出型参数,它指向一个保存着共享内存的模式状态和访问权限的内核数据结构。
IPC_STAT:把shmid ds结构中的数据设置为共享内存的当前关联值。
IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmidds数据结构中给出的值。
IPC_RMID:删除共享内存。
🏠 共享内存相关指令
ipcs ;查看共享内存,消息队列,信号量
ipcs -m ;仅查看共享内存
ipcrm -m (shmid) ; 删除对应的共享内存
🏠 共享内存代码展示
- comm.h—头文件
#pragma once
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
using namespace std;
#define MAX_SIZE 4096
#define PATHNAME "."
#define PROJ_ID 0x66
key_t getkey()
{
key_t key = ftok(PATHNAME, PROJ_ID);
if(key == -1)
{
perror("ftok");
exit(1);
}
return key;
}
int getshm(key_t key, int flags)
{
int shmid = shmget(key, MAX_SIZE, flags);
if(shmid == -1)
{
perror("shmget");
exit(2);
}
return shmid;
}
int delshm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
if(n == -1)
{
perror("shmctl");
exit(3);
}
return n;
}
void* attachshm(int shmid)
{
void* mem = shmat(shmid, nullptr, 0);
if((long long) mem == -1L)
{
perror("shmat");
exit(4);
}
return mem;
}
void detahshm(void* start)
{
if(shmdt(start) == -1)
{
perror("shmdt");
exit(5);
}
}
- client – 客户端/发送发
#include "comm.h"
int main()
{
key_t ckey = getkey();
//cout << "ckey: " << ckey << endl;
int cshmid = getshm(ckey, IPC_CREAT);
//cout << "cshmid: " << cshmid << endl;
char* start = (char*)attachshm(cshmid);
//printf("client:%p\n", start);
const char* message = "hello server, 我是另一个进程,正在和你通信";
int cnt = 1;
while(true)
{
snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, getpid(), cnt++);
sleep(1);
if(cnt == 6)
break;
}
detahshm(start);
//cout << "client detahshm" << endl;
//cout << "client quit" << endl;
sleep(3);
return 0;
}
- server – 服务端/接收方
#include "comm.h"
int main()
{
key_t skey = getkey();
//cout << "skey: " << skey << endl;
int sshmid = getshm(skey, IPC_CREAT | IPC_EXCL | 0600);
//cout << "sshmid: " << sshmid << endl;
char* start = (char*)attachshm(sshmid);
//printf("server:%p\n", start);
while(true)
{
printf("client say:%s\n",start);
sleep(1);
}
detahshm(start);
//cout << "server detahshm" << endl;
sleep(5);
int ret = delshm(sshmid);
//cout << "server quit" << endl;
return 0;
}
🏠 共享内存,消息队列,信号量内核数据结构
-
共享内存
-
消息队列:
-
信号量
通过观察我们发现,system V标准的通信机制其内核数据结构都是内嵌了一个结构体struct ipc_perm。
如果我们有一个数组,该数组是一个结构体指针数组struct ipc_perm* array[],那么这个结构体里面存放的是这三中机制任意一种。只要我们拿出数组中的任何一个元素,对这个指针进行类型转换,转换成对应的struct shmid_ds * ,struct msqid_ds *,struct semid_ds *,然后就能访问对应的内核数据结构保存到信息。
如果我们把struct ipc_perm看作一个父类,把struct shmid_ds ,struct msqid_ds ,struct semid_ds 看作子类。我们就实现了通过父类指针的方式访问对应子类,这就是用C语言实现的多态!