Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析

Android社区 收藏文章

在上一文章Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划中,我们简要介绍了Android系统的匿名共享内存机制,其中,简要提到了它具有辅助内存管理系统来有效地管理内存的特点,但是没有进一步去了解它是如何实现的。在本文中,我们将通过分析Android系统的匿名共享内存Ashmem驱动程序的源代码,来深入了解它是如何辅助内存管理系

Android系统的匿名共享内存Ashmem机制并没有自立山头,从头搞一套自己的共享内存机制,而是建立在Linux内核实现的共享内存的基础上的。与此同时,它又向Linux内存管理系统的内存回收算法注册接口,告诉Linux内存管理系统它的某些内存块不再使用了,可以被回收了,不过,这些不再使用的内存需要由它的使用者来告诉Ashmem驱动程序。通过这种用户-Ashmem驱动程序-内存管理系统三者的紧密合作,实现有效的内存管理机制,适合移动设备小内存的特点。

Android系统的匿名共享内存Ashmem驱动程序利用了Linux的共享内存子系统导出的接口来实现自己的功能,因此,它的实现非常小巧,总共代码不到700行。虽然代码很少,但是这里不打算机械式地一行一行地阅读和分析Ashmem驱动程序的源代码,而是通过使用情景来分析,这样可以帮助我们清晰地理解它的实现原理。我们这里所说的使用情景,将从Android系统的应用程序框架层提供的匿名共享内存接口开始,经过系统运行时库层,最终到达驱动程序层,通过这样一个完整的过程来理解Android系统的匿名共享内存Ashmem机制。这里,我们将从上一篇文章Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划介绍的Android应用程序框架层提供MemoryFile接口开始,分别介绍Android系统匿名共享内存的创建(open)、映射(mmap)、读写(read/write)以及锁定和解锁(pin/unpin)四个使用情景。

在进入到这个四个使用情景前,我们先来看一下Ashmem驱动程序模块的初始化函数,看看它给用户空间暴露了什么接口,即它创建了什么样的设备文件,以及提供了什么函数来操作这个设备文件。Ashmem驱动程序实现在kernel/common/mm/ashmem.c文件中,它的模块初始化函数定义为ashmem_init:

static struct file_operations ashmem_fops = {
        .owner = THIS_MODULE,
        .open = ashmem_open,
        .release = ashmem_release,
        .mmap = ashmem_mmap,
        .unlocked_ioctl = ashmem_ioctl,
        .compat_ioctl = ashmem_ioctl,
    };

    static struct miscdevice ashmem_misc = {
        .minor = MISC_DYNAMIC_MINOR,
        .name = "ashmem",
        .fops = &ashmem_fops,
    };

    static int __init ashmem_init(void)
    {
        int ret;

        ......

        ret = misc_register(&ashmem_misc);
        if (unlikely(ret)) {
            printk(KERN_ERR "ashmem: failed to register misc device!\n");
            return ret;
        }

        ......

        return 0;
    }

这里,我们可以看到,Ahshmem驱动程序在加载时,会创建一个/dev/ashmem的设备文件,这是一个misc类型的设备。注册misc设备是通过misc_register函数进行的,关于这个函数的详细实现,可以参考前面Android日志系统驱动程序Logger源代码分析一文,调用这个函数成功后,就会在/dev目录下生成一个ashmem设备文件了。同时,我们还可以看到,这个设备文件提供了open、mmap、release和ioctl四种操作。为什么没有read和write操作呢?这是因为读写共享内存的方法是通过内存映射地址来进行的,即通过mmap系统调用把这个设备文件映射到进程地址空间中,然后就直接对内存进行读写了,不需要通过read 和write文件操作,后面我们将会具体分析是如何实现的。 有了这个基础之后,下面我们就分四个部分来分别介绍匿名共享内存的创建(open)、映射(mmap)、读写(read/write)以及锁定和解锁(pin/unpin)使用情景。

一. 匿名共享内存的创建操作

在Android应用程序框架层提供MemoryFile类的构造函数中,进行了匿名共享内存的创建操作,我们先来看一下这个构造函数的实现,它位于frameworks/base/core/java/android/os/MemoryFile.java文件中:

public class MemoryFile
    {
        ......

        private static native FileDescriptor native_open(String name, int length) throws IOException;

        ......

        private FileDescriptor mFD;        // ashmem file descriptor
        ......
        private int mLength;    // total length of our ashmem region

        ......

        /**
        * Allocates a new ashmem region. The region is initially not purgable.
        *
        * @param name optional name for the file (can be null).
        * @param length of the memory file in bytes.
        * @throws IOException if the memory file could not be created.
        */
        public MemoryFile(String name, int length) throws IOException {
            mLength = length;
            mFD = native_open(name, length);
            ......
        }

        ......
    }

这里我们看到,这个构造函数最终是通过JNI方法native_open来创建匿名内存共享文件。这个JNI方法native_open实现在frameworks/base/core/jni/adroid_os_MemoryFile.cpp文件中:

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

这个函数又通过运行时库提供的接口ashmem_create_region来创建匿名共享内存,这个函数实现在system/core/libcutils/ashmem-dev.c文件中:

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

这里,一共通过执行三个文件操作系统调用来和Ashmem驱动程序进行交互,分虽是一个open和两个ioctl操作,前者是打开设备文件ASHMEM_DEVICE,后者分别是设置匿名共享内存的名称和大小。

在介绍这三个文件操作之前,我们先来了解一下Ashmem驱动程序的一个相关数据结构struct ashmem_area,这个数据结构就是用来表示一块共享内存的,它定义在kernel/common/mm/ashmem.c文件中:

/*
     * ashmem_area - anonymous shared memory area
     * Lifecycle: From our parent file's open() until its release()
     * Locking: Protected by `ashmem_mutex'
     * Big Note: Mappings do NOT pin this structure; it dies on close()
     */
    struct ashmem_area {
        char name[ASHMEM_FULL_NAME_LEN];/* optional name for /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 */
    };

域name表示这块共享内存的名字,这个名字会显示/proc//maps文件中,表示打开这个共享内存文件的进程ID;域unpinned_list是一个列表头,它把这块共享内存中所有被解锁的内存块连接在一起,下面我们讲内存块的锁定和解锁操作时会看到它的用法;域file表示这个共享内存在临时文件系统tmpfs中对应的文件,在内核决定要把这块共享内存对应的物理页面回收时,就会把它的内容交换到这个临时文件中去;域size表示这块共享内存的大小;域prot_mask表示这块共享内存的访问保护位。

在Ashmem驱动程中,所有的ashmem_area实例都是从自定义的一个slab缓冲区创建的。这个slab缓冲区是在驱动程序模块初始化函数创建的,我们来看一个这个初始化函数的相关实现:

static int __init ashmem_init(void)
    {
        int ret;

        ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
            sizeof(struct ashmem_area),
            0, 0, NULL);
        if (unlikely(!ashmem_area_cachep)) {
            printk(KERN_ERR "ashmem: failed to create slab cache\n");
            return -ENOMEM;
        }

        ......

        return 0;
    }

全局变量定义在文件开头的地方:

static struct kmem_cache *ashmem_area_cachep __read_mostly;

它的类型是struct kmem_cache,表示这是一个slab缓冲区,由内核中的内存管理系统进行管理。

这里就是通过kmem_cache_create函数来创建一个名为"ashmem_area_cache"、对象大小为sizeof(struct ashmem_area)的缓冲区了。缓冲区创建了以后,就可以每次从它分配一个struct ashmem_area对象了。关于Linux内核的slab缓冲区的相关知识,可以参考前面Android学习启动篇一文中提到的一本参考书籍《Understanding the Linux Kernel》的第八章Memory Managerment。

有了这些基础知识后,我们回到前面的ashmem_create_region函数中。

首先是执行打开文件的操作:

fd = open(ASHMEM_DEVICE, O_RDWR);

ASHMEM_DEVICE是一个宏,定义为:

#define ASHMEM_DEVICE   "/dev/ashmem"

这里就是匿名共享内存设备文件/dev/ashmem了。

从上面的描述我们可以知道,调用这个open函数最终会进入到Ashmem驱动程序中的ashmem_open函数中去:

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

首先是通过nonseekable_open函数来设备这个文件不可以执行定位操作,即不可以执行seek文件操作。接着就是通过kmem_cache_zalloc函数从刚才我们创建的slab缓冲区ashmem_area_cachep来创建一个ashmem_area结构体了,并且保存在本地变量asma中。再接下去就是初始化变量asma的其它域,其中,域name初始为ASHMEM_NAME_PREFIX,这是一个宏,定义为:

#define ASHMEM_NAME_PREFIX "dev/ashmem/"
    #define ASHMEM_NAME_PREFIX_LEN (sizeof(ASHMEM_NAME_PREFIX) - 1)

函数的最后是把这个ashmem_area结构保存在打开文件结构体的private_data域中,这样,Ashmem驱动程序就可以在其它地方通过这个private_data域来取回这个ashmem_area结构了。

到这里,设备文件/dev/ashmem的打开操作就完成了,它实际上就是在Ashmem驱动程序中创建了一个ashmem_area结构,表示一块新的共享内存。

再回到ashmem_create_region函数中,又调用了两次ioctl文件操作分别来设备这块新建的匿名共享内存的名字和大小。在kernel/comon/mm/include/ashmem.h文件中,ASHMEM_SET_NAME和ASHMEM_SET_SIZE的定义为:

#define ASHMEM_NAME_LEN     256

    #define __ASHMEMIOC     0x77

    #define ASHMEM_SET_NAME     _IOW(__ASHMEMIOC, 1, char[ASHMEM_NAME_LEN])
    #define ASHMEM_SET_SIZE     _IOW(__ASHMEMIOC, 3, size_t)

先来看ASHMEM_SET_NAME命令的ioctl调用,它最终进入到Ashmem驱动程序的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_NAME:
            ret = set_name(asma, (void __user *) arg);
            break;
        ......
        }

        return ret;
    }

这里通过set_name函数来进行实际操作:

static int set_name(struct ashmem_area *asma, void __user *name)
    {
        int ret = 0;

        mutex_lock(&ashmem_mutex);

        /* cannot change an existing mapping's name */
        if (unlikely(asma->file)) {
            ret = -EINVAL;
            goto out;
        }

        if (unlikely(copy_from_user(asma->name + ASHMEM_NAME_PREFIX_LEN,
                        name, ASHMEM_NAME_LEN)))
            ret = -EFAULT;
        asma->name[ASHMEM_FULL_NAME_LEN-1] = '\0';

    out:
        mutex_unlock(&ashmem_mutex);

        return ret;
    }

这个函数实现很简单,把用户空间传进来的匿名共享内存的名字设备到asma->name域中去。注意,匿名共享内存块的名字的内容分两部分,前一部分是前缀,这是在open操作时,由驱动程序默认设置的,固定为ASHMEM_NAME_PREFIX,即"dev/ashmem/";后一部分由用户指定,这一部分是可选的,即用户可以不调用ASHMEM_SET_NAME命令来设置匿名共享内存块的名字。

再来看ASHMEM_SET_SIZE命令的ioctl调用,它最终也是进入到Ashmem驱动程序的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;
    }

这个实现很简单,只是把用户空间传进来的匿名共享内存的大小值保存在对应的asma->size域中。

这样,ashmem_create_region函数就执先完成了,层层返回,最后回到应用程序框架层提供的接口Memory的构造函数中,整个匿名共享内存的创建过程就完成了。前面我们说过过,Ashmem驱动程序不提供read和write文件操作,进程若要访问这个共享内存,必须要把这个设备文件映射到自己的进程空间中,然后进行直接内存访问,这就是我们下面要介绍的匿名共享内存设备文件的内存映射操作了。

二. 匿名共享内存设备文件的内存映射操作

在MemoryFile类的构造函数中,进行了匿名共享内存的创建操作后,下一步就是要把匿名共享内存设备文件映射到进程空间来了:

public class MemoryFile
    {
        ......

        // returns memory address for ashmem region
        private static native int native_mmap(FileDescriptor fd, int length, int mode)
            throws IOException;

        ......

        private int mAddress;   // address of ashmem memory

        ......

        /**
        * Allocates a new ashmem region. The region is initially not purgable.
        *
        * @param name optional name for the file (can be null).
        * @param length of the memory file in bytes.
        * @throws IOException if the memory file could not be created.
        */
        public MemoryFile(String name, int length) throws IOException {
            ......
            mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);
            ......
        }
    }

映射匿名共享内存设备文件到进程空间是通过JNI方法native_mmap来进行的。这个JNI方法实现在frameworks/base/core/jni/adroid_os_MemoryFile.cpp文件中:

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

这里的文件描述符fd是在前面open匿名设备文件/dev/ashmem获得的,有个这个文件描述符后,就可以直接通过mmap来执行内存映射操作了。这个mmap系统调用最终进入到Ashmem驱动程序的ashmem_mmap函数中:

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

        /* user needs to SET_SIZE before mapping */
        if (unlikely(!asma->size)) {
            ret = -EINVAL;
            goto out;
        }

        /* requested protection bits must match our allowed protection mask */
        if (unlikely((vma->vm_flags & ~asma->prot_mask) & PROT_MASK)) {
            ret = -EPERM;
            goto out;
        }

        if (!asma->file) {
            char *name = ASHMEM_NAME_DEF;
            struct file *vmfile;

            if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')
                name = asma->name;

            /* ... and allocate the backing shmem file */
            vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
            if (unlikely(IS_ERR(vmfile))) {
                ret = PTR_ERR(vmfile);
                goto out;
            }
            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;
    }

这个函数的实现也很简单,它调用了Linux内核提供的shmem_file_setup函数来在临时文件系统tmpfs中创建一个临时文件,这个临时文件与Ashmem驱动程序创建的匿名共享内存对应。函数shmem_file_setup是Linux内核中用来创建共享内存文件的方法,而Linux内核中的共享内存机制其实是一种进程间通信(IPC)机制,它的实现相对也是比较复杂,Android系统的匿名共享内存机制正是由于直接使用了Linux内核共享内存机制,它才会很小巧,它站在巨人的肩膀上了。关于Linux内核中的共享内存的相关知识,可以参考前面Android学习启动篇一文中提到的一本参考书籍《Linux内核源代码情景分析》的第六章传统的Unix进程间通信第七小节共享内存。

通过shmem_file_setup函数创建的临时文件vmfile最终就保存在vma->file中了。这里的vma是由Linux内核的文件系统层传进来的,它的类型为struct vm_area_struct,它表示的是当前进程空间中一块连续的虚拟地址空间,它的起始地址可以由用户来指定,也可以由内核自己来分配,这里我们从JNI方法native_mmap调用的mmap的第一个参数为NULL可以看出,这块连续的虚拟地址空间的起始地址是由内核来指定的。文件内存映射操作完成后,用户访问这个范围的地址空间就相当于是访问对应的文件的内容了。有关Linux文件的内存映射操作,同样可以参考前面Android学习启动篇一文中提到的一本参考书籍《Linux内核源代码情景分析》的第二章内存管理第十三小节系统调用mmap。从这里我们也可以看出,Android系统的匿名共享内存是在虚拟地址空间连续的,但是在物理地址空间就不一定是连续的了。

同时,这个临时文件vmfile也会保存asma->file域中,这样,Ashmem驱动程序后面就可以通过在asma->file来操作这个匿名内存共享文件了。

函数ashmem_mmap执行完成后,经过层层返回到JNI方法native_mmap中去,就从mmap函数的返回值中得到了这块虚拟空间的起始地址了,这个起始地址最终返回到应用程序框架层的MemoryFile类的构造函数中,并且保存在成员变量mAddress中,后面,共享内存的读写操作就是对这个地址空间进行操作了。

三. 匿名共享内存的读写操作

因为前面对匿名共享内存文件进行内存映射操作,这里对匿名内存文件内容的读写操作就比较简单了,就像访问内存变量一样就行了。

我们来看一下MemoryFile类的读写操作函数:

public class MemoryFile
    {
        ......

        private static native int native_read(FileDescriptor fd, int address, byte[] buffer,
            int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;
        private static native void native_write(FileDescriptor fd, int address, byte[] buffer,
            int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;

        ......

        private FileDescriptor mFD;        // ashmem file descriptor
        private int mAddress;   // address of ashmem memory
        private int mLength;    // total length of our ashmem region
        private boolean mAllowPurging = false;  // true if our ashmem region is unpinned

        ......

        /**
        * Reads bytes from the memory file.
        * Will throw an IOException if the file has been purged.
        *
        * @param buffer byte array to read bytes into.
        * @param srcOffset offset into the memory file to read from.
        * @param destOffset offset into the byte array buffer to read into.
        * @param count number of bytes to read.
        * @return number of bytes read.
        * @throws IOException if the memory file has been purged or deactivated.
        */
        public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) 
        throws IOException {
            if (isDeactivated()) {
                throw new IOException("Can't read from deactivated memory file.");
            }
            if (destOffset < 0 || destOffset > buffer.length || count < 0
                || count > buffer.length - destOffset
                || srcOffset < 0 || srcOffset > mLength
                || count > mLength - srcOffset) {
                    throw new IndexOutOfBoundsException();
            }
            return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
        }

        /**
        * Write bytes to the memory file.
        * Will throw an IOException if the file has been purged.
        *
        * @param buffer byte array to write bytes from.
        * @param srcOffset offset into the byte array buffer to write from.
        * @param destOffset offset  into the memory file to write to.
        * @param count number of bytes to write.
        * @throws IOException if the memory file has been purged or deactivated.
        */
        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);
        }

        ......
    }

这里,我们可以看到,MemoryFile的匿名共享内存读写操作都是通过JNI方法来实现的,读操作和写操作的JNI方法分别是native_read和native_write,它们都是定义在frameworks/base/core/jni/adroid_os_MemoryFile.cpp文件中:

static jint android_os_MemoryFile_read(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);
            jniThrowException(env, "java/io/IOException", "ashmem region was purged");
            return -1;
        }

        env->SetByteArrayRegion(buffer, destOffset, count, (const jbyte *)address + srcOffset);

        if (unpinned) {
            ashmem_unpin_region(fd, 0, 0);
        }
        return count;
    }

    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);
            jniThrowException(env, "java/io/IOException", "ashmem region was purged");
            return -1;
        }

        env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);

        if (unpinned) {
            ashmem_unpin_region(fd, 0, 0);
        }
        return count;
    }

这里的address参数就是我们在前面执行mmap来映射匿名共享内存文件到内存中时,得到的进程虚拟地址空间的起始地址了,因此,这里就直接可以访问,不必进入到Ashmem驱动程序中去,这也是为什么Ashmem驱动程序没有提供read和write文件操作的原因。

这里我们看到的ashmem_pin_region和ashmem_unpin_region两个函数是系统运行时库提供的接口,用来执行我们前面说的匿名共享内存的锁定和解锁操作,它们的作用是告诉Ashmem驱动程序,它的哪些内存块是正在使用的,需要锁定,哪些内存是不需要使用了,可以它解锁,这样,Ashmem驱动程序就可以辅助内存管理系统来有效地管理内存了。下面我们就看看Ashmem驱动程序是如果辅助内存管理系统来有效地管理内存的。

四. 匿名共享内存的锁定和解锁操作 前面提到,Android系统的运行时库提到了执行匿名共享内存的锁定和解锁操作的两个函数ashmem_pin_region和ashmem_unpin_region,它们实现在system/core/libcutils/ashmem-dev.c文件中:

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

它们的实现很简单,通过ASHMEM_PIN和ASHMEM_UNPIN两个ioctl操作来实现匿名共享内存的锁定和解锁操作。

我们先看来一下ASHMEM_PIN和ASHMEM_UNPIN这两个命令号的定义,它们的定义可以在kernel/common/include/linux/ashmem.h文件中找到:

#define __ASHMEMIOC     0x77

    #define ASHMEM_PIN      _IOW(__ASHMEMIOC, 7, struct ashmem_pin)
    #define ASHMEM_UNPIN        _IOW(__ASHMEMIOC, 8, struct ashmem_pin)

它们的参数类型为struct ashmem_pin,它也是定义在kernel/common/include/linux/ashmem.h文件中:

struct ashmem_pin {
        __u32 offset;   /* offset into region, in bytes, page-aligned */
        __u32 len;  /* length forward from offset, in bytes, page-aligned */
    };

这个结构体只有两个域,分别表示要锁定或者要解锁的内块块的起始大小以及大小。

在分析这两个操作之前,我们先来看一下Ashmem驱动程序中的一个数据结构struct ashmem_range,这个数据结构就是用来表示某一块被解锁(unpinnd)的内存:

/*
     * ashmem_range - represents an interval of unpinned (evictable) pages
     * Lifecycle: From unpin to pin
     * Locking: Protected by `ashmem_mutex'
     */
    struct ashmem_range {
        struct list_head lru;       /* entry in LRU list */
        struct list_head unpinned;  /* entry in its area's unpinned list */
        struct ashmem_area *asma;   /* associated area */
        size_t pgstart;         /* starting page, inclusive */
        size_t pgend;           /* ending page, inclusive */
        unsigned int purged;        /* ASHMEM_NOT or ASHMEM_WAS_PURGED */
    };

域asma表示这块被解锁的内存所属于的匿名共享内存,它通过域unpinned连接在asma->unpinned_list表示的列表中;域pgstart和paend表示这个内存块的开始和结束页面号,它们表示一个前后闭合的区间;域purged表示这个内存块占用的物理内存是否已经被回收;这块被解锁的内存块除了保存在它所属的匿名共享内存asma的解锁列表unpinned_list之外,还通过域lru保存在一个全局的最近最少使用列表ashmem_lru_list列表中,它的定义如下:

/* LRU list of unpinned pages, protected by ashmem_mutex */
    static LIST_HEAD(ashmem_lru_list);

了解了这个数据结构之后,我们就可以来看ashmem_ioctl函数中关于ASHMEM_PIN和ASHMEM_UNPIN的操作了:

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_PIN:
        case ASHMEM_UNPIN:
            ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
            break;
        ......
        }

        return ret;
    }

它们都是通过ashmem_pin_unpin来进一步处理:

static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
                    void __user *p)
    {
        struct ashmem_pin pin;
        size_t pgstart, pgend;
        int ret = -EINVAL;

        if (unlikely(!asma->file))
            return -EINVAL;

        if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
            return -EFAULT;

        /* per custom, you can pass zero for len to mean "everything onward" */
        if (!pin.len)
            pin.len = PAGE_ALIGN(asma->size) - pin.offset;

        if (unlikely((pin.offset | pin.len) & ~PAGE_MASK))
            return -EINVAL;

        if (unlikely(((__u32) -1) - pin.offset < pin.len))
            return -EINVAL;

        if (unlikely(PAGE_ALIGN(asma->size) < pin.offset + pin.len))
            return -EINVAL;

        pgstart = pin.offset / PAGE_SIZE;
        pgend = pgstart + (pin.len / PAGE_SIZE) - 1;

        mutex_lock(&ashmem_mutex);

        switch (cmd) {
        case ASHMEM_PIN:
            ret = ashmem_pin(asma, pgstart, pgend);
            break;
        case ASHMEM_UNPIN:
            ret = ashmem_unpin(asma, pgstart, pgend);
            break;
        ......
        }

        mutex_unlock(&ashmem_mutex);

        return ret;
    }

首先是获得用户空间传进来的参数,并保存在本地变量pin中,这是一个struct ashmem_pin类型的变量,这个结构体我们在前面已经见过了,它包括了要pin/unpin的内存块的起始地址和大小,这里的起始地址和大小都是以字节为单位的,因此,通过转换把它们换成以页面为单位的,并且保存在本地变量pgstart和pgend中。这里除了要对参数作一个安全性检查外,还要一个处理逻辑是,如果从用户空间传进来的内块块的大小值为0 ,则认为是要pin/unpin整个匿名共享内存。

函数最后根据当前要执行的是ASHMEM_PIN操作还是ASHMEM_UNPIN操作来分别执行ashmem_pin和ashmem_unpin来进一步处理。创建匿名共享内存时,默认所有的内存都是pinned状态的,只有用户告诉Ashmem驱动程序要unpin某一块内存时,Ashmem驱动程序才会把这块内存unpin,之后,用户可以再告诉Ashmem驱动程序要重新pin某一块之前被unpin过的内块,从而把这块内存从unpinned状态改为pinned状态,也就是说,执行ASHMEM_PIN操作时,目标对象必须是一块当前处于unpinned状态的内存块。

我们先来看一下ASHMEM_UNPIN操作,进入到ashmem_unpin函数:

/*
     * ashmem_unpin - unpin the given range of pages. Returns zero on success.
     *
     * Caller must hold ashmem_mutex.
     */
    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) {
            /* short circuit: this is our insertion point */
            if (range_before_page(range, pgstart))
                break;

            /*
             * The user can ask us to unpin pages that are already entirely
             * or partially pinned. We handle those two cases here.
             */
            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);
    }

这个函数的主体就是在遍历asma->unpinned_list列表,从中查找当前处于unpinned状态的内存块是否与将要unpin的内存块[pgstart, pgend]是否相交,如果相交,则要执行合并操作,即调整pgstart和pgend的大小,然后通过调用range_del函数删掉原来的已经被unpinned过的内存块,最后再通过range_alloc函数来重新unpinned这块调整过后的内存块[pgstart, pgend],这里新的内存块[pgstart, pgend]已经包含了刚才所有被删掉的unpinned状态的内存。注意,这里如果找到一块相并的内存块,并且调整了pgstart和pgend的大小之后,要重新再扫描一遍asma->unpinned_list列表,因为新的内存块[pgstart, pgend]可能还会与前后的处于unpinned状态的内存块发生相交。

我们来看一下range_before_page的操作,这是一个宏定义:

#define range_before_page(range, page) \
      ((range)->pgend < (page))

表示range描述的内存块是否在page页面之前,如果是,则整个描述就结束了。从这里我们可以看出asma->unpinned_list列表是按照页面号从大到小进行排列的,并且每一块被unpin的内存都是不相交的。

再来看一下page_range_subsumed_by_range的操作,这也是一个宏定义:

#define page_range_subsumed_by_range(range, start, end) \
      (((range)->pgstart <= (start)) && ((range)->pgend >= (end)))

表示range描述的内存块是不是包含了[start, end]这个内存块,如果包含了,则说明当前要unpin的内存块已经处于unpinned状态,什么也不用操作,直接返回即可。

再看page_range_in_range的操作,它也是一个宏定义:

#define page_range_in_range(range, start, end) \
      (page_in_range(range, start) || page_in_range(range, end) || \
       page_range_subsumes_range(range, start, end))

它用到的其它两个宏分别定义为:

#define page_range_subsumed_by_range(range, start, end) \
      (((range)->pgstart <= (start)) && ((range)->pgend >= (end)))

    #define page_in_range(range, page) \
     (((range)->pgstart <= (page)) && ((range)->pgend >= (page)))

它们都是用来判断两个内存区间是否相交的。

两个内存块相交分为四种情况:

|-------range-----| |-------range------| |--------range---------| |----range---|

|-start----end-| |-start-----end-| |-start-------end-| |-start-----------end-| (1) (2) (3) (4) 第一种情况,前面已经讨论过了,对于第二到第四种情况,都是需要执行合并操作的。

再来看从asma->unpinned_list中删掉内存块的range_del函数:

static void range_del(struct ashmem_range *range)
    {
        list_del(&range->unpinned);
        if (range_on_lru(range))
            lru_del(range);
        kmem_cache_free(ashmem_range_cachep, range);
    }

这个函数首先把range从相应的unpinned_list列表中删除,然后判断它是否在lru列表中:

#define range_on_lru(range) \
      ((range)->purged == ASHMEM_NOT_PURGED)

如果它的状态purged等于ASHMEM_NOT_PURGED,即对应的物理页面尚未被回收,它就位于lru列表中,通过调用lru_del函数进行删除:

static inline void lru_del(struct ashmem_range *range)
    {
        list_del(&range->lru);
        lru_count -= range_size(range);
    }

最后调用kmem_cache_free将它从slab缓冲区ashmem_range_cachep中释放。

这里的slab缓冲区ashmem_range_cachep定义如下:

static struct kmem_cache *ashmem_range_cachep __read_mostly;

它和前面介绍的slab缓冲区ashmem_area_cachep一样,是在Ashmem驱动程序模块初始化函数ashmem_init进行初始化的:

static int __init ashmem_init(void)
    {
        int ret;

        ......

        ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
            sizeof(struct ashmem_range),
            0, 0, NULL);
        if (unlikely(!ashmem_range_cachep)) {
            printk(KERN_ERR "ashmem: failed to create slab cache\n");
            return -ENOMEM;
        }

        ......

        printk(KERN_INFO "ashmem: initialized\n");

        return 0;
    }

回到ashmem_unpin函数中,我们再来看看range_alloc函数的实现:

/*
     * range_alloc - allocate and initialize a new ashmem_range structure
     *
     * 'asma' - associated ashmem_area
     * 'prev_range' - the previous ashmem_range in the sorted asma->unpinned list
     * 'purged' - initial purge value (ASMEM_NOT_PURGED or ASHMEM_WAS_PURGED)
     * 'start' - starting page, inclusive
     * 'end' - ending page, inclusive
     *
     * Caller must hold ashmem_mutex.
     */
    static int range_alloc(struct ashmem_area *asma,
                   struct ashmem_range *prev_range, unsigned int purged,
                   size_t start, size_t end)
    {
        struct ashmem_range *range;

        range = kmem_cache_zalloc(ashmem_range_cachep, GFP_KERNEL);
        if (unlikely(!range))
            return -ENOMEM;

        range->asma = asma;
        range->pgstart = start;
        range->pgend = end;
        range->purged = purged;

        list_add_tail(&range->unpinned, &prev_range->unpinned);

        if (range_on_lru(range))
            lru_add(range);

        return 0;
    }

这个函数的作用是从slab 缓冲区中ashmem_range_cachep分配一个ashmem_range,然后对它作相应的初始化,放在相应的ashmem_area->unpinned_list列表中,并且还要判断这个range的purged是否是ASHMEM_NOT_PURGED状态,如果是,还要把它放在lru列表中:

static inline void lru_add(struct ashmem_range *range)
    {
        list_add_tail(&range->lru, &ashmem_lru_list);
        lru_count += range_size(range);
    }

这样,ashmem_unpin的源代码我们就分析完了。

接着,我们再来看一下ASHMEM_PIN操作,进入到ashmem_pin函数:

/*
     * ashmem_pin - pin the given ashmem region, returning whether it was
     * previously purged (ASHMEM_WAS_PURGED) or not (ASHMEM_NOT_PURGED).
     *
     * Caller must hold ashmem_mutex.
     */
    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;

            /*
             * The user can ask us to pin pages that span multiple ranges,
             * or to pin pages that aren't even unpinned, so this is messy.
             *
             * Four cases:
             * 1. The requested range subsumes an existing range, so we
             *    just remove the entire matching range.
             * 2. The requested range overlaps the start of an existing
             *    range, so we just update that range.
             * 3. The requested range overlaps the end of an existing
             *    range, so we just update that range.
             * 4. The requested range punches a hole in an existing range,
             *    so we have to update one side of the range and then
             *    create a new range for the other side.
             */
            if (page_range_in_range(range, pgstart, pgend)) {
                ret |= range->purged;

                /* Case #1: Easy. Just nuke the whole thing. */
                if (page_range_subsumes_range(range, pgstart, pgend)) {
                    range_del(range);
                    continue;
                }

                /* Case #2: We overlap from the start, so adjust it */
                if (range->pgstart >= pgstart) {
                    range_shrink(range, pgend + 1, range->pgend);
                    continue;
                }

                /* Case #3: We overlap from the rear, so adjust it */
                if (range->pgend <= pgend) {
                    range_shrink(range, range->pgstart, pgstart-1);
                    continue;
                }

                /*
                 * Case #4: We eat a chunk out of the middle. A bit
                 * more complicated, we allocate a new range for the
                 * second half and adjust the first chunk's endpoint.
                 */
                range_alloc(asma, range, range->purged,
                        pgend + 1, range->pgend);
                range_shrink(range, range->pgstart, pgstart - 1);
                break;
            }
        }

        return ret;
    }

前面我们说过,被pin的内存块,必须是在unpinned_list列表中的,如果不在,就什么都不用做。要判断要pin的内存块是否在unpinned_list列表中,又要通过遍历相应的asma->unpinned_list列表来找出与之相交的内存块了。这个函数的处理方法大体与前面的ashmem_unpin函数是一致的,也是要考虑四种不同的相交情况,这里就不详述了,读者可以自己分析一下。

这里我们只看一下range_shrink函数的实现:

/*
     * range_shrink - shrinks a range
     *
     * Caller must hold ashmem_mutex.
     */
    static inline void range_shrink(struct ashmem_range *range,
                    size_t start, size_t end)
    {
        size_t pre = range_size(range);

        range->pgstart = start;
        range->pgend = end;

        if (range_on_lru(range))
            lru_count -= pre - range_size(range);
    }

这个函数的实现很简单,只是调整一下range描述的内存块的起始页面号,如果它是位于lru列表中,还要调整一下在lru列表中的总页面数大小。

这样,匿名共享内存的ASHMEM_PIN和ASHMEM_UNPIN操作就介绍完了,但是,我们还看不出来Ashmem驱动程序是怎么样辅助内存管理系统来有效管理内存的。有了前面这些unpinned的内存块列表之后,下面我们就看一下Ashmem驱动程序是怎么样辅助内存管理系统来有效管理内存的。

首先看一下Ashmem驱动程序模块初始化函数ashmem_init:

static struct shrinker ashmem_shrinker = {
        .shrink = ashmem_shrink,
        .seeks = DEFAULT_SEEKS * 4,
    };

    static int __init ashmem_init(void)
    {
        int ret;

        ......

        register_shrinker(&ashmem_shrinker);

        printk(KERN_INFO "ashmem: initialized\n");

        return 0;
    }

这里通过调用register_shrinker函数向内存管理系统注册一个内存回收算法函数。在Linux内核中,当系统内存紧张时,内存管理系统就会进行内存回收算法,将一些最近没有用过的内存换出物理内存去,这样可以增加物理内存的供应。因此,当内存管理系统进行内存回收时,就会调用到这里的ashmem_shrink函数,让Ashmem驱动程序执行内存回收操作:

/*
     * ashmem_shrink - our cache shrinker, called from mm/vmscan.c :: shrink_slab
     *
     * 'nr_to_scan' is the number of objects (pages) to prune, or 0 to query how
     * many objects (pages) we have in total.
     *
     * 'gfp_mask' is the mask of the allocation that got us into this mess.
     *
     * Return value is the number of objects (pages) remaining, or -1 if we cannot
     * proceed without risk of deadlock (due to gfp_mask).
     *
     * We approximate LRU via least-recently-unpinned, jettisoning unpinned partial
     * chunks of ashmem regions LRU-wise one-at-a-time until we hit 'nr_to_scan'
     * pages freed.
     */
    static int ashmem_shrink(int nr_to_scan, gfp_t gfp_mask)
    {
        struct ashmem_range *range, *next;

        /* We might recurse into filesystem code, so bail out if necessary */
        if (nr_to_scan && !(gfp_mask & __GFP_FS))
            return -1;
        if (!nr_to_scan)
            return lru_count;

        mutex_lock(&ashmem_mutex);
        list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
            struct inode *inode = range->asma->file->f_dentry->d_inode;
            loff_t start = range->pgstart * PAGE_SIZE;
            loff_t end = (range->pgend + 1) * PAGE_SIZE - 1;

            vmtruncate_range(inode, start, end);
            range->purged = ASHMEM_WAS_PURGED;
            lru_del(range);

            nr_to_scan -= range_size(range);
            if (nr_to_scan <= 0)
                break;
        }
        mutex_unlock(&ashmem_mutex);

        return lru_count;
    }

这里的参数nr_to_scan表示要扫描的页数,如果是0,则表示要查询一下,当前Ashmem驱动程序有多少页面可以回收,这里就等于挂在lru列表的内块页面的总数了,即lru_count;否则,就要开始扫描lru列表,从中回收内存了,直到回收的内存页数等于nr_to_scan,或者已经没有内存可回收为止。回收内存页面是通过vm_truncate_range函数进行的,这个函数定义在kernel/common/mm/memory.c文件中,它是Linux内核内存管理系统实现的,有兴趣的读者可以研究一下。

这样,Android系统匿名共享内存Ashmem驱动程序源代码就分析完了,在下一篇文章中,我们将继续分析Android系统的匿名共享内存机制,研究它是如何通过Binder进程间通信机制实现在不同进程程进行内存共享的,敬请关注。

相关标签

扫一扫

在手机上阅读