共享内存的使用和原理
还是先看共享内存的使用方法,我主要介绍两个函数:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); //申请共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg); //把共享内存映射到进程的地址空间
通过shmget()函数申请共享内存,它的入参如下
- key:用来唯一确定这片内存的标识,。
- size:就是我们申请内存的大小
- shmflg:读写权限
- 返回值:这个操作会返回一个id, 我们一般称为 shmid
通过shmat()函数将我们申请到的共享内存映射到自己的用户空间,映射成功会返回地址,有了这个地址,我们就可以随意的读写数据了,我们继续看一下这个函数的入参
- shmid :我们申请内存时, 返回的shmid
- shmaddr:共享内存在进程的内存地址,传NULL让内核自己决定一个合适的地址位置.
- shmflg:读写权限
- 返回值:映射后进程内的地址指针, 代表内存的头部地址
共享内存的原理是在内存中单独开辟的一段内存空间,这段内存空间其实就是一个tempfs(临时虚拟文件),tempfs是VFS的一种文件系统,挂载在/dev/shm上,前面提到的管道pipefs也是VFS的一种文件系统。
由于共享的内存空间对使用和接收进程来讲,完全无感知,就像是在自己的内存上读写数据一样,所以也是效率最高的一种IPC方式。
上面提到的IPC的方式都是在内核空间中开辟内存来存储数据,写数据时,需要将数据从用户空间拷贝到内核空间,读数据时,需要从内核空间拷贝到自己的用户空间,
共享内存就只需要一次拷贝,而且共享内存不是在内核开辟空间,所以可以传输的数据量大。
但是共享内存最大的缺点就是没有并发的控制,我们一般通过信号量配合共享内存使用,进行同步和并发的控制。
Android中共享内存的使用场景
共享内存在Android系统中主要的使用场景是用来传输大数据,并且Android并没有直接使用Linux原生的共享内存方式,而是设计了Ashmem匿名共享内存。
之前说到有名管道和匿名管道的区别在于有名管道可以在vfs目录树中查看到这个管道的文件,但是匿名管道不行,所以匿名共享内存同样也是无法在vfs目录中查看到的,Android之所以要设计匿名共享内存,我觉得主要是为了安全性的考虑吧。
我们来看看共享内存的一个使用场景,在Android中,如果我们想要将当前的界面显示出来,需要将当前界面的图元数据传递Surfaceflinger去做图层混合,图层混合之后的数据会直接送入帧缓存,送入帧缓存后,显卡就会直接取出帧缓存里的图元数据显示了。
那么我们如何将应用的Activity的图元数据传递给SurfaceFlinger呢?想要将图像数据这样比较大的数据跨进程传输,靠binder是不行的,所以这儿便用到匿名共享内存。
从谷歌官方提供的架构图可以看到,图元数据是通过BufferQueue传递到SurfaceFlinger去的,当我们想要绘制图像的时候,需要从BufferQueue中申请一个Buffer,Buffer会调用Gralloc模块来分配共享内存当作图元缓冲区存放我们的图元数据。
//文件-->hardware/libhardware/modules/gralloc/gralloc.cpp
static int gralloc_alloc_buffer(alloc_device_t* dev,
size_t size, int usage, buffer_handle_t* pHandle)
{
int err = 0;
int fd = -1;
size = roundUpToPageSize(size);
// 创建共享内存,并且设定名字跟size
fd = ashmem_create_region("gralloc-buffer", size);
if (err == 0) {
private_handle_t* hnd = new private_handle_t(fd, size, 0);
gralloc_module_t* module = reinterpret_cast<gralloc_module_t*>(
dev->common.module);
// 执行mmap,将内存映射到自己的进程
err = mapBuffer(module, hnd);
if (err == 0) {
*pHandle = hnd;
}
}
return err;
}
int mapBuffer(gralloc_module_t const* module,
private_handle_t* hnd)
{
void* vaddr;
return gralloc_map(module, hnd, &vaddr);
}
static int gralloc_map(gralloc_module_t const* module,
buffer_handle_t handle,
void** vaddr)
{
private_handle_t* hnd = (private_handle_t*)handle;
if (!(hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER)) {
size_t size = hnd->size;
//映射创建的匿名共享内存
void* mappedAddress = mmap(0, size,
PROT_READ|PROT_WRITE, MAP_SHARED, hnd->fd, 0);
if (mappedAddress == MAP_FAILED) {
return -errno;
}
hnd->base = intptr_t(mappedAddress) + hnd->offset;
}
*vaddr = (void*)hnd->base;
return 0;
}
可以看到Android的匿名共享内存是通过ashmem_create_region() 函数来申请共享内存的,它会在/dev/ashmem下创建一个虚拟文件,Linux原生共享内存是通过shmget()函数,并会在/dev/shm下创建虚拟文件。
匿名共享内存是通过mmap()函数将申请到的内存映射到自己的进程空间,而Linux是通过*shmat()函数。
虽然函数不一样,但是Android的匿名共享内存和Linux的共享内存在本质上是大同小异的。
6.1 什么是共享内存?
- 共享内存是系统处于多个进程之间通讯的考虑,而预留的一块内存区。
- 共享内存允许两个或更多的进程访问同一块内存,就如同malloc()函数向不同进程返回了指向同一个物理内存区域的指针。
- 当一个进程改变了这块地址中的内容的时候,其他进程都会觉察到这个更改。
6.2 关于共享内存
- 当一个程序加载进内存后,它就被分成叫做页的块。
- 通信将存在内存的两个页之间或者两个独立的进程之间。
- 当一个程序想和另外一个程序通信的时候,那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存。
- 由于所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。
- 访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其他需要切入内核的过程来完成。同时它也也避免了对数据的跟中不必要的复制。
- 如果没有共享内存的概念,那一个进程不能存取另外一个进程的内存部分,因而导致共享数据或者通信失效。因为系统内核没有对访问共享内存进行同步,开发者必须提供自己的同步措施。
- 解决了这些问题的常用方法是是通过信号量进行同步。不过通常我们程序只有一个进程访问了共享内存,因此在集中展示了共享内存机制的同时,我们避免了让代码被同步逻辑搞的混乱不堪。
- 为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者Mutex对象。
6.3 Mutex对象
- 例如,在数据被写入前不允许进程从共享内存中读取信息、不允许两个进程同时向一个共享内存地址写入数据等。
- 当一个基础想和两一个进程通信的时候,它将按以下顺序运行:
- 1、获取Mutex对象,锁定共享区域
- 2、将要通信的数据写入共享区域
- 3、释放Mutex对象
- 当一个进程从这个区域读取数据的时候,它将重复同样的步骤,只是将第二步变成读取。
6.4 内存模型
要使用一块共享内存
- 进程必须首先分配它
- 随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
- 当完成通信之后,所有进程都脱离共享内存,并且由一个进程释放该共享内存块。
- 在/proc/sys/kernel/目录下,记录着共享内存的一些限制,如一个共享内存区的最大字节数shmmax,系统范围内最大的共享内存区标志符数shmmni等。
6.5 Linux系统内存模型
- 在Linux系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个页面页面映射到自己的地址空间,从而达到共享内存的目的。
- 分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标示该内存块的标识符。
- 一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。
- 这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。
- 当再也没有进程需要使用这个共享内存块的时候,必须有一个(有且只有一个)进程负责释放这个被共享的内存页面。
- 所有共享内存块的大小必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在Linux系统中,内存页面大小是4KB,不过您仍然应高通过调用getPageSize获取这个值。