之前负责过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:销毁缓存
实现一个驱动程序,一般需要经过以下几步:
- 驱动装载,通过调用module_init实现
- 注册驱动程序,一般在初始化时调用misc_register或者 register_chrdev实现,注册完成后,自动生成设备文件
- 应用程序打开对应设备文件,并调用open/ioctl/write/release等函数和驱动实现交互
- 驱动卸载,通过调用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函数主要实现了以下内容:
- 调用kmem_cache_create为struct ashmem_area创建cache节点,后续所有的ashmem_area内存分配都与该cache节点有关联
- 调用kmem_cache_create为struct ashmem_range创建cache节点,后续所有的ashmem_range内存分配都与该cache节点有关联
- 调用misc_register注册该驱动程序
- 调用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的一般用法是:
- open Ashmem
- mmap
- ioctl
- pin/unpin
- 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] != '