前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android音频系统-Ashmem

Android音频系统-Ashmem

原创
作者头像
飞翔的小小小企鹅
发布2023-12-08 13:21:59
2250
发布2023-12-08 13:21:59
举报
文章被收录于专栏:Android开发分析Android开发分析

之前负责过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: initialized\n");
    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] != '\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;
    }
    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]这块内存进行读写,会重新触发缺页异常,系统又重新分配物理内存给这块区域。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 驱动装载
  • 2. 驱动注册
  • 3. 和应用程序的交互
    • 3.1 open Ashmem
      • 3.2 mmap
        • 3.3 ioctl
          • 3.4 pin
            • 3.5 unpin
              • 3.6 close
              • 4. 驱动卸载
              • 5. ashmem_shrink
              • 6. 举个栗子
              相关产品与服务
              轻量应用服务器
              轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、Web应用、小程序/小游戏、游戏服、电商应用、云盘/图床和开发测试环境,相比普通云服务器更加简单易用且更贴近应用,以套餐形式整体售卖云资源并提供高带宽流量包,将热门软件打包实现一键构建应用,提供极简上云体验。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档