且听穿林打叶声———Ashmem机制讲解

  且听穿林打叶声———Ashmem机制讲解
  
  侯亮
  
  (Android 7.0)
  
  在Android平台上,提供了一种共享内存的机制——Ashmem。该机制内部其实复用了Linux的共享内存机制。Ashmem机制使用linux的mmap系统调用,可以将同一段物理内存映射到不同进程各自的虚拟地址空间,从而实现高效的进程间共享。
  
  大家都知道,在linux上“一切皆文件”,一块共享内存当然也不例外。因此,在用户态,我们能看到的重要概念就是共享内存的“文件描述符”,文件描述符可以对应一个内核态的ashmem file。file中又可以管理自己的逻辑数据(ashmem_area)。不同进程里的不同文件描述符可以对应同一个内核态的file,这就是跨进程共享的基础。当我们对这个文件描述符做完mmap操作后,一般都会记下映射好的起始地址,这是后续进行读取、写入操作的一个基准值,在后文要说的MemoryFile里,这个基准值会记在mAddress成员变量里。
  
  我们先画一张示意图对ashmem有个大体上的了解:
  
  1.以MemoryFile为切入点
  
  我们不大可能直接使用Ashmem,为此Android提供了一个MemoryFile类,其内部实现就是基于ashmem的。MemoryFile本身虽不太常用,但我们可以以这个类为切入点,来看看ashmem的细节。各位如有兴趣,可以详细查看一下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 static native long native_mmap(FileDescriptor fd, int length, int mode)
  
  throws IOException;
  
  . . . . . .
  
  public MemoryFile(String name, int length) throws IOException {
  
  mLength = length;
  
  if (length >= 0) {
  
  mFD = native_open(name, length);
  
  } else {
  
  throw new IOException("Invalid length: " + length);
  
  }
  
  if (length > 0) {
  
  mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);
  
  } else {
  
  mAddress = 0;
  
  }
  
  }
  
  在其构造之时,主要就是调用了native_open()和native_mmap()。这两个函数对应着C++层次的android_os_MemoryFile_open()和android_os_MemoryFile_mmap(),前者用于创建一个共享内存区域,后者用于进行内存映射。但具体的创建和映射动作其实都是在内核态完成的,这就涉及到Ashmem驱动程序的内容。
  
  在Android平台上,Ashmem是作为一个驱动程序存在的。我们在Ashmem.c文件中,可以看到这个驱动的入口函数ashmem_init():
  
  【kernel/drivers/staging/android/Ashmem.c】
  
  module_init(ashmem_init); // 初始化动作
  
  module_exit(ashmem_exit); // 退出动作
  
  ashmem驱动的初始化动作如下:
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static int __init ashmem_init(void)
  
  {
  
  . . . . . .
  
  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);www.tdcqpt.cn // 注册file_operations
  
  . . . . . .
  
  }
  
  其中,ashmem_misc的定义如下:
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static const struct file_operations ashmem_fops = {
  
  .owner = THIS_MODULE,
  
  .open = ashmem_open,
  
  .release = ashmem_release,
  
  .read = ashmem_read,
  
  .llseek = ashmem_llseek,
  
  .mmap = ashmem_mmap,
  
  .unlocked_ioctl = ashmem_ioctl,
  
  #ifdef CONFIG_COMPAT
  
  .compat_ioctl = compat_ashmem_ioctl,
  
  #endif
  
  };
  
  static struct miscdevice ashmem_misc = www.dongfangyuld.com{ // 用于注册杂项从设备
  
  .minor = MISC_DYNAMIC_MINOR,
  
  .name = "ashmem",
  
  .fops = &ashmem_fops,
  
  };
  
  其实是向系统内部注册了一个“ashmem杂项从设备”。在linux系统中,杂项设备(misc device)其实是个特殊的字符设备。我们可以为杂项设备注册多个“从设备”,“ashmem杂项从设备”只是其中之一。
  
  要使用一块ashmem内存,其实说到底就是要操作“ashmem杂项从设备”,而操作设备的动作主要就体现在对设备执行诸如open、read、write、mmap、ioctl等文件操作。这些文件操作在ashmem驱动层就对应为上面代码中的ashmem_open、ashmem_mmap等函数。
  
  2.创建共享内存区域
  
  我们回过头说前文的android_os_MemoryFile_open()函数:
  
  【frameworks/base/core/jni/android_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(www.zzxcscl.com env, result);
  
  }
  
  其中最重要的一句是调用ashmem_create_region(www.kunLunyuLegw.com),它的返回值如果大于等于0,就说明返回的是个合法的文件描述符,这种描述符还得进一步包装成java层的FileDescriptor,所以需要在最后调用jniCreateFileDescriptor()。
  
  当我们要创建一块共享内存区域时,我们需要指明这个区域的名字以及该区域的大小。我们参考一下system/core/libcutils/Ashmem-dev.c文件里的ashmem_create_region()函数的调用关系,可以绘制出下图:
  
  可以看到,在创建一块共享内存区域时,我们用到了open()、ioctl()等操作,它们分别对应着前文ashmem_fops里的ashmem_open()、ashmem_ioctl()函数。
  
  ashmem_create_region()所返回的指代匿名共享内存的文件描述符,可以在手机等设备的/proc/[pid]/maps里看到,同时还能看到这块共享内存对应的名字。当然,我们也可以创建多个ashmem共享内存区域,它们会对应不同的inode和file。
  
  2.1 ashmem_open()操作
  
  ashmem驱动程序一般位于Ashmem.c文件中。其中ashmem_open()的实现代码如下:
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static int ashmem_open(www.jmaguojiyL.com struct inode *inode, struct file *file)
  
  {
  
  struct ashmem_area *asma;
  
  int ret;
  
  ret = generic_file_open(inode, file); // 做了一点防护性判断,不太重要
  
  if (unlikely(ret))
  
  return ret;
  
  asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL); // 申请一块ashmem_area内存
  
  if (unlikely(!asma)www.chuangyyuLe.com)
  
  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; // 将申请的ashmem_area记入file->private_data
  
  return 0;
  
  }
  
  上面代码说明,当我们创建一块ashmem共享内存时,其实是在内核层打开了一个ashmem file,而且这个file的private_data里记录了一块ashmem_area。如图所示:
  
  ashmem_area的定义如下:
  
  struct ashmem_area {
  
  char name[ASHMEM_FULL_NAME_LEN]; /* optional name in /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 vm_start; www.zbyL2019.com /* Start address of vm_area
  
  * which maps this ashmem */
  
  unsigned long prot_mask; /* allowed prot bits, as vm_flags */
  
  };
  
  其中最重要的无疑是那个file域了,当然,一开始这个域的值为null。
  
  2.2 ashmem_ioctl()操作
  
  ashmem_open()之后,紧接着要设置刚打开的共享内存文件的一些属性,于是调用到ioctl()对应的ashmem_ioctl()。主要的设置动作其实就是向ashmem_area里写入一些数据啦。ashmem_ioctl()函数的定义如下:
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static long ashmem_ioctl(struct www.fusyLwg.com 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_SET_SIZE:
  
  ret = -EINVAL;
  
  if (!asma->file) {
  
  ret = 0;
  
  asma->size = (size_t) arg;
  
  }
  
  break;
  
  . . . . . .
  
  . . . . . .
  
  }
  
  return ret;
  
  }
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static int set_name(struct ashmem_area *asma, void __user *name)
  
  {
  
  . . . . . .
  
  len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
  
  . . . . . .
  
  mutex_lock(&ashmem_mutex);
  
  . . . . . .
  
  strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);
  
  mutex_unlock(&ashmem_mutex);
  
  . . . . . .
  
  }
  
  实际设置的区域名是:"dev/ashmem/" + “传入的名字”。这样,我们就可以得到下面这张图:
  
  3.映射共享内存区域
  
  在上图这种“file里面套file”的结构中,内层那个file究竟是什么时候打开的呢?简单地说,就是在我们针对这块共享内存做mmap操作的时候。mmap操作在驱动层对应的是ashmem_mmap()函数。3.1ashmem_mmap()操作
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
  
  {
  
  struct ashmem_area *asma = file->private_data;
  
  . . . . . .
  
  vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
  
  if (!asma->file) {
  
  . . . . . .
  
  vmfile = shmem_file_setup(name, asma->size, vma->vm_flags); // 创建文件节点
  
  . . . . . .
  
  asma->file = vmfile; // ashmem_area里的file终于有值了!
  
  }
  
  get_file(asma->file);
  
  if (vma->vm_flags & VM_SHARED)
  
  shmem_set_file(vma, asma->file);
  
  else {
  
  . . . . . .
  
  }
  
  asma->vm_start = vma->vm_start; // vm_area_struct里的必要信息,复制到ashmem_area中
  
  . . . . . .
  
  return ret;
  
  }
  
  最关键的一步是vmfile = shmem_file_setup(...),调用的其实是linux系统的接口,在linux“内存文件系统”里创建一个文件节点。shmem_file_setup()的代码如下:
  
  【kernel/msm-3.18/mm/Shmem.c】
  
  struct file *shmem_file_setup(const char *name, loff_t size, unsigned long flags)
  
  {
  
  return __shmem_file_setup(name, size, flags, 0);
  
  }
  
  static struct file *__shmem_file_setup(const char *name, loff_t size,
  
  unsigned long flags, unsigned int i_flags)
  
  {
  
  struct file *res;
  
  struct inode *inode;
  
  . . . . . .
  
  . . . . . .
  
  inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
  
  . . . . . .
  
  d_instantiate(path.dentry, inode);
  
  inode->i_size = size;
  
  . . . . . .
  
  res = alloc_file(&path, FMODE_WRITE | FMODE_READ,
  
  &shmem_file_operations);
  
  . . . . . .
  
  return res;
  
  . . . . . .
  
  }
  
  shmem_file_setup()建立的file对应的文件操作(shmem_file_operations)如下:
  
  【kernel/msm-3.18/mm/Shmem.c】
  
  static const struct file_operations shmem_file_operations = {
  
  .mmap = shmem_mmap,
  
  #ifdef CONFIG_TMPFS
  
  .llseek = shmem_file_llseek,
  
  .read = new_sync_read,
  
  .write = new_sync_write,
  
  .read_iter = shmem_file_read_iter,
  
  .write_iter = generic_file_write_iter,
  
  .fsync = noop_fsync,
  
  .splice_read = shmem_file_splice_read,
  
  .splice_write = iter_file_splice_write,
  
  .fallocate = shmem_fallocate,
  
  #endif
  
  };
  
  ashmem_mmap()的调用关系图如下:
  
  经过mmap操作,file里面套file的结构就完成了,示意图如下:
  
  注意,上图中的两个file对应的文件操作是不一样的。第一个是ashmem层次的file,第二个是tmpfs虚拟文件系统里的file。补充说明一下,tmpfs(temporary filesystem)是Linux特有的文件系统,标准挂载点是/dev/shm。其内部使用物理内存或swap交换空间实现了一套独立的文件系统。该系统不是块设备系统,所以不需要格式化操作,只要成功挂载,就可以立即使用。
  
  现在,我们可以把“创建共享内存区域”和“映射共享内存区域”两小节的内容汇整成一张示意图,图中表示了两个步骤,创建和映射,最终完成双file结构:
  
  3.2 munmap()操作
  
  ashmem共享内存只有在成功mmap以后,才能够读写。不过MemoryFile允许用户在需要时收回成命,取消mmap。为此,它提供了deactivate()函数。该函数的代码如下:
  
  【frameworks/base/core/java/android/os/MemoryFile.java】
  
  void deactivate() {
  
  if (!isDeactivated()) {
  
  try {
  
  native_munmap(mAddress, mLength); // 其实就是在做munmap动作
  
  mAddress = 0; // 一旦销毁了映射,mAddress也必须设为0
  
  } catch (IOException ex) {
  
  Log.e(TAG, ex.toString());
  
  }
  
  }
  
  }
  
  native_munmap()对应的C++层函数是android_os_MemoryFile_munmap(),其定义如下:
  
  【frameworks/base/core/jni/android_os_MemoryFile.cpp】
  
  static void android_os_MemoryFile_munmap(JNIEnv* env, jobject clazz, jlong addr, jint length)
  
  {
  
  int result = munmap(reinterpret_cast<void *>(addr), length);
  
  if (result < 0)
  
  jniThrowException(env, "java/io/IOException", "munmap failed");
  
  }
  
  可以看到其实就是在调用munmap()操作。
  
  munmap()并不像mmap()那样有对应的ashmem_mmap(),也就是说不存在ashmem_munmap()。munmap()的工作完全由系统内核完成。注意,munmap()只会解除内存映射关系,却不会关闭共享内存。这也就是说,此时的读写操作虽然会失败,但调用getFileDescriptor()还是可以拿到一个合法的文件描述符的。
  
  4.pin和unpin操作
  
  在拥有了一块共享内存之后,我们还可以对它进行更细的控制。比如进行pin和unpin操作。不过MemoryFile并没有向外提供pin和unpin接口,它认为pin和unpin都只能是内部行为。
  
  那么pin和unpin到底是什么意思呢?pin本身的意思是压住、定住,因此pin一块内存指的就是锁定一块内存,明确表示这块内存现在正被使用着。如果后续在某种情况下,比如说内存吃紧时,我们可以解锁某些内存区域,把相应的内存用到其他地方去。从这个角度说,ashmem驱动程序可以在一定程度上辅助内存管理,提供少许的内存优化能力。
  
  匿名共享内存创建之初,所有的内存都是pinned状态,后续用户可以申请unpin一块内存区域,反过来说,只有对一块unpinned状态的内存区域,用户才可以重新pin。
  
  MemoryFile内部的pin函数是native_pin(),其实unpin操作也是靠这个函数完成的。
  
  private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException;
  
  该函数对应于C++层的android_os_MemoryFile_pin():
  
  【frameworks/base/core/jni/android_os_MemoryFile.cpp】
  
  static void android_os_MemoryFile_pin(JNIEnv* env, jobject clazz,
  
  jobject fileDescriptor, jboolean pin)
  
  {
  
  int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
  
  // 【注意】这两个函数的最后两个参数都为0.
  
  int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));
  
  if (result < 0) {
  
  jniThrowException(env, "java/io/IOException", NULL);
  
  }
  
  }
  
  该函数通过参数pin,来说明是要pin一块内存区域,还是unpin内存区域。如果要执行pin操作,就调用ashmem_pin_region(),反之则调用ashmem_unpin_region(),不过其实这两个函数最终调用的都是ioctl()。
  
  【system/core/libcutils/Ashmem-dev.c】
  
  int ashmem_pin_region(int fd, size_t offset, size_t len)
  
  {
  
  struct ashmem_pin pin = { offset, len }; // 输入的参数汇整进ashmem_pin
  
  int ret = __ashmem_is_ashmem(fd);
  
  if (ret < 0) {
  
  return ret;
  
  }
  
  return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin)); // 将ashmem_pin参数传入ioctl
  
  }
  
  int ashmem_unpin_region(int fd, size_t offset, size_t len)
  
  {
  
  struct ashmem_pin pin = { offset, len };
  
  int ret = __ashmem_is_ashmem(fd);
  
  if (ret < 0) {
  
  return ret;
  
  }
  
  return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_UNPIN, &pin)); // 将ashmem_in参数传入ioctl
  
  }
  
  请注意,android_os_MemoryFile_pin()在调用ashmem_pin_region()或ashmem_unpin_region()时,传递的offset参数和len参数都为0,这是为什么呢?简单地说,这表示调用者希望内核按自己的规则,帮我们计算最终的偏移和大小,并以这块共享内存整体来执行锁定或解锁。
  
  ioctl()对应于驱动层的ashmem_ioctl(),代码如下:
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  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:
  
  case ASHMEM_GET_PIN_STATUS:
  
  ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg); // PIN/UNPIN都是调用它
  
  break;
  
  . . . . . .
  
  . . . . . .
  
  }
  
  return ret;
  
  }
  
  【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
  
  static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
  
  void __user *p)
  
  {
  
  struct ashmem_pin pin; // 传入的ashmem_pin,其offset和len都为0
  
  size_t pgstart, pgend;
  
  int ret = -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; // 注意这句,当pin.len为0时,
  
  // 会计算pin.len
  
  ......
  
  // 计算出涉及的内存区域的起始和终止,此时是以页为单位
  
  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_unpin()。首先调用copy_from_user()读取从用户态传来的参数。大家应该还记得前文提到的ashmem_pin参数吧,在MemoryFile里,强行把它的offset和len成员都是设为0了。当然,内核里其他地方也可能执行ashmem的pin操作,那时有可能为offset和len设置非0值。作为ashmem_pin_unpin()函数,它肯定要兼顾各种offset和len值,所以才有了上面代码里调用PAGE_ALIGN和整除PAGE_SIZE的句子。
  
  不过有一点需要说明,因为在内核中是以页为单位来管理内存的,一般来说,一页的大小为4KB(即PAGE_SIZE)。所以在pin/unpin时,指定的len必须是页大小的整数倍,否则pgend的计算会有误。大家来看:
  
  对于MemoryFile来说,因为传入的offset和len都为0,所以计算的pgstart和pgend都是正确的。比如我们的共享内存区有12KB,那么计算的情况是:pin.offset = 0
  
  pin.len = PAGE_ALIGN(12KB) - 0 = 12KB
  
  pgstart = 0 / PAGE_SIZE = 0
  
  pgend = 0 + (12KB / PAGE_SIZE) - 1 = 2
  
  即从第0页到第2页,这3个页会锁定。这个结果是正确的。这也说明MemoryFile了只允许整体性地将自己这块共享内存pin或unpin,它不涉及更细化地锁定解锁动作。
  
  但如果我们不使用MemoryFile,而希望pin一块offset为1KB,len为5KB的内存,计算的情况就是:
  
  pin.offset = 1KB
  
  pin.len = 5KB
  
  pgstart = 1KB / PAGE_SIZE = 0
  
  pgend = 0 + (5KB / PAGE_SIZE) - 1 = 0
  
  即只有第0页会锁定,这就没法交货了嘛。但如果len为8KB,则pgend为1,那么正确锁定两页。事实上我们建议,连offset都是按4KB整数倍对齐的。
  
  好,不伤脑筋了。我们姑且认为拿到了正确的pgstart和pgend,接下来会调用的ashmem_pin()或ashmem_unpin()。一块虚拟内存刚映射好时,整个区域都是pinned状态,所以即便执行ashmem_pin()也不起什么作用。但是可以执行ashmem_unpin(),该函数的代码如下:
  
  【kernel/msm-3.18/drivers/staging/android/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:
  
  // 遍历列表中的range
  
  list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
  
  // 如果当前遍历的range节点的结束位置比pgstart还小,那么就可以在列表的
  
  // 这个位置插入新range节点了
  
  if (range_before_page(range, pgstart))
  
  break;
  
  // 如果当前遍历的range节点已经涵盖了pgstart、pgend所指定的范围,那么直接return即可
  
  if (page_range_subsumed_by_range(range, pgstart, pgend))
  
  return 0;
  
  // 如果当前遍历的range和pgstart、pgend指定的范围有交集,那么要合并一下。即重新计
  
  // 算pgstart、pgend,并且删掉旧range节点,然后用goto重新走一遍遍历动作
  
  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);
  
  }
  
  这段代码还算比较清晰。我们在前文阐述ashmem_area结构时,没有细说它里面的unpinned_list成员:
  
  struct list_head unpinned_list;
  
  现在来补充说明一下,简单说来,它是一条节点类型为ashmem_range的双向链表,其中每一个ashmem_range节点表示一块连续的已解除锁定的内存区域。对于一块ashmem共享内存来说,我们可以多次从中切割出一部分来,释放掉对应的内存。这些被切除的部分,在逻辑上就可以作为一个个节点,插入到unpinned_list中。有时候被切除的部分刚好可以和其左右区域形成一块更大的区域,那么unpin动作里就会把它们拼接成一块大的节点,替换掉以前零散的节点。实际向unpinned_list插入节点的动作是range_alloc(),其代码如下:
  
  【kernel/msm-3.18/drivers/staging/android/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;
  
  }
  
  现在我们画一张关于unpinned_list的示意图:
  
  相应地,pin操作就是在unpinned_list里寻找会影响到的unpinned的子块,然后调整这些子块的大小。因为pin操作只会让unpinned块更加零散,所以不牵扯合并区域的动作,倒是有可能添加新的unpinned节点。
  
  【kernel/msm-3.18/drivers/staging/android/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) {
  
  if (range_before_page(range, pgstart))
  
  break;
  
  if (page_range_in_range(range, pgstart, pgend)) {
  
  ret |= range->purged;
  
  // 1:如果当前遍历的range可以成功纳入[pgstart,pgend]范围,则直接删除当前range
  
  if (page_range_subsumes_range(range, pgstart, pgend)) {
  
  range_del(range);
  
  continue;
  
  }
  
  // 2:如果当前遍历的range的起始位置大于pgstart,则修改该range的大小,去掉
  
  // 和[pgstart,pgend]重叠的部分
  
  if (range->pgstart >= pgstart) {
  
  range_shrink(range, pgend + 1, range->pgend);
  
  continue;
  
  }
  
  // 3:如果当前遍历的range的结束位置小于等于pgend
  
  if (range->pgend <= pgend) {
  
  range_shrink(range, range->pgstart, pgstart-1);
  
  continue;
  
  }
  
  // 4:pin操作只会让unpinned块更加零散,所以不牵扯合并区域的动作,
  
  // 倒是有可能添加新的unpinned节点。这里就是为被pin区域打断的后半
  
  // 部分unpinned区域申请节点。
  
  range_alloc(asma, range, range->purged,
  
  pgend + 1, range->pgend);
  
  range_shrink(range, range->pgstart, pgstart - 1);
  
  break;
  
  }
  
  }
  
  return ret;
  
  }
  
  5.purge行为
  
  ashmem还支持一种行为,即允许系统对它做部分或全部清除。这牵扯到操作系统对内存的管理。按我们初步的理解来说,ashmem驱动程序需要和系统内核一起协作起来才能较好地完成工作。我们可以设想有这样的规定:
  
  1)即使在内存比较吃紧时,系统内核也不会清除ashmem里pin住的内存区域;
  
  2)系统内核可以在合适的时机,清除某些unpinned的区域。
  
  3)刚刚unpin的区域的状态为ASHMEM_NOT_PURGED,但系统内核清除这部分区域后,会将其状态修改为ASHMEM_WAS_PURGED。
  
  MemoryFile里也有部分功能和purge相关,比如它提供有allowPurging()函数:
  
  【frameworks/base/core/java/android/os/MemoryFile.java】
  
  synchronized public boolean allowPurging(boolean allowPurging) throws IOException {
  
  boolean oldValue = mAllowPurging;
  
  if (oldValue != allowPurging) {
  
  native_pin(mFD, !allowPurging);
  
  mAllowPurging = allowPurging;
  
  }
  
  return oldValue;
  
  }
  
  当allowPurging参数为true时,表示允许系统内核在合适的时机,清除其unpinned部分。MemoryFile里的动作倒是干脆,只要调用者允许系统清除,就直接把整块共享内存区域unpin了。知道用户再次调用allowPurging,并传入false参数时,才会pin回来。
  
  正因为MemoryFile支持了allowPurging()操作,所以在写入内容时,就得兼顾考虑这块共享内存是不是已经被purge了。这也就是为什么MemoryFile的writeBytes()在调用native_write()时,要把mAllowPurging作为最后一个参数传进来的原因。最后这个参数表示当前这块共享内存,是不是允许系统内核在合适的时机清除。如果该状态为true,表示这块共享内存目前处于unpinned状态,而如果为false,则表示处于pinned状态。
  
  【frameworks/base/core/java/android/os/MemoryFile.java】
  
  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();
  
  }
  
  // 注意,最后一个参数是表示当前是否允许系统purge
  
  native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
  
  }
  
  native_write()对应的C++层函数为android_os_MemoryFile_write():
  
  【frameworks/base/core/jni/android_os_MemoryFile.cpp】
  
  static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,
  
  jobject fileDescriptor, jlong 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;
  
  }
  
  中间那句if语句表达的意思就是,如果当前这个内存处于没有锁定的状态,那么write操作就会尝试“临时性地”做一次锁定,如果pin的结果反馈的是ASHMEM_NOT_PURGED,说明系统内核还没有清除这块内存区域,因此可以放心写入数据。相反,如果pin的结果反馈的是ASHMEM_WAS_PURGED,说明系统已经清除了这块内存区域,那么就将“临时性”的pin恢复回去,并抛出一个IOException异常。具体写入数据的动作是GetByteArrayRegion(),它会把源buffer里的一部分,写到以address+destOffset地址为起始地址的内存去。
  
  6.跨进程传递文件描述符
  
  说完pin和purge操作,接下来我们来说说跨进程传递文件描述符。我们已经知道,在MemoryFile的构造函数里,会创建出一块共享内存,并用一个FileDescriptor文件描述符记录它,这个在前文已有说明:
  
  mFD = native_open(name, length);
  
  那么很明显,MemoryFile内部最核心的东西,也就来源于这个文件描述符。事实上,在早期的Android版本中,MemoryFile有两个构造函数,除了前文我们看到的MemoryFile(String name, int length),还有另一个可接受文件描述符的构造函数MemoryFile(FileDescriptor fd, int lenght, String mode),其内部会对传入的文件描述符重新mmap。这大概是为了更方便地使用跨进程传来的文件描述符。然而后来也许MemoryFile的设计师的设计思路变化了,变得不再希望开发人员跨进程地使用MemoryFile了,所以在后来的Android版本中,彻底去除了这个构造函数。
  
  MemoryFile倒是还保有一个成员函数getFileDescriptor(),可以返回已打开的文件描述符:
  
  public FileDescriptor getFileDescriptor() throws IOException {
  
  return mFD;
  
  }
  
  只不过MemoryFile的设计者并不希望普通的应用程序开发人员直接调用这个函数,所以这个函数是用@hide标注的。网上有一些例子,为了说明如何跨进程地使用共享内存,会使用反射机制来调用这个函数,从而拿到FileDescriptor,然后再利用binder机制来跨进程传递FileDescriptor。严格说起来,这种例子可以作为参考,但已经不是Android上建议的使用共享内存的做法了。实际上,Android上建议的做法是利用pipe,这个本文就不细说了。
  
  在Android上,要跨进程传递文件描述符,我们常常会用到一个ParcelFileDescriptor类。这个类倒是和MemoryFile有些许交集,比如ParcelFileDescriptor里提供有一个静态的fromData()函数,其内部就会创建一个MemoryFile,并返回对应的ParcelFileDescriptor,参考代码如下:
  
  【frameworks/base/core/java/android/os/ParcelFileDescriptor.java】
  
  @Deprecated
  
  public static ParcelFileDescriptor fromData(byte[] data, String name) throws IOException {
  
  if (data == null) return null;
  
  MemoryFile file = new MemoryFile(name, data.length);
  
  if (data.length > 0) {
  
  file.writeBytes(data, 0, 0, data.length);
  
  }
  
  file.deactivate(); // unmmap操作
  
  FileDescriptor fd = file.getFileDescriptor(); // 不用反射,即可直接调用getFileDescriptor()
  
  return fd != null ? new ParcelFileDescriptor(fd) : null;
  
  }
  
  从这部分代码和注释里,我们可以看到:
  
  1)fromData()内部使用的是MemoryFile的隐藏接口getFileDescriptor();(因为它是framework里的类,所以可以直接访问隐藏接口)
  
  2)fromData()已经不建议使用了(有@Deprecated标注);
  
  3)新的推荐方法为createPipe()或ContentProvider.openPipeHelper()。
  
  目标端收到ParcelFileDescriptor之后,简单的做法可以这样:
  
  ParcelFileDescriptor pfd = ......;
  
  fileDescriptor = pfd.getFileDescriptor(); // 从ParcelFileDescriptor获取到FileDescriptor
  
  fi = new FileInputStream(fileDescriptor);
  
  fi.read(buffer);
  
  fi.close();
  
  至于传递的细节,大家可以参考binder相关的具体代码,此处不赘述。我们只需知道,源端和目标端进程可以拿到各自的关于共享内存的文件描述符,而这两个文件描述符在内核里对应着同一个file和ashmem_area,本文一开始绘制的示意图大体就是这个意思。
  
  7.小结
  
  有关Ashmem的机制,我们就先说这么多。说起来主要是以MemoryFile为切入点,对Ashmem做了一些说明。但大家自己要清楚,MemoryFile受限于技术,对普通应用开发者而言,并不是一个好的选择。大家在实际项目中一定要谨慎使用之。因为本文的重点是说明Ashmem技术,所以就不扩展来说Android上其他常用的跨进程共享内存的方法了,大家如有兴趣,可以找找ContentProvider和pipe方面的资料看看,应该会有所收获。当然,以后我也会写其他文章,专门来说说ContentProvider和pipe。

转载于:https://www.cnblogs.com/qwangxiao/p/11205620.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值