Android音频系统-Ashmem

2023-12-08 13:21:59 浏览数 (1)

之前负责过QQ音乐Android版的播放功能,对于Android音频系统有过一些了解,因此将这些内容整理成文。本文是Android音频系统的基础篇,主要介绍了匿名内存内部实现以及对外的接口。下篇文章将介绍Ashmem对外提供的接口以及MemoryBase MemoryHeapBase实现进程间共享内存的原理。

Ashmem,全名Anonymous Shared Memory。是Android提供的一种内存管理机制,基于Linux Slab实现了一套内存分配/管理/释放的功能,以驱动的形式运行在内核空间,提供了Native和Java接口供应用程序使用。代码位于:

代码语言:javascript复制
# 驱动代码
ashmem.h
ashmem.c

Ashmem使用到了Linux Slab机制,SLab是linux中的一种内存分配机制,其工作对象是经常分配并释放的对象,如进程描述符,这些对象的大小一般比较小,频繁申请和释放会造成内存碎片,而且频繁的系统调用也比较慢。Slab提供了一种缓存机制,针对同类对象,统一缓存,每当要申请这样一个对象,Slab分配器就从一个Slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给系统,从而避免频繁的系统调用,并减少这些内存碎片。类似于Java中为减少频繁创建/销毁对象而造成频繁GC的对象复用。

Ashmem用到的Slab API 如下:

代码语言:javascript复制
kmem_cache_create:创建一块新缓存,此时并没有分配任何内存
kmem_cache_alloc:从一个缓存中分配一个对象
kmem_cache_free:将一个对象释放回缓存
kmem_cache_destroy:销毁缓存

实现一个驱动程序,一般需要经过以下几步:

  1. 驱动装载,通过调用module_init实现
  2. 注册驱动程序,一般在初始化时调用misc_register或者 register_chrdev实现,注册完成后,自动生成设备文件
  3. 应用程序打开对应设备文件,并调用open/ioctl/write/release等函数和驱动实现交互
  4. 驱动卸载,通过调用module_exit实现

本文将结合上述四个步骤来介绍Ashmem。

1. 驱动装载

驱动装载函数module_init的原型是:

代码语言:javascript复制
#define module_init(initfn) 
static inline initcall_t __inittest(void) 
{ return initfn; } 

需要传入函数指针用来执行实际的初始化操作,Ashmem中调用如下:

代码语言:javascript复制
module_init(ashmem_init);

下面分析下ashmem_init的函数实现:

代码语言:javascript复制
static int __init ashmem_init(void)
{
    int ret;
    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);
    register_shrinker(&ashmem_shrinker);
    printk(KERN_INFO "ashmem: initializedn");
    return 0;
}

ashmem_init函数主要实现了以下内容:

  1. 调用kmem_cache_create为struct ashmem_area创建cache节点,后续所有的ashmem_area内存分配都与该cache节点有关联
  2. 调用kmem_cache_create为struct ashmem_range创建cache节点,后续所有的ashmem_range内存分配都与该cache节点有关联
  3. 调用misc_register注册该驱动程序
  4. 调用register_shrinker,用于在内存不足时进行内存释放

2. 驱动注册

驱动注册调用了函数misc_register(&ashmem_misc)。ashmem_misc的类型是file_operation。Linux内核为驱动定义了一个结构体,file_operation,其中包含了一系列函数指针,驱动可以实现一部分函数指针。file_operation把系统调用和驱动程序关联起来的关键数据结构。 内核中关于file_operations的结构体如下:

代码语言:javascript复制
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);  
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    //省略
};

Ashmem的file_operations结构体定义如下(注意,每个Android版本Ashmem实现的函数不一定相同):

代码语言:javascript复制
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,
    .compat_ioctl = ashmem_ioctl,
};

这里定义的函数何时被调用到呢? Ashmem的设备节点是dev/ashmem?,假设应用层有如下代码:

代码语言:javascript复制
fd = open( "/dev/ashmem ",O_RDWR);

应用层调用open函数,首先会发出open系统调用,然后进入内核,调用sys_open函数,打开文件系统中的/dev/ashmem文件,读取其文件属性,如果是设备文件,就调用Linux内核中的设备管理部分,根据其属性的设备号,查找内核中相关联的file_operations,最终找到定义的 ashmem_open函数。

Ashmem的核心操作pin/unpin均通过ioctl实现(ioctl一般用于驱动的参数设置和获取),最终调用到ashmem_ioctl。

3. 和应用程序的交互

应用程序使用Ashmem的一般用法是:

  1. open Ashmem
  2. mmap
  3. ioctl
  4. pin/unpin
  5. close Ashmem

以下章节分别从上述几个步骤加以说明。

3.1 open Ashmem

Ashmem中定义了ashmem_area结构体,代表一块匿名内部区域,其中unpinned_list表示该区域所对应的所有ashmem_range,定义如下:

代码语言:javascript复制
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 prot_mask;     /* allowed prot bits, as vm_flags */
};

ashmem_range结构体代表一块被unpin的内存区域,定义如下:

代码语言:javascript复制
struct ashmem_range {
    struct list_head lru;        /* entry in LRU list */
    struct list_head unpinned;    /* entry in its area's unpinned list */
    struct ashmem_area *asma;    /* associated area */
    size_t pgstart;            /* starting page, inclusive */
    size_t pgend;            /* ending page, inclusive */
    unsigned int purged;        /* ASHMEM_NOT or ASHMEM_WAS_PURGED */
};

这里采用Linux内核链表,初次接触有些晦涩难懂,如有不适者请服用 Linux内核链表介绍。 另外有全局变量ashmem_lru_list,以Lru的算法存储,存储所有的unpinned ashmem_range,用于在内存紧张时按照Lru释放部分ashmem_range以回收内存。 最终的数据结构为:

代码语言:javascript复制
ashmem_lru_list:全局Lru算法保存所有unpinned range,关联到ashmem_range.lru
ashmem_area.unpinned_list:该区域所有unpinned range,关联到ashmem_range.unpinned

每一次打开Ashmem设备节点,都会有一个与之对应的ashmem_area结构体被创建,并关联到File的private_data,这样后续的Ashmem调用就能通过private_data获取到对应的ashmem_area,代码如下:

代码语言:javascript复制
static int ashmem_open(struct inode *inode, struct file *file)
{
    //省略
    ret = generic_file_open(inode, file);
    asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
    //初始化链表,这个链表的内容是一系列ashmem_range
    INIT_LIST_HEAD(&asma->unpinned_list);
    memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
    asma->prot_mask = PROT_MASK;
    //保存ashmem_area到private_data,类似于jni编程中的native引用保存方式
    file->private_data = asma;
    return 0;
}

3.2 mmap

在应用层调用mmap时,Ashmem的ashmem_mmap会被调用到,代码如下:

代码语言:javascript复制
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] != '')
            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;
    }
    if (vma->vm_flags & VM_SHARED)
        shmem_set_file(vma, asma->file);

    //省略
    return ret;
}

如上所示,主要执行了shmem_file_setup函数。shmem_file_setup函数用来在tmfps系统中创建一个临时文件,并将临时文件保存在asma->file中,后续Ashmem就可以通过asma->file来访问该文件了。shmem_set_file函数是Android对Linux的扩展,代码如下:

代码语言:javascript复制
void shmem_set_file(struct vm_area_struct *vma, struct file *file)
{
    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_vm_ops;
    vma->vm_flags |= VM_CAN_NONLINEAR;
}

vm_area_struct描述的是一段连续的、具有相同访问属性的虚存空间,ashmem_mmap中vma是由内核传过来的,这里将vma->vm_file 和上一步在tmfps系统中创建的临时文件关联在一起。后续对于这块内存区域的操作相当于对这个临时文件的操作。

3.3 ioctl

ioctl函数本来是用来更改驱动的配置,Ashmem对ioctl函数进行了扩展,除了可以更改配置,还能完成业务调用(pin/unpin),代码如下:

代码语言:javascript复制
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_PIN:
    case ASHMEM_UNPIN:
    case ASHMEM_GET_PIN_STATUS:
        //pin、unpin
        ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
        break;
    return ret;
}

如上所示,ashmem_pin_unpin函数在ioctl中被调用。ashmem_pin_unpin代码如下:

代码语言:javascript复制
static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
                void __user *p)
{
    //省略参数检查
    //页对齐
    pgstart = pin.offset / PAGE_SIZE;
    pgend = pgstart   (pin.len / PAGE_SIZE) - 1;

    mutex_lock(&ashmem_mutex);
    switch (cmd) {
    case ASHMEM_PIN:
        //pin区域[pgstart,pgend]
        ret = ashmem_pin(asma, pgstart, pgend);
        break;
    case ASHMEM_UNPIN:
         //unpin区域[pgstart,pgend]
        ret = ashmem_unpin(asma, pgstart, pgend);
        break;
    }

    mutex_unlock(&ashmem_mutex);
    return ret;
}

如上所示,ashmem_pin_unpin函数中再根据cmd来决定是调用ashmem_pin来pin某块区域,还是调用ashmem_unpin来unpin某块区域。

3.4 pin

当使用Ashmem分配一段内存空间后,默认都是pin状态。当某些内存不再被使用时,可以将这块内存unpin掉,unpin后,内核可以将这块内存回收以作他用。这里内核只是将这块内存对应的物理页面回收,并不会影响到后续对这块内存的访问,因为unpin并未改变已经nmap的地址控件,后续再次访问这块内存时,系统由于有缺页机制将再次分配物理页面给这块内存。当然,unpin后,可以再pin。pin只针对处于unpinned状态的内存有效。pin的代码如下:

代码语言:javascript复制
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
    //省略
    list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
        //如果要pin的区间大于range,则什么也不用做,这说明了unpinned_list是从大到小排序的
        if (range_before_page(range, pgstart))
            break;

        if (page_range_in_range(range, pgstart, pgend)) {
            ret |= range->purged;

            //情况1
            if (page_range_subsumes_range(range, pgstart, pgend)) {
                range_del(range);
                continue;
            }

            //情况2
            if (range->pgstart >= pgstart) {
                range_shrink(range, pgend   1, range->pgend);
                continue;
            }

            //情况3
            if (range->pgend <= pgend) {
                range_shrink(range, range->pgstart, pgstart-1);
                continue;
            }

            //情况4
            range_alloc(asma, range, range->purged,
                    pgend   1, range->pgend);
            range_shrink(range, range->pgstart, pgstart - 1);
            break;
        }
    }

这个函数的主体就是在遍历asma->unpinned_list列表,从中查找当前处于unpinned状态的内存块是否与将要pin的内存块[pgstart, pgend]相交,如果相交,则要执行踢出操作(range_del函数),或者调整pgstart和pgend的大小(range_shrink),或者分割之前的range(range_alloc range_shrink)。

相交分为以上四种情况:

  1. 情况1:range全部被包含在要pin的区域,直接把整块range pin,调用range_del删除该range即可
  2. 情况2:需要pin的区域是[range->pgstart,pgend],因此unpin的区域被调整成[pgend 1,range->pgend]
  3. 情况3:需要pin的区域是[pgstart,range->pgend],因此unpin的区域被调整成[range->pgstart,pgstart-1]
  4. 情况4:需要pin的区域是[pgstart,pgend],unpin的区域被分割成[range->pgstart,pgstart-1]和[pgend 1,range->pgend],分别对应以下代码

range_alloc(asma, range, range->purged,pgend 1, range->pgend); range_shrink(range, range->pgstart, pgstart - 1);

3.5 unpin

函数代码如下:

代码语言:javascript复制
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
        //省略
restart:
    list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
        //如果要unpin的区间比当前区间大,则直接创建新区间
        if (range_before_page(range, pgstart))
            break;

        //情况4
        if (page_range_subsumed_by_range(range, pgstart, pgend))
            return 0;
        //情况1、2、3
        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);
}

这个函数的主体就是在遍历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状态的内存块发生相交。所以这里使用了goto restart来控制。

同样针对上述四种相交情况进行讨论:

  1. 情况1、2、3:均需要与range进行合并,删除旧range,生成新range
  2. 情况4:range完整包含需要unpin的区间,不需要调整

3.6 close

从全局的Lru链表ashmem_lru_list中删除该区域所对应的unpin range(这里如果不从全局链表中删除,会导致该缓存被释放后,后续ashmem_shrink回收内存时再次释放这些unpin的range),并释放该区域的缓存。

4. 驱动卸载

调用misc_deregister取消驱动注册,并调用kmem_cache_destroy删除Slab缓存。

5. ashmem_shrink

Linux内核会定期/内存紧缺时进行内存回收,回收的内存就包括Slab缓存,只要调用register_shrinker注册过shrinker,在Slab缓存回收时都会被调用到。 看下ashmem_shrink如何工作:

代码语言:javascript复制
static int ashmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
    //省略
    list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
        struct inode *inode = range->asma->file->f_dentry->d_inode;
        loff_t start = range->pgstart * PAGE_SIZE;
        loff_t end = (range->pgend   1) * PAGE_SIZE - 1;

        vm_truncate_range(inode, start, end);
        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;
}

遍历全局ashmem_lru_list链表,调用vm_truncate_range回收内存,并调用lru_del从全局ashmem_lru_list中移除该range,直到回收的内存页数等于nr_to_scan,或者已经没有内存可以回收为止。 同时,Android的LowMemoryKiller机制也调用register_shrinker注册了shrinker,在内核定期检查/内存不足时选择性杀死某些进程来回收内存。

6. 举个栗子

写了这么多,这里以一个栗子来说明整个Ashmem的工作流程:

代码语言:javascript复制
 int fd = ashmem_create_region("test", 1024*1024);
 int *base = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0);
 env->GetByteArrayRegion(buffer, 0, 4*1024, (jbyte *)base );
 ashmem_unpin_region(fd, 0, 4*1024))

第一步:打开Ashmem,大小是1M。 第二步:调用mmap进行映射,调用完毕后,系统会通过tmpfs创建一个1M的临时文件,在该进程分配了1M的虚拟空间,基地址是base。 第三步:JNI方法,表示要把buffer数组中的4096字节内容拷贝到base基地址的内存区域,由于此时base基地址对应的虚拟内存空间并没有映射到真实的物理内存,会触发却页异常,缺页异常程序处理后,为该进程分配了4096字节(1页)的物理内存,并映射到到[base,base 4096]的虚拟地址空间。此时,这段匿名内存分配了1页的物理内存。 第四步:如果不再需要上述拷贝的内容,就调用ashmem_unpin_region unpin这块区域。上文介绍过,ashmem_unpin_region最终会触发Ashmem的ashmem_unpin,于是range[0,4096]被加入到全局的ashmem_lru_list链表。此时如果系统内存不足触发内存回收/周期性回收,会执行到上文的ashmem_shrink,于是之前分配的这1页物理内存被回收。注意此时这段匿名内存不再占有物理内存,达到了系统内存紧张时内存释放的目的。 如果需要对[base,base 4096]这块内存进行读写,会重新触发缺页异常,系统又重新分配物理内存给这块区域。

0 人点赞