共享内存是一种在多个进程之间进行进程间通信的机制。它允许多个进程访问相同的物理内存区域,从而实现高效的数据交换和通信。
因为进程具有独立性(隔离性)
,内核数据结构包括对应的代码、数据与页表都是独立的。OS系统为了让进程间进行通信,必须让不同的进程看到同一份资源。所以共享内存的原理如下:
1.申请一块空间
2.将创建好的内存映射进进程的地址空间。
共享内存让不同的进程看到同一份的资源就是在物理内存上申请一块内存空间,将创建好的内存分别与各个进程的页表之间建立映射,然后在虚拟地址空间中将虚拟地址填充到各自页表的对应位置,建立起物理地址与虚拟地址的联系。
我们把创建好的内存称为共享内存
,把进程和共享内存建立映射关系的操作称为挂接
,把取消进程和内存的映射关系称为去关联
,把释放内存称为释放共享内存
。
共享内存的建立: 在物理内存当中申请共享内存空间;将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放: 共享内存与地址空间去关联,即取消映射关系;释放共享内存空间,即将物理内存归还给系统。
对共享内存的理解:
共享内存不属于通信的任意一个进程,其属于操作系统,由操作系统所管理。
管道的本质是文件,操作系统已经有相应的内核数据结构来管理文件,因此不需要再去设计新的内核数据结构去管理管道。而共享内存是专门为了进程间通信而设计的,操作系统可能会有很多共享内存,那么操作系统就需要将这些共享内存管理起来。
管理的方式是先描述再组织,那么共享内存就等于共享内存块加上共享内存对应的内核数据结构。
对共享内存的修改包括对属性的修改和对内容的修改。
shmget:
用来创建或者获取共享内存。失败时返回-1。
参数:
shmflg: 通常被设置成两个选项: IPC_CREAT、 IPC_EXCL
size: 共享内存的大小
key: 共享内存字段的名字,通信的进程需要通过该key值找到同一个共享内存,从而进行通信。因此key能保证多个进程看到同一份共享内存,能进行唯一性标识。
ftok:
生成key。失败时返回-1。
ftok函数将pathname和project id 经过一定的算法转换成 key,pathname必须存在,projectid不能为0。
OS一定会存在很多的共享内存,共享内存本质就是在内存中申请一块空间,而key能进行唯一标识。OS申请的,自然要做管理,共享内存也是如此,如何管理:先描述,在组织。所以共享内存=物理内存块+共享内存的相关属性。进程如果在内存中创建了共享内存,为了让共享内存在系统中保证唯一的,通过key来进行标识,只要让另一个进程也看到同一个key。
shmat:
将共享内存段连接到进程地址空间(建立页表映射关系)。成功返回一个指针,指向共享内存第一个字节;失败返回 (void*) -1。
参数:
shmid: 共享内存的标识符。
shmaddr: 指定连接地址,如果设置为nullptr,则让操作系统指定连接到合适的地址上。
shmflg: 它的两个可能取值是 SHM_RND 和 SHM_RDONLY。shmflg 等于 SHM_RDONLY 时,表示连接操作用来只读共享内存。
shmdt:
将共享内存段与当前进程脱离。成功返回0,失败返回-1。
参数:
shmaddr: 由shmat函数所返回的指针。
这里我们需要注意:将共享内存和当前进程脱离不等于删除共享内存。
shmctl:
用语控制共享内存。
参数:
shmid: 由shmget函数返回的共享内存标识符。
cmd: 将要采取的动作。(有三个可以选择)
buf: 为指向一个保存着共享内存的模式状态和访问权限的数据结构。不关心共享内存的内核数据结构时,buf可以设置为nullptr。
这里我们需要注意的是:当进程运行结束时,进程创建的共享内存还会存在,这是因为system V IPC资源的生命周期是随其内核的。其内核可以通过代码删除(
shmctl
函数),也可以通过ipcrm -m shmid
指令手动删除。使用ipcs -m
指令可以查看系统中已经创建好的共享内存。
makefile
.PHONY:all
all: shmclient shmserver
shmclient:client.cc
g++ -o $@ $^ -std=c++11
shmserver:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shmclient shmserver
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP_
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATHNAME "."
#define PROJID 0x6666
const int gsize = 4096;
//获取key
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;
}
//共享内存公共函数
static int createShmHelper(key_t k, size_t size, int flag)
{
int shmid = shmget(k, size, flag);
if(shmid == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
//创建共享内存
int createShm(key_t k, size_t size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
int getShm(key_t k, size_t size)
{
return createShmHelper(k, size, IPC_CREAT);
}
//关联进程
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;
}
//释放共享内存
void delShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t)
:_type(t)
{
key_t key = getKey();
if(_type == SERVER)
_shmid = createShm(key, gsize);
else
_shmid = getShm(key, gsize);
_start = attachShm(_shmid);
}
char* getChar(){return _start;}
~Init()
{
detachShm(_start);
if(_type == SERVER) delShm(_shmid);
}
private:
char* _start;
int _type; // server or client
int _shmid;
};
#endif
服务端:server.cc
#include "comm.hpp"
int main()
{
Init init(SERVER);
char* start = init.getChar();
int n = 0;
while(n <= 35)
{
cout <<"client -> server# "<< start << endl;
sleep(1);
n++;
}
// // 1. 创建key
// key_t k = getKey();
// cout << "server:" << toHex(k) << endl;
// // 2. 创建共享内存
// int shmid = createShm(k, gsize);
// cout << "shmid:" << shmid << endl;
// sleep(8);
// // 3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(20);
// // 4. 将自己和共享内存去关联
// detachShm(start);
// sleep(3);
// 5. 删除共享内存
// delShm(shmid);
return 0;
}
客户端:client
#include "comm.hpp"
int main()
{
Init init(CLIENT);
char *start = init.getChar();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
// // 1. 获取key
// key_t k = getKey();
// cout << "client:" << toHex(k) << endl;
// // 2. 获取共享内存
// int shmid = getShm(k, gsize);
// cout << "shmid:" << shmid << endl;
// // 3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(15);
// // 4. 将自己和共享内存去关联
// detachShm(start);
return 0;
}
优点:
下面我们来比对一下共享内存和管道:
管道通信的拷贝:
C++输入设备把数据拷贝到cin或者stdin文件缓存区不考虑,这里总共进行了4次拷贝。
共享内存通信的拷贝:
直接从输入到共享内存,从共享内存到输出。
缺点:
以共享内存的方式进行进程间通信缺乏访问控制,会带来同步问题!比如:写端还没将全部数据写入,读端就已经开始读取了,这将会带来巨大的问题!
消息队列 是OS提供的内核级队列
,消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
常用系统调用:ftok,msgget(创建消息队列),msgctl(控制消息队列),msgsnd(向消息队列发送数据),msgrcv(从消息队列中读取数据)等。
下面,我们先来引出几个概念:
"公共资源:"
被多个进程同时访问的资源,访问没有保护的公共资源:数据不一致问题。要让不同的进程看到同一份资源是为了通信,通信是为了让进程间实现协同,而进程之间具有独立性,所以为了解决独立性问题要让进程看到同一份资源,但是会导致数据不一致的问题。
"互斥":
任何一个时刻,都只允许一个执行流在进行共享资源的访问。各进程间竞争使用这些资源,竞争的这种关系为进程的互斥。
"临界资源":
任何一个时刻,都只允许一个执行流在进行访问的共享资源,叫做临界资源。
"临界区":
临界资源是需要通过代码访问的,凡是访问临界资源的代码,叫做临界区。
"原子性":
要么不做、要么做完,只有两种确定状态的属性,叫做原子性。
任何一个执行流,想访问临界资源中的一个子资源时,不能直接访问。得先申请信号量,信号量/信号灯
本质是一个计数器,描述资源数量的计数器。 信号量是对临界资源的预定机制。
我们可以发现,共享内存、消息队列、信号量接口相似度非常高,获取与删除,都是system V标准的进程间通信。
OS如何管理:先描述,在组织,对相关资源的内核数据结构做管理,对于共享内存、消息队列、信号量的第一个成员都是ipc_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 */
};
虽然内部的属性差别很大,但是维护它们的数据结构的第一个成员都是ipc_perm类型的成员变量,都可以通过key来标识唯一性。这样设计的好处:在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。((struct shmid_ds*)perms[0],强转,此时就可以访问其他剩下的属性)