Ashmem(匿名内存共享)
Ashmem是Android的内存分配与共享机制,它在dev目录下对应的设备文件为/dev/ashmem,其实现的源文件为:
include/linux/ashmem.h
kernel/mm/ashmem.c
相比于malloc和anonymous/named mmap等传统的内存分配机制,其优势是通过内核驱动提供了辅助内核的内存回收算法机制(pin/unpin)。什么是pin和unpin呢?具体来讲,就是当你使用Ashmem分配了一块内存,但是其中某些部分却不会被使用时,那么就可以将这块内存unpin掉。unpin后,内核可以将它对应的物理页面回收,以作他用。你也不用担心进程无法对unpin掉的内存进行再次访问,因为回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经 mmap的地址空间。下面就来分析Ashmem的内核驱动是如何完成这些功能。
首先,打开其头文件(ashmem.h),可以看到定义了以下一些宏和结构体:
#define ASHMEM_NAME_DEF "dev/ashmem"
//从ASHMEM_PIN返回的值,判断是否需要清楚
#define ASHMEM_NOT_PURGED 0
#define ASHMEM_WAS_PURGED 1
//从ASHMEM_GET_PIN_STATUS返回的值,判断是pin还是unpin
#define ASHMEM_IS_UNPINNED 0
#define ASHMEM_IS_PINNED 1
struct ashmem_pin {
__u32 offset; //在Ashmem区域的偏移量
__u32 len; //从偏移量开始的长度
};
另外一些宏用于设置Ashmem的名称和状态,以及pin和unpin等操作。接下来看一下Ashmem的具体实现,打开(ashmem.c)文件,首先大致预览一下它有哪些功能函数,如图2-1所示。
▲图2-1 Ashmem实现函数列表
可以看到Ashmem是通过以下代码来管理其初始化和退出操作的,我们分别需要实现其初始化函数ashmem_init和退出函数ashmem_exit。
module_init(ashmem_init);
module_exit(ashmem_exit);
ashmem_init的实现很简单,首先,定义一个结构体ashmem_area代表匿名共享内存区;然后,定义一个结构体ashmem_range代表unpinned页面的区域,代码如下:
char name[ASHMEM_FULL_NAME_LEN];/* 用于/proc/pid/maps中的一个标识名称 */
struct list_head unpinned_list; /* 所有的匿名共享内存区列表 */
struct file *file; /* Ashmem所支持的文件 */
size_t size; /* 字节数 */
unsigned long prot_mask; /* vm_flags */
};
struct ashmem_range {
struct list_head lru; /* LRU列表 */
struct list_head unpinned; /* unpinned列表 */
struct ashmem_area *asma; /* ashmem_area结构 */
size_t pgstart; /* 开始页面 */
size_t pgend; /* 结束页面 */
unsigned int purged; /* 是否需要清除(ASHMEM_NOT_PURGED 或者ASHMEM_WAS_PURGED) */
};
ashmem_area的生命周期为文件的open()和release()操作之间,而ashmem_range的生命周期则是从unpin到pin,初始化时首先通过kmem_cache_create创建一个高速缓存cache,所需参数如下:
name 用于/proc/slabinfo文件中来识别这个cache
size 在对应的cache中所创建的对象的长度
align 对象对齐尺寸
flags SLAB标志
ctor 构造函数
如果创建成功,则返回指向cache的指针;如果创建失败,则返回NULL。当针对cache的新的页面分配成功时运行ctor构造函数,然后采用unlikely来对其创建结果进行判断。如果成功,就接着创建ashmem_range的cache(实现原理与ashmem_area一样)。创建完成之后,通过misc_register函数将Ashmem注册为misc设备。这里需要注意,我们对所创建的这些cache都需要进行回收,因此,再紧接着需调用register_shrinker注册回收函数ashmem_shrinker。而从图2-1可以看出,ashmem_shrinker实际上是一个结构体,真正的回收函数是在ashmem_shrinker中定义的ashmem_shrink。到这里,初始化操作则完成了,实现代码如下:
{
int ret;
ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
sizeof(struct ashmem_area),
0, 0, NULL);
if (unlikely(!ashmem_area_cachep)) {
printk(KERN_ERR "ashmem: failed to create slab cache\n");
return -ENOMEM;
}
ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
sizeof(struct ashmem_range),
0, 0, NULL);
if (unlikely(!ashmem_range_cachep)) {
printk(KERN_ERR "ashmem: failed to create slab cache\n");
return -ENOMEM;
}
ret = misc_register(&ashmem_misc);
if (unlikely(ret)) {
printk(KERN_ERR "ashmem: failed to register misc device!\n");
return ret;
}
/* 注册回收函数 */
register_shrinker(&ashmem_shrinker);
printk(KERN_INFO "ashmem: initialized\n");
return 0;
}
当Ashmem退出时,又该执行什么操作呢?下面是Ashmem退出时需要执行的ashmem_exit函数的具体实现:
{
int ret;
/* 卸载回收函数 */
unregister_shrinker(&ashmem_shrinker);
/* 卸载Ashmem设备 */
ret = misc_deregister(&ashmem_misc);
if (unlikely(ret))
printk(KERN_ERR "ashmem: failed to unregister misc device!\n");
/* 卸载cache */
kmem_cache_destroy(ashmem_range_cachep);
kmem_cache_destroy(ashmem_area_cachep);
printk(KERN_INFO "ashmem: unloaded\n");
}
现在我们已经很清楚Ashmem的初始化和退出操作了,接下来我们将分析使用Ashmem对内存进行分配、释放和回收等机制的实现过程。在了解这些实现之前,我们先看看Ashmem分配内存的流程:
1)打开“/dev/ashmem”文件。
2)通过ioctl来设置名称和尺寸等。
3)调用mmap将Ashmem分配的空间映射到进程空间。
由于Ashmem支持pin/unpin机制,所以还可以通过ioctl来pin和unpin某一段映射的空间。Ashmem的作用就是分配空间,打开多少次/dev/ashmem设备并mmap,就会获得多少个不同的空间。
下面来分析如何通过打开设备文件来分配空间,并对空间进行回收。我们在初始化Ashmem时注册了Ashmem设备,其中包含的相关方法及其作用如下面的代码所示。
.owner = THIS_MODULE,
.open = ashmem_open, /* 打开Ashmem */
.release = ashmem_release, /* 释放Ashmem */
.mmap = ashmem_mmap, /* mmap函数 */
.unlocked_ioctl = ashmem_ioctl, /* ioctl */
.compat_ioctl = ashmem_ioctl,
};
static struct miscdevice ashmem_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "ashmem",
.fops = &ashmem_fops,
};
其中,ashmem_open方法主要是对unpinned列表进行初始化,并将Ashmem分配的地址空间赋给file结构的private_data,这就排除了进程间共享的可能性。ashmem_release方法用于将指定的节点的空间从链表中删除并释放掉。需要指出的是,当使用list_for_each_entry_safe(pos, n, head,member)函数时,需要调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos节点的下一个节点的地址,避免因pos节点被释放而造成断链。ashmem_release函数的实现如下:
{
struct ashmem_area *asma = file->private_data;
struct ashmem_range *range, *next;
mutex_lock(&ashmem_mutex);
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned)
range_del(range);/* 删除 */
mutex_unlock(&ashmem_mutex);
if (asma->file)
fput(asma->file);
kmem_cache_free(ashmem_area_cachep, asma);
return 0;
}
接下来就是将分配的空间映射到进程空间。在ashmem_mmap函数中需要指出的是,它借助了Linux内核的shmem_file_setup(支撑文件)工具,使得我们不需要自己去实现这一复杂的过程。所以ashmem_mmap的整个实现过程很简单,大家可以参考它的源代码。最后,我们还将分析通过ioctl来pin和unpin某一段映射的空间的实现方式。ashmem_ioctl函数的功能很多,它可以通过其参数cmd来处理不同的操作,包括设置(获取)名称和尺寸、pin/unpin以及获取pin的一些状态。最终对pin/unpin的处理会通过下面这个函数来完成:
//pin/unpin处理函数
static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,void __user *p)
//如果页面是unpinned和ASHMEM_IS_PINNED,则返回ASHMEM_IS_UNPINNED状态
static int ashmem_get_pin_status(struct ashmem_area *asma, size_t pgstart,size_t pgend)
//unpin 指定区域页面,返回0表示成功
//调用者必须持有ashmem_mutex
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
//pin ashmem指定的区域
//返回是否曾被清除过(即ASHMEM_WAS_PURGED或者ASHMEM_NOT_PURGED)
//调用者必须持有ashmem_mutex
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
最后需要说明:回收函数cache_shrinker同样也参考了Linux内核的slab分配算法用于页面回收的回调函数。具体实现如下:
{
struct ashmem_range *range, *next;
if (nr_to_scan && !(gfp_mask & __GFP_FS))
return -1;
if (!nr_to_scan)
return lru_count;
mutex_lock(&ashmem_mutex);
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;
vmtruncate_range(inode, start, end);
range->purged = ASHMEM_WAS_PURGED;
lru_del(range);
nr_to_scan -= range_size(range);
if (nr_to_scan <= 0)
break;
}
mutex_unlock(&ashmem_mutex);
return lru_count;
}
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不能进行安全的死锁。
Ashmem的源代码实现很简单,注释和代码总共不到700行。主要因为它借助了Linux内核已经有的工具,例如shmem_file_setup(支撑文件)和cache_shrinker(slab分配算法用于页面回收的回调函数)等,实现了高效的内存使用和管理,但是用户需进行额外的ioctl调用来设置名字和大小,以及执行pin和unpin操作等。
到这里,对Ashmem驱动的分析已经结束了。因为我们讲述的是实现的原理和机制,所以没有将代码全部贴出来,建议大家参考源代码进行理解。