前言
我们知道,在Linux中,进程是相互独立存在的,不存在直接让进程之间互相通信的方式。但是如果我们能让不同进程之间见到同一块内存,也就是都能读写这片区域是不是就能够达到进程间通信呢?
事实证明确实如此。在之前我们可以利用管道来实现出进程之间的通信。(匿名管道和命名管道)但是似乎在传输过程中都是通过操作系统之手(系统调用)来完成的。那么进程之间能否拥有一块属于自己管辖的共享的内存呢?这篇文章就是关于System V共享内存方式实现进程间通信的学习笔记~
『但是呢,我遇到了美丽的天使』
上一篇Linux学习笔记传送门!
【Linux】静动态库的制作和使用_柒海啦的博客-CSDN博客
目录
一、理解一个共享内存
二、共享内存相关接口
1.ftok创建唯一key
2.shmget获取共享内存
3.shmctl操作共享内存
4.shmat挂接共享内存
5.shmdt与共享内存去关联
三、简单demo
一、理解一个共享内存
顾名思义,共享内存就是进程之间可以共享的内存。
此内存就是操作系统开辟的一段内存,通过页表映射-到进程地址空间里的共享区,就可以让不同进程之间共享内存。释放的时候先去掉映射,然后释放掉即可。
其实,当不同进程之间挂接了共享内存后,此时ipc(Inter-Process Communication-进程间通信)是最快的,因为数据传输之间不在涉及内核,即不通过系统调用来传递彼此的信息。
共享内存同样是操作系统申请的,那么操作系统也需要对其进行管理(先描述在组织),所以对于共享内存的组成实际上就是:共享内存 = 共享内存块 + 共享内存的内核数据结构。内核数据结构存放相关信息,比如挂接的进程个数等等。
那么我们如何让操作系统创建共享内存,并且让不同进程挂接到同一个共享内存呢?
二、共享内存相关接口
1.ftok创建唯一key
类似于标识符一样。为了区分不同的共享内存段的名称,我们就需要一个独一无二的共享内存段名字。
(man 3 ftok)
函数原型:
key_t ftok(const char *pathname, int proj_id);
依赖头文件:
#include <sys/types.h>
#include <sys/ipc.h>参数:
pathname:给定一个现有的路径-必须有权限。(方便创建文件)
proj_id:大小不超过8bit的数字,即一个字节的内的数据(0 ~ 255)
返回值:
key_t:typedef int 类似于标识符key,用于下面获取共享内存的参数。如果返回值小于零表示创建失败。
注意:
不一定百分百创建成功,可以多试几次。
2.shmget获取共享内存
有了一个唯一的标识符后,我们可以利用此标识符通过系统调用获取一段可以供进程挂接的共享内存。获取可以是找现有的或者重新创建。一般在实际运用中服务端创建,客户端进行获取。
(man 2 shmget)
函数原型:
int shmget(key_t key, size_t size, int shmflg);
依赖头文件:
#include <sys/ipc.h>
#include <sys/shm.h>参数:
key:利用ftok算法函数生成的唯一标识符。
size:自定义共享内存的开辟大小,以字节为单位。(最好是页的大小(4KB)的整数倍)
shmflg:操作数。(一共由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的)
IPC_CREAT 单独如果创建共享,底层存在,会获取并且返回,不存在创建就返回。(Create key if key does not exist.)
IPC_EXCL 单独使用没有价值,和IPC_CREAT一起使用如果底层不存在创建并且返回。底层存在出错并且返回。(Fail if key exists.)
权限码(对文件的权限【Linux】权限管理_柒海啦的博客-CSDN博客)
0:取共享内存标识符,若不存在则函数会报错。
......
(至少包含 IPC_CREAT | 权限码)
返回值:
成功就返回合法的标识符,用于表示此段共享内存的编号(类似于文件操作里的fd,这里就是shm+id),失败的话返回-1并且设置错误码。
注意:
开辟的内存空间大小之所以是页的整数倍是为了防止浪费。虽然在监视窗口(ipcs -m 可以查看共享内存资源ipcrm -m shmid可以删除对应的共享内存-命令操作)是你设定的那样,但是实际上操作系统是四舍五入的提供的内存,即比如你要了1个字节的共享内存,实际上操作系统分配的是一整页的内存,然后其余内存就没法用内存不就造成了资源的浪费嘛。
3.shmctl操作共享内存
通常,我们可以利用这个系统调用来释放掉我们所创建的共享内存。
(man 2 shmctl)
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
依赖头文件:
#include <sys/ipc.h>
#include <sys/shm.h>参数:
shmid:是shmget接口获取共享内存返回的id,表示操作此段共享内存。
cmd:操作数。
IPC_RMID 释放此共享内存(无论是否存在进程和其挂接)
......
buf:是指向shmid_ds strucc‐的指针,一般情况下我们无需关系,设置为nullptr即可。
返回值:
成功操作返回0,失败返回-1。
注意:
通常情况下在服务端最后结束的时候使用此接口对创建了的共享内存进行删除,所以真正用到的参数也就两个,操作数也就只有一个。
4.shmat挂接共享内存
在获取到共享内存的id后,就要和本身进程地址空间进行一个挂接,获取到对应的地址,准备进程间通信。
(man 2 shmat)
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
依赖头文件:
#include <sys/types.h>
#include <sys/shm.h>参数:
shmid:是shmget接口获取共享内存返回的id,表示操作此段共享内存。
shmaddr:一般是挂接的形式相关,一般不进行操作,设置为nullptr即可。
shmflg:操作数。
0:取共享内存标识符,若不存在则函数会报错。
.......
返回值:
成功挂接上返回此共享内存的地址。挂接失败返回(void*)-1,并且设置错误码。
注意:
此接口的目的是和当前进程进行挂接,初学阶段不必了解挂接的形式相关,只需要挂接到即可,并且知道挂接成功返回的是共享内存的地址,就和平时我们操作的地址类似。
5.shmdt与共享内存去关联
在客户端(服务端可以不用去关联-最后会进行释放)最后我们需要将此进程和共享内存之间的连接断开就可以使用此系统接口。
(man 2 shmdt)
函数原型:
int shmdt(const void *shmaddr);
依赖头文件:
#include <sys/types.h>
#include <sys/shm.h>参数:
shmaddr:共享内存地址。(可以是上面挂接shmat函数的返回的共享内存地址值)
返回值:
去关联成功返回0,失败返回-1,设置错误码。
注意:
共享内存和进程之间去关联函数。
三、简单demo
下面这段简单代码就演示简单的服务端创建共享内存,服务端、客户端挂接共享内存,服务端读取共享内存,客服端写入共享内存达成进程间通信。最后服务端和客户端去关联共享内存,服务端释放共享内存,结束。(关闭信号可以用quit进行识别)
利用上面介绍的接口就可以完成上述要求,快来试试吧~
//common.h
#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <cstdio>
#include <string>
#include <cstring>
#define PATH_NAME "/home/QiHai/code/2022/TestProject"
#define PROJ_ID 0x69 // 0 ~ 255 非零
#define SHMSIZE 4096 // 页的整数倍(4KB = 4 * 1024byte) - 否则就会造成空间上的浪费
//client.cpp
#include "common.h"
// 服务端
int main()
{
// 首先生成秘钥
key_t key = ftok(PATH_NAME, PROJ_ID);
// std::cout << key << std::endl;
// 创建共享内存
umask(0); // 掩码
int shmid = shmget(key, SHMSIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
perror("shmget:");
exit(1);
}
std::cout << "创建共享内存成功!" << std::endl;
// 挂接到当前进程的共享区
char* shmarr = (char*)shmat(shmid, nullptr, 0); // 共享内存id 挂接形式... 操作
if (*((int*)shmarr) < 0)
{
perror("shmat:");
exit(2);
}
std::cout << getpid() << "进程挂接共享内存成功!" << std::endl;
// 进程通信 服务端进行循环读取,由于是直接访问内存,所以没有阻塞,需要配合管道食用更佳
while (true)
{
char* tmp = shmarr;
if (strcmp(shmarr, "quit") == 0) break;
printf("%s\n", tmp);
sleep(1);
}
// 去关联 - 实际上服务端可以不用做这一步,因为服务端最后会将此共享内存给删除
int s = shmdt(shmarr);
if (s < 0)
{
perror("shmdt:");
exit(2);
}
std::cout << getpid() << "进程去关联成功!" << std::endl;
// 删除共享内存
int m = shmctl(shmid, IPC_RMID, nullptr); // 共享内存id 操作(当前操作是无论有多少个进程挂接上都会删除掉) null
if (m < 0)
{
perror("shmctl:");
exit(3);
}
std::cout << "删除共享内存成功!" << std::endl;
return 0;
}
//server.cpp
#include "common.h"
// 客户端
int main()
{
// 首先生成秘钥
key_t key = ftok(PATH_NAME, PROJ_ID);
int shmid = shmget(key, SHMSIZE, 0); // 客户端给0即可
if (shmid < 0)
{
perror("shmget:");
exit(1);
}
// 挂接到当前进程的共享区
char* shmarr = (char*)shmat(shmid, nullptr, 0); // 共享内存id 挂接形式... 操作
if (*((int*)shmarr) < 0)
{
perror("shmat:");
exit(2);
}
std::cout << getpid() << "进程挂接共享内存成功!" << std::endl;
// 进程通信 客户端写即可
std::string buffer;
while (true)
{
std::getline(std::cin, buffer); // 等待键盘输入
snprintf(shmarr, buffer.size() + 1, "%s", buffer.c_str());
if (buffer == "quit") break;
}
// 去关联
int s = shmdt(shmarr);
if (s < 0)
{
perror("shmdt:");
exit(2);
}
std::cout << getpid() << "进程去关联成功!" << std::endl;
return 0;
}
运行结果:
服务端会不断的读取客户端的输入数据。当然可以控制如果客户端不输入就不进行读入(利用管道的特性),那么中间就要加上管道操作就可以实现了。