深入解析Android 虚拟机
上QQ阅读APP看书,第一时间看更新

5.2 匿名共享内存子系统详解

Android系统中提供了独特的匿名共享内存子系统Ashmem(Anonymous Shared Memory),它以驱动程序的形式实现在内核空间中。Ashmem有如下两个特点。

能够辅助内存管理系统来有效地管理不再使用的内存块。

通过Binder进程间通信机制来实现进程间的内存共享。

对于Android系统的匿名共享内存子系统来说,其主体是以驱动程序的形式实现在内核空间的,同时,在系统运行时库层和应用程序框架层提供了访问接口。其中在系统运行时库层提供了C/C++调用接口,而在应用程序框架层提供了Java调用接口。在此将直接通过应用程序框架层提供的Java调用接口来说明匿名共享内存子系统Ashmem的使用方法,毕竟在Android开发应用程序时,是基于Java语言的。其实对于Android系统中的应用程序框架层的Java调用接口来说,是通过JNI方法来调用系统运行时库层的C/C++调用接口,最后需要进入到内核空间的Ashmem驱动程序中去。

Android系统中的匿名共享内存Ashmem驱动程序,利用Linux的共享内存子系统导出的接口来实现自己的功能。在Android系统匿名共享内存系统中,其核心功能是实现创建(open)、映射(mmap)、读写(read/write)以及锁定和解锁(pin/unpin)。在本节的内容中,将详细讲解Android匿名共享内存子系统的基本知识。

5.2.1 基础数据结构

在Ashmem驱动程序中需要用到3个结构体,分别是ashmem_area、ashmem_range和ashmem_range。其中前两个结构体在文件“kernel/goldfish/mm/ashmem.c”中定义,具体实现代码如下所示:

        struct ashmem_area {
              char name[ASHMEM_FULL_NAME_LEN];    /* 匿名共享内存的名称 */
              struct list_head unpinned_list;     /* 解锁内存列表 */
              struct file *file;                    /* 指向临时文件系统tmpfs中的一个文件 */
              size_t size;                           /* 文件大小 */
              unsigned long prot_mask;             /* 匿名共享内存的访问保护位 */
        }
        struct ashmem_range {
              struct list_head lru;                 /* 最近最少使用的列表 */
              struct list_head unpinned;           /* entry in its area's unpinned list */
              struct ashmem_area *asma;            /* associated area */
              size_t pgstart;                       /* 处于解锁状态内存的开始地址 */
              size_t pgend;                          /*处于解锁状态内存的结束地址*/
              unsigned int purged;                 /* 解锁内存是否被收回 */
        }

结构体ashmem_area用于表示一块匿名共享内存单元,结构体ashmem_range用于表示处于解锁状态的内存。

结构体ashmem_range用于表示被锁定或被解锁的内存,在文件“kernel/goldfish/include/linux/ashmem.h”中定义,具体实现代码如下所示:

        struct ashmem_pin {
              __u32 offset;        /* 这块内存的偏移值*/
              __u32 len;           /* 这块内存的大小 */
        }

结构体ashmem_fops定义了“dev/ashmem”的操作方法列表,具体实现代码如下所示:

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

5.2.2 初始化处理

通过Ashmem驱动的初始化函数,可以获取如下两点信息。

Ashmem给用户空间暴露了什么接口,即创建了什么样的设备文件。

Ashmem提供了什么函数来操作这个设备文件。

Ashmem驱动程序在文件“kernel/common/mm/ashmem.c”中实现,其中函数ashmem_init用于实现模块初始化处理,主要实现代码如下所示:

        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_register来注册misc设备,调用这个函数后会在“/dev”目录下生成一个ashmem设备文件。在设备文件中一共提供了open、mmap、release和ioctl 4种操作,此处并没有read和write操作,原因是读写共享内存的方法是通过内存映射地址来进行的,通过mmap系统调用将这个设备文件映射到进程地址空间中。与此同时,直接对内存进行了读写操作,所以不需要通过read和write方式进行文件操作。

匿名共享内存创建功能是在文件“frameworks/base/core/java/android/os/MemoryFile.java”中实现的,此文件调用了类MemoryFile的构造函数,MemoryFile的构造函数调用了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);
        }

函数native_open通过运行时库提供的接口ashmem_create_region创建匿名共享内存,这个接口在文件system/core/libcutils/ashmem-dev.c中实现,具体代码如下所示:

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

在上述代码中,通过执行3个文件操作系统调用的方式与Ashmem驱动程序进行交互。通过open操作打开设备文件“ASHMEM_DEVICE”,通过ioctl操作设置匿名共享内存的名称和大小。

5.2.3 打开匿名共享内存设备文件

当open进入内核后,会调用函数ashmem_open打开匿名共享内存设备文件,此函数能够为程序创建一个ashmem_area结构体,具体实现代码如下所示:

        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,宏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结构。

在函数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中,此函数能够将从用户空间传进来的匿名共享内存的大小值保存在对应的asma->size域中。函数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;
              case ASHMEM_GET_NAME:
                      ret = get_name(asma, (void __user *) arg);
                      break;
              case ASHMEM_SET_SIZE:
                      ret  =  -EINVAL;
                      if  (! asma->file)  {
                            ret  =  0;
                            asma->size = (size_t) arg;
                      }
                      break;
              case ASHMEM_GET_SIZE:
                      ret  =  asma->size;
                      break;
              case ASHMEM_SET_PROT_MASK:
                      ret = set_prot_mask(asma, arg);
                      break;
              case ASHMEM_GET_PROT_MASK:
                      ret = asma->prot_mask;
                      break;
              case ASHMEM_PIN:
              case ASHMEM_UNPIN:
              case ASHMEM_GET_PIN_STATUS:
                      ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
                      break;
              case ASHMEM_PURGE_ALL_CACHES:
                      ret  =  -EPERM;
                      if (capable(CAP_SYS_ADMIN)) {
                            ret = ashmem_shrink(0, GFP_KERNEL);
                            ashmem_shrink(ret, GFP_KERNEL);
                      }
                      break;
              }
              return  ret;
        }

上述代码主要完成如下两个功能。

struct ashmem_area *asma = file->private_data:获取描述将要改名的匿名共享内存asma。

ret = set_name(asma, (void __user *) arg):调用函数set_name修改匿名共享内存的名称。

函数set_name也是在文件“kernel/goldfish/mm/ashmem.c”中实现的,功能是把用户空间传进来的匿名共享内存的名字设置到asma->name域中。函数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;
        }

到此为止,创建匿名共享内存的过程就全部介绍完毕了。

5.2.4 内存映射

Ashmem驱动程序并不提供文件的read操作和write操作,如果进程要访问这个共享内存,则必须将这个设备文件映射到自己的进程空间中,然后才能进行内存访问。在类MemoryFile的构造函数中,创建匿名共享内存后需要把匿名共享内存设备文件映射到进程空间。映射功能是通过调用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;
        }

在上述代码中,在open匿名设备文件“/dev/ashmem”获得文件描述符fd。有了这个文件描述符后,就可以直接通过函数mmap执行内存映射操作了。当调用函数mmap打开映射到进程的地址空间时,会立即执行ashmem中的函数ashmem_mmap。函数ashmem_mmap的功能是,调用Linux内核中的函数shmem_file_setup在临时文件系统tmpfs中创建一个临时文件,这个临时文件与Ashmem驱动程序创建的匿名共享内存对应。函数ashmem_mmap在文件“kernel/goldfish/mm/ashmem.c”中定义,具体实现代码如下所示:

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

在上述代码中,检查了虚拟内存vma是否允许在不同进程之间实现共享。如果允许,则调用函数shmem_set_file来设置它的映射文件和内存操作方法表。

5.2.5 读写操作

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

通过对上述代码的分析可知,是通过调用JNI方法实现读写匿名共享内存操作功能。读操作的JNI方法是native_read,写操作的NI方法是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;
        }

在上述代码中,函数ashmem_pin_region和ashmem_unpin_region用于为系统运行时的库提供接口,功能是执行匿名共享内存的锁定和解锁操作。这样便能够通知Ashmem驱动程序哪些内存块是正在使用的,哪些需要锁定,哪些不需要使用,哪些可以解锁。这两个函数在文件“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驱动程序就可以在整个内存管理系统中管理内存了。

5.2.6 锁定和解锁

在Android系统中,通过如下两个ioctl操作实现匿名共享内存的锁定和解锁操作。

ASHMEM_PIN。

ASHMEM_UNPIN。

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 {
            __u32 offset;      /* offset into region, in bytes, page-aligned */
            __u32 len;         /* length forward from offset, in bytes, page-aligned */
        }

再看函数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处理控制命令ASHMEM_PIN和ASHMEM_UNPIN。函数ashmem_pin_unpin的实现流程如下所示。

获取传递到用户空间的参数,并将获取值保存在本地变量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_unpin在文件“kernel/goldfish/ashmem.c”中定义,具体的实现代码如下所示:

        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;
            case ASHMEM_GET_PIN_STATUS:
                ret = ashmem_get_pin_status(asma, pgstart, pgend);
                break;
            }
            mutex_unlock(&ashmem_mutex);
            return  ret;
        }

由此可见,执行ASHMEM_PIN操作的目标对象必须是一块处于unpinned状态的内存块。

函数ashmem_unpin的功能是对某一块匿名共享内存进行解锁操作,具体处理流程如下所示。

在遍历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状态的内存块发生相交。

函数ashmem_unpin在文件“kernel/goldfish/ashmem.c”中定义,具体的实现代码如下所示:

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

range_before_page的操作是一个宏定义,功能是判断range描述的内存块是否在page页面之前,如果是则表示结束整个描述。asma->unpinned_list列表是按照页面号从大到小进行排列的,并且每一块被unpin的内存都是不相交的。range_before_pag的定义代码如下所示:

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

page_range_subsumed_by_range的操作也是一个宏定义,功能是判断内存块是否包含[start, end]这个内存块,如果包含则说明当前要unpin的内存块已经处于unpinned状态。如果什么也不用操作则直接返回。page_range_subsumed_by_range的定义代码如下所示:

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

page_range_in_range的操作也是一个宏定义,功能是判断内存块[start, end]是否互相包或者相交。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))

page_range_subsumed_by_range的操作也是一个宏定义,功能是判断内存块range是否包含内存块[start, end]。page_range_subsumed_by_range的定义代码如下所示:

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

range_in_range的操作也是一个宏定义,功能是判断内存块地址page是否包含在内存块range中。range_in_range的定义代码如下所示:

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

再看函数range_del,功能是从asma->unpinned_list中删掉内存块,并判断它是否在lru列表中。函数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);
        }

再看函数lru_del,内存块的状态purged值为ASHMEM_NOT_PURGED,表示现在没有收回对应的物理页面,那么内存块就位于lru列表中,则使用函数lru_del删除这个内存块。函数lru_del的具体实现代码如下所示:

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

再看在函数ashmem_unpin中调用的range_alloc函数,其功能是从slab缓冲区中ashmem_range_cachep分配一个ashmem_range,并进行相应的初始化处理。然后放在对应的列表ashmem_area->unpinned_list中,并判断这个range的purged是否处于ASHMEM_NOT_PURGED状态,如果是则要把它放在lru列表中。函数range_alloc在文件“kernel/goldfish/ashmem.c”中实现,具体的实现代码如下所示:

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

再看函数lru_add,其功能是将未被回收的已解锁内存块添加到全局列表ashmem_lru_list中。函数lru_add在文件“kernel/goldfish/ashmem.c”中实现,具体的实现代码如下所示:

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

再看函数ashmem_pin,功能是锁定一块匿名共享内存区域。被pin的内存块肯定被保存在unpinned_list列表中,如果不在则什么都不用做。要想判断在unpinned_list列表中是否存在pin的内存块,需要通过遍历asma->unpinned_list列表的方式找出与之相交的内存块。函数ashmem_pin在文件“kernel/goldfish/ashmem.c”中实现,具体的实现代码如下所示:

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

在上述代码中对重新锁定内存块操作实现了判断,通过if语句处理了如下所示的4种情形。

指定要锁定的内存块[start, end]包含了解锁状态的内存块range,此时只要将解锁状态的内存块range从其宿主匿名共享内存的解锁内存块列表unpinned_list中删除即可。

合并要锁定内存块[pgstart, pgend]后部分和解锁状态内存块range的前半部分,此时将解锁状态内存块range的开始地址设置为要锁定内存块的末尾地址的下一个页面地址。

合并要锁定内存块[pgstart, pgend]前部分和解锁状态内存块range的后半部分,此时将解锁状态内存块range的末尾地址设置为要锁定内存块的开始地址的下一个页面地址。

设置要锁定内存块[pgstart, pgend]包含在解锁状态内存块range中。

再看函数range_shrink,功能是设置range描述的内存块的起始页面号,如果还存在于lru列表中,则需要调整在lru列表中的总页面数大小。函数range_shrink在文件“kernel/goldfish/ashmem.c”中定义,具体的实现代码如下所示:

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

5.2.7 回收内存块

接下来开始看最后一步:回收匿名共享内存块。再次回到前面介绍的初始化步骤,先看Ashmem驱动初始化函数ashmem_init,此函数会调用函数register_shrinker向内存管理系统注册一个内存回收算法函数,具体实现代码如下所示:

        static struct shrinker ashmem_shrinker = {
            .shrink = ashmem_shrink,
            .seeks = DEFAULT_SEEKS * 4,
        };
        static int __init ashmem_init(void)
        {
            ......
            register_shrinker(&ashmem_shrinker);
            printk(KERN_INFO "ashmem: initialized\n");
            return 0;
        }

其实在Linux内核程序中,当系统内存不够用时,内存管理系统就会通过调用内存回收算法的方式删除最近没有用过的内存,将这些内存从物理内存中清除,这样可以增加物理内存的容量。所以在Android系统中也借用了这种机制,当内存管理系统回收内存时会调用函数ashmem_shrink以执行内存回收操作。函数ashmem_shrink在文件“kernel/goldfish/ashmem.c”中实现,具体的实现代码如下所示:

        static int ashmem_shrink(struct shrinker *s, struct shrink_control *sc)
        {
            struct ashmem_range *range, *next;
            /* We might recurse into filesystem code, so bail out if necessary */
            if (sc->nr_to_scan && ! (sc->gfp_mask & __GFP_FS))
              return -1;
            if (! sc->nr_to_scan)
              return lru_count;
            mutex_lock(&ashmem_mutex);
            list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
              loff_t start = range->pgstart * PAGE_SIZE;
              loff_t end = (range->pgend + 1) * PAGE_SIZE;
              do_fallocate(range->asma->file,
                    FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
                    start, end - start);
              range->purged = ASHMEM_WAS_PURGED;
              lru_del(range);
              sc->nr_to_scan -= range_size(range);
              if (sc->nr_to_scan <= 0)
                  break;
            }
            mutex_unlock(&ashmem_mutex);
            return lru_count;
        }