您现在的位置是:首页 > 文章详情

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

日期:2019-07-17点击:361

且听穿林打叶声———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);  // 注册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 = {  // 用于注册杂项从设备     .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(env, result); }

其中最重要的一句是调用ashmem_create_region(),它的返回值如果大于等于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(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))         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;         /* 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 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://my.oschina.net/youranhongcha/blog/3075518
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章