Understanding Android Source: ASHMEM

本文介绍了对作者Android匿名共享内存的认识,这些认识来自于对三本书的学习,分别是《Android系统源代码情景分析》、《深入理解Linux内核》、《Unix网络编程 卷二:进程间通信》。作者能力水平有限,难免在一些文字上鹦鹉学舌,错误也难免,敬请读者注意甄别。

1 虚拟内存与进程间通信

进程是程序在执行时的一个实例,其作用是充当分配系统资源(CPU时间、内存等)的实体。在计算机早期,比如8086/8088时代的DOS,如果某一进程占用了太多内存,就会导致其他进程没有内存来运行。同时,如果一个进程无意中操作了其他进程的内存,很可能导致该进程产生莫名其妙的逻辑错误。

80286在保护模式(Protected Mode)中引入了虚拟内存(Virtual Memory)的支持,情况所有改变。虚拟内存具有如下特征:

  1. 视内存为缓存,只在内存中保留活动区域,根据需要将数据在内存和磁盘间交换。
  2. 每个进程的地址空间一样,简化内存管理管理。
  3. 每个进程的地址空间独立,避免被其他进程破坏。

由于上面的第三条,各个进程之间树立起了屏障,一个进程无法直接操作其他进程内存。但是,进程间互相通信这种需求还是存在的,理论上可以这样做到:

  1. 两个进程共享存留于文件系统中某个文件上的某些信息。
  2. 两个进程共享驻留于内核中的某些信息。
  3. 两个进程共享同一块内存。

在实际开发中,基于上面的理论,共存在有五种途径:

  1. Pipe, FIFO(Named Pipe)
  2. Semaphore
  3. Message Queue
  4. Shared Memory Region
  5. Sockets

本文接下来要讲的就是Shared Memory Region。

2 Linux/Android与共享内存

当两个进程需要共享一块匿名共享内存时,最直观的想法是,把文件描述符(File descriptor)从一个进程传递到另外一个进程。但是,这个方法在实践中是行不通的。因为文件描述符只在进程范围内有效,即便是值相同的两个文件描述符,在两个不同的进程中也是具有不同的意义的。

在Linux内核中,一个文件描述符对应有一个文件结构体(struct file)(如下)(源码),一个文件结构体对应有一个文件。这也意味着,不同的文件描述符可以对应同一个文件结构体,不同的文件结构体也可以对应同一个文件。

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path     f_path;
#define f_dentry    f_path.dentry
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;
    // ... ...
}

虽然不同进程拥有不同的文件描述符,但是这些文件描述符可以都指向同一个文件结构体,而如果这个文件结构体指向一块匿名共享内存,那么在不同进程间,就可以共享匿名共享内存了。

Android实现匿名共享内存与传统的Linux系统一样,都是基于内核层临时文件系统tmpfs。在Android系统里,使用open函数打开设备文件/dev/ashmem,获得一个文件描述符,以此来标示一块匿名共享内存,并且使用Binder进程机制,传输这个文件描述符,从而在不同的应用程序之间共享。

文件描述符在Binder进程间通信机制里,并非是一个普通类型,而是使用BINDER_TYPE_FD。当Binder驱动程序发现进程间通信数据中包含了这种对象,就会将对应的文件描述符复制到目标进程中,从而在两个进程中共享一个文件。

3 Ashmem Driver

Android匿名共享内存系统是以Ashmem驱动程序为基础,系统中所有的匿名共享内存都是由其分配和管理。深入到内核源码当中,会注意到有三个重要的结构体ashmem_areaashmem_rangeashmem_pin,以下将分别介绍。

3.1 Structure

3.1.1 ashmem_area

struct ashmem_area {
    char name[ASHMEM_FULL_NAME_LEN];
    struct list_head unpinned_list;
    struct file *file;
    size_t size;
    unsigned long vm_start;
    unsigned long prot_mask;
};

用来描述一块匿名共享内存:

  • name用来保存一块匿名共享内存的名称,它同时会写入到文件/proc/p_i_d/maps中。
  • unpinned_list用来描述一个解锁内存块列表。一块匿名共享内存可以动态地划分为若干个小块,当处于解锁状态时,就会被加入到unpinned_list中。
  • file指向临时文件系统(tmpfs)的一个文件。
  • size即为file的大小。
  • prot_mask用来描述一块匿名共享内存的访问保护位。

3.1.2 ashmem_range

struct ashmem_range {
    struct list_head lru;
    struct list_head unpinned;
    struct ashmem_area *asma;
    size_t pgstart;
    size_t pgend;
    unsigned int purged;
};

一块匿名共享内存可以被划分为若干个小块,内核就用ashmem_range来描述一小块处于解锁状态的内存:

  • 每一块处于解锁状态的内存都会通过其成员变量lru,链入到Ashmem驱动程序中一个全局列表ashmem_lru_list。当系统内存不足时,内存管理系统就会按照最近最少使用的原则来回收保存在全局列表ashmem_lru_list中的内存块。
  • unpinned链入宿主匿名共享内存的unpinned_list
  • asma即是宿主匿名共享内存。
  • pgstartpgend用来描述一块处于解锁状态的内存的开始地址和结束地址,它们的单位是页。
  • purged用来描述一块处于已解锁状态的内存是否已经被回收。

3.1.3 ashmem_pin

struct ashmem_pin {
    __u32 offset;
    __u32 len;
};

是Ashmem 驱动程序定义的 IO 控制命令ASHMEM_PINASHMEM_UNPIN的参数,用来描述一小块即将被锁定或者解锁的内存区域:

  • offset表示这块内存块在宿主匿名共享内存块中的偏移值。
  • len表示这块内存块的大小,以字节为单位,对齐到页面边界。

3.2 Function

3.2.1 init

static int __init ashmem_init(void)
{
    int ret;
 
    ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
                      sizeof(struct ashmem_area),
                      0, 0, NULL);
    if (unlikely(!ashmem_area_cachep)) {
        pr_err("failed to create slab cache\n");
        return -ENOMEM;
    }
 
    ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
                      sizeof(struct ashmem_range),
                      0, 0, NULL);
    if (unlikely(!ashmem_range_cachep)) {
        pr_err("failed to create slab cache\n");
        return -ENOMEM;
    }
 
    // ......
}

调用kmem_cache_create,分别创建了两个slab缓冲区分配器:ashmem_area_cachepashmem_range_cachep,分别来分配ashmem_areaashmem_range结构体。

static int __init ashmem_init(void)
{
    // ......
 
    ret = misc_register(&ashmem_misc);
    if (unlikely(ret)) {
        pr_err("failed to register misc device!\n");
        return ret;
    }
 
    // ......
}

调用misc_register,将匿名共享内存设备注册为一个类型为misc的字符设备,提供操作方法列表ashmem_fops,包含了打开、关闭、内存映射、IO控制等。由于匿名共享内存的访问方式是直接地址访问,即先映射进程的地址空间,然后在通过虚拟地址直接访问,因此没有对应的读写函数。

static int __init ashmem_init(void)
{
    // ......
 
    register_shrinker(&ashmem_shrinker);
 
    pr_info("initialized\n");
 
    return 0;
}

调用register_shrinker,向内存管理系统注册一个内存内存回收函数ashmem_shrinker,当系统内存不足时,内存管理系统就会通过一个页框回收算法(Page Frame Reclaiming Algorithm)来回收内存,这时候所有调用了函数register_shrinker注册的内存回收函数都会被调用,以便它们可以向系统贡献空闲内存。

3.2.2 open

/**
 * ashmem_open() - Opens an Anonymous Shared Memory structure
 * @inode:     The backing file's index node(?)
 * @file:      The backing file
 *
 * Please note that the ashmem_area is not returned by this function - It is
 * instead written to "file->private_data".
 *
 * Return: 0 if successful, or another code if unsuccessful.
 */
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;
 
    // ......
}

调用generic_file_open打开设备文件。

static int ashmem_open(struct inode *inode, struct file *file)
{
    // ......
    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;
}

调动kmem_cache_zalloc从slab缓冲区ashmem_area_cachep中分配一个ashmem_area,并设置给参数fileprivate_data变量。

3.2.3 mmap

当应用程序调动函数mmap将前面打开的设备文件/dev/ashmem映射到进程的地址空间时,Ashmem驱动程序就会为一块匿名共享内存创建一个临时文件。

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 & ~calc_vm_prot_bits(asma->prot_mask)) &
             calc_vm_prot_bits(PROT_MASK))) {
        ret = -EPERM;
        goto out;
    }
    vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
    // ......
}

这段代码逻辑完成如下检查:

  1. 检查匿名共享内存asma的大小是否为0。
  2. 检查要映射的虚拟内存vma的访问权限是否超过了匿名共享内存asma的访问保护权限prot_mask
  3. 检查是否为匿名共享内存asma创建过临时文件。
  4. 检查虚拟内存vma是否允许在不同进程间共享。

然后:

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
    // ......
    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);
    // ......
}

调用函数shmem_file_setup,在临时文件系统tmpfs中创建临时文件,并将打开的文件结构体保存在匿名共享内存asma的成员变量file中。

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
    // ......
    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;
    }
    asma->vm_start = vma->vm_start;
 
out:
    mutex_unlock(&ashmem_mutex);
    return ret;
}

调用函数shmem_set_file,设置映射文件,以及内存操作方法表。

开始的时候,虚拟内存vma是没有映射物理页面的,因此,当它第一次被访问时,就会发生缺页异常(Page Fault)。这时,内核就会调用它的内存操作方法表中的sheme_fault给它映射到物理页面。sheme_fault首先会在页面缓冲区中检查是否存在与缺页的虚拟地址相对应的物理页面。如果存在,就直接将它们映射到缺页的虚拟地址。否则,再去页面换出设备中检查是否存在与缺页的虚拟地址对应的换出页面。如果存在,就先把它添加到页面缓冲区里,然后再映射到缺页的虚拟地址。否则,就要为缺页的虚拟地址分配新的物理页面,并且从虚拟内存vma的映射文件vm_file中读入相应的内容,来初始化这些新分配的物理页面,最后将这些物理页面加入到页面缓冲区里。

将一个物理页面映射到两个不同进程的虚拟地址空间,从而通过内存映射机制实现内存共享的功能。

3.2.4 pin & unpin

/*
 * ashmem_pin - pin the given ashmem region, returning whether it was
 * previously purged (ASHMEM_WAS_PURGED) or not (ASHMEM_NOT_PURGED).
 *
 * Caller must hold ashmem_mutex.
 */
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)

一块匿名共享内存在创建时是处于锁定状态的,接下来,应用程序可以根据需要把它划分成若干个小块来使用。

/*
 * ashmem_unpin - unpin the given range of pages. Returns zero on success.
 *
 * Caller must hold ashmem_mutex.
 */
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)

当其中的某些小块内存不再使用时,应用程序就可以对它们执行解锁操作,从而可以在内存紧张时,为内存管理系统贡献内存。处于解锁状态的内存还没有被回收时,那么应用程序还可以对它们执行锁定操作,从而阻止它们被内存管理系统回收。

3.2.5 shrink

Ashmem驱动程序在启动时,向内存管理系统注册一个内存回收函数ashmem_shrinker。当系统内存不足时,函数ashmem_shrinker就会被调用,来回收那些已经解锁状态的匿名共享内存。

/*
 * ashmem_shrink - our cache shrinker, called from mm/vmscan.c :: shrink_slab
 *
 * 'nr_to_scan' is the number of objects to scan for freeing.
 *
 * 'gfp_mask' is the mask of the allocation that got us into this mess.
 *
 * Return value is the number of objects freed or -1 if we cannot
 * proceed without risk of deadlock (due to gfp_mask).
 *
 * We approximate LRU via least-recently-unpinned, jettisoning unpinned partial
 * chunks of ashmem regions LRU-wise one-at-a-time until we hit 'nr_to_scan'
 * pages freed.
 */
static unsigned long
ashmem_shrink_scan(struct shrinker *shrink, struct shrink_control *sc)

使用list_for_each_entry_safe来遍历ashmem_lru_list,对解锁内存块逐一调用vmtruncate_range来回收物理页面。直到要回收的物理页面数达到了nr_to_scan,或者全局列表ashmem_lru_list中已经没有内存可回收为止。

4 ashmem & binder

如下源码显示了Ashmem与Binder之间的联系:

static void binder_transaction(struct binder_proc *proc,
               struct binder_thread *thread,
               struct binder_transaction_data *tr, int reply)
{
    // ......
    fp = (struct flat_binder_object *)(t->buffer->data + *offp);
    // ......
    switch (fp->type) {
    // ......
    case BINDER_TYPE_FD: {
        int target_fd;
        struct file *file;
 
        // ... ...
        file = fget(fp->handle);
        // ... ...
        target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
        // ... ...
        task_fd_install(target_proc, target_fd, file);
        trace_binder_transaction_fd(t, fp->handle, target_fd);
        // ... ...
        fp->binder = 0;
        fp->handle = target_fd;
    } break;
    // ... ...
}

匿名共享内存与Binder进程间通信机制息息相关,进程必须要借助Binder,将共享内存分享给其他进程。

fp = (struct flat_binder_object *)(t->buffer->data + *offp);

从进程间通信数据中获得flat_binder_object结构体fp,其源码显示:

struct flat_binder_object {
    /* 8 bytes for large_flat_header. */
    __u32       type;
    __u32       flags;
    //......
};

成员变量handle就是用来描述一个文件描述符,成员变量type值为BINDER_TYPE_FD

target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);

在目标进程target_proc中获取一个未使用的文件描述符target_fd

task_fd_install(target_proc, target_fd, file);

将文件描述符与文件结构体关联起来

fp->handle = target_fd;

将fp的成员变量handle的值设置为文件描述符target_fd,进而返回给目标进程,即返回给Client组件。

Client组件从Binder驱动程序中得到了flat_binder_object结构体fp之后,首先将它封装成一个ParcelFileDescriptor对象,然后再将它转换成FileDescriptor,最后使用它来创建一个MemoryFile对象。

5 API

5.1 C API

运行时库cutils提供了ashmem.h,定义了五个接口,来操作匿名共享内存:

5.1.1 ashmem_create_region

/*
 * ashmem_create_region - creates a new ashmem region and returns the file
 * descriptor, or <0 on error
 *
 * `name' is an optional label to give the region (visible in /proc/pid/maps)
 * `size' is the size of the region, in page-aligned bytes
 */
int ashmem_create_region(const char *name, size_t size)

请求Ashmem驱动程序创建一块匿名共享内存:

  • 参数namesize分别表示请名称和大小,
  • 返回值为打开设备文件/dev/ashmem所得到的一个文件描述符。

在其中,需要用到ASHMEM_SET_NAME命令来设置匿名共享内存名称,以及使用ASHMEM_SET_SIZE命令设置大小。

5.1.2 ashmem_pin_region

int ashmem_pin_region(int fd, size_t offset, size_t len)

请求Ashmem驱动程序锁定匿名共享内存中的一个片段:

  • 参数fdashmem_create_region的返回值,
  • 参数offset用来指定待锁定的片段,在其宿主匿名共享内存中的偏移地址,
  • len是指长度。

这个逻辑需要依赖 ASHMEM_PIN命令。

5.1.3 ashmem_unpin_region

int ashmem_unpin_region(int fd, size_t offset, size_t len)

ashmem_pin_region相反,请求Ashmem驱动程序解锁匿名共享内存,这个逻辑需要依赖ASHMEM_UNIPIN命令。

5.1.4 ashmem_set_prot_region

int ashmem_set_prot_region(int fd, int prot)

请求Ashmem驱动程序修改匿名共享内存的访问保护位:

  • 参数fd如前所述,
  • 参数prot为目标访问保护位,取值可以为PROT_EXECPROT_READPROT_WRITE或其组合值。注意,访问保护位只能削减,而不能增加。

这个逻辑需要依赖ASHMEM_SET_PROT_MASK命令。

5.1.5 ashmem_get_size_region

int ashmem_get_size_region(int fd)

请求Ashmem驱动程序返回匿名共享内存的大小,其参数fd如前所述。这个逻辑需要依赖ASHMEM_GET_SIZE命令。

5.2 C++ API

Shared Memory

Shared Memory

Android在Frameworks中定义了IMemoryHeapIMemory两个接口,二者都在IMemory.h文件中。但是,需要提醒的是,该文件并没有出现在NDK里,因此这两个接口也就只能在AOSP开发里应用了。

IMemoryHeap接口定义了四个成员函数:

  • getHeapID,获取一个匿名共享内存块的文件描述符。
  • getBase,获取一个匿名共享内存块的映射地址。
  • getSize,获取一个匿名共享内存块的大小。
  • getFlags,获取一个匿名共享内存块的访问保护位。

IMemory接口也定义了四个成员函数:

  • getMemory,获取匿名共享内存片段的宿主。
  • size,获取匿名共享内存片段的大小。
  • offset,获取匿名共享内存片段的在宿主匿名共享内存中的偏移值。
  • pointer,获取匿名共享内存片段的地址。

二者都是匿名共享内存,那么要如何识别该用哪个呢?进程要创建一块匿名共享内存,如果是要和其他进程共享一整块内存,那么它可以使用IMemoryHeap接口来创建。如果只希望与其他进程共享其中的一部分,那么可以使用IMemory类来创建。

MemoryBase实现了’IMemory’接口,这与它的三个成员变量有非常大的关系:

  • mHeap,是指向类型IMemoryHeap的强指针,描述宿主匿名共享内存的。
  • mOffset,表示这一小块匿名共享内存在宿主匿名共享内存中的偏移值。
  • mSize,表示这一小块匿名共享内存的大小。

5.3 Java API

在Android API中,存在一个类:android.os.MemoryFile,它的文档清晰地显示,其是对ashmem驱动的封装,基于共享内存,并提供可供系统内核回收机制。使用MemoryFile,可以创建匿名共享内存,在不同的Android应用程序之间进行共享。这些内容来自文档,看起来非常美妙,但实际上并不是这样,因为在应用开发中,这个类几乎无法使用。

之所以这么讲,是因为尽管调用如下构造函数,在Server端创建匿名共享内存:

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

但是,没有任何途径在Client端构造MemoryFile,根据《Android系统源代码情景分析》一书的讲解,分明还存在另外一个构造函数,供Client端使用。

查看Git提交记录,我们找到了2010/04/14的一次修改,revision为a006b47298539d89dc7a06b54c070cb3e986352a,它将如下一个构造函数给移除了:

/**
 * Creates a reference to an existing memory file. Changes to the original file
 * will be available through this reference.
 * Calls to {@link #allowPurging(boolean)} on the returned MemoryFile will fail.
 *
 * @param fd File descriptor for an existing memory file, as returned by
 *        {@link #getFileDescriptor()}. This file descriptor will be closed
 *        by {@link #close()}.
 * @param length Length of the memory file in bytes.
 * @param mode File mode. Currently only "r" for read-only access is supported.
 * @throws NullPointerException if <code>fd</code> is null.
 * @throws IOException If <code>fd</code> does not refer to an existing memory file,
 *         or if the file mode of the existing memory file is more restrictive
 *         than <code>mode</code>.
 *
 * @hide
 */
public MemoryFile(FileDescriptor fd, int length, String mode) throws IOException {
    // ... ...
}

基于上述变动可以看出,MemoryFile基本处于被废弃的状态,不用说应用开发用不了,即便是Android Frameworks开发,这个类也是被阉割了,用处不大,只在几处小地方有露脸。

但是这并不意味着应用开发中,就不能使用匿名共享内存了,比如Faccebook推出的fresco,其使用了ashmem来处理图片缓存 1

Android has another region of memory, called ashmem. This operates much like the native heap, but has additional system calls. Android can “unpin” the memory rather than freeing it. This is a lazy free; the memory is freed only if the system actually needs more memory. When Android “pins” the memory back, old data will still be there if it hasn’t been freed.

6 Reference

  1. include/linux/fs.h
  2. drivers/staging/android/ashmem.c
  3. drivers/staging/android/binder.c
  4. drivers/staging/android/uapi/binder.h

Leave a comment

Your comment