一、Ashmem C 语言接口
通常可以使用 libcutils 库中的 ashmem_create_region 函数创建一块共享内存区域:
#define ASHMEM_DEVICE "/dev/ashmem"
/*
* ashmem_create_region - creates a new ashmem region and returns the file
* descriptor, or <0 on error
*
* `name' is an optional label to give the region (visible in /proc/pid/maps)
* `size' is the size of the region, in page-aligned bytes
*/
int ashmem_create_region(const char *name, size_t size)
{
int fd, ret;
fd = open(ASHMEM_DEVICE, O_RDWR);
if (fd < 0)
return fd;
if (name) {
char buf[ASHMEM_NAME_LEN];
strlcpy(buf, name, sizeof(buf));
ret = ioctl(fd, ASHMEM_SET_NAME, buf);
if (ret < 0)
goto error;
}
ret = ioctl(fd, ASHMEM_SET_SIZE, size);
if (ret < 0)
goto error;
return fd;
error:
close(fd);
return ret;
}
- name 参数是给内存区域的一个标签
- size 参数表示共享内存的大小
- 返回值是打开的 ashmem 驱动节点的文件描述符
拿到文件描述符后,可以通过 mmap 将 kernel 空间分配的内存映射到用户空间,这样就能像普通内存一样使用。
如果是其他进程需要使用这块内存,并不能通过 open 设备节点的方式来重新打开此块内存,而是需要获取创建此共享内存时打开的文件描述符。具体原因可以阅读 ashmen 的驱动代码(每次 open 都会创建新的共享内存区域,close 会回收之前创建的内存区域)。
那么怎么跨进程获取文件描述符呢?当然是借助于 binder,服务端通过 writeFileDescriptor 写入文件描述符参数,客户端则通过 readFileDescriptor 读取文件描述符参数,具体实现原理参考 binder 驱动。
客户端进程获取到文件描述符后,同样通过 mmap 将其映射到用户空间,这样就实现内存共享。
二、Ashmem 的C++ 封装
为了方便使用共享内存,Android 在 C++ 层提供两个接口:IMemoryHeap 和 IMemory。
1、IMemoryHeap
MemoryHeap 可以理解为内存堆,它是一个 Binder 接口,服务端实现类是 MemoryHeapBase,客户端实现类是 BpMemoryHeap。他们之前的继承关系如下:
在 IMemoryHeap 类中定义的接口,都不会直接触发 binder 调用,而是通过代理类的 assertReallyMapped 函数发起 binder 调用的,其它函数分别在子类中本地实现。因此通过继承来约束客户端和服务端的接口,使其保一致,者并非 Binder 的专利,而是一种面向对象的设计手段,我们同样可以对本地方法进行约束。
(1)服务端实现
首先看 MemoryHeapBase 的构造函数实现,它有很多重载版本,这里只分析其中一个,其它版本实现的功能类似。
MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const * name)
: mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags),
mDevice(0), mNeedUnmap(false), mOffset(0)
{
const size_t pagesize = getpagesize();
size = ((size + pagesize-1) & ~(pagesize-1));
int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name, size);
ALOGE_IF(fd<0, "error creating ashmem region: %s", strerror(errno));
if (fd >= 0) {
if (mapfd(fd, size) == NO_ERROR) {
if (flags & READ_ONLY) {
ashmem_set_prot_region(fd, PROT_READ);
}
}
}
}
首先通过 ashmem_create_region 创建共享内存区域,然后调用 mapfd 将内存映射到用户空间。
status_t MemoryHeapBase::mapfd(int fd, size_t size, uint32_t offset)
{
if (size == 0) {
// try to figure out the size automatically
#ifdef HAVE_ANDROID_OS
// first try the PMEM ioctl
pmem_region reg;
int err = ioctl(fd, PMEM_GET_TOTAL_SIZE, ®);
if (err == 0)
size = reg.len;
#endif
if (size == 0) { // try fstat
struct stat sb;
if (fstat(fd, &sb) == 0)
size = sb.st_size;
}
// if it didn't work, let mmap() fail.
}
if ((mFlags & DONT_MAP_LOCALLY) == 0) {
void* base = (uint8_t*)mmap(0, size,
PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset);
if (base == MAP_FAILED) {
ALOGE("mmap(fd=%d, size=%u) failed (%s)",
fd, uint32_t(size), strerror(errno));
close(fd);
return -errno;
}
//ALOGD("mmap(fd=%d, base=%p, size=%lu)", fd, base, size);
mBase = base;
mNeedUnmap = true;
} else {
mBase = 0; // not MAP_FAILED
mNeedUnmap = false;
}
mFD = fd;
mSize = size;
mOffset = offset;
return NO_ERROR;
}
映射完内存,将虚拟地址保存到 mBase,文件描述符保存到 mFD,内存大小保存到 mSize,偏移量保存到 mOffset。其它属性获取函数只是简单的返回变量的值,比如 getHeapID:
int MemoryHeapBase::getHeapID() const {
return mFD;
}
跨进程调用的函数在父类 BnMemoryHeap 的 onTransact 处理:
status_t BnMemoryHeap::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
switch(code) {
case HEAP_ID: {
CHECK_INTERFACE(IMemoryHeap, data, reply);
reply->writeFileDescriptor(getHeapID());
reply->writeInt32(getSize());
reply->writeInt32(getFlags());
reply->writeInt32(getOffset());
return NO_ERROR;
} break;
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
这里仅处理 HEAP_ID 一个 case,说明只需一次 binder 调用就能获取关于共享内存的所有参数。这些参数包括文件描述符、内存区域大小、标记和偏移,它们是通过 MemoryHeapBase 的成员函数获取。
(2)客户端实现
客户端的实现相对比较复杂,先看代码:
int BpMemoryHeap::getHeapID() const {
assertMapped();
return mHeapId;
}
void* BpMemoryHeap::getBase() const {
assertMapped();
return mBase;
}
size_t BpMemoryHeap::getSize() const {
assertMapped();
return mSize;
}
uint32_t BpMemoryHeap::getFlags() const {
assertMapped();
return mFlags;
}
uint32_t BpMemoryHeap::getOffset() const {
assertMapped();
return mOffset;
}
首先所有的接口都调用 assertMapped 函数,并且直接返回属性的值。很显然 assertMapped 函数会对属性进行初始化:
void BpMemoryHeap::assertMapped() const
{
if (mHeapId == -1) {
sp<IBinder> binder(const_cast<BpMemoryHeap*>(this)->asBinder());
sp<BpMemoryHeap> heap(static_cast<BpMemoryHeap*>(find_heap(binder).get()));
heap->assertReallyMapped();
if (heap->mBase != MAP_FAILED) {
Mutex::Autolock _l(mLock);
if (mHeapId == -1) {
mBase = heap->mBase;
mSize = heap->mSize;
mOffset = heap->mOffset;
android_atomic_write( dup( heap->mHeapId ), &mHeapId );
}
} else {
// something went wrong
free_heap(binder);
}
}
}
构造函数中属性变量都被初始化成非法值,第一此访问时 mHeapId 等于 -1,find_heap 函数为确认 BpMemoryHeap 对象是否已经存在一个实例,assertReallyMapped 如果没有映射过内存则执行映射。这么做的目的是保证进程中只有第一个 BpMemoryHeap 对象会执行 mmap 操作,其它 BpMemoryHeap 对象直接拷贝映射好的属性值即可。这种设计与单例模型很类似,只是因为代理类只能通过进程成员函数 asInterface 实例化,故通过以上方式保证所有实例都是第一个实例的拷贝。
void BpMemoryHeap::assertReallyMapped() const
{
if (mHeapId == -1) {
// remote call without mLock held, worse case scenario, we end up
// calling transact() from multiple threads, but that's not a problem,
// only mmap below must be in the critical section.
Parcel data, reply;
data.writeInterfaceToken(IMemoryHeap::getInterfaceDescriptor());
status_t err = remote()->transact(HEAP_ID, data, &reply);
int parcel_fd = reply.readFileDescriptor();
ssize_t size = reply.readInt32();
uint32_t flags = reply.readInt32();
uint32_t offset = reply.readInt32();
ALOGE_IF(err, "binder=%p transaction failed fd=%d, size=%ld, err=%d (%s)",
asBinder().get(), parcel_fd, size, err, strerror(-err));
int fd = dup( parcel_fd );
ALOGE_IF(fd==-1, "cannot dup fd=%d, size=%ld, err=%d (%s)",
parcel_fd, size, err, strerror(errno));
int access = PROT_READ;
if (!(flags & READ_ONLY)) {
access |= PROT_WRITE;
}
Mutex::Autolock _l(mLock);
if (mHeapId == -1) {
mRealHeap = true;
mBase = mmap(0, size, access, MAP_SHARED, fd, offset);
if (mBase == MAP_FAILED) {
ALOGE("cannot map BpMemoryHeap (binder=%p), size=%ld, fd=%d (%s)",
asBinder().get(), size, fd, strerror(errno));
close(fd);
} else {
mSize = size;
mFlags = flags;
mOffset = offset;
android_atomic_write(fd, &mHeapId);
}
}
}
}
通过 binder 向服务端发送 HEAP_ID 消息,通过 readFileDescriptor 获取共享内存的文件描述符,然后获取其它参数来初始化属性,然后调用 mmap,将虚拟地址保存到 mBase。注意内存的映射地址并不是通过 binder 从服务端获取,而是通过 mmap 从内核映射过来。
2、IMemory
涉及类的继承关系:
Memory 用于表示共享内存堆中的一块内存,它可看作 MemoryHeap 的子集。
(1)服务端的实现。
MemoryBase 是服务的实现类,其定义如下:
class MemoryBase : public BnMemory
{
public:
MemoryBase(const sp<IMemoryHeap>& heap, ssize_t offset, size_t size);
virtual ~MemoryBase();
virtual sp<IMemoryHeap> getMemory(ssize_t* offset, size_t* size) const;
protected:
size_t getSize() const { return mSize; }
ssize_t getOffset() const { return mOffset; }
const sp<IMemoryHeap>& getHeap() const { return mHeap; }
private:
size_t mSize;
ssize_t mOffset;
sp<IMemoryHeap> mHeap;
};
构造函数源码如下:
MemoryBase::MemoryBase(const sp<IMemoryHeap>& heap,
ssize_t offset, size_t size)
: mSize(size), mOffset(offset), mHeap(heap)
{
}
这里初始化了所有的成员变量。mHeap 表示内存所在的堆,mOffset 表示内存起始地址相对堆内存的偏移值,mSize 表示内存的大小。
MemoryBase 所有方法中,只有 getMemory 函数可以通过 Binder 跨进程访问。在 BnMemory 类中处理客户端发送的消息:
status_t BnMemory::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
switch(code) {
case GET_MEMORY: {
CHECK_INTERFACE(IMemory, data, reply);
ssize_t offset;
size_t size;
reply->writeStrongBinder( getMemory(&offset, &size)->asBinder() );
reply->writeInt32(offset);
reply->writeInt32(size);
return NO_ERROR;
} break;
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
可以看到这仅仅处理 GET_MEMORY 消息。getMemory 的实现代码如下:
sp<IMemoryHeap> MemoryBase::getMemory(ssize_t* offset, size_t* size) const
{
if (offset) *offset = mOffset;
if (size) *size = mSize;
return mHeap;
}
从实现看 getMemory 获取了关于这块内存的所有信息。返回值 mHeap 是 MemoryHeapBase 类型,通过 Binder 传输,客户端会获取一个 BpMemoryHeap 对象。
(2)客户端实现
客户端的实现类是 BpMemory,只有 getMemory 会发起 Binder 调用。
sp<IMemoryHeap> BpMemory::getMemory(ssize_t* offset, size_t* size) const
{
if (mHeap == 0) {
Parcel data, reply;
data.writeInterfaceToken(IMemory::getInterfaceDescriptor());
if (remote()->transact(GET_MEMORY, data, &reply) == NO_ERROR) {
sp<IBinder> heap = reply.readStrongBinder();
ssize_t o = reply.readInt32();
size_t s = reply.readInt32();
if (heap != 0) {
mHeap = interface_cast<IMemoryHeap>(heap);
if (mHeap != 0) {
mOffset = o;
mSize = s;
}
}
}
}
if (offset) *offset = mOffset;
if (size) *size = mSize;
return mHeap;
}
对于客户端,通过 interface_cast 转换后,mHeap 初始化成 BpMemoryHeap 类型的对象。该 Binder 调用同时也初始化了 mOffset 和 mSize。
那么怎么获取内存的首地址呢?父类中定义了 pointer 函数用于返回该内存的首地址。
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;
}
其实就是基地址加上偏移。
3、MemoryDealer
Android 还提供一个内存分配的工具类 MemoryDealer,用于管理内存堆,注意该类是不具备跨进程通信能力的,它仅在服务端工作。
MemoryDealer::MemoryDealer(size_t size, const char* name)
: mHeap(new MemoryHeapBase(size, 0, name)),
mAllocator(new SimpleBestFitAllocator(size))
{
}
从构造函数来看,首先会 new 一个内存堆 MemoryHeapBase,然后创建一个内存分配器 SimpleBestFitAllocator,后续如果服务端需要内存,则通过 allocate 函数从堆中分配一块。
sp<IMemory> MemoryDealer::allocate(size_t size)
{
sp<IMemory> memory;
const ssize_t offset = allocator()->allocate(size);
if (offset >= 0) {
memory = new Allocation(this, heap(), offset, size);
}
return memory;
}
这里调用内存分配器进行分配。分配好的内存以 Allocation 的形式返回。Allocation 是 MemoryBase 的子类。因此它是可以被客户端使用的内存。Allocation 对象在析构的时候,会自动将内存回收。具体是通过 deallocate 回收内存。
void MemoryDealer::deallocate(size_t offset)
{
allocator()->deallocate(offset);
}
内存的分配和回收算法,请阅读 SimpleBestFitAllocator 源码。