先在内存中申请空间,然后将这段空间映射到不同进程的地址空间中,这就叫做共享内存;
一般都是映射在进程的堆栈之间的共享区;
共享内存不属于任何一个进程,它属于操作系统;
操作系统对共享内存的管理,是先描述再组织,先通过内核数据结构描述共享内存的属性信息,再将它们组织起来;
共享内存 = 共享内存块 + 对应的共享内存的内核数据结构;
共享区属于用户空间,不用经过系统调用,直接可以访问;
双方进程如果要通信,直接进行内存级的读写即可;
之前的管道是一种文件。是OS中的一种数据结构,所以用户无权直接访问,需要进行系统调用;
.PHONY:all
all:shmClient shmServer
shmServer:shmServer.cc
g++ -o $@ $^ -std=c++11
shmClient:shmClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
claen:
rm -f shmServer shmClient
#ifndef _LOG_H_
#define _LOG_H_
#include
#include
#define DeBug 0
#define Notice 1
#define Waring 2
#define Error 3
const std::string msg[] = {
"DeBug",
"Notice",
"Waring",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
#ifndef _COMM_H_
#define _COMM_H_
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/usr/lmx" //路径,一定保证有权限
#define PROJ_ID 0X66
#define SHM_SIZE 4096 //共享内存大小,最好是页(4byte)的整数倍
#endif
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), “0x%x”, k);
return buffer;
}
int main()
{
// 1.创建公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
perror(“ftok”);
exit(1);
}
Log("creat key done", DeBug) << "server key : " << TransToHex(key) << endl;
// 2.创建共享内存 -- 建议创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(2);
}
Log("shm creat done", DeBug) << "shmid : " << shmid << endl;
//3.将指定的共享内存,挂接到自己的地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", DeBug) << "shmid : " << shmid << endl;
//这里就是通信逻辑了
//将共享内存看作一个大字符串
//shmaddr就是这个字符串的起始地址
for(;;)
{
printf("%s\n", shmaddr);//不断打印这个字符串的内容
if(strcmp(shmaddr, "quit") == 0)
{
break;
}
sleep(1);
}
//4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
if(n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "shmid : " << shmid << endl;
//5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
if(n == -1)
{
perror("shmctl");
exit(4);
}
Log("delete shm done", DeBug) << "shmid : " << shmid << endl;
return 0;
}
注意:
(1)
要保证创建出唯一的key;*
(2)
创建全新的共享内存,0666代表共享内存的权限;
共享内存的大小最好是页的整数倍,否则会造成空间浪费,多开空间,但是没有权限访问;
第二次创建的时候,提示共享内存已存在;
(3)ipcs -m:查看共享内存信息;
ipcrm -m shmid:删除共享内存(不能用key删除)
共享内存的生命周期随内核;
与文件不一样,文件的生命周期,如果进程退出,没有其他进程再关联这个文件,那么就会被回收;
perms属性就是共享内存的权限,
(4)因此,当进程结束后,共享内存还存在,我们继续要删除它,使用系统接口:
shmctl:删除共享内存
(5)nattch属性是挂接的共享内存个数,共享内存创建好之后,需要挂接在自己的进程地址空间;
shmat:挂接共享内存
参数:
shmid:共享内存id
shmaddr:挂接虚拟地址,直接设为0,让os挂接
shmflg:挂接方式
返回值:成功返回共享内存addr虚拟地址,失败返回-1
使用:
将返回值作为共享内存的起始地址;
shmdt:去关联
参数:
shmaddr:共享内存地址
返回值:成功返回0,失败返回-1
#include "comm.hpp"
int main()
{
// 客户端也获取key
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
Log("creat key failed", Error) << "client key : " << key << endl;
exit(1);
}
Log("creat key done", DeBug) << "client key : " << key << endl;
// 获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid == -1)
{
Log("creat shm failed", Error) << "client key : " << key << endl;
exit(2);
}
Log("creat shm done", DeBug) << "client key : " << key << endl;
// 挂接共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << "client key : " << key << endl;
}
Log("attach shm done", DeBug) << "client key : " << key << endl;
// 使用
//client将共享内存看作一个char类型的buffer
//客户端从键盘读取消息,直接读到共享内存中
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = 0;
if(strcmp(shmaddr, "quit") == 0)//读到quit,客户端退出
{
break;
}
}
}
// char a = 'a';
// for(; a <= 'z'; a++)
// {
// //每一次都向shmaddr(共享内存的起始地址)写入
// snprintf(shmaddr, SHM_SIZE - 1,
// "hello server, 我是其他进程,我的pid: %d, inc: %c\n",
// getpid(), a);
// sleep(2);
// }
// 去关联
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "client key : " << key << endl;
// client不需要删除shm
return 0;
}
注意:
(1)共享内存的使用,直接将共享内存看作一个char类型的buffer,直接向里面写入数据
从stdin中键盘读取消息,直接读取到shmaddr这个地址,即共享内存的起始地址;
(3)共享内存只需要两次拷贝,从键盘输入的数据直接写入shm,这是一次拷贝,直接将shm的数据打印出来,这是第二次拷贝;
从上面的结果可以看出,即便是客户端还没有挂接共享内存,服务端就已经开始不停读取数据了,这就表明共享内存是不带访问控制的,会带来一定的并发问题;
然而,管道是自带访问控制的,我们可以利用管道通信来为共享内存添加访问控制;
comm.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/home/lmx" // 路径,一定保证有权限
#define PROJ_ID 0X66
#define SHM_SIZE 4096 // 共享内存大小,最好是页(4byte)的整数倍
#define FIFO_NAME "./fifo"
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("creat fifo succsee", Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);
Log("remove fifo succsee", Notice) << "\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
Log("waiting...", Notice) << "\n";
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
Log("aweaking...", Notice) << "\n";
}
void CloseFIFO(int fd)
{
close(fd);
}
#endif
注:
(1)创建了一个类,类的构造函数有创建管道文件,一旦类实例化出对象,调用构造函数,就能够创建一个管道文件,后面就是对管道文件的读写控制了;
shmServer.cc
#include "comm.hpp"
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
int main()
{
Init init;
// 对应的程序在加载的时候,会自动构建全局变量,就要调用该类构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,会自动删除管道文件
// 1.创建公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
perror("ftok");
exit(1);
}
Log("creat key done", DeBug) << "server key : " << TransToHex(key) << endl;
// 2.创建共享内存 -- 建议创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(2);
}
Log("shm creat done", DeBug) << "shmid : " << shmid << endl;
// 3.将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm done", DeBug) << "shmid : " << shmid << endl;
// 这里就是通信逻辑了
// 将共享内存看作一个大字符串
// shmaddr就是这个字符串的起始地址
//使用管道进行访问控制
int fd = OpenFIFO(FIFO_NAME, READ);
for (;;)
{
Wait(fd);//等待客户端响应,
//使用管道文件的访问控制,如果客户端没有向管道内写入数据,那么该进程会一直阻塞
printf("%s\n", shmaddr); // 不断打印这个字符串的内容
if (strcmp(shmaddr, "quit") == 0)
{
break;
}
sleep(1);
}
CloseFIFO(fd);
// 4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "shmid : " << shmid << endl;
// 5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
if (n == -1)
{
perror("shmctl");
exit(4);
}
Log("delete shm done", DeBug) << "shmid : " << shmid << endl;
return 0;
}
注:
(1)在服务端先创建一个管道文件
(2)在读取共享内存中的数据前,先读取管道数据,看客户端是否响应;
shmClient.cc
#include "comm.hpp"
int main()
{
// 客户端也获取key
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
Log("creat key failed", Error) << "client key : " << key << endl;
exit(1);
}
Log("creat key done", DeBug) << "client key : " << key << endl;
// 获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid == -1)
{
Log("creat shm failed", Error) << "client key : " << key << endl;
exit(2);
}
Log("creat shm done", DeBug) << "client key : " << key << endl;
// 挂接共享内存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << "client key : " << key << endl;
}
Log("attach shm done", DeBug) << "client key : " << key << endl;
// 使用
//client将共享内存看作一个char类型的buffer
//客户端从键盘读取消息,直接读到共享内存中
//使用管道进行访问控制
int fd = OpenFIFO(FIFO_NAME, WRITE);
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = 0;
Signal(fd);//向管道写入数据
if(strcmp(shmaddr, "quit") == 0)//读到quit,客户端退出
{
break;
}
}
}
CloseFIFO(fd);
// 去关联
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "client key : " << key << endl;
// client不需要删除shm
return 0;
}
注:
(1)在向共享内存写入数据前,先向管道写入信号,表明客户端准备写入数据,唤醒服务端:
运行结果:
当运行服务端,但是客户端未响应时,服务端会等待客户端响应,进程阻塞;
当客户端响应时,服务端会被唤醒,读取共享内存中的数据:
退出:
基于对共享内存的理解:
为了让进程间通信,让不同的进程之间,看到同一份资源,我们之前讲的所有的进程间通信都是基于这种方式;
而让不同的进程看到同一份资源,比如共享内存,也带来了一些时序问题,会造成数据的不一致
概念
(1)临界资源:多个进程(执行流)看到的公共的一份资源;
(2)临界区:自己的进程,访问临界资源的代码;
(3)互斥:为了更好的进行临界区的维护,可以让多执行流在任何时刻,都只能有一个进程进入临界区;
(4)原子性:要么不做,要么做完,没有中间状态;
我们平常看电影前,会先买票,电影院中的座位就相当于资源,当你买了票,这个座位就真正属于你,买票的本质就是对座位的预定机制;
对于进程来说,访问临界资源中的一部分,不能让进程直接去使用临界资源,需要先申请信号量;
信号量的本质是一个计数器;
申请信号量:
(1)申请信号量的本质,就是让信号量技术器 - -;
(2)申请信号量成功,临界资源内部,一定给进程预留了需要的资源,申请信号量的本质就是对临界资源的一种预定机制;
释放信号量:
释放信号量就是将计数器++;
如果将信号量计数器设为全局变量(整数n,存放在共享内存),让多个进程看到同一个全局变量,大家都能够进行信号量的申请,这样是不行的;
因为CPU在执行n++这个指令的时候,其实执行了三条语句:
(1)将内存中的数据加载到CPU内的寄存器(读指令);
(2)n–(执行指令);
(3)将CPU修改完毕的值再写入内存(写指令);
而执行流在执行的时候,在任何时刻都是能被切换的;
例如:
如果信号量刚开始是5,client在申请信号量的时候,第一步就被切换了,寄存器里的数据保存为上下文数据,
由server申请信号量,如果server将信号量减到2了,此时server被切换,client回来
client回来的时候就会将上下文数据恢复,将信号量恢复为5,再申请信号量,这时信号量就变为了4;
寄存器只有一套,被所有执行流共享,但是寄存器内的数据,属于每一个执行流,属于该执行流的上下文数据;
这样设计,会导致信号量是不安全的;
因此,申请和释放信号量这两个操作,必须是原子的;