Ashmem: Anonymous Shared Memory
是 Android
提供的一种共享内存的机制,它基于 mmap
系统调用,不同进程可以将同一段物理内存映射到各自的虚拟地址控制实现共享,因此进程间不需要再拷贝数据。
特点
Android
系统提供了独特的匿名共享内存子系统 Ashmem
,它以驱动程序的形式实现在内核空间中,有两个典型特点:
- 能够辅助内存管理系统来有效地管理不再使用的内存块
- 通过
Binder
进程间通信机制来实现进程间的内存共享
Linux
共享内存通信效率非常高,进程间不需要传递数据,便可以直接访问,缺点也很明显,Linux
共享内存没有提供同步的机制,在使用时要借助其他的手段来处理进程间同步。所以在 Android
系统中实现的匿名共享内存 Ashmem
驱动中添加了互斥锁,另外通过传递文件描述符来实现共享内存的传递。
源码速查表
1 | Framework: |
System
层
函数列表
Ashmem
匿名共享内存,全都是通过 System
层和 Driver
层交互的。函数列表:
1 | // ashmem.h |
使用到的宏
函数列表中值使用到了这些 ioctl
命令宏:
1 |
驱动
文件路径
1 | ./drivers/staging/android/ashmem.c |
Ashmem
结构体
1 | /* |
name
表示这块共享内存的名字,这个名字会显示/proc/<pid>/maps
文件中,<pid>
表示打开这个共享内存文件的进程ID
。unpinned_list
是一个列表头,它把这块共享内存中所有被解锁的内存块连接在一起,和内存块的锁定和解锁操作有关。file
表示这个共享内存在临时文件系统tmpfs
中对应的文件,在内核决定要把这块共享内存对应的物理页面回收时,就会把它的内容交换到这个临时文件中去。size
表示这块共享内存的大小。prot_mask
表示这块共享内存的访问保护位。
关键函数
1 | // 打开 Ashmem 设备 |
匿名共享内存文件进行内存映射操作,对匿名内存文件内容的读写操作就像访问内存变量一样,驱动不用参与到读写操作中。
pin/unpin
锁定和解锁内存区
1 |
|
匿名共享内存的锁定和解锁操作,表示哪些内存块是正在使用的,需要锁定(pin
);哪些内存是不需要使用了,解除锁定(unpin
)。创建匿名共享内存时,默认所有的内存都是 pinned
状态的,用户先告诉 Ashmem
驱动程序要解锁某一块内存,内核可以将它对应的物理页面回收;因为 unpin
操作并不会改变已经 mmap
的地址空间,所以之后用户可以再告诉驱动程序要重新锁定某一块之前被解锁过的内块,从而修改这块内存的状态。也就是说,执行锁定前,目标对象必须是一块当前处于解锁状态的内存块。
匿名共享内存 Ashmem
机制是建立在 Linux
内核实现的共享内存的基础上的。同时它又向 Linux
内存管理系统的内存回收算法注册接口,系统内存不足时,会回收 Ashmem
区域中状态是 unpin
的对象内存块,如果不希望对象被回收,可以通过 pin
来保护它。
Java
层接口及应用
MemoryFile
所有 Java
代码都是通过 MemoryFile
来使用匿名共享内存 Ashmem
的,它是唯一的入口。
不过在 Andoid O
开始匿名共享内存推荐使用 SharedMemory
。Api: Applications should generally prefer to use SharedMemory which offers more flexible access & control over the shared memory region than MemoryFile does.
重要字段:
1 | private FileDescriptor mFD; // ashmem file descriptor |
调用流程
App --> (Framework) MemoryFile.java --> (Native) android_os_MemoryFile.cpp --> (System) ashmem-dev.c --> (Driver)ashmem.c
序列图中可以看到,Java
接口文件直接通过 JNI
调用了系统 System
的函数,通过它来和驱动做数据交互。
如何使用
思路:通过 Binder
机制,传递匿名共享机制文件描述符,通过该描述符实现进程间的数据共享。
在 Linux
系统中,文件描述符其实就是一个整数。每一个进程在内核空间都有一个打开文件的数组,这个文件描述符的整数值就是用来索引这个数组的,而且这个文件描述符只是在本进程内有效,也就是说,在不同的进程中,相同的文件描述符的值,代表的可能是不同的打开文件。因此在进程间传输文件描述符时,并不是简要把一个文件描述符从一个进程传给另外一个进程,中间通过 Binder
机制做过转换,映射了相同的地址空间,使得它和源进程的文件描述符所对应的打开文件是一致的,这样才能保证共享。文件描述符进程间转换图:
AIDL
文件
通过Binder
机制,传递匿名共享机制文件描述符。1
2
3interface IAshmemFd {
ParcelFileDescriptor getParcelFileDescriptor();
}Server
服务端
利用Ashmem
机制,创建MemoryFile
文件后,写入数据。注意:这里需要使用反射机制调用getFileDescriptor
因为它是@hide
的,也就是说,Android
系统并不推荐这种使用方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private ParcelFileDescriptor getParcelFileDescriptor(){
Log.d(TAG, "getParcelFileDescriptor: ");
ParcelFileDescriptor pfd = null;
try {
// method 1: Ashmem: MemoryFile.
MemoryFile file =new MemoryFile(ASHMEM_FILENAME,ASHMEM_LENGTH);
file.getOutputStream().write(testData);
Method method = MemoryFile.class
.getDeclaredMethod("getFileDescriptor");
FileDescriptor des = (FileDescriptor) method.invoke(file);
pfd = ParcelFileDescriptor.dup(des);
} catch (Exception e){
Log.e(TAG, "getParcelFileDescriptor: ", e);
}
return pfd;
}
private IBinder mBinder = new IAshmemFd.Stub() {
public ParcelFileDescriptor getParcelFileDescriptor()
throws RemoteException {
return AshmemService.this.getParcelFileDescriptor();
}
};Client
客户端
客户端通过Binder
拿到ParcelFileDescriptor
文件描述符后,读取MemoryFile
文件的内容并打印。1
2
3
4
5
6
7
8
9
10
11ParcelFileDescriptor pfd = mIAshmemFd.getParcelFileDescriptor();
FileInputStream fileInputStream =
new FileInputStream(pfd.getFileDescriptor());
byte[] content = new byte[10];
fileInputStream.read(content);
StringBuilder builder = new StringBuilder();
for (byte b : content){
builder.append(b);
}
String value = builder.toString();
Log.d(TAG, "getData: value = " + value);
总结
Android O
之后推荐使用 SharedMemory
,之前的版本也几乎没有看到 MemoryFile
的应用。网上搜到的案例是 AIDL
传递文件描述符 ParcelFileDescriptor
,而仅仅传递它其实并不需要使用 Ashmem
,因为 ParcelFileDescriptor
中推荐直接使用 createPipe
创建管道。Server
服务端使用管道实现的示例:
1 | // method 2: Pipe |
Native
层接口及应用
类图结构
1 | IMemory.h: IMemoryHeap, BnMemoryHeap, IMemory, BnMemory |
可以看出,Native
层的 Ashmem
本身就是基于 Binder
机制的。
MemoryHeapBase
用于在进程间共享一个完整的匿名共享内存块。MemoryBase
用于在进程间共享一个匿名共享内存块中其中的一部分。MemoryBase
接口是建立在MemoryHeapBase
接口的基础上面的,它们都可以作为一个Binder
对象来在进程间传输。
IMemoryHeap
定义的重要方法
1 | // 获得匿名共享内存块的文件描述符 |
MemoryHeapBase
服务端
MemoryHeapBase
继承了 BnMemoryHeap
,是 IMemoryHeap
的本地实现类,实现了具体的函数功能,用于服务端。
1 | MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const * name) |
mFD
:匿名共享内存文件描述符mBase
:匿名共享内存起始地址mSize
:匿名共享内存的大小
HeapCache
缓存
HeapCache
继承了 IBinder::DeathRecipient
,维护生命周期。
1 | class HeapCache : public IBinder::DeathRecipient |
heap_info_t
结构体heap
保存了BpMemoryHeap
对象;count
引用计数,表示被引用了多少次,只有在等于 1 时,才能被释放。mHeapCache
缓存KeyedVector
容器类型,存储了以IBinder
和heap_info_t
的键值对。find_heap
查找
在mHeapCache
中查找,如果找不到则添加到mHeapCache
中。get_heap
获取
在mHeapCache
中查找,如果找不到则直接将IBinder
转换,并没有加入mHeapCache
中。gHeapCache
全局实例gHeapCache
全局实例,维护了本进程中所有的BpMemoryHeap
引用对象。
BpMemoryHeap
客户端
BpMemoryHeap
是 IMemoryHeap
的代理类,用于客户端。Binder
机制中客户端通过代理类访问服务端的具体实现。
1 | virtual int getHeapID() const; |
在获取当前匿名共享内存信息时,都会先执行 assertMapped/assertReallyMapped
断言函数,确保获取到服务端的匿名共享内存信息后并映射到了当前进程。
IMemory
的定义
IMemory
是用来管理进程间匿名共享内存块 IMemoryHeap
中的一部分,所以这个类中保存了 IMemoryHeap
代理实例,以及被管理的匿名共享内存这部分的基地址,大小,在整个 Ashmem
中的偏移量。
- 头文件定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14class IMemory : public IInterface
{
public:
...
// 获取匿名共享内存客户端代理实例 BpMemoryHeap
virtual sp<IMemoryHeap> getMemory(ssize_t* offset=0, size_t* size=0) const = 0;
void* fastPointer(const sp<IBinder>& heap, ssize_t offset) const;
// 获取匿名共享内存的基地址
void* pointer() const;
// 获取匿名共享内存的大小
size_t size() const;
// 维护的这部分共享内存,在整个匿名共享内存中的偏移量
ssize_t offset() const;
}; - 具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 从缓存中快速获取被管理的 Ashmem 基地址:MemoryHeap的基地址 + 偏移量
void* IMemory::fastPointer(const sp<IBinder>& binder, ssize_t offset) const
{
sp<IMemoryHeap> realHeap = BpMemoryHeap::get_heap(binder);
void* const base = realHeap->base();
if (base == MAP_FAILED)
return 0;
return static_cast<char*>(base) + offset;
}
// 从具体实现类中获取被管理的 Ashmem 基地址:MemoryHeap的基地址 + 偏移量
void* IMemory::pointer() const {
ssize_t offset;
sp<IMemoryHeap> heap = getMemory(&offset);
void* const base = heap!=0 ? heap->base() : MAP_FAILED;
if (base == MAP_FAILED)
return 0;
return static_cast<char*>(base) + offset;
}
// 被管理 Ashmem 的大小
size_t IMemory::size() const {
size_t size;
getMemory(NULL, &size);
return size;
}
// 被管理 Ashmem 的偏移量
ssize_t IMemory::offset() const {
ssize_t offset;
getMemory(&offset);
return offset;
}
MemoryBase
服务端
MemoryBase
继承 BpMemory
,是 IMemory
的本地实现类,用于服务端。
1 | MemoryBase::MemoryBase(const sp<IMemoryHeap>& heap, |
MemoryBase
类非常简单,构造函数中传入了匿名共享内存的具体实现 MemoryHeapBase
对象,以及被管理部分的偏移量和大小。getMemory
简单的返回了整个匿名共享内存对象及对应的偏移量和大小,所以可以理解 MemoryBase
是 MemoryHeapBase
的简单封装。
BpMemory
客户端
1 | sp<IMemoryHeap> BpMemory::getMemory(ssize_t* offset, size_t* size) const |
通过 Binder
机制,从服务端读取偏移量,大小,并返回 BpMemory
代理实例(mHeap
)。
Ashmem
总结
- 文件描述符
在形式上是一个非负整数,实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。 - 进程间共享原理
Ashmem
进程间共享原理:两个在不同进程中的文件描述符对应同一个指向设备文件/dev/ashmem
的文件结构体。Binder
机制在数据交互时大小不能超过 1M,可以通过传递匿名共享内存(Ashmem
)的文件描述符或者IBinder
对象(IMemoryHeap/IMemory
),来实现大数据的共享。 - 优缺点
匿名共享内存不会占用Heap
,不会导致OOM
,但是如果肆意使用,会导致系统资源不足性能下降。 - 应用场景
在camera.preview
预览,SurfaceFlinger
绘制等,可以看到匿名共享内存的应用。