目录
共享内存
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V IPC提供的通信方式有以下三种:
1. system V共享内存
2. system V消息队列
3. system V信号量
共享内存的原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
注意:由于进程独立性的原因,上述的操作不可能由进程独立完成,例如:若进程直接向物理内存申请空间,空间就只属于进程,因为是进程间需要通信,所以进程是需求方,它会通过调用系统接口来实现进程间通信,所以操作系统不仅仅需要管理进程和文件系统还需要管理共享内存,如何管理?六字真言:先描述,再组织
共享内存的数据结构如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
共享内存的建立和释放
共享内存的建立
1. 操作系统向物理空间申请内存
2.将申请的内存通过页表映射到进程地址空间的共享区中,然后向上层返回该空间在虚拟地址的起始地址
共享内存的释放
1. 先让共享内存和进程地址空间去关联
2.然后释放共享内存,即:将物理内存归还给系统
共享内存的创建
创建共享内存,使用shmget
原型如下:
int shmget(key_t key, size_t size, int shmflg);
shmget参数说明:
- key:待创建共享内存在系统当中的唯一标识
- size:待创建共享内存的大小
- shmflg:创建共享内存的方式
shmget函数的返回值说明:
- shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
- shmget调用失败,返回-1
注意:句柄是一个抽象的、用于指代资源的标识符。它就像是一把钥匙,通过这把 “钥匙”,程序可以访问、操作对应的资源。这些资源可以是文件、窗口、设备、内存块等各种对象。例如:文件指针
传入shmget函数的第一个参数key,需要我们使用 ftok 函数进行获取
ftok函数原型如下:
key_t ftok(const char *pathname, int proj_id); //key_t就是int类型
ftok参数说明:
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id进行算法运行,最后得到一个key值,称为IPC键值,并返回这个key。在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。
pathname和proj_id都是用户自由指定的
key为什么不是由操作系统指定呢,这样不是更方便吗?
并不是操作系统不能生成,而是生成之后操作系统不知道该进程应该与那个进程通信
注意:
1.使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
2.需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:
IPC_CREAT:单独使用,创建一个共享内存,如果你申请的共享内存不存在,就创建,存在,就获取并返回
IPC_CREAT | IPC_EXCL:与IPC_EXCL混合使用, 创建一个共享内存,如果你申请的共享内存不存在,就创建,存在,就出错返回(可以确定该空间指定是新创建的)
至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:
#include "comm.hpp"
int main()
{
key_t key = ftok(PATHNAME.c_str(), PROJ_ID);
if(key < 0)
{
perror("main::ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("main::shmget");
return 1;
}
printf("key: 0x%x\n", key);
printf("shmid: %d\n", shmid);
return 0;
}
该代码编写完毕运行后,我们可以看到输出的key值和句柄值:
在Linux当中,我们可以使用ipcs
命令查看有关进程间通信设施的信息。
单独使用ipcs
命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:
对以上信息的解释
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
注意:key和shmid的区别就是:key是操作系统内标定唯一值,shmid在进程层面用来标定资源的唯一性
共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,
一,就是使用命令释放共享内存
二,就是在进程通信完毕后调用释放共享内存的函数进行释放。
使用命令释放共享内存资源
ipcrm -m shmid 释放用户指定的共享内存
注意:指定删除是用户层id,用户层管理shmid一律使用shmid,key是操作系统使用的
使用函数释放共享内存
控制共享内存的函数为:shmctl
原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl函数的参数说明:
- 第一个参数shmid,表示所控制共享内存的用户级标识符。
- 第二个参数cmd,表示具体的控制动作。
- 第三个参数buf,用于获取或设置所控制共享内存的数据结构。
shmctl函数的返回值说明:
- shmctl调用成功,返回0。
- shmctl调用失败,返回-1。
shmctl第二个参数常用的控制动作一般有以下三个
选项 | 作用 |
---|---|
IPC_STAT | 获取共享内存的当前关联值,此时参数buf作为输出型参数 |
IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 将该共享内存标记为删除 |
例如,在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。
#include "comm.hpp"
int main()
{
umask(0);
key_t key = ftok(PATHNAME.c_str(), PROJ_ID);
if(key < 0)
{
perror("main::ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("main::shmget");
return 1;
}
printf("key: 0x%x\n", key);
printf("shmid: %d\n", shmid);
sleep(2);
int n = shmctl(shmid, IPC_RMID, nullptr);
if(n < 0)
{
std::cout << "return fail!" << std::endl;
return 1;
}
else
{
std::cout << "normal quit!" << std::endl;
}
sleep(2);
return 0;
}
我们可以提前设置监控脚本来监视代码运行情况:
while : do ipcs -m ; echo "-----------------" ; sleep 1 ; done
根据观察脚本的情况,我们可以发现,我们申请共享内存之后,过了2秒确实被释放了
共享内存的关联
将共享内存挂接到进程地址空间,我们需要使用shmat函数
shmat原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat函数的参数说明:
- 第一个参数shmid,表示待关联共享内存的用户级标识符。
- 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为nullptr,表示让内核自己决定一个合适的地址位置。
- 第三个参数shmflg,表示关联共享内存时设置的某些属性。
shmat函数的返回值说明:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat调用失败,返回(void*)-1。
shmat函数第三个参数的常用的设置属性:
选项 | 作用 |
---|---|
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
0 | 默认为读写权限 |
我们尝试一下使用shmat函数来给进程进行关联
#include "comm.hpp"
int main()
{
//与进程地址空间进行挂接
std::cout << " attack start! " << std::endl;
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (void*)-1)
{
perror("processa.cpp::main::shmaddr");
return 1;
}
int cnt = 2;
//向共享内存中写入数据
while(cnt--)
{
printf("1\n");
sleep(1);
}
//sleep(2);
std::cout << " attack end! " << std::endl;
//释放共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
if(n < 0)
{
std::cout << "return fail!" << std::endl;
return 1;
}
else
{
std::cout << "normal quit!" << std::endl;
}
return 0;
}
代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此processa进程没有权限关联该共享内存。
我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
#include "comm.hpp"
int main()
{
//与进程地址空间进行挂接
std::cout << " attack start! " << std::endl;
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (void*)-1)
{
perror("processa.cpp::main::shmaddr");
return 1;
}
int cnt = 2;
//向共享内存中写入数据
while(cnt--)
{
printf("1\n");
sleep(1);
}
//sleep(2);
std::cout << " attack end! " << std::endl;
//释放共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
if(n < 0)
{
std::cout << "return fail!" << std::endl;
return 1;
}
else
{
std::cout << "normal quit!" << std::endl;
}
return 0;
}
此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。
共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数
shmdt函数的函数原型如下:
int shmdt(const void *shmaddr);
shmdt函数的参数说明:
- 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。
shmdt函数的返回值说明:
- shmdt调用成功,返回0。
- shmdt调用失败,返回-1。
现在我们就可以取消与共享内存的关联了
#include "comm.hpp"
int main()
{
//与进程地址空间进行挂接
std::cout << " attack start! " << std::endl;
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (void*)-1)
{
perror("processa.cpp::main::shmaddr");
return 1;
}
sleep(2);
printf("nxbw\n");
std::cout << " attack end! " << std::endl;
//切断该进程与共享内存的联系
int n = shmdt(shmaddr);
if(n < 0)
{
perror("processa.cpp::main::shmdt\n");
return 1;
}
else
{
printf("Disassociation successful\n");
}
sleep(5);
//释放共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
if(n < 0)
{
std::cout << "return fail!" << std::endl;
return 1;
}
else
{
std::cout << "normal quit!" << std::endl;
}
return 0;
}
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联
注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。
用共享内存实现processa&processb通信
processa负责创建共享内存,创建好后将共享内存和服务端进行关联
进程a代码如下:
#include "comm.hpp"
int main()
{
//与进程地址空间进行挂接
int shmid = GetShm();
std::cout << " attack start! " << std::endl;
printf("key: 0x%x\n", Get_Key());
printf("shmid: %d\n", shmid);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (void*)-1)
{
perror("processa.cpp::main::shmaddr");
return 1;
}
while(true)
{
printf("processb say@ ");
//直接往共享内存中写入数据
fgets(shmaddr, SIZE, stdin);
}
std::cout << " attack end! " << std::endl;
//切断该进程与共享内存的联系
int n = shmdt(shmaddr);
if(n < 0)
{
perror("processa.cpp::main::shmdt\n");
return 1;
}
else
{
printf("Disassociation successful\n");
}
sleep(5);
//释放共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
if(n < 0)
{
std::cout << "return fail!" << std::endl;
return 1;
}
else
{
std::cout << "normal quit!" << std::endl;
}
return 0;
}
进程b代码如下:
#include "comm.hpp"
int main()
{
printf("key: 0x%x\n", Get_Key());
printf("shmid: %d\n", GetShm());
while(true)
{
std::cout << "processb say@ ";
std::cout << shmaddr << std::endl;
sleep(1);
}
shmdt(shmaddr);
return 0;
}
共用的comm.h头文件
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/shm.h>
#include <cstring>
#include <fcntl.h>
#define SIZE 4096
#define PROJ_ID 0X66666
const std::string PATHNAME = "/home/zyx/code";
key_t Get_Key()
{
key_t key = ftok(PATHNAME.c_str(), PROJ_ID);
if(key < 0)
{
perror("comm.hpp::Get_Key::ftok");
exit(0);
}
return key;
}
int GetSharememHelper(int flag)
{
//umask(0);
int shmid = shmget(Get_Key(), SIZE, flag);
if(shmid < 0)
{
perror("comm.hpp::GetSharememHelper::shmget");
exit(0);
}
return shmid;
}
int CreateShm()
{
return GetSharememHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetSharememHelper(IPC_CREAT);
}
#endif
processb只需要直接和服务端创建的共享内存进行关联即可,之后进入死循环,便于观察客户端是否挂接成功。
共享内存与管道进行对比
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。
我们先来看看管道通信:
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
- 服务端将信息从输入文件复制到服务端的临时缓冲区中。
- 将服务端临时缓冲区的信息复制到管道中。
- 客户端将信息从管道复制到客户端的缓冲区中。
- 将客户端临时缓冲区的信息复制到输出文件中。
我们再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
- 从输入文件到共享内存。
- 从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。
总结以上分析:
共享内存的特征如下:
1.共享内存没有同步互斥的保护机制
2.共享内存是所有进程间通信最快的
3.共享内存的数据由用户自己维护