一、前言
共享内存的核心思想是将物理内存通过页表映射到进程地址空间的共享区中,当两个进程映射同一块物理内存时,便可以进行进程间通信。
共享内存是 最快 的IPC(进程间通信)形式。一旦这样的物理内存被映射到共享它的进程地址空间中,进程间数据的传递将不再涉及到内核,换句话说进程不再需要执行进入内核的系统调用来传递彼此的数据,因而速度是最快的。
二、创建共享内存
[参数说明]:
-
1.size:共享内存的大小
指定共享内存的大小(单位B)。注意分配的大小最好为页(4KB)的整数倍,因为操作系统就以页为最小单位(向上取整)来分配内存的。例如,你申请 1.01个页大小的空间,操作系统也会分配给你2个页,但是你只能使用1.01页大小的空间。 -
2.shmflg:权限标识
标记宏,使用|
实现多种标记组合。常见的两种标记:IPC_CREAT
:创建共享内存。如果已经存在,则获取之;不存在,则创建之IPC_EXCL
:不单独使用,必须和 IPC_CREAT 配合使用。如果指定的共享内存不存在,则创建之;如果存在,则出错返回。由此可以保证一定会创建一段全新的共享内存。
-
3.key:内存段的名字
Linux操作系统内核中,管理共享内存的数据结构如下:
其中的__key
就是用来标识共享内存的唯一性, 这个key值一般是由 程序员 提供的。如果服务端用某个key值申请了共享内存,那么客户端使用同样的key就可以看到同一份共享内存,由此实现了进程间通信。
共享内存本质就是使用约定好的唯一的一个key值来进行通信。理论上key值可以自己任意取,只要不和操作系统中现有的共享内存冲突即可。但是我们一般不会自己取key值,推荐使用 ftok()
函数随机生成键值:
ftok函数 将一个路径名和一个整数标识符转换成一个key_t值,称为IPC键值。标识符取值在1~255之间,因为只用到后8位,同时键值不能取为0
[返回值]: 返回一个有效的共享内存标识符
[使用demo]:
#define PATH_NAME "/home/whc/linux-daily/process/shm"
#define PROJ_ID 0x1
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
exit(1);
}
// 必须创建一个新的共享内存
if(shmget(key, 4096, IPC_CREAT | IPC_EXCL) < 0)
{
perror("shmget");
exit(2);
}
return 0;
}
三、共享内存的基础操作
共享内存的生命周期是随内核的,进程退出,共享内存仍然存在。如果不显示删除,只有重启系统才会清空共享内存:
[问题一]:如何查看系统中现有的共享内存信息?
答:使用
ipcs -m
指令查看
由于我们设置flag选项为IPC_CREAT | IPC_EXCL
,因此当指定的共享内存已经存在时会出错返回:
[问题二]:如何显式删除共享内存?
- 使用
ipcrm -m
+ 对应的shmid
- 如果我们希望程序运行完毕自动释放共享内存,我们可以调用系统接口
shmctl()
。需要注意的是共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
// cmd 参数用于指定操作,设置为IPC_RMID即可用于删除共享内存 shmctl(key, IPC_RMID, nullptr);
[问题三]:如何设置共享内存的权限
通过观察
ipcs -m
显示的共享内存信息我们不难发现,perm(权限) 一栏的值为0,即我们当前创建的共享内存没有任何读写执行的权限。共享内存的权限是在创建的时候确定的:// 指定共享内存的权限为 0666 shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666)
四、关联共享内存
[作用]: 关联共享内存。将共享内存映射到当前进程的共享区中
[参数说明]:
- shmid:共享内存标识符
- shmaddr:指定连接的地址。一般设置为
NULL
即可,系统会自动关联一个合适的未使用的地址 - shmflag:一般设置为0即可
[返回值]: 返回挂接的共享内存段的地址(可以类比malloc的返回值)
char* s = shmat(shmid, nullptr, 0);
shmdt(s); // 类比于free函数
【注意】:将共享内存段与当前进程脱离不等于删除共享内存段
五、使用管道实现共享内存的访问控制
创建共享内存 → 关联共享内存 → 读写操作 → 去关联 → 删除共享内存。通过对上面接口的学习,我们基本能够自如的操控共享内存了。由于共享内存已经通过页表映射到进程地址空间的共享区中,因此我们使用共享内存就像访问一段连续的数组一样,非常的简单,不需要借助任何系统接口了。
但我们也不难发现,共享内存缺乏访问控制机制,并且也没有同步和互斥机制。使用共享内存的双方并不知道彼此的存在,因此同时向共享内存中写入时都是你争我抢的,写入的数据自然是混乱的;再比如,一方的数据还没有写完,另一方就把共享内存中的数据读走了,就会造成读取数据的不完全。
在这里呢,我将利用管道自身的访问控制机制帮助共享内存也实现一套访问控制机制:
// Comm.hpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <cstring>
#include <string>
#include <fcntl.h>
using namespace std;
#define FIFO_PATH "./.Fifo"
#define PATH_NAME "/home/whc/linux-daily/process/shm"
#define PROJ_ID 0x1
#define MEM_SIZE 128
void CreateFifo()
{
if(access(FIFO_PATH, F_OK)) // 管道文件不存在时才创建
{
if(mkfifo(FIFO_PATH, 0666) < 0)
{
perror("mkfifo"); // 打印错误信息
exit(1);
}
}
}
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
exit(2);
}
return key;
}
int OpenFifo(int flags) // 打开管道文件
{
return open(FIFO_PATH, flags);
}
int Wait(int fd)
{
int tmp = 0;
return read(fd, &tmp, sizeof(tmp));
}
void Signal(int fd)
{
int cmd = 1;
write(fd, &cmd, sizeof(cmd));
}
// shmService.cc
#include "Comm.hpp"
int main()
{
CreateFifo();
int shmid = shmget(CreateKey(), MEM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmid");
exit(3);
}
char* str = (char*)shmat(shmid, nullptr, 0);
int fd = OpenFifo(O_RDONLY);
while(true)
{
printf("服务端# ");
fflush(stdout);
if(Wait(fd) <= 0) break;
printf("%s\n", str);
}
printf("服务端退出了……\n");
shmdt(str);
close(fd);
unlink(FIFO_PATH);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
// shmClient.cc
#include "Comm.hpp"
int main()
{
int shmid = shmget(CreateKey(), MEM_SIZE, IPC_CREAT);
if(shmid < 0)
{
perror("shmid");
exit(3);
}
char* str = (char*)shmat(shmid, nullptr, 0);
int fd = OpenFifo(O_WRONLY);
while(true)
{
printf("请输入# ");
fflush(stdout);
ssize_t s = read(0, str, MEM_SIZE);
if(s > 0)
{
str[s - 1] = '\0'; // 注意换行符也会被读取
}
else if(s == 0)
{
printf("客户端退出了……\n");
break;
}
else
{
perror("read");
exit(4);
}
Signal(fd);
}
shmdt(str);
close(fd);
return 0;
}