匿名共享内存 C++ 实现

2022/2/8 7:14:03

本文主要是介绍匿名共享内存 C++ 实现,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、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 类中定义的接口,只有 getHeapID 是需要跨进程调用的,其它函数分别在子类中本地实现。因此通过继承来约束客户端和服务端的接口,使其保一致,者并非 Binder 的专利,而是一种没想对象的设计手段,我们同样可以对本地方法进行约束。

(1)服务端实现

首先看 BpMemoryHeap 的构造函数实现:

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, &reg);
        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,正如前面所说,只有 getHeapID 函数是跨进程访问的。这里主要通过 writeFileDescriptor 写入文件描述符。其它参数还有内存大小、标记和偏移。

(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 对象直接拷贝映射好的属性值即可。

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 源码。



这篇关于匿名共享内存 C++ 实现的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程