您的位置:首页 > 运维架构 > Linux

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

2019-07-19 18:19 1766 查看

且听穿林打叶声———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。
 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Linux Android Java