详细的原理内容就不愿在这里赘述了,网上有很多解释,可以参照:
https://www.cnblogs.com/wuyepeng/p/9748889.html(共享内存的物理存储与详细使用)
https://blog.csdn.net/guoping16/article/details/6584058(Linux共享内存的各系统调用)
这里我会用几个实例具体讲解一下更务实的使用:
程序A
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#include<string.h>
#define F(i,a,b) for (int i=a;i<=b;i++)
int main()
{
pid_t pid;
pid = fork();
if(pid){
//father process
int shmId = shmget(12138, 50000, 0666 | IPC_CREAT);
char* pshm = shmat(shmId, NULL, 0);
while(1){
char s[50000];
scanf("%s", s);
//memcpy(pshm, s, 50000);
F(i,0,strlen(s)-1){
pshm[i] = s[i];
}
if (pshm[0]=='E'){
shmdt(pshm);
break;
}
}
}else{
//son process
int shmId = shmget(12138, 50000, 0666 | IPC_CREAT);
char* pshm = shmat(shmId, NULL, 0);
while(1){
if (pshm[0]!=0){
if(pshm[0]=='E'){
shmdt(pshm);
break;
}
printf("!!! Readed info %s\n", pshm);
memset(pshm, 0, 50000);
}
}
}
return 0;
}
在这个程序中,我将创建两个进程(父子进程)
进行基于共享内存的进程间通信
首先看父进程中,第一个函数就是 创建/获取
到一片共享内存的系统调用
shmget(12138, 50000, 0666 | IPC_CREATE)
这里第一个参数12138
,为用户自定义的标识数值,称为共享内存的“键Key“。这样不同的进程通过这个键就可以找到指定的那一片共享内存了
第二个参数50000
,是使用这片共享内存的多大的空间,单位为字节
第三个参数为权限设定。0666
、0777
这样的数字序列在Linux权限设定中有着特定的意义:
0~7
有8种情况,对于三位二进制位000~111
,第一个为读权限,第二个为写权限,第三个为执行权限。所以说数字6
即110
代表可读可写不可执行。连续的三个6
表示对三种用户组(拥有者,同组用户,其余用户)我们这里只用得着对共享内存读写,且所有用户可用,故参数为0666
;
后面|
一个IPC_CREATE
表明如果这个共享内存不存在的话,就创建。
最后的返回值为这片共享内存的ID,后一个函数shmat就是根据此ID获取到这片共享内存的首地址
父进程的主任务就是接受用户输入,然后写入到共享内存中,分享给子进程
当输入值‘E’时退出。
其中有个shmdt()
函数,功能为告知共享内存断开链接,从表现上应该是为了程序的完整性安全性。因为即使所有程序都断开与已经被创建的共享内存的链接,共享内存空间也会存在除非被显示的关闭。
子进程的任务也很简单,一旦发现共享内存中内容非空,就读取出来,输出。然后清空之。
在读到‘E’时退出。
运行结果如图:
可见发生了很奇怪的问题,前两行输入明明是正确的,为什么后两次包含空格的输入就发生了很严重的bug?
显然首先代码上的原因是scanf函数读取字符串时,遇到空格会停止。
因此父进程总是一个词一个词地读取写入。
而由于操作系统在其中的调度,父进程可能写入完其中一个词,突然就切换成子进程运行,子进程就读取出了父进程刚刚写好的词儿。后续调度父进程写入,又可能被子进程给清空。
总之,就是并发访问的问题
此外,还有共享内存生命周期的问题
下面我们分别解决共享内存生命周期问题和基于信号量解决并发访问的问题:
当我们第二次运行时可能会发生严重的bug,而且使用命令ipcs -m
可以看到共享内存还存在着即使创建之的程序已经结束。
因为共享内存是由系统进行维护的,我们想要真正关闭它,必须显式指定
通过ipcrm -m [shmid]
可以将之删去,随后再运行就没有bug了
在代码中最好也用完即删,防止出现安全问题:
if (pshm[0]=='E'){
shmdt(pshm);
int status;
wait(&status);
shmctl(shmId, IPC_RMID, NULL);
break;
}
在父进程中,等待子进程断开链接后,使用shmctl()
函数,指定共享内存ID,使用IPC_RMID
参数将至彻底关闭
程序B:共享内存中放一个信号量实现并发访问!
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#include<string.h>
#include<sys/wait.h>
#include<semaphore.h>
#define F(i,a,b) for (int i=a;i<=b;i++)
int main()
{
pid_t pid;
pid = fork();
if(pid){
//father process
int shmSemId = shmget(12139, sizeof(sem_t), 0666 | IPC_CREAT);
sem_t* semP = shmat(shmSemId, NULL, 0);
sem_init(semP,1,1);
int shmId = shmget(12138, 50000, 0666 | IPC_CREAT);
char* pshm = shmat(shmId, NULL, 0);
while(1){
char s[50000];
scanf("%s",s);
sem_wait(semP);
F(i,0,strlen(s)-1){
pshm[i]=s[i];
}
sem_post(semP);
if (pshm[0]=='E'){
shmdt(pshm);
shmdt(semP);
int status;
wait(&status);
shmctl(shmSemId, IPC_RMID, NULL);
shmctl(shmId, IPC_RMID, NULL);
break;
}
}
}else{
//son process
sleep(2);
int shmSemId = shmget(12139, sizeof(sem_t), 0666 | IPC_CREAT);
sem_t* semP = shmat(shmSemId, NULL, 0);
int shmId = shmget(12138, 50000, 0666 | IPC_CREAT);
char* pshm = shmat(shmId, NULL, 0);
while(1){
sem_wait(semP);
if (pshm[0]!=0){
if(pshm[0]=='E'){
shmdt(semP);
shmdt(pshm);
break;
}
printf("!!! Readed info %s\n", pshm);
pshm[0] = 0;
}
sem_post(semP);
}
}
return 0;
}
这里注意我们的信号量在被不同进程调用了,所以init时把第二个参数变成1(见第二章的博客)最后用gcc name.c -o m -pthread
链接编译
由于水平有限,上述程序还有不安全的地方,但为了防止逻辑过于混乱,暂且忽略(忽略也是解决问题的一种方法嘛!),爱钻研的同学可以继续思考喔