Linux——共享内存
- 什么是共享内存
- 共享内存原理
- Linux下共享内存的接口
- 创建/获取共享内存:shmget
- ftok函数
- 映射共享内存到进程地址空间:shmat
- 解除共享内存映射:shmdt
- 删除共享内存段:shmctl
- 利用共享内存进行通信
我们之前学习了匿名管道,命名管道来进行进程之间的通信,其实除了管道之外,我们还有一种方式就是共享内存
什么是共享内存
共享内存(Shared Memory)是进程间通信(Inter-Process Communication, IPC)的一种方式,它允许两个或多个进程访问同一块物理内存区域,从而实现数据的快速、直接交换。在操作系统层面,共享内存指的是操作系统创建或映射到多个进程地址空间的同一块内存区域,使得这些进程可以直接读写这块内存,如同访问本进程的私有内存一样。
共享内存的工作机制通常涉及以下步骤:
创建共享内存:通过系统调用(如Unix/Linux下的shmget函数)创建一个共享内存段,并指定其大小和权限。
映射共享内存:每个需要访问共享内存的进程都需通过系统调用(如shmat函数)将共享内存段映射到自身的地址空间。
访问共享内存:映射成功后,进程就可以像访问普通内存一样读写这块共享内存区域,从而实现实时的数据交换。
同步与互斥:由于多个进程可以同时访问同一内存区域,为了避免数据竞争和不一致,通常需要借助其他同步机制(如信号量、互斥锁等)来保证对共享内存的有序和安全访问。
共享内存的优点在于速度快,因为它是内存级别的通信,没有额外的复制开销。缺点则是需要用户程序自行处理同步问题,否则容易引发竞态条件和死锁等问题。
共享内存原理
共享内存是操作系统支持的一种进程间通信(IPC,Inter-Process Communication)机制,它允许多个进程访问同一块物理内存区域,从而实现高效的数据共享和通信。以下是共享内存的基本原理:
- 内存区域创建:
在操作系统层面,通过系统调用(如Unix/Linux下的shmget()
)创建一块共享内存区域。创建时需要指定一个键值(通常通过ftok()
函数生成)来标识这块内存,同时指定内存区域的大小。- 内存映射:
一旦共享内存区域被创建,各个希望参与通信的进程可以调用shmat()
函数,将这块共享内存映射到它们各自的地址空间。映射成功后,每个进程都可以通过本地内存地址访问这块共享内存,就像访问普通的内存一样。- 数据同步:
由于多个进程可以直接读写同一块内存区域,因此必须有适当的同步机制来保证数据的一致性和完整性,如互斥锁(mutexes)、信号量(semaphores)或其他同步原语,以避免数据竞争(race conditions)。- 内存解除映射和删除:
当进程不再需要访问共享内存时,可以调用shmdt()
函数来解除映射关系,解除映射后,进程无法再通过本地地址访问共享内存。当所有进程都解除映射后,如果有必要,可以通过shmctl()
函数并设置适当的命令来删除共享内存区域。- 优点:
- 高效性:由于数据不需要在进程间复制,共享内存是最快捷的IPC方式之一。
- 低开销:相比消息队列、管道等其他IPC机制,共享内存不需要额外的复制和包装开销。
- 挑战:
- 同步复杂性:确保多个进程对共享内存的并发访问是一致的是一项复杂任务,需要良好的同步策略和编程技巧。
- 内存管理:操作系统需要跟踪哪些进程正在使用共享内存,何时应该回收内存资源。
简而言之,共享内存的核心原理是利用操作系统提供的功能,让多个进程可以直接读写同一块物理内存区域,从而实现进程间的数据交换。为了正确使用共享内存,程序员需要谨慎处理同步问题,并且在进程生命周期中妥善管理内存映射和解除映射。
Linux下共享内存的接口
在Linux系统中,使用共享内存进行进程间通信涉及以下几个关键的系统调用接口:
创建/获取共享内存:shmget
shmget
函数用于创建一个新的共享内存段或者获取已存在的共享内存标识符(shmid)。参数说明如下:
key
:通常是通过 ftok() 函数生成的一个键值,用来唯一标识共享内存段。
size
:要创建的共享内存段的大小(字节数)。
shmflg
:标志位,可以指定创建模式(如 IPC_CREAT 表示若不存在则创建)、权限位(如 S_IRUSR | S_IWUSR 表示所有者具有读写权限)和其他选项。
ftok函数
ftok()
函数在 Unix 和 Linux 系统中用于生成一个用于进程间通信(IPC)的唯一键值(key),尤其是配合 System V IPC 机制中的消息队列(message queues)、信号量(semaphores)以及共享内存(shared memory)。这个键值是系统内核用来识别不同 IPC 资源的关键标识符。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int id);
参数说明:
pathname
:是一个字符串,代表系统中一个已存在的文件的路径名。通常会选择应用程序能够访问并知道其稳定的路径,例如可执行文件或配置文件。id
:一个整数值,作为项目的子序列号。它可以被用来区分同一文件的不同 IPC 资源,不过通常设置为非零的常数值即可。
函数返回:- 如果成功,返回一个类型为
key_t
的 IPC 键值,该键值是基于给定的文件路径和项目 ID 计算得出的,理论上在同一系统上应当是唯一的。- 如果失败,返回
(key_t) -1
,并且会设置errno
以指示出错原因。
我们结合这两个来创建一块共享内存
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<iostream>const char* path_name = "../include";
#define MY_PROJECT_ID 1
#define MYSIZE 4096//获取key值
key_t GetKey()
{key_t key = ftok(path_name,MY_PROJECT_ID);if(key < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;exit(1);}return key;
}//获取shmid值
int Getshmid(const key_t& key)
{int shmid = shmget(key,MYSIZE,IPC_CREAT | 0666);if( shmid == -1){perror("shmget fail");exit(EXIT_FAILURE);}return shmid;
}
这个时候,我们再来看:
#include"shmnt.hpp"int main()
{key_t key = GetKey();int shmid = Getshmid(key);return 0;
}
我们创建好了共享内存,我们可以用ipcs -m来查看:
我们创建好了共享内存,下一步就是将共享内存连接到进程的地址空间:
映射共享内存到进程地址空间:shmat
shmat
函数将共享内存段连接到调用进程的地址空间中。
shmid
:由 shmget 返回的共享内存标识符。
shmaddr
:通常设为 NULL,表示让系统选择合适的地址来映射;也可以指定特定地址,但这样做有风险且需要额外注意。
shmflg
:标志位,比如 SHM_RDONLY 表示以只读方式映射共享内存。
//将该shmid挂到虚拟地址空间
//将该shmid挂到虚拟地址空间void* Attachshmid(int shmid){return shmat(shmid,nullptr,0);}
这个时候,我们再来看:
int main()
{key_t key = GetKey();int shmid = Getshmid(key);void *share_adderss = Attachshmid(shmid);if(share_adderss == (void*)-1){perror("shmat fail");return 1;}else{std::cout<<"has be attached"<<std::endl;sleep(10);}return 0;
}
我们看到,我们的连接数从0变成了1,意味着程序运行期间,进程已经将该共享内存段映射到了它们自己的地址空间。
解除共享内存映射:shmdt
对共享内存段进行取消映射。
int shmdt(const void * __shmaddr);
shmaddr
:这是通过 shmat() 函数成功映射共享内存时返回的地址指针。成功返回0,错误返回1
我们写这样一段函数:
//解除void Disattachshmid(const void* share_address){if(shmdt(share_address) == -1){perror("shmdt fail");return;}std::cout<<"has be disattached process"<<std::endl;sleep(10);}
#include"shmnt.hpp"int main()
{key_t key = GetKey();int shmid = Getshmid(key);void *share_adderss = Attachshmid(shmid);if(share_adderss == (void*)-1){perror("shmat fail");return 1;}else{std::cout<<"has be attached"<<std::endl;sleep(10);}Disattachshmid(share_adderss);return 0;
}
执行这段脚本,我们可以监视共享内存的使用情况:
我们看到,已经成功将映射关系消除了。
删除共享内存段:shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
对共享内存段进行控制操作,其中 cmd 参数可以是 IPC_RMID 来删除共享内存段。
shmid
:共享内存标识符。
cmd
:控制命令,如 IPC_RMID 表示删除。
buf
:如果是其他命令可能需要指向 shmid_ds 结构体的指针,但在删除操作中通常设置为NULL。
//删除共享内存void Deletshare(int shmid){if(shmctl(shmid,IPC_RMID,nullptr) == -1){perror("shcmtl fail");exit(EXIT_FAILURE);}else{std::cout<<"has successfully deleted"<<std::endl;}}
#include"shmnt.hpp"int main()
{key_t key = GetKey();int shmid = Getshmid(key);void *share_adderss = Attachshmid(shmid);if(share_adderss == (void*)-1){perror("shmat fail");return 1;}else{std::cout<<"has be attached"<<std::endl;sleep(10);}Disattachshmid(share_adderss);Deletshare(shmid);return 0;
}
利用共享内存进行通信
我们之前的大部分工作都只是把准备工作做好了,我们还没有进行通信,我们可以利用共享内存进行通信:
我们准备一个client.cc:
#include"shmnt.hpp"int main()
{key_t key = GetKey();int shmid = Getshmid(key);//挂载int* share_adderss = (int*)Attachshmid(shmid); //强转为int*类型//进行通信for(int i = 0; i < 10; i++){share_adderss[i] = i; //写入数据,以便读取std::cout<<"client say "<< share_adderss[i] <<std::endl;sleep(1);}//取消挂载Disattachshmid(share_adderss);return 0;
}
再准备一个server.cc,读取client.cc写入共享内存中的内容:
#include"shmnt.hpp"int main()
{key_t key = GetKey();int shmid = Getshmid(key);int* share_adderss = (int*)Attachshmid(shmid); //强转为int*类型if(share_adderss == (void*)-1){perror("shmat fail");return 1;}else{std::cout<<"has be attached"<<std::endl;//sleep(10);}//进行通信int i = 0;while(true){if(i < 10){std::cout<<"server say: "<< share_adderss[i] << std::endl;i++;sleep(1);}else{break;}}Disattachshmid(share_adderss);Deletshare(shmid);return 0;
}
这里注意,共享内存实现通信并不保证同步机制,如果我这里写入的速度变慢一点:
就会出现乱读,这时候我们要保证手动保证同步机制。