之前负责过QQ音乐Android版的播放功能,对于Android音频系统有过一些了解,因此将这些内容整理成文。本文是Android音频系统的基础篇,主要介绍了匿名内存内部实现以及对外的接口。下篇文章将介绍Ashmem对外提供的接口以及MemoryBase+MemoryHeapBase实现进程间共享内存的原理。
Ashmem,全名Anonymous Shared Memory。是Android提供的一种内存管理机制,基于Linux Slab实现了一套内存分配/管理/释放的功能,以驱动的形式运行在内核空间,提供了Native和Java接口供应用程序使用。代码位于:
# 驱动代码
ashmem.h
ashmem.c
Ashmem使用到了Linux Slab机制,SLab是linux中的一种内存分配机制,其工作对象是经常分配并释放的对象,如进程描述符,这些对象的大小一般比较小,频繁申请和释放会造成内存碎片,而且频繁的系统调用也比较慢。Slab提供了一种缓存机制,针对同类对象,统一缓存,每当要申请这样一个对象,Slab分配器就从一个Slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给系统,从而避免频繁的系统调用,并减少这些内存碎片。类似于Java中为减少频繁创建/销毁对象而造成频繁GC的对象复用。
Ashmem用到的Slab API 如下:
kmem_cache_create:创建一块新缓存,此时并没有分配任何内存
kmem_cache_alloc:从一个缓存中分配一个对象
kmem_cache_free:将一个对象释放回缓存
kmem_cache_destroy:销毁缓存
实现一个驱动程序,一般需要经过以下几步:
本文将结合上述四个步骤来介绍Ashmem。
驱动装载函数module_init的原型是:
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
需要传入函数指针用来执行实际的初始化操作,Ashmem中调用如下:
module_init(ashmem_init);
下面分析下ashmem_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);
//省略
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函数主要实现了以下内容:
驱动注册调用了函数misc_register(&ashmem_misc)。ashmem_misc的类型是file_operation。Linux内核为驱动定义了一个结构体,file_operation,其中包含了一系列函数指针,驱动可以实现一部分函数指针。file_operation把系统调用和驱动程序关联起来的关键数据结构。 内核中关于file_operations的结构体如下:
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实现的函数不一定相同):
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?,假设应用层有如下代码:
fd = open( "/dev/ashmem ",O_RDWR);
应用层调用open函数,首先会发出open系统调用,然后进入内核,调用sys_open函数,打开文件系统中的/dev/ashmem文件,读取其文件属性,如果是设备文件,就调用Linux内核中的设备管理部分,根据其属性的设备号,查找内核中相关联的file_operations,最终找到定义的 ashmem_open函数。
Ashmem的核心操作pin/unpin均通过ioctl实现(ioctl一般用于驱动的参数设置和获取),最终调用到ashmem_ioctl。
应用程序使用Ashmem的一般用法是:
以下章节分别从上述几个步骤加以说明。
Ashmem中定义了ashmem_area结构体,代表一块匿名内部区域,其中unpinned_list表示该区域所对应的所有ashmem_range,定义如下:
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的内存区域,定义如下:
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以回收内存。 最终的数据结构为:
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,代码如下:
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;
}
在应用层调用mmap时,Ashmem的ashmem_mmap会被调用到,代码如下:
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的扩展,代码如下:
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系统中创建的临时文件关联在一起。后续对于这块内存区域的操作相当于对这个临时文件的操作。
ioctl函数本来是用来更改驱动的配置,Ashmem对ioctl函数进行了扩展,除了可以更改配置,还能完成业务调用(pin/unpin),代码如下:
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代码如下:
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某块区域。
当使用Ashmem分配一段内存空间后,默认都是pin状态。当某些内存不再被使用时,可以将这块内存unpin掉,unpin后,内核可以将这块内存回收以作他用。这里内核只是将这块内存对应的物理页面回收,并不会影响到后续对这块内存的访问,因为unpin并未改变已经nmap的地址控件,后续再次访问这块内存时,系统由于有缺页机制将再次分配物理页面给这块内存。当然,unpin后,可以再pin。pin只针对处于unpinned状态的内存有效。pin的代码如下:
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)。
相交分为以上四种情况:
range_alloc(asma, range, range->purged,pgend + 1, range->pgend); range_shrink(range, range->pgstart, pgstart - 1);
函数代码如下:
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来控制。
同样针对上述四种相交情况进行讨论:
从全局的Lru链表ashmem_lru_list中删除该区域所对应的unpin range(这里如果不从全局链表中删除,会导致该缓存被释放后,后续ashmem_shrink回收内存时再次释放这些unpin的range),并释放该区域的缓存。
调用misc_deregister取消驱动注册,并调用kmem_cache_destroy删除Slab缓存。
Linux内核会定期/内存紧缺时进行内存回收,回收的内存就包括Slab缓存,只要调用register_shrinker注册过shrinker,在Slab缓存回收时都会被调用到。 看下ashmem_shrink如何工作:
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,在内核定期检查/内存不足时选择性杀死某些进程来回收内存。
写了这么多,这里以一个栗子来说明整个Ashmem的工作流程:
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 删除。