目录
实现原理
使用系统调用创建共享内存
使用shmget函数创建共享内存:
使用shmat函数将共享内挂接到进程地址空间的共享区
使用shmdt函数取消共享内存与进程地址空间的关联
编辑
使用shmctl函数释放共享内存
共享内存的属性
System V消息队列 (了解)
System V信号量
理解信号量
这里我们知道进程间通信的本质是: 先让不同的进程看到同一份资源!!!
实现原理
这里我们知道每一个进程都要有地址空间,通过页表映射到内存中。
这里是由操作系统创建一块内存,然后通过页表将其映射到进程地址空间中的共享区上。然后给应用层返回这块内存的起始地址(映射到共享区中的起始地址)。当我们拿到内存的起始地址,就可以在上面进行读/写操作。
如果要释放共享内存,我们要将它与所有的进程去关联(将共享内存在页表里的映射关系进行删除),然后由操作系统释放内存。
因此这里我们在使用共享内存实现进程间通信的步骤大致为:
- 在内存中开辟一块共享内存。
- 将共享内存通过页表映射到进程地址空间中的共享区上,获取共享内存的起始地址,然后就可以对共享内存进行读/写操作,实现进程间的通信。
- 当没有一个进程与共享内存相关联时,共享内存就会被操作自动释放掉。
多个进程之间可能会有多个进程通信,因此在内存中会存在多个共享内存,操作系统为了管理共享内存创建一个struct XXX的结构体,里面存储着各种属性等。将他们用链表的方式管理起来。
注:上述所有都是内核级别的操作,因此要有操作系统完成,因此我们就需要调用一系列有关的系统调用接口。
共享内存时最快的IPC形式,一旦这样的内存空间映射到地址空间中,这些进程间传递的数据就可以直接写道共享内存中。而不是像管道一样先写到语言的缓冲区里,再从语言缓存区里调用系统调用拷贝到内存中。
使用系统调用创建共享内存
使用shmget函数创建共享内存:
作用:
在内存中申请一块共享内存。
第一个参数:
- key是一个数字,这个数字是几并不重要,关键在于它必须在内核中具有唯一性,能让不同的进程进行唯一标识。
key可以唯一标识的共享内存的原因:
这里我们知道共享内存会被struct XXX的结构体用链表管理起来,在结构体里有一个属性就是来记录key的,当我们使用shmget函数时,它会拿着key到管理的链表中找是否存在key,若不存在就创建共享内存,然后赋值给结构体里的key。若找与其相同的key值,看第三个参数,是直接获取该共享内存还是报错返回。
- 第一个进程可以通过key创建一个共享内存,第二之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了!
- 对于一个已经创建好的共享内存,key在共享内存的描述对象中。
- key是具有唯一性的,但是key是我们手动传参的,那么这个key我们是怎么生成的呢?
这里如果我们由我们给key赋值可能会导致各种问题,因此这里设置了一个系统调用来给key赋值:
这里,它是一个算法,它拿着pathname和proj_id进行数值计算,得到一个冲突概率特别小的key值,(理论上pathname和proj_id可以由用户随便写)如果发生冲突,只需要调整一下即可。
- 通过用户自主设定key值,让通信的进程使用同一个key连接一个共享内存(实现不同进程看见同一份资源)
第二参数:指定共享内存的大小,单位:字节。
第三个参数:这里操作系统指定了具体要填什么,这里我们介绍最主要的三个。
- IPC_CREATE:当进程创建一个共享内存时,若不存在直接创建;若存在,直接获取。一般单独使用。
- IPC_EXCL:常与IPC_CREATE一起使用,即:IPC_CRETE | IPC_EXCL : 作用为:如果申请的的共享内存不存在,就创建;存在,报错返回。这样可以确保我们获取的共享内存一定是一个新的。
- flags :设置共享内存的权限。
这里我们虽然知道了共享内存实现的原理,但是我们并不了解其本身,这里大家可能会疑问:
怎么保证让不同的进程看到同一个共享内存呢?
怎么知道这个共享内存是存在还是不存在呢?
参考上面对key的理解。
返回值:创建成功,返回共享内存的标识符。若失败返回-1,错误码表示失败的原因。
key与shmid的返回值是一样的东西吗?
不是的。key是操作系统内表标定唯一性的。
shmid的返回值只在你的进程内,用来标识资源的唯一性。
在用key创建完之后就不会再用key了,接下来都是用的shmid。
comm3.hpp文件
#pragma once#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<string>
using namespace std;const int size=4096;
const char* pathname="/home/wzy/进程间的通信/共享内存";const int proj=0x6668;key_t Getkey()
{key_t key=ftok(pathname,proj);if(key<0){cout<<"Getkey failed"<<endl;exit(1);}return key;
}int GetShm()
{key_t key=Getkey();//获得key值int shmid=shmget(key,size,IPC_CREAT|IPC_EXCL|0664);//创建新的共享内存if(shmid<0){cout<<"create share memory failed"<<endl;exit(2);}cout<<"create share memory is success"<<endl;return shmid;
}
processa.cc文件
#include"comm3.hpp"int main()
{int shmid=GetShm();cout<<shmid<<endl;return 0;
}
我们可以发现第一次创建共享内存成功,第二次失败了。
这是因为第一次我们创建了共享内存,当进程退出时,共享内存没有被操作系统自动释放。由于我们在使用shmget函数的第三个参数中由IPC_EXCL这个选项,所以报错。
这里想要查看内存中的共享内存,我们可以使用指令:
ipcs -m
这里perms为共享内存的权限默认为0,需要我们在自己设置;bytes为共享内存的大小,建设置为4096字节的整数倍(因为在Linux上,是以4096字节为单位开空间),如果你设置大小为4097,这里会给你开辟4096*2的空间,但是你只能使用4097;nattch为共享内存与进程的关联数默认。
如果想要删除共享内存,可以使用指令:
ipcrm -m shmid(共享内存标识符)
这里如果我们想要程序再次运行,我们必须要删除第一次创建的共享内存:
因此这里我们可以得出结论:
共享内存的声明周期时随内核的!
用户不主动关闭,共享内存会一直存在(除非内核重启或用户手动释放)。
使用shmat函数将共享内挂接到进程地址空间的共享区
- 这里第一个参数:为共享内存标识符shmid,为shmget函数成功调用的返回值。
- 第二个参数:为你想让共享内存挂接到共享区的那个位置。这里由于我们不清楚,所以我们通常将它置为nullptr,交由操作系统决定。
- 第三个参数:这里我们知道我们给共享内存设置了权限,这里是为了对进程进一步设置权限,这里我们并不关心,我们设置为默认的0.
- 返回值:成功:为共享内存挂接到地址空间共享区里的起始地址。
失败为(void) -1。
这里返回值与malloc函数的返回值类似,在未来你想怎么样使用共享内存,就强转成什么类型。如你想使用int,就强转成int *。
#include"comm3.hpp"int main()
{int shmid=GetShm();sleep(5);char* s=(char*)shmat(shmid,nullptr,0);cout<<"共享内存成功挂接到地址空间"<<endl;sleep(5);return 0;
}
使用shmdt函数取消共享内存与进程地址空间的关联
这里参数为shmat函数的返回值,即:共享内存在地址空间共享区的起始地址。
使用shmctl函数释放共享内存
这里第三个参数是:一个结构体,里面记录着共享内存的各种属性,类似于内核中共享内存的struct 结构体。
第二个参数:操作系统帮我们设定了好:
这里我们讲一下:IPC_STAT ,IPC_SET与IPC_RMID。
这里当我们使用这些系统调用函数后,使进程可以看到共享内存。那么如何使用共享内存进行通信呢?
在使用shmat函数时,我们获取了在共享区的起始地址,因此这里我们可以使用解引或数组下标的方式对共享内存进行读写操作,从而实现了进程间的通信。
使用共享内存,实现进程键的通信:一个进行发送信息,一个进程接受信息。
comm3.h
#pragma once#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<string>
using namespace std;const int size=4096;
const char* pathname="/home/wzy/进程间的通信/共享内存";const int proj=0x6668;key_t Getkey()
{key_t key=ftok(pathname,proj);if(key<0){cout<<"Getkey failed"<<endl;exit(1);}return key;
}int GetShm()
{key_t key=Getkey();//获得key值int shmid=shmget(key,size,IPC_CREAT|IPC_EXCL|0664);//创建新的共享内存if(shmid<0){cout<<"create share memory failed"<<endl;exit(2);}cout<<"create share memory is success"<<endl;return shmid;
}
processa.cc
#include"comm3.hpp"int main()
{int shmid=GetShm();char* s=(char*)shmat(shmid,nullptr,0);cout<<"共享内存成功挂接到地址空间"<<endl;while(true){cout<<"processa get # "<<s<<endl;sleep(1);}shmdt(s);cout<<"解除共享内存与地址空间的关联"<<endl;sleep(5);shmctl(shmid,IPC_RMID,nullptr);cout<<"删除共享内存"<<endl;return 0;
}
processb.cc
#include"comm3.hpp"int main()
{key_t key=ftok(pathname,proj);int shmid=shmget(key,size,IPC_CREAT|0666);cout<<"获取共享内存"<<endl;char* s=(char*)shmat(shmid,nullptr,0);cout<<"将共享内存进行挂接"<<endl;while(true){cout<<"processb say# ";fgets(s,sizeof(s),stdin);}return 0;
}
在我们执行代码的时候发现进行读操作的进程并不会像管道那样同步。这里如果我们想要实现共享内存的同步,我们可以利用管道的同步去实现:
创建一个命名管道,然后在写进程中向管道内写入一个数据,在读进程中读一个数据。这样在读进程中就会阻塞到读操作,一直等待写进程向管道中写入文件。
注意:写进程中向管道中写入数据,在向共享内存中写入数据之后。
读进程中读取数据,要在读共享内存之前。 这里为了好看我对函数对了一定的封装。
comm.hpp
#pragma once#include<iostream>
#include<string>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/ipc.h>
#include<sys/shm.h>const std::string pathname="/home/wzy/进程间的通信/共享内存";
const int proj_id=0x11223344;const std::string filename="fifo";//共享内存的大小,强烈建议设置为n*4096
const int size=4096;key_t Getkey()//获得key值
{key_t key=ftok(pathname.c_str(),proj_id);if(key<0){std::cerr<<"errno:"<<errno<<", errstring: "<<strerror(errno)<<std::endl;exit(1);}return key;
}std::string ToHex(int id)
{char buffer[1024];snprintf(buffer,sizeof(buffer),"0x%x",id);return buffer;
}int CreateShmHelper(key_t key,int flag)
{int shmid=shmget(key,size,flag);if(shmid<0){std::cout<<"errno"<<errno<<"errstring"<<strerror(errno)<<std::endl;exit(2);}return shmid;
}int CreateShm(key_t key)//创建共享内存
{return CreateShmHelper(key,IPC_CREAT|IPC_EXCL|0644);
}int GetShm(key_t key)//获取共享内存
{return CreateShmHelper(key,IPC_CREAT);
}bool MakeFifo()//创建管道
{int n=mkfifo(filename.c_str(),0666);if(n<0){std::cerr<<"errno: "<<errno<<"errstring"<<strerror(errno)<<std::endl;return false;}std::cout<<"mkfifo success...read"<<std::endl;return true;
}
server.cc
#include<iostream>
#include"comm.hpp"
#include"unistd.h"class Init
{
public:Init(){bool r=MakeFifo();if(!r){return ;}key_t key = Getkey();std::cout << "key: " << ToHex(key) << std::endl;shmid=CreateShm(key);std::cout<<"开始将shm映射到进程的地址空间中"<<std::endl;s=(char*)shmat(shmid,nullptr,0);fd=open(filename.c_str(),O_RDONLY);}~Init(){close(fd);shmdt(s);std::cout<<"开始将shm从进程的地址空间中移除"<<std::endl;std::cout<<"开始将shm从OS中删除"<<std::endl;}
public:int shmid;int fd;char* s;};int main()
{Init init;//类的默认初始化会创建共享内存与管道,记录shmid,记录管道的文件描述符,和共享内存在共享区的首地址。while(true){int code=0;ssize_t n=read(init.fd,&code,sizeof(code));//读管道内容if(n>0){std::cout<<"共享内存的内容: "<<init.s<<std::endl;//读共享内存sleep(1);}else if(n==0){break;}}sleep(10);return 0;
}
client.cc
#include<iostream>
#include<cstring>
#include<unistd.h>
#include"comm.hpp"int main()
{//关联共享内存与管道key_t key=Getkey();int shmid=GetShm(key);char* s=(char*)shmat(shmid,nullptr,0);std::cout<<"attach shm done"<<std::endl;int fd=open(filename.c_str(),O_WRONLY);char c='a';for(;c<'z';c++){s[c-'a']=c;std::cout<<"write: "<<c<<"done"<<std::endl;//写共享内存sleep(1);//通知对方int code=1;write(fd,&code,sizeof(code));//写管道}shmdt(s);std::cout<<"detach shm done"<<std::endl;close(fd);return 0;
}
这样就做到了共享内存之间实现同步性。
共享内存的属性
这里使用指令:
man shmctl
我们可以看到两个数据结构:
获取共享内存属性:
#include"comm3.hpp"int main()
{int shmid=GetShm();char* s=(char*)shmat(shmid,nullptr,0);cout<<"共享内存成功挂接到地址空间"<<endl;struct shmid_ds shmds;shmctl(shmid,IPC_STAT,&shmds);cout<<"shm size: "<<shmds.shm_segsz<<endl;cout<<"shm nattch: "<<shmds.shm_nattch<<endl;cout<<"shm _key: "<<shmds.shm_perm.__key<<endl;cout<<"shm mode: "<<shmds.shm_perm.mode<<endl;while(1)sleep(1);shmdt(s);cout<<"解除共享内存与地址空间的关联"<<endl;sleep(5);shmctl(shmid,IPC_RMID,nullptr);cout<<"删除共享内存"<<endl;return 0;
}
注:这里struct shmid_ds操作系统存在这个结构体,不需要我们定义。
System V消息队列 (了解)
- 消息队列(先进先出)提供了从一个进程向另一个进程发送一块数据的方法。
- 每个数据块都被认为是一个类型,接收者进程接收的数据块可以由不同类型。
- IPC资源必须删除,否则不会自动清楚,除非重启,所以System V IPC资源的声明周期随内核。
- 常用系统调用ftok,msgget(创建消息队列),msgctl(控制消息队列),msgrcv(从消息队列中读取数据),msgsnd(向消息队列发送数据)
ipcs -q (查看消息队列)ipcrm -q 消息队标识符 (删除指定的消息队列)
System V信号量
这里我们知道进程通信的本质是让不同进程看见同一份资源。
但是不同这里也存在着些许问题:在管道中A进程写入一部分,就被B进程拿走,导致双方发和收数据不完整。
这是我们就要通过互斥来解决问题。(互斥:任何时刻,只允许一个执行流访问共享资源)
这里我们把任何时刻只允许一个执行流访问的资源叫做临界资源。
访问临界资源的代码,叫做临界代码。
这里我们可以通过信号量来实现互斥。
理解信号量
这里以电影票为例:
很多人都会看见电影,所以电影院宏观意义上就是临界资源。
里面的座位就是划分成的小资源,你买了票这个作为就是你的了,就不会在属于别人的。
这里买票的本质就是对资源的预定。
每个进程进入临界区,访问临界资源中的一部分资源,不能直接就去访问,而是先申请资源。信号量本质就是一个计数器,类似于int count=你,但也不太准确。
进程获取资源的步骤:
- 申请信号量,申请成功,就会在临界区为该进程预留一份资源,然后把信号量减1。信号量为0时,进程无法申请成功,只能阻塞等待其他进程退出释放资源,才能再次申请。
- 访问临界资源,就是执行临界区代码。
- 释放资源,信号量加1.
信号量就是对临界资源的预定机制!!!