【Linux手册】共享内存:零拷贝实现共享的优势与实操指南

文章目录
- 前言
- 一. 什么是共享内存
- 二. 共享内存的原理
- 三. 共享内存的使用方法
- 3.1 创建共享内存
- 3.2 挂接共享内存
- 3.3 共享内存的释放
- 四. 共享内存demo代码
- 五. 共享内存与管道对比
前言
在上一篇关于进程间通信的文章中,我们谈到了可以使用一个文件来让两个进程进行交互,但是直接使用外设上的文件进行IO的时候效率太低,而且进程间通信的目的就不是将信息存储到外设上,因此我们引入了一个内存级文件:管道文件;其中管道分为匿名管道和有名管道,分别来让有血缘关系的进程通信以及毫不相干的进程进行通信。
此篇将会讲解另一种通信的方式——共享内存,不需要依赖文件就可以实现通信。
本文将分为5个板块:
- 共享内存是什么;
- 共享内存的原理;
- 共享内存的使用方法;
- 共享内存demo代码;
- 共享内存与管道对比。
一. 什么是共享内存
进程间通信的主要目的就是让解决进程间独立的阻碍,让两个进程能够看到并使用同一份资源,而管道文件就是一种实现方法,让两个进程看到同一份文件,通过这一个文件进行交互。
两个进程使用的都是同一个物理内存,这块物理内存不就是他们共享的资源嘛,只不过将这块物理内存进行了划分,让不同进程使用不同的物理内存互不干扰。
能否直接在内存中开辟一块区域让两个进程都能使用???
通过这种方式来去除文件的概念,让进程间直接使用公共的内存空间。共享内存就是来实现多个进程如何访问同一块物理内存的。
二. 共享内存的原理
下图是共享内存的原理示意图:
共享内存实际上就是在物理内存中开辟一段公共空间,将该公共空间与两个要进行交互得进程建立联系,这样就可以实现让两个独立得进程看到同一份资源。
具体操作如下:
- 在物理内存中开辟一块空间;
- 让该公共内存分别映射到两个进程到两个进程得共享区;
- 让两个进程能获取到公共内存的起始地址。
以上就是共享内存的原理和具体需要实现的操作,通过以上操作后,两个进程分别得到一个指针,而该指针指向物理内存中的同一块区域,当一个进程对该区域进行操作时,另一个进程也能看到。
以上就是共享内存的原理,下面看一下操作系统提供了那些接口帮助我们实现:
三. 共享内存的使用方法
3.1 创建共享内存
首先要解决的是:如何在内存开辟一块空间供进程使用。
操作系统中提供了int shmget(int key , size_t size , int shmflg)
:
- 创建失败返回-1,创建成功返回共享内存标识符,就是共享内存ID;
- 第二个参数
size
,共享内存的大小,单位是字节; - 第三个参数
shmflg
,选项与open
的选项类似,其中常用的有IPC_CREAT
如果共享内存不存在就创建,如果已经存在就不再创建;IPC_CRAET|IPC_EXIT
也是创建共享内存只不过如果共享内存已经存在就直接报错,保证返回的共享内存是最新创建的。
通信双方都需要调用shmget
来获取共享内存的ID,但是只需要创建一次共享内存就可以了。
调用
shmget
时,如何分辨两个进程是否请求的是同一块物理内存,即如何保证两者能拿到同一块物理内存???
shmget
第一个参数key
来保证两个进程请求的是同一块资源;key
是数字,该数字在操作系统内核中是唯一的,用key
来区分内核中不同的的共享内存。- 两个进程如果
key
值是相同的就说明要访问同一块物理内存。
key值在操作系统内是唯一的,那我在传参的时候如何传,我怎么知道自己的key有没有被使用???
在操作系统中该key
值不需要用户来做,操作系统也提供了接口来让我们设置key
值:
key_t ftok(const char* pathname , int proj_id)
:
- 返回值,返回key值,失败返回-1;
- 第一个参数:一个字符串是表示一个有效路径,第二个参数一个数字。
ftok()
是一个算法,通过一个有效路径字符串和一个数字来获得一个冲突最小的key
值。
只能说冲突最小,也有可能出现冲突,也就是说ftok()
产生的key
可能是已经存在的了,此时就会返回-1,通过调整路径或后面的数字重新尝试。
3.2 挂接共享内存
共享内存已经在物理内存中开辟出来了,但是要先对内存进行操作,还需要将物理内存与共享区建立映射关系,操作系统中通过:void* shmat(int shmid , const void* shmaddr , int shmflag)
:
- 第一个参数
shmid
决定挂接哪一个物理内存; - 第二个参数
shmaddr
挂接的位置,可以直接填NULL,让操作系统自己分配到共享区上; - 第三个参数
shmflag
表示以什么方式进行挂接:读还是写,一般使用0表示以读和写的方式挂接。 - 返回值,一个指针指向挂接后的虚拟地址。
操作系统也运行进程与共享内存接关联:int shmdt(const void* shmaddr)
,参数是共享内存的起始地址。
3.3 共享内存的释放
共享内存的生命周期并不是随进程,而是随内核,也就是说当进程退出后,共享内存也不会自动释放,不主动关闭就不会释放,除非操作系统关闭了。
int shmctl(int shmid , int cmd , struct shmid_ds *buf)
:
- 该函数使用来控制共享内存的,使用
shmctrl(shmid , IPC_RMID ,nulltr)
实现对共享内存的释放。
四. 共享内存demo代码
下面简单实现两个程序,让他们能够进行交互,一个进程向另一个进程发送消息,接收方将信息打印出来即可:
先对各种接口进行封装,对获取shmid
接口进行封装:封装获取key的接口以及换取ID的接口,其中创建一方要进行创建,而另一方直接打开,所以shmget()
的选项是不一样的。
const string path_name_ = "/usr/bin";
const int pj_id_ = 100;
const int sz_ = 1024;// 获取key值
key_t Get_Key()
{key_t k = ftok(path_name_.c_str(), pj_id_);if (k < 0){cout << " ftok errro int " << __FILE__ << " at line :" << __LINE__ << endl;exit(1);}return k;
}// 创建共享内存
int _Base_shmid(int flag , int sz)
{int k = Get_Key();int id = shmget(k, sz, flag);if (id < 0){cout << " shmget errro int " << __FILE__ << " at line :" << __LINE__ << endl;exit(2);}return id;
}int Creat_shm(int sz = sz_)
{return _Base_shmid(IPC_CREAT | IPC_EXCL | 0666 , sz);
}int Get_shm(int sz = sz_)
{return _Base_shmid(IPC_CREAT , sz);
}
封装进行挂接,取消挂接以及释放共享内存的接口:
void* To_attach(int id)
{// 进行挂接void* pshm = shmat(id , nullptr , 0);return pshm;
}void To_Detach(const void* pshm)
{shmdt(pshm);
}void Del_shm(int id)
{if(shmctl(id , IPC_RMID , nullptr) < 0 ){perror("shmctl failed");exit(1);}
}
编写发送消息的一方,发送字符从A到Z:
int main()
{int id = Get_shm();void* pshm = To_attach(id);char* p = static_cast<char*>(pshm);for(int i = 0 ; i < 26 ; i++){p[i] = 'A' + i;p[i + 1] = 0;usleep(500000);}
}
接收方打印出接受到的信息:
int main()
{int id = Creat_shm();void* pshm = To_attach(id);char* p = static_cast<char*>(pshm);sleep(1);for(int i = 0 ; i < 26; i++){cout << " client send message :" << p << endl;usleep(500000);}To_Detach(pshm);Del_shm(id);return 0;}
以下是代码效果展示:
五. 共享内存与管道对比
- 共享内存没有同步互斥机制,可能出现两个进程同时访问共享资源,即同时进行写入和读取;
- 共享内存是所有进程间通信中最快的,因为其不需要进行多余的拷贝:管道通信要先将数据拷贝到管道文件中,读取时又要间信息从从管道从读出来,而共享内存,只要有一方写入另一方就可以进行读取;
- 共享内存内部的书记由用户自己维护,需要用户手动释放资源。
操作系统也要对共享内存进行管理,在内核中由一个结构体struct shmid_ds
专门用来描述共享内存:
/* Obsolete, used only for backwards compatibility and libc5 compiles */
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 */
};
如果在程序中想要获取一个共享内存的属性,也可以使用之前说的shmctl()
来获取:
shmctl(id , IPC_STAT , &shmds)
其中选项使用IPC_STAT
,第三个参数是输出型参数,调用之后内部就保存共享内存的各种属性。