让两个不同的进程看到同一个内存块(存在于物理内存当中)实现共享内存,看到同一份资源。
代码
//comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
// IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!
#define PATHNAME "."
#define PROJID 0x6666//保证客户端和服务端可以看到同一份资源
const int gsize = 4096; //共享内存的大小以page页(4KB) 为单位的
key_t getKey()//获得shmget的第一个参数key值
{
key_t k = ftok(PATHNAME, PROJID);
if(k == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x)//将key值转换成为16进制的字符串
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
static int createShmHelper(key_t k, int size, int flag)//加上static是为了让函数只在本文件里面有效,该函数创建的原因是createShm和getShm两个函数是比较相似的,所以统一一下。
{
{
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);//0666是创建共享内存时设置的权限操作,通过ipcs -m可以查看perms字段即可。
}
int getShm(key_t k, int size)//客户端接收共享内存
{
return createShmHelper(k, size, IPC_CREAT);
}
char* attachShm(int shmid)
{
char *start = (char*)shmat(shmid, nullptr, 0);//未来想将共享内存当成字符串来使用,所以强转称为char*,挂接到哪里由os来指定,第三个参数权限默认为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);//IPC_RMID是用来对shmid进行删除操作
assert(n != -1);
(void)n;
}
//下面的init类是对上面函数的封装,其实我们共享内存创建好了之后我们想要拿到的就是共享内存的其实地址,所以该类里面只需提供getstart这个函数,不用提供别的冗余的函数了。
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t):type(t)
{
key_t k = getKey();
if(type == SERVER) shmid = createShm(k, gsize);
else shmid = getShm(k, gsize);
start = attachShm(shmid);
}
char *getStart(){ 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"
#include <unistd.h>
int main()
{
Init init(SERVER);
char *start = init.getStart();
//我们在通信的时候,没有使用任何接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了。
//因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信速度最快的。
//共享内存没有任何的保护机制,其实就是同步互斥,这也是共享内存快速的另一个原因,现象是:client不加载的时候,我们运行server会发现server依旧会不断地读取共享内存的数据,只不过字符串置零读取的都是空而已。
//但是对于之前的管道,当client没有向管道中发送数据的时候对应的服务端也不会去读。
//为什么共享内存没有任何的保护机制呢?问题在于管道是通过系统接口(write和read)通信的,系统接口底层实现还干了许多事情,比如说检测管道里面是否由空间、是否有数据等等。但是共享内存是直接通信的。
int n = 0;
while(n <= 30)
{
cout <<"client -> server# "<< start << endl;
sleep(1);
n++;
}
// //1. 创建key
// key_t k = getKey();
// cout << "server key: " << toHex(k) << endl;
// //2. 创建共享内存
// int shmid = createShm(k, gsize);
// cout << "server shmid: " << shmid << endl;
// sleep(3);
// //3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(20);
// // 通信代码在这里!
// // 4. 将自己和共享内存去关联
// detachShm(start);
// sleep(3);
// struct shmid_ds ds;
// int n = shmctl(shmid, IPC_STAT, &ds);//IPC_STAT是将shmid对应的struct shmid_ds结构体的内容拷贝到ds指向的空间里面。这也说明了共享内存除了开辟对应的空间,我们还需要创立内核数据结构,里面包含了共享内存的各种属性信息。
// if(n != -1)
// {
// cout << "perm: " << toHex(ds.shm_perm.__key) << endl;//我需要对共享内存拥有读权限才可以查看这些字段的内容,所以在创建共享内存的时候我们需要进行一个权限的操作。
// cout << "creater pid: " << ds.shm_cpid << " : " << getpid() << endl;
// }
// ?. 删除共享内存
//delShm(shmid);
return 0;
}
//client.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init init(CLIENT);
char *start = init.getStart();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
// key_t k = getKey();
// cout << "client key: " << toHex(k) << endl;
// int shmid = getShm(k, gsize);
// cout << "client shmid: " << shmid << endl;
// //3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(15);
// // 4. 将自己和共享内存去关联
// detachShm(start);
return 0;
}
int shmget(key_t key, size_t size, int shmflg):获取共享内存函数,key值、
key_t ftok(const char *pathname, int proj_id):结合路径和项目id帮我们形成一个冲突概率非常低的key值
void *shmat(int shmid, const void *shmaddr, int shmflg):将共享内存和进程进行关联,进程创建共享内存也有可能时为别的进程创建的。shmaddr是说你将共享内存放到虚拟地址的那一块位置,其实这个值我们并不能直到,所以就直接设置成nullptr,让os自己去决定。 shmflg设置为0默认权限是可读可写。
int shmdt(const void *shmaddr):去关联,shmaddr就是shmat函数对应的返回值,卸载就是将映射关系去掉。
系统种可以用shm共享内存进行通信,是不是只能有一对进程来使用共享内存实现通信?当让可以, 所以在任何时刻可能会有多个共享内存被用来进行通信。所以系统中一定会同时存在很多shm, 最终操作系统一定要整体管理所有的共享内存,操作系统如何管理多个shm,那就是先描述再组织。所以共享内存不只是只要在内存中开辟空间即可,系统也要为了管理shm,构建对应的描述共享内存的结构体对象struct shm。 共享内存=共享内存的内核数据结构(struct shm)+真正开辟的空间。两个进程要进行通信,一定有一个式创建共享内存,一个是获取共享内存。服务端创建共享内存最好创建全新的。
创建共享内存的进程退出之后,但是我们发现共享内存还存在。通过ipcs -m(ipc是进程间通信inter process communication),-m是只看共享内存部分,我们可以查看已经存在的共享内存,根据key 和shmid可以看到进程退出依旧存在。perms是permission权限的缩写。nattch说明的是有几个进程和当前共享内存相关联。
ipcrm -m shmid:删除共享内存,shimid指代对应共享内存的shmid值。由上面知道每次共享内存操作完成之后删除共享内存是非常有必要的。共享内存的生命周期不随进程,随os。进程挂掉了,只要进程没有显式删掉共享内存,共享内存会一直存在,直到用指令、或者os的接口删掉共享内存,否则共享内存一直存在, 直到os重启方可。所以这也证明了共享内存和文件系统是两套机制,文件系统进程释放文件也会自动释放。
int shmctl(int shmid, int cmd, struct shmid_ds *buf):系统接口来删除共享内存,和上面的指令删除是不一样的。cmd是你想对当前共享内存做哪种操作。
一般进程间的通信要经过第一次拷贝从外设读到用户的buffer缓冲区,第二次拷贝从用户缓冲区write发送到管道当中,第三次拷贝read从管道中读到用户的buffer缓冲区中,第四次拷贝就是buffer刷新到显示器上。共享内存是第一次拷贝将数据放到共享内存当中,第二次直接中共享内存中读取数据就两次。
nattch选项是可以看到有几个进程和当前的共享内存相关联
共享内存的生命周期不随进程,而是随os。也就是如果创建共享内存了,哪怕该进程推出了共享内存还是存在,解决方法就是:1、用指令进行删除;2、调用系统调用接口。
信号量
我们把大家都能看到的资源叫做公共资源。任何一个时刻,都只允许一个执行流在进形共享资源的访问叫做加锁。将任何一个时刻都只允许一个执行流进行共享资源访问叫做互斥。我们把任何一个时刻只允许一个执行流进行访问的共享资源叫做临界资源。临界资源是通过代码访问的,凡是访问临界资源的代码叫做临界区。 通过代码进行加锁实际上是对临界区进行保护的,进临界区之前做加锁,出临界区做解锁就可以了。原子性:要么不做,要么做完,只有两种确定状态的属性。
信号量和信号灯的本质就是一个描述资源数量的计数器,有点像int count。任何一个执行流,像访问临界资源中的额一个子资源的时候,不能直接访问,得先申请信号资源,只要申请信号量成功,我们未来一定能够拿到一个子资源,此时count--。进入临界区,访问临界资源。当我们离开的时候,释放信号量资源,相当于让count++,只要将计数器增加,就表示将我们对应的资源进行了归还。所有的进程都必须先看到信号量,所以信号量就是共享资源,而信号量又是共享资源,信号量是用来保护共享资源的,此时信号量自己成了共享资源,那信号量保护人家(其它共享资源),那谁来保护信号量,这就要求信号量经过设计必须保证自己的++和--操作时原子性的,也就是要么不加减,要么加减工作已经完成了。我们将申请信号量称为p操作,释放信号量称为v操作。
信号量是资源的一种预定机制,信号量一定是另一种共享资源,为了保证自身操作的安全,它的操作必须是原子性的,它的操作又叫做原子性的pv操作。那么两个进程,能看到同一个int count吗?可以,但是我们想让不同的进程看到同一个计数器,不能发生写实拷贝,否则这就不是同一个计数器了,如果a进程有a进程的计数器,b进程有b进程的计数器 ,彼此之间互不影响,这是不合理的,所以我们将信号量归类为进程间的通信,信号量是不以传送数据为目的而用来协同两个进程工作的一种通信方案,也属于进程间通信的一种。如果计数器是1呢?那么现在很多进程都想要访问该资源,当其中一个进程申请到该信号量的时候,将计数器减减成为0,此时别的进程就不发在申请资源了,这就完成了二元信号互斥功能。互斥的本质就是就是将临界资源独立进行使用。
信号量的相关接口(认识一下就行)
int semget(key_t key, int nsems, int semflg):获取信号量函数,nsems指的是信号量集也就是允许一次申请多个信号量,多个信号量和单个信号量是几是两码事。sem是semaphore的意思。
查看信号量的指令:ipc -s
删除信号量的指令:ipcrm -s semid
int semctl(int semid, int semnum, int cmd, ...):因为一次允许我们创建多个信号量,semnum是对哪一个信号量进行操作。
int semop(int semid, struct sembuf *sops, unsigned nsops):对指定的信号进行pv操作
理解ipc
system V系统的ipc有消息队列、共享内存、信号量三种方式,如何对其进行管理呢?操作系统管理这些ipc资源并不是分开管理的,先描述,分别描述成为struct shmid_ds、struct semid_ds、struct msqid_ds结构体,再通过一个数组struct ipc_perm* ipc_id_arr[]来将内核中所有的ipc资源统一以数组的方式进行管理。结构体的地址和结构体首元素的地址在树值上是一样的。如果我们想通过该数组访问某个ipc中的除了struct ipc_perm的其它元素,可以通过((struct shmid_ds*)pc_id_arr[n])->other这种强转方式来实现访问,这样虽然ipc之间的数据结构存在差异,但是我们依然实现了统一的管理。而这就是多态,其中struct ipc_perm perm是基类