How does the SurfaceFlinger service transmit data from the View View on the APP side in Android? The data drawn by View will be displayed on the screen frame by frame, and each frame will occupy a certain amount of storage space. When executing DRAW on the APP side, the data will obviously be drawn to the process space of the APP, but the View window will generate the final frame only after SurfaceFlinger layer mixing. SurfaceFlinger runs in a separate server process, so how does View data transfer between the two processes? Binder communication is not suitable for this kind of communication. What IPC is used for View data communication? The answer is shared memory, or more precisely anonymous shared memory. Shared Memory is a built-in IPC mechanism in Linux. Android uses this model directly, but makes its own improvements to form Anonymous Shared Memory-Ashmem for Android. Through Ashmem, APP process and SurfaceFlinger share the same memory, so there is no need to copy data. After drawing, APP side will notify SurfaceFlinger side to synthesize, and then output to hardware for display. Of course, the details will be more complicated. This article mainly analyzes the principle of anonymous shared memory and features in Android, the following details, but first take a look at Linux shared memory usage, a brief understanding:

View drawing with shared memory. JPG

Linux Shared Memory

So let’s look at the two key functions,

  • int shmget(key_t key, size_t size, int shmflg); This function is used to create shared memory
  • void shmat(int shm_id, const void shm_addr, int shmflg); To access shared memory, it must be mapped to the address space of the current process

Linux shared memory is named shared memory, and its name is key

Reading processes

int main() { void *shm = NULL; Struct shared_use_st *shared; // point to SHM int shmid; / / / / create the Shared memory the Shared memory identifier shmid = shmget ((key_t) 12345, sizeof (struct shared_use_st), 0666 | IPC_CREAT); // map shared memory to the address space of the current process SHM = shmat(shmid, 0, 0); Struct shared_use_st*) SHM; shared->written = 0; // Access shared memorywhile(1) {if(shared->written ! = 0) {printf("You wrote: %s", shared->text);
             if(strncmp(shared->text, "end", 3) = = 0)break; }} // Separate shared memory from the current processif(SHMDT (SHM) == -1) {} // Delete shared memoryif(shmctl(shmid, IPC_RMID, 0) == -1)   {  }  
    exit(EXIT_SUCCESS);  
}  Copy the code

Writing process

int main() { void *shm = NULL; struct shared_use_st *shared = NULL; char buffer[BUFSIZ + 1]; // Save the input text int shmid; / / create the Shared memory shmid = shmget ((key_t) 12345, sizeof (struct shared_use_st), 0666 | IPC_CREAT); SHM = shmat(shmid, (void*)0, 0);printf("Memory attached at %X\n", (int)shm); Struct shared_use_st*) SHM;while(1)// Write data to the shared memory {// If the data has not been read, the data is waiting to be read, and the text cannot be written to the shared memorywhile(shared->written == 1) { sleep(1); } // Write data to shared memory fgets(buffer, BUFSIZ, stdin); strncpy(shared->text, buffer, TEXT_SZ); shared->written = 1;if(strncmp(buffer, "end", 3) == 0) running = 0; } // Separate shared memory from the current processif(shmdt(shm) == -1)   {    }  
    sleep(2);  
    exit(EXIT_SUCCESS);  
} Copy the code

It can be seen that Linux shared memory communication efficiency is very high, between processes do not need to transfer data, it can be directly accessed, the disadvantages are also very obvious, Linux shared memory does not provide synchronization mechanism, when using, to use other means to deal with inter-process synchronization. The bionic library removes the glibc shmget function, which prevents Android from implementing named shared memory in shmGET mode. Of course, it does not want to use that function. Android based on this, Created its own anonymous shared memory approach.

Anonymous shared memory for Android

Android can use all of Linux’s IPC communication methods, including Shared Memory. However, Android mainly uses Anonymous Shared Memory (Ashmem), which is different from native. For example, it adds mutex to its driver. In addition, the transfer of FD to achieve shared memory transfer. MemoryFile is an object encapsulated by Android for anonymous shared memory. Here, MemoryFile is used to analyze how Android uses shared memory to realize big data transmission. Meanwhile, MemoryFile is also a means of big data transmission between processes.

IMemoryAidlInterface.aidl

package com.snail.labaffinity;
import android.os.ParcelFileDescriptor;

interface IMemoryAidlInterface {
    ParcelFileDescriptor getParcelFileDescriptor();
}Copy the code

MemoryFetchService

public class MemoryFetchService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MemoryFetchStub();
    }
    static class MemoryFetchStub extends IMemoryAidlInterface.Stub {
        @Override
        public ParcelFileDescriptor getParcelFileDescriptor() throws RemoteException {
            MemoryFile memoryFile = null;
            try {
                memoryFile = new MemoryFile("test_memory", 1024);
                memoryFile.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
                Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
                FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);
                return ParcelFileDescriptor.dup(des);
            } catch (Exception e) {}
            return null;
     }}}Copy the code

TestActivity.java

 Intent intent = new Intent(MainActivity.this, MemoryFetchService.class);
        bindService(intent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {

                byte[] content = new byte[10];
                IMemoryAidlInterface iMemoryAidlInterface
                        = IMemoryAidlInterface.Stub.asInterface(service);
                try {
                    ParcelFileDescriptor parcelFileDescriptor = iMemoryAidlInterface.getParcelFileDescriptor();
                    FileDescriptor descriptor = parcelFileDescriptor.getFileDescriptor();
                    FileInputStream fileInputStream = new FileInputStream(descriptor);
                    fileInputStream.read(content);
                } catch (Exception e) {
                }}

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, Service.BIND_AUTO_CREATE);    Copy the code

This is how the application layer uses anonymous shared memory. The key point is the passing of file descriptors. File descriptors are the main way to access and update files in Linux. MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile: MemoryFile You can go directly to proc/pid:

The requested share is stored in display.jpg in proc

The following is a brief analysis of the allocation and transfer of shared memory based on MemoryFile. Let’s first look at the constructor of MemoryFile

public MemoryFile(String name, int length) throws IOException {
    mLength = length;
    mFD = native_open(name, length);
    if (length > 0) {
        mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);
    } else{ mAddress = 0; }}Copy the code

Native_open calls ashmem_create_region to create shared memory.

    static jobject android_os_MemoryFile_open(JNIEnv* env, jobject clazz, jstring name, jint length)
{
    const char* namestr = (name ? env->GetStringUTFChars(name, NULL) : NULL);

    int result = ashmem_create_region(namestr, length);

    if (name)
        env->ReleaseStringUTFChars(name, namestr);

    if (result < 0) {
        jniThrowException(env, "java/io/IOException"."ashmem_create_region failed");
        return NULL;
    }

    return jniCreateFileDescriptor(env, result);
}Copy the code

Then mmap is called by native_mmap to map the shared memory to the current process space, and then the Java layer can access the shared memory as if it were a FileDescriptor, using FileDescriptor.

static jint android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor, jint length, jint prot) { int fd = jniGetFDFromFileDescriptor(env, fileDescriptor); <! --> jint result = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0);if(! result) jniThrowException(env,"java/io/IOException"."mmap failed");
    return result;
}            Copy the code

How does the ashmem_create_region function request a block of shared memory from Linux?

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;
}Copy the code

ASHMEM_DEVICE is an abstract shared memory device. It is a miscellaneous device (a type of character device). After the driver is loaded, an AShem file is inserted under /dev, and the user can access the device file. Different from ordinary disk device files, serial port field device files:

#define ASHMEM_DEVICE "/dev/ashmem"
static struct miscdevice ashmem_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "ashmem",
    .fops = &ashmem_fops,
};Copy the code

How to apply for shared memory? The open function is very common. It creates an ashmem_area object

static int ashmem_open(struct inode *inode, struct file *file)
{
    struct ashmem_area *asma;
    int ret;

    ret = nonseekable_open(inode, file);
    if (unlikely(ret))
        return ret;

    asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
    if(unlikely(! asma))return -ENOMEM;

    INIT_LIST_HEAD(&asma->unpinned_list);
    memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
    asma->prot_mask = PROT_MASK;
    file->private_data = asma;
    return 0;
}Copy the code

Next, set the size of the shared memory using ashmem_ioctl.

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct ashmem_area *asma = file->private_data;
    long ret = -ENOTTY;
    switch (cmd) {
    ...
    case ASHMEM_SET_SIZE:
        ret = -EINVAL;
        if(! asma->file) { ret = 0; asma->size = (size_t) arg; }break; . }return ret;
}    Copy the code

As you can see, memory is not actually allocated, which is also true of Linux style. Memory is allocated only when it is actually used by the page break, and then the Mmap function, does it allocate memory?

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma) { struct ashmem_area *asma = file->private_data; int ret = 0; mutex_lock(&ashmem_mutex); .if(! asma->file) { char *name = ASHMEM_NAME_DEF; struct file *vmfile;if(asma->name[ASHMEM_NAME_PREFIX_LEN] ! ='\ 0') name = asma->name; // Create temporary files for backup, such as temporary files in kernel mode, user mode is not visible, we can not use the command to query, can be considered as a hidden file, user space can not see! <! --> vmfile = shmem_file_setup(name, ASMA ->size, vMA ->vm_flags); asma->file = vmfile; } get_file(asma->file);if (vma->vm_flags & VM_SHARED)
        shmem_set_file(vma, asma->file);
    else {
        if (vma->vm_file)
            fput(vma->vm_file);
        vma->vm_file = asma->file;
    }
    vma->vm_flags |= VM_CAN_NONLINEAR;
out:
    mutex_unlock(&ashmem_mutex);
    return ret;
}Copy the code

If the name is not set, ASHMEM_NAME_PREFIX will be used as the default name. However, you don’t see the memory allocation function directly here. However, the shmem_file_setup and shmem_set_file functions are important and a bit confusing for shared memory. Shmem_file_setup is the native Linux shared memory mechanism, but Android also modifies the Linux shared memory driver code. Anonymous shared memory is an improvement on Linux shared memory,

struct file *shmem_file_setup(char *name, loff_t size, unsigned long flags) { int error; struct file *file; struct inode *inode; struct dentry *dentry, *root; struct qstr this; error = -ENOMEM; this.name = name; this.len = strlen(name); this.hash = 0; /* will go */ root = shm_mnt->mnt_root; dentry = d_alloc(root, &this); Dentry cat/proc/pid/maps error = -enfile; file = get_empty_filp(); // Allocate file error = -enospc; inode = shmem_get_inode(root->d_sb, S_IFREG | S_IRWXUGO, 0, flags); D_instantiate (dentry, inode) _instantiate(dentry, inode); // bind inode->i_size = size; inode->i_nlink = 0; /* It is unlinked */ // the file operator is not created in memory. init_file(file, shm_mnt, dentry, FMODE_WRITE | FMODE_READ, &shmem_file_operations); // bind and specify the file operation pointer as shmem_file_operations... }Copy the code

Shmem_file_setup creates a temporary file in the TMPFS temporary file system (perhaps just an inode node in the kernel) that corresponds to the anonymous shared memory created by the Ashmem driver, but the temporary file is not visible in user mode and can then be used. Vma ->vm_file = asMA ->file is used to replace the map object. Vma ->vm_file = asMA ->file is used to replace the map object. Call shmem_file_setup instead of ashmem. Look at the corresponding hook function for temporary files.

void shmem_set_file(struct vm_area_struct *vma, struct file *file)
{
    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_vm_ops;
}Copy the code

Go back to the previous MemoryFile and look at the write operations:

public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
        throws IOException {
    if (isDeactivated()) {
        throw new IOException("Can't write to deactivated memory file.");
    }
    if (srcOffset < 0 || srcOffset > buffer.length || count < 0
            || count > buffer.length - srcOffset
            || destOffset < 0 || destOffset > mLength
            || count > mLength - destOffset) {
        throw new IndexOutOfBoundsException();
    }
    native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
}Copy the code

Go into native code

static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,
        jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,
        jint count, jboolean unpinned)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {
        ashmem_unpin_region(fd, 0, 0);
        return- 1; } env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);if (unpinned) {
        ashmem_unpin_region(fd, 0, 0);
    }
    return count;
}Copy the code

In the kernel, a chunk of memory corresponds to a data structure called ashmem_area:

struct ashmem_area { char name[ASHMEM_FULL_NAME_LEN]; /* optional namefor /proc/pid/maps */
    struct list_head unpinned_list;    /* list of all ashmem areas */
    struct file *file;        /* the shmem-based backing file */
    size_t size;            /* size of the mapping, in bytes */
    unsigned long prot_mask;    /* allowed prot bits, as vm_flags */
};Copy the code

When a block of memory is allocated using Ashmem and part of the memory is not used, it can be unpin away, and the kernel can reclaim the unpin physical page. The reclaimed memory can be retrieved again (via the missing page handler), because the unpin operation does not change the existing Mmap address space. However, A MemoryFile will only operate on the entire shared memory and will not be accessed in chunks. Therefore, pin and unpin are not important for memoryfiles. It can be treated as if the entire region is pin or unpin. Then call the corresponding operation of TMPFS file and allocate the physical page. In the current Android kernel, the function of vm_OPERATIONs_struct corresponding to the page missing interrupt is fault, and in the shared memory implementation, the corresponding function is shmem_fault as follows:

static struct vm_operations_struct shmem_vm_ops = {
    .fault        = shmem_fault,

#ifdef CONFIG_NUMA
    .set_policy     = shmem_set_policy,
    .get_policy     = shmem_get_policy,
#endif
};Copy the code

When mmap’s TMPFS file causes a page-missing interrupt, the shmem_fault function is called,

static int shmem_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
    struct inode *inode = vma->vm_file->f_path.dentry->d_inode;
    int error;
    int ret;

    if (((loff_t)vmf->pgoff << PAGE_CACHE_SHIFT) >= i_size_read(inode))
        return VM_FAULT_SIGBUS;

    error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret);
    if (error)
        return ((error == -ENOMEM) ? VM_FAULT_OOM : VM_FAULT_SIGBUS);

    return ret | VM_FAULT_LOCKED;
}Copy the code

At this point, you can see that the shmem_getPage function is called to allocate the actual physical page, but the specific allocation strategy is more complicated than we need to analyze.

Android anonymous shared memory pin and unpin

Ashmem_pin_region and ashmem_unpin_region are literally used to lock and unlock anonymous shared memory to identify which memory is in use and which memory is not. Thus, The Ashmem driver can assist memory management to a certain extent and provide some memory optimization capabilities. At the beginning of the anonymous shared memory, all the data are pinned. Only users who actively apply for an nvdata block will be unpinned, and only those who have an unpinned state can be re-pinned. Now take a closer look at the driver and the implementation of pin and unpin

static int __init ashmem_init(void) { int ret; <! Ashmem_area_cachep = kmem_cache_create("ashmem_area_cache", sizeof(struct ashmem_area), 0, 0, NULL); . <! Ashmem_range_cachep = kmem_cache_create("ashmem_range_cache", sizeof(struct ashmem_range), 0, 0, NULL); . <! --> ret = misc_register(&ashmem_misc); . register_shrinker(&ashmem_shrinker);return 0;
}Copy the code

When asHEM is opened, ashmem_areA_cachep is used to tell the cache to create a new ashmem_area object and to initialize the unpinned_list, which must start with null

static int ashmem_open(struct inode *inode, struct file *file) { struct ashmem_area *asma; int ret; ret = nonseekable_open(inode, file); asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL); <! INIT_LIST_HEAD(& asMA ->unpinned_list); memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN); asma->prot_mask = PROT_MASK; file->private_data = asma;return 0;
}Copy the code

We started with pin, so let’s see how pin and unpin are called:

int ashmem_pin_region(int fd, size_t offset, size_t len)
{
    struct ashmem_pin pin = { offset, len };
    return ioctl(fd, ASHMEM_PIN, &pin);
}

int ashmem_unpin_region(int fd, size_t offset, size_t len)
{
    struct ashmem_pin pin = { offset, len };
    return ioctl(fd, ASHMEM_UNPIN, &pin);
}Copy the code

Then see ashmem_unpin

static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
    struct ashmem_range *range, *next;
    unsigned int purged = ASHMEM_NOT_PURGED;
    restart:
    list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {

        if (range_before_page(range, pgstart))
            break;

        if (page_range_subsumed_by_range(range, pgstart, pgend))
            return 0;
        if(page_range_in_range(range, pgstart, pgend)) { pgstart = min_t(size_t, range->pgstart, pgstart), pgend = max_t(size_t, range->pgend, pgend); purged |= range->purged; range_del(range); goto restart; }}return range_alloc(asma, range, purged, pgstart, pgend);
}Copy the code

Ashmem_range unpinned_list ashmem_area unpinned_list ashmem_range unpinned_list ashmem_area unpinned_list ashmem_area Then create a new merged ashmem_range and insert unpinned_list.

Shared memory. JPG

The pin function is used to put a block of shared memory into use. If it is in the unpinedList, remove it:

static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
    struct ashmem_range *range, *next;
    int ret = ASHMEM_NOT_PURGED;

    list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
        /* moved past last applicable page; we can short circuit */

        if (range_before_page(range, pgstart))
            break;
        if (page_range_in_range(range, pgstart, pgend)) {
            ret |= range->purged;

            if (page_range_subsumes_range(range, pgstart, pgend)) {
                range_del(range);
                continue;
            }

            if (range->pgstart >= pgstart) {
                range_shrink(range, pgend + 1, range->pgend);
                continue;
            }
            if (range->pgend <= pgend) {
                range_shrink(range, range->pgstart, pgstart-1);
                continue;
            }

            range_alloc(asma, range, range->purged,
                    pgend + 1, range->pgend);
            range_shrink(range, range->pgstart, pgstart - 1);
            break; }}return ret;
}Copy the code

Pin Shared memory. JPG

Android process shared memory transfer – FD file descriptor transfer

Native Linux shared memory is handled by passing known keys, but Android does not have this mechanism. How does Android handle this? Android’s Binder also ADAPTS the transfer of FDS by converting FDS to the target process at the kernel level. Linux’s FDS are only valid and unique for this process. Process A opens A file and gets an FD. It cannot be used directly by process B, because the FD in B may not be valid at all, or it may correspond to other files. However, the same file can have multiple file descriptors, but there is only one file, and only one inode node and file object will correspond to the kernel layer. This is the basis for the kernel layer to pass fd. Binder drivers use the current process fd to find the corresponding file, create A new FD for the target process, and pass it to the target process. Binder drivers convert the FD in process A to that in process B.

void binder_transaction(){
   ...
        caseBINDER_TYPE_FD: { int target_fd; struct file *file; <! Fget (fp->handle); fget(fp->handle); <! -- Keypoint 2, get the target process free fd- > target_fd = task_get_unusED_fd_flags (target_proc, O_CLOEXEC); <! --> task_fd_install(target_proc, target_fd, file); fp->handle = target_fd; }break; . } <! Struct file *fget(unsigned int fd) {struct file *file; struct files_struct *files = current->files; rcu_read_lock(); file = fcheck_files(files, fd); rcu_read_unlock();return file;
}


static void task_fd_install(
    struct binder_proc *proc, unsigned int fd, struct file *file)
{
    struct files_struct *files = proc->files;
    struct fdtable *fdt;
    if (files == NULL)
        return;
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    rcu_assign_pointer(fdt->fd[fd], file);
    spin_unlock(&files->file_lock);
}Copy the code

Fd. JPG

Why can’t I see files corresponding to anonymous shared memory

If the kernel is not defined(CONFIG_TMPFS), TMPFS will not be visible to users.

If CONFIG_TMPFS is not set, the user visible part of tmpfs is not build. But the internal mechanisms are always present.

There is no defined(CONFIG_TMPFS) in the Android shmem.c driver, this is just a guess, maybe there are other explanations, if you know, please guide.

conclusion

Android anonymous shared memory is based on Linux shared memory. All files are created on TMPFS file system and mapped to different process Spaces, so as to achieve the purpose of shared memory. However, Android has been modified on the basis of Linux. Binder+ FD file descriptor is used to transfer shared memory.

Android anonymous shared memory (Ashmem) principle is only for reference, welcome to point out