Ashmem 是什么?
Ashmem(Anonymous Shared Memory 匿名共享内存),是在 Android 的内存管理中提供的一种机制。它基于mmap系统调用,不同的进程可以将同一段物理内存空间映射到各自的虚拟空间,从而实现共享。
mmap机制
mmap系统调用是将一个打开的文件映射到进程的用户空间,mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
mmap 函数原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr: 指定为文件描述符fd应被映射到的进程空间的起始地址。它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。一般默认为NULL
length: 是映射到调用进程地址空间中的字节数,从被映射文件开头offset个字节处开始算
prot: 负责保护内存映射区的保护。常用值是代表读写访问的PROT_READ | PROT_WRITE.当然还包括数据的执行(PROT_EXEC)、数据不可访问(PROT_NONE)
flag: flags常用值有MAP_SHARED或MAP_PRIVATE这两个标志必须选一个,并可以选上MAP_FIXED。如果指定了,那么调用进程对被映射数据所做的修改只对该进程可见,而不该变其底层支撑对象。如果指定了,那么调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象
fd: 参数fd为映射文件的描述符,offset为文件的起点,默认为0
offset: 偏移量
ashmem 在 mmap 上的改进
ashmem通过内核驱动提供了辅助内核的内存回收算法机制(pin/unpin)
什么是pin和unpin呢?
具体来讲,就是当你使用Ashmem分配了一块内存,但是其中某些部分却不会被使用时,那么就可以将这块内存unpin掉。unpin后,内核可以将它对应的物理页面回收,以作他用。你也不用担心进程无法对unpin掉的内存进行再次访问,因为回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经 mmap的地址空间。
Ashmem 的定义
- 我们先来看一下部分 ashmem 实现的头文件(ashmem.h)
1 | #define ASHMEM_NAME_LEN 256 |
Ashmem 是怎么实现的?
下面我们开始按照 Ashmem 的实现代码来看看它是怎么样工作的(ashmem.c)
我们先来看一下两个结构体ashmem_area
和ashmem_range
:
1 | /* |
我们可以看到 ashmem_area
定义了一个内存共享区域,它的生命周期是从文件打开open()
到它被释放release()
,并且支持原子性
1 | /* |
我们看到ashmem_range
的生命周期是从 unpin 到 pin
初始化 - ashmem_init(void)
1 | static int __init ashmem_init(void) |
我们从代码中可以看到初始化函数ashmem_init(void)
主要做了以下几件事:
通过
kmem_cache_create
[^1]创建 ahemem_area 高速缓存通过
kmem_cache_create
创建 ahemem_range 高速缓存通过
misc_register
将 Ashmem 注册为 misc 设备[^2]通过
register_shrinker
注册回收函数
退出 - ashmem_exit(void)
1 | static void __exit ashmem_exit(void) |
我们在代码中看到了所有在退出时所做的操作:
卸载回收函数
unregister_shrinker
卸载设备
misc_deregister
回收两段高速缓存(ashmem_area & ashmem_range)
kmem_cache_destroy
对内存进行分配、释放和回收
我们先看看Ashmem分配内存的流程:
- 打开“/dev/ashmem”文件
- 通过ioctl来设置名称和大小等
- 调用mmap将Ashmem分配的空间映射到进程空间
打开多少次/dev/ashmem设备并mmap,就会获得多少个不同的空间
我们在初始化Ashmem时注册了Ashmem设备,其中包含的相关方法及其作用如下面的代码所示:
1 | static const struct file_operations ashmem_fops = { |
其中,ashmem_open
方法主要是对unpinned列表进行初始化,并将Ashmem分配的地址空间赋给file结构的private_data
,这就排除了进程间共享的可能性。ashmem_release
方法用于将指定的节点的空间从链表中删除并释放掉
ashmem_open
方法
1 | static int ashmem_open(struct inode *inode, struct file *file) |
ashmem_release
方法
1 | static int ashmem_release(struct inode *ignored, struct file *file) |
需要指出的是,当使用list_for_each_entry_safe(pos, n, head,member)
函数时,需要调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos节点的下一个节点的地址,避免因pos节点被释放而造成断链
接下来就是将分配的空间映射到进程空间。在ashmem_mmap
函数中需要指出的是,它借助了Linux内核的shmem_file_setup
(支撑文件)工具,使得我们不需要自己去实现这一复杂的过程。所以ashmem_mmap
的整个实现过程很简单,大家可以参考它的源代码:
1 | static int ashmem_mmap(struct file *file, struct vm_area_struct *vma) |
最后,我们还将分析通过ioctl来pin和unpin某一段映射的空间的实现方式。ashmem_ioctl函数的功能很多,它可以通过其参数cmd来处理不同的操作,包括设置(获取)名称和尺寸、pin/unpin以及获取pin的一些状态。最终对pin/unpin的处理会通过下面这个函数来完成:
1 | static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend) |
最后需要说明:回收函数cache_shrinker同样也参考了Linux内核的slab分配算法用于页面回收的回调函数。具体实现如下:
1 | static int ashmem_shrink(struct shrinker *s, struct shrink_control *sc) |
cache_shrinker
同样先取得了ashmem_mutex
,通过list_for_each_entry_safe
来确保其被安全释放。该方法会被mm/vmscan.c :: shrink_slab
调用,其中参数nr_to_scan
表示有多少个页面对象。如果该参数为0,则表示查询所有的页面对象总数。而“gfp_mask”是一个配置,返回值为被回收之后剩下的页面数量;如果返回-1,则表示由于配置文件(gfp_mask)产生的问题,使得mutex_lock
不能进行安全的死锁
[^1]:kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags,void (*ctor)(void*, struct kmem_cache *, unsigned long))
用于创建 SLAB 高速缓存
[^2]:Minimal instruction set computer
====
本文的版权归作者 罗远航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!