C + + implementation of anonymous shared memory

Posted by neoboffins on Mon, 07 Feb 2022 20:51:24 +0100

1, Ashmem C language interface

You can usually use ashmem in the libcutils library_ create_ The region function creates a shared memory area:

#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;
}
  • The name parameter is a label for the memory area
  • The size parameter indicates the size of the shared memory,
  • The return value is the file descriptor of the open ashmem driver node.

After getting the file descriptor, you can map the memory allocated in the kernel space to the user space through mmap, so that it can be used like ordinary memory.

If other processes need to use this memory, they cannot reopen this memory by opening the device node, but need to obtain the file descriptor opened when creating this shared memory. For specific reasons, you can read ashmen's driver code (each time you open, you will create a new shared memory area, and close will recycle the previously created memory area).

So how to get file descriptors across processes? Of course, with the help of binder, the server writes the file description symbol parameters through writeFileDescriptor, and the client reads the file descriptor parameters through readFileDescriptor. For the specific implementation principle, refer to binder driver.

After the client process obtains the file descriptor, it also maps it to the user space through mmap, so as to realize memory sharing.

2, Ashmem's C + + package

To facilitate the use of shared memory, Android provides two interfaces in the C + + layer: imemoryhep and imememory.

1,IMemoryHeap

Memoryhead can be understood as a memory heap. It is a Binder interface. The server implementation class is memoryheadbase and the client implementation class is bpmemoryhead. Their previous inheritance relationship is as follows:

For the interface defined in imemoryhep class, only getHeapID needs to be called across processes, and other functions are implemented locally in subclasses. Therefore, it is not Binder's patent to restrict the interfaces between the client and the server through inheritance, but a design method of unexpected objects. We can also restrict the local methods.

(1) Server implementation

First, let's look at the constructor implementation of bpmemoryhep:

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);
            }
        }
    }
}

There are other overloaded versions of constructors, which will not be analyzed one by one here. First through ashmem_create_region creates shared memory regions, and then calls mapfd to map memory to user space.

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;
}

After mapping the memory, save the virtual address to mBase, the file descriptor to mFD, the memory size to mSize and the offset to mofset. Other attribute acquisition functions simply return the value of variables, such as getHeapID:

int MemoryHeapBase::getHeapID() const {
    return mFD;
}

The functions called across inheritance are processed in the onTransact of the parent class bnmemoryhep:

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);
    }
}

Only heap is processed here_ ID is a case. As mentioned earlier, only the getHeapID function is accessed across processes. Here, the file descriptor is written mainly through writeFileDescriptor. Other parameters include memory size, tag, and offset.

(2) Client implementation

The implementation of the client is relatively complex. Let's look at the code first:

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;
}

First, all interfaces call the assertMapped function and directly return the value of the property. Obviously, the assertMapped function initializes the attribute:

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);
        }
    }
}

The attribute variables in the constructor are initialized to illegal values. First, mhepid is equal to - 1 and find_ The heap function confirms whether an instance of the bmemoryheap object already exists. assertReallyMapped performs the mapping if no memory has been mapped. The purpose of this is to ensure that only the first bpmemoryhead object in the process will perform mmap operation, and other bpmemoryhead objects can directly copy the mapped attribute values.

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);
            }
        }
    }
}

Send heap to the server through the binder_ ID message, get the shared memory file descriptor through readFileDescriptor, then get other parameters to initialize the property, then call mmap to save the virtual address to mBase. Note that the server-side address is not mapped from the memory through the binder.

2,IMemory

Inheritance relationship involving class:

Memory is used to represent a piece of memory in the shared memory heap. It can be regarded as a subset of memoryhead.

(1) Implementation of server.

MemoryBase is the implementation class of the service, which is defined as follows:

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;

};
The constructor source code is as follows:

MemoryBase::MemoryBase(const sp<IMemoryHeap>& heap,
ssize_t offset, size_t size)
: mSize(size), mOffset(offset), mHeap(heap)
{
}

All member variables are initialized here. Mhep represents the heap where the memory is located, mofffset represents the offset value of the starting address of the memory from the heap memory, and mSize represents the size of the memory.
Among all methods of MemoryBase, only getMemory function can be accessed across processes through Binder. Handle messages sent by clients in BnMemory class:

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);
    }
}

You can see that this only deals with GET_MEMORY message. The implementation code of getMemory is as follows:

sp<IMemoryHeap> MemoryBase::getMemory(ssize_t* offset, size_t* size) const
{
    if (offset) *offset = mOffset;
    if (size) *size = mSize;
    return mHeap;
}

From the implementation point of view, getMemory gets all the information about this memory. The return value mhep is of memoryhearpbase type. Through Binder transmission, the client will obtain a bpmemoryhep object.

(2) Client implementation

The implementation class of the client is BpMemory, and only getMemory will initiate Binder calls.

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;
}

For clients, through the interface_ After cast conversion, mhep is initialized to an object of type bpmemoryhep. The Binder call also initializes both mofffset and mSize.

So how to get the first address of memory? The pointer function is defined in the parent class to return the first address of the memory.

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;
}

It's actually the base address plus the offset.

3,MemoryDealer

Android also provides a memory allocation tool class MemoryDealer, which is used to manage the memory heap. Note that this class does not have the ability of cross process communication. It only works on the server side.

MemoryDealer::MemoryDealer(size_t size, const char* name)
: mHeap(new MemoryHeapBase(size, 0, name)),
mAllocator(new SimpleBestFitAllocator(size))
{
}

From the constructor point of view, first create a new memory heap memoryhepbase, and then create a memory allocator SimpleBestFitAllocator. Later, if the server needs memory, allocate a block from the heap through the allocate function.

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;
}

Here, the memory allocator is called for allocation. The allocated memory is returned in the form of allocation. Allocation is a subclass of MemoryBase. Therefore, it is the memory that can be used by the client. When the allocation object is destructed, it will automatically reclaim the memory. Specifically, deallocate is used to reclaim memory.

void MemoryDealer::deallocate(size_t offset)
{
    allocator()->deallocate(offset);
}

For the memory allocation and recycling algorithm, please read the source code of SimpleBestFitAllocator.

Topics: C++ Android Framework ipc