【Linux】「共享内存揭秘」:高效进程通信的终极指南
本文所讲的共享内存为System V
共享内存
1. 什么是共享内存
共享内存(Shared Memory)是一种进程间通信(IPC,Inter-Process Communication)的方式,允许多个进程通过访问同一块内存区域来实现数据共享和快速通信。它是一种效率极高的通信机制,因为数据不需要在进程间进行复制,只需在同一块内存中直接读写即可。
1.2 共享内存的特点
- 高效:数据在高效内存区域是直接共享的,不需要在进程之间进行复制,从而减少了CPU和IO的消耗。
- 全局性:共享内存是所有附加到该内存的进程都可以访问的,由此它是一种全局资源。
- 同步机制依赖:虽然共享内存提供了数据共享的功能,但是不会自动提供对数据的访问同步机制。需要结合其他IPC方法(如信号量、互斥锁等)来避免多个进程同时读写时产生的数据竞争。
1.3 共享内存的工作原理
- 操作系统内核会在物理内存中分配一个共享内存段。
- 各个进程通过特定的标识符(shm_id)访问同一块共享内存空间。
- 共享内存区域是进程的地址空间外的内存,进程需要将其映射到自己的地址空间中才能访问。
还记得在进程地址空间时的内容吗?
共享内存的工作原理可以理解为:操作系统在内存中开辟了一块共享内存段,让两个不同的进程的虚拟地址同时对这块空间建立起映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行写入或者读取操作。
2.在Linux中使用共享内存
虽然Linux提供了POSIX
和System V
两种共享内存接口。但是本文将聚焦于System V
2.1 介绍System V
System V(读作“System Five”)是 UNIX 操作系统家族的一个版本,由美国 AT&T 的贝尔实验室开发。它是早期 UNIX 的一个重要分支,并对后来的 UNIX 系统以及其他现代操作系统产生了深远影响。
虽然 System V 本身已经很少被直接使用,但它的思想和功能在现代操作系统中得到了传承。
比如:POSIX:吸收了 System V 的许多特性,成为跨平台的通用标准。
2.1.1 创建共享内存
创建共享内存时,需要使用到shmget
函数
int shmget(key_t key,size_t size,int shmflg)
返回值:创建成功返回共享内存的shmid
,失败返回-1
。
参数介绍:
- key:共享内存的标识符。
- size:共享内存的大小,一般为
4096
的整数倍。 - shmflg:设置共享内存的创建方式以及创建权限。
关于返回值
在OS中,共享内存也拥有自己的数据结构,所以返回值有点类似于文件系统中的fd
,用于对不同的共享内存块进行操作。
关于参数2
可能有读者会感到疑惑,为什么共享内存的大小是4096
字节的整数倍,这是因为大小刚好与PAGE
页大小相同,有利与提高IO
效率。但是你可以设置不是4096
大小的内存,不过OS在底层依然会分配向上取整的4096
整数倍的空间给共享内存,可是你只能使用你设置的空间大小。
关于参数3
该参数为位图操作,也是就是状态压缩。与open
函数的参数2类似,常用的选项有: IPC_CREAT
创建共享内存,如果存在则使用已经存在的共享内存。IPC_EXCL
避免使用已经存在的共享内存,单独使用没有意义,需要配合IPC_CREAT
使用,当使用已经创建的共享内存时,会创建失败。权限
共享内存也是文件,需要权限掩码如0666
。
关于参数1
key是共享内存的标识符,是让不同进程看到同一块空间的关键,虽然可以自己指定一个数字,但是OS提供了一个根据目标路径+项目编号生成独一无二数字的特殊算法有点类似于哈希。key_t
类型也就是对int
类型的封装,来表示一个数字,用来标识不同的共享内存块,可以理解为inode
。
2.1.1.1 key
值的获取
OS提供了函数ftok
来生成key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char* pathname,int proj_id);
返回值:
- 返回生成的标识符。
参数: pathname
:项目路径,绝对/相对都可以。proj_id
:项目编号,自定义。
key是打开共享内存的钥匙,让两个进程看到同一块空间的关键就在于key
下面我们写一段代码来看看吧,依然是吧代码分为3个部分。
公共部分common.hpp
,服务部分server.cc
,客户部分client.cc
common.hpp
/**
* 公共文件,用于key值获取,进制转化,创建共享内存,获取共享内存
*/
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <unistd.h>
#include <string>
#include <cstring>
#define PATH "./"
#define PROJID 0x333
#define SIZE 4096
const mode_t mode = 0666;
//机制转化,转16机制
std::string toHEX(int x)
{
/**
* parame: x:一个十进制整数
* return: string:16进制字符串
* @return
*/
char buff[128];
snprintf(buff,sizeof(buff),"0x%x",x);
return buff;
}
//获取key
key_t getKey()
{
/**
* return : ket_t :返回key值
* @return
*/
key_t key = ftok(PATH,PROJID);
assert(key!=-1);//失败就终止进程
return key;
}
//创建共享内存
int createShm(key_t key,size_t size)
{
/**
* parame: key 标识符
* param: size 创建size大小的共享内存
* return: 返回共享内存id
* @return
*/
int id = shmget(key,size,IPC_CREAT|IPC_EXCL|mode);
assert(id!=-1);
return id;
}
//获取共享内存
int getShm(key_t key,size_t size)
{
/**
* parame: key 标识符
* param: size 创建size大小的共享内存
* return: 返回共享内存id
* @return
*/
int id = shmget(key,size,IPC_CREAT);
assert(id!=-1);
return id;
}
server.cc
/**
* 服务端,用于接受客户端发来的信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = getShm(key,SIZE);
std::cout<<"server key:"<<toHEX(key)<<std::endl;
std::cout<<"server shmid:"<<shmid<<std::endl;
return 0;
}
client.cc
/**
* 客户端,用于向服务端发送信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;
return 0;
}
下面是运行结果:
在shell命令行,我们可以通过指令ipcs -m
来查看创建出来的共享内存
该共享内存表的标识依次为:
key值、shmid、拥有者、权限、大小、挂载数、状态
2.1.2 通过指令回收共享内存
无论我们使用那种通信方式,都要记得在使用结束后将资源释放。
对于共享内存的释放,我们可以在shell的命令行释放。使用指令ipcrm -m shmid
当然,在命令行里释放还是太让人不舒适了,我们可以直接在程序中控制共享内存的释放。
2.1.3 通过共享内存控制函数释放
shmctl
是一个用于操作和管理 共享内存段 的 System V IPC 函数。它提供了对共享内存段的多种控制功能,比如删除共享内存段、获取信息、修改权限等。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:共享内存标识符。cmd
:控制操作的命令,有以下值:IPC_STAT
IPC_SET
IPC_RMID
:标识共享内存段为删除状态,等待所有关联进程分离后释放。
buf
:指向struct shmid_ds
的指针,用于传递或者接收共享内存段的元段数据信息。- 在
IPC_RMID
模式下:buf
可设置为nullptr
.
返回:
成功返回0,失败返回-1。
修改以下代码
- 在
client.cc
/**
* 客户端,用于向服务端发送信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;
//释放共享内存
int cnt = 5;
while(cnt)
{
std::cout<<cnt<<std::endl;
sleep(1);
cnt--;
}
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
可以看到5秒后,共享内存被释放了。
2.1.4 进程关联共享内存
共享内存的开辟就是为给不同的进程同一块空间的,那么我们要怎么给不同的进程看到同一块空间呢?就需要用到进程关联函数shmat
.
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid
:共享内存的标识符。shmaddr
:用于指定共享内存要附加的地址,如果提供nullptr
,操作系统会自动选择一个合适的地址。shmflg
:附加时的默认标志:0
:默认行为,读写属性。SHM_RDONLY
:只读模式
返回值:
成功返回共享内存的首地址,失败返回(void*)-1
关于参数2
默认情况我们就传nullptr,让OS自己去找。
关于参数3
默认情况也直接传0,默认为读写属性。
关于返回值
一般情况下,我们的通信都是通过字符来进行通信的,所有我们可以将其强转为char*
.
然后我们再来改一改,client.cc和server.cc
的代码
client.cc
/**
* 客户端,用于向服务端发送信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;
//开始进行进程关联
char* start = (char*)shmat(shmid,nullptr,0);
if((void*)start == (void*)-1)
{
perror("shmat");
shmctl(shmid,IPC_RMID,nullptr);
exit(1);
}
//成功
printf("client start:%p\n",start);//打印地址看看
//释放共享内存
sleep(5);
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
server.cc
/**
* 服务端,用于接受客户端发来的信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = getShm(key,SIZE);
std::cout<<"server key:"<<toHEX(key)<<std::endl;
std::cout<<"server shmid:"<<shmid<<std::endl;
//开始关联
char* start = (char*)shmat(shmid,nullptr,0);
if((void*)start==(void*)-1)
{
perror("shmat");
exit(1);
}
//成功
printf("server start:%p\n",start);
sleep(3);
return 0;
}
从这个图中我们可以发现,关联数的变化。
最重要的是,我们的地址居然是不一样的,这也就更加印证来虚拟地址的存在,两个进程通过页表映射指向同一块空间。
程序结束后,会自动取消关联状态
当然我们也可以手动去关联。
2.1.5 进程去关联
进程去关联需要用到函数shmdt
.
shmdt
是用于分离共享内存段的函数,它将之前通过 shmat
附加到进程地址空间的共享内存段移除。调用此函数后,进程将无法通过原地址访问该共享内存段。
int shmdt(const void *shmaddr);
参数:
shmaddr
:共享内存段的首地址,必须是之前通过shmat
返回的地址。
返回值
成功返回1,失败返回-1
注意:共享内存被删除后,已成功挂接的进程仍然可以继续进行正常的通信,不过此时无法再关联其他进程。
2.1.6 再讲shmctl
在上文,笔者已经使用了它的释放共享内存的功能了。除此之外,它还具有其他的功能。
当我们给参数2传递以下的参数时:
IPC_STAT
:获取或设置控制共享内存的数据结构。IPC_SET
:在进程有足够权限的情况下,将共享内存的当前关联值设置为buf
数据结构中的值。
buf
就是共享内存的数据结构,可以使用IPC_STAT
获取,也可以使用IPC_SET
设置。
除去释放共享内存的IPC_RMID
不需要传递参数3,这两种情况都是需要传递参数3的。
通过
shmctl
获取共享内存的数据结构,并获取pid
和key
.
/**
* 客户端,用于向服务端发送信息
*/
#include "common.hpp"
int main()
{
//创建共享内存
key_t key = getKey();
int shmid = createShm(key,SIZE);
std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
std::cout<<"cilent shmid:"<<shmid<<std::endl;
//开始进行进程关联
char* start = (char*)shmat(shmid,nullptr,0);
if((void*)start == (void*)-1)
{
perror("shmat");
shmctl(shmid,IPC_RMID,nullptr);
exit(1);
}
//成功
printf("client start:%p\n",start);//打印地址看看
std::cout<<"============="<<std::endl;
struct shmid_ds buf;
int n = shmctl(shmid,IPC_STAT,&buf);
if(n == -1)
{
perror("shmctl");
shmctl(shmid,IPC_RMID,nullptr);
exit(1);
}
std::cout<<"buf.shm_cpid:"<<buf.shm_cpid<<std::endl;
std::cout<<"buf.shm_perm.__key:"<<toHEX(buf.shm_perm.__key)<<std::endl;
//释放共享内存
shmdt(start);//去关联
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
共享内存 = 共享内存的内核数据结构(struct shmid_ds)+真正开辟的空间
2.2 共享内存的简单使用
创建、关联共享内存
服务端向客户端写入数据
客户端端每个一秒读取一次。
common.cc
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <unistd.h>
#include <string>
#include <cstring>
#define PATH "./"
#define PROJID 0x333
#define SIZE 4096
class Shm{
private:
key_t key;
int shmid;
mode_t mode = 0666;
public:
Shm(){
key = getKey();
}
key_t getKey()
{
/**
* function:返回并设置key值
* return: 返回key值
* @return
*/
key_t k = ftok(PATH,PROJID);
assert(k!=-1);
return key = k;
}
int createShm()
{
/**
* function:创建共享内存
* return:返回共享内存的标识符
* @return
*/
int sid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|mode);
assert(sid!=-1);
return shmid = sid;
}
int getShm()
{
/**
* function:获取共享内存
* return: 返回共享内存的标识符
* @return
*/
int sid = shmget(key,SIZE,IPC_CREAT);
assert(sid!=-1);
return shmid = sid;
}
char* processAssociation()
{
/**
* function:进行进程于共享内存间的关联
* return:返回共享内存的是起始地址
* @return
*/
char* start = (char*)shmat(shmid,nullptr,0);
assert((void*)start!=(void*)-1);
return start;
}
void deAssociation(void* start)
{
/**
* function:进行去关联
* :parame start 共享内存的起始地址
*/
shmdt(start);
}
void release()
{
/**
* function:释放共享内存
*/
int n = shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
}
int getShmid()
{
/**
* @return
*/
return shmid;
}
};
server.cc
/**
* 向cilent端发送信息
*/
#include "common.hpp"
int main()
{
Shm shm;
shm.createShm();//创建
char* start = shm.processAssociation();//关联
printf("server:");
for(int i = 0;i<26;++i)
{
start[i] = ('a'+i);
printf("%c",start[i]);
fflush(stdout);
start[i+1] = 0;
sleep(1);
}
shm.deAssociation(start);
shm.release();
return 0;
}
cilent.cc
/**
* 接受server的消息
*/
#include "common.hpp"
int main()
{
Shm shm;
shm.getShm();
char* start = shm.processAssociation();
for(int i = 0;i<26;++i)
{
printf("cilent :%s\n",start);
sleep(1);
}
shm.deAssociation(start);
shm.release();
return 0;
}
运行结果:
3. 总结
什么共享内存比管道快。
共享内存快的原因就在于比管道少了两次IO
(输入输出)操作。IO是很慢的,怎么证明呢?
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <stdio.h>
int cnt = 0;
void alarm_handler(int signum) {
printf("cnt: %d\n", cnt);
exit(1);
}
int main() {
signal(SIGALRM, alarm_handler); // 注册信号处理器
alarm(1); // 设置一个1秒的闹钟
while(true)
{
printf("%d\n",cnt++);
}
return 0;
}
频繁的IO操作,导致cnt最终才累加到135073。
现在我们换个写法
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <stdio.h>
int cnt = 0;
void alarm_handler(int signum) {
printf("cnt: %d\n", cnt);
exit(1);
}
int main() {
signal(SIGALRM, alarm_handler); // 注册信号处理器
alarm(1); // 设置一个1秒的闹钟
while(true)
{
//printf("%d\n",cnt++);
cnt++;
}
return 0;
}
效率差距非常之大。
管道:
共享内存:
共享内存的缺点
多个进程无限制地访问同一块区域,导致共享内存的数据无法确保安全。
共享内存没有同步和互斥机制,某个进程可能数据还没有写完,就被别人读走了,或者被覆盖了。
- 点赞
- 收藏
- 关注作者
评论(0)