匿名共享内存
匿名共享内存的实现是已Ashmem驱动程序为基础所构建起来的一套方案,基于linux的临时文件系统tmpfs.
ashmem系统大概分成三层,如下:
最下方的是kernel层,也就是我们即将介绍的ashmem驱动程序,在启动时它会创建一个/dev/ashmem的设备文件。
而上层的cutils库就通过文件访问操作open,ioctl来访问驱动程序。另外cutils主要提供了三个函数ashmem_create_region,ashmen_pin_region和ashmem_unpin_region。
最后还有一层应用层的封装,我们实际使用的就是这层提供的接口。
Android会使用一个文件描述符来表示一块被标记的共享内存,并且通过binder进程间通信的BINDER_TYPE_FD命令来传递文件描述符。
Ashmem 驱动程序
ashmem驱动程序定义在内核中,在最新的3.18内核中可能会涉及到三个文件
基本数据结构
驱动中有三个比较重要的数据结构。
struct ashmem_area {
char name[ASHMEM_FULL_NAME_LEN];
struct list_head unpinned_list;
struct file *file;
size_t size;
unsigned long prot_mask;
};
结构体 ashmem_area 用来描述一块匿名共享内存
成员变量 name 表示这块匿名共享内存的名字(吐槽:说好的匿名呢?),它同时会被写入到文件 /proc//maps中(pid表示创建这块匿名共享内存的进程号PID)。
#define ASHMEM_NAME_PREFIX "dev/ashmem/"
#define ASHMEM_NAME_PREFIX_LEN (sizeof(ASHMEM_NAME_PREFIX) - 1)
#define ASHMEM_FULL_NAME_LEN (ASHMEM_NAME_LEN + ASHMEM_NAME_PREFIX_LEN)
每个匿名共享内存名字都已dev/ashmem为前缀,在创建时如果没有指定名字,就会默认使用
一块匿名共享内存可以动态划分若干个小块,当这些小块的内存处于解锁状态,就会被添加到解锁内存块列表中,也就是上面的 unpinned_list 中。
每一块匿名共享内存都会在临时文件系统tmpfs中对应一个文件,也就是file,文件的大小 size,就是这块匿名共享内存的大小。
prot_mask是访问保护位,默认是
PROT_EXEC 表示可执行,依次是可读,可写。
struct ashmem_range {
struct list_head lru;
struct list_head unpinned;
struct ashmem_area *asma;
size_t pgstart;
size_t pgend;
unsigned int purged;
};
ashmem_range就是刚才所说的解锁内存块。
asma表示宿主匿名共享内存(匿名共享内存被分成一个个小块,它就是这些小块的宿主),通过unpinned链如到宿主的unpinned_list中。
有一个全局列表ashmem_lru_list,定义如下
他是一个最近最少使用列表,当内存不足时,内存回收系统会按照最近最少使用原则回收该列表中的内存。
而ashmem_range的lru字段就是用来将他链如这个ashmem_lru_list的。
pgstart和pgend用来表示这块内存块的开始地址和结束地址。
如果ashmem_range表示的内存已经被回收,那么purged的值就是ASHMEM_WAS_PRUGED否则就是ASHMEM_NOT_PRUGED。
struct ashmem_pin {
__u32 offset;/* offset into region, in bytes, page-aligned */
__u32 len;/* length forward from offset, in bytes, page-aligned */
};
ashmem_pin是驱动程序定义的IO控制命令ASHMEM_PIN和ASHMEM_UNPIN的参数,用来描述一小块内存即将被上锁或者解锁。offset表示这块小内存在整个匿名共享内存中的偏移值,而len表示这个小块的长度。
启动ashmem驱动
ashmem的驱动是在ashmem_init中进行初始化的
static struct kmem_cache *ashmem_area_cachep __read_mostly;
static struct kmem_cache *ashmem_range_cachep __read_mostly;
static int __init ashmem_init(void)
{
int ret;
//创建一个使用slap缓存 用于分配 ashmem_area 的分配器
ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
sizeof(struct ashmem_area),
0, 0, NULL);
if (unlikely(!ashmem_area_cachep)) {
pr_err("failed to create slab cache\n");
return -ENOMEM;
}
//创建一个使用slap缓存 用于分配 ashmem_range 的分配器
ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
sizeof(struct ashmem_range),
0, 0, NULL);
if (unlikely(!ashmem_range_cachep)) {
pr_err("failed to create slab cache\n");
return -ENOMEM;
}
//注册匿名共享内存设备
ret = misc_register(&ashmem_misc);
if (unlikely(ret)) {
pr_err("failed to register misc device!\n");
return ret;
}
//向内存管理注册一个内存回收函数,当系统内存不足时,所有使用register_shrinker注册的函数都会被调用。
register_shrinker(&ashmem_shrinker);
pr_info("initialized\n");
return 0;
}
PS:这里或许你需要一些slab缓存的知识,主要用来管理内存已加快内存分配速度的,这里不是我们介绍的东西。它是linux中的一种机制。
从上面这么一看的话,init方法还算清晰,其中 匿名内存设备是一个misc设备类型,所以它使用一个miscdevice类型的结构体结构体ashmem_misc进行注册,定义如下:
static struct miscdevice ashmem_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "ashmem",
.fops = &ashmem_fops,
};
注册过程我们省略了,总之这个折标叫做 /dev/ashmem,ashmem_fops表示其操作方法列表。
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,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_ashmem_ioctl,
#endif
};
匿名共享内存设备打开过程
从上所知,当我们需要使用匿名共享内存时,我们会先打开匿名共享内存的设备文件,ashmen_open函数用来打开设备文件。
static int ashmem_open(struct inode *inode, struct file *file)
{
struct ashmem_area *asma;
int ret;
//打开设备文件/dev/ashmem
ret = generic_file_open(inode, file);
if (unlikely(ret))
return ret;
//创建一个共享内存结构体
asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
if (unlikely(!asma))
return -ENOMEM;
//初始化结构体中的 unpinned_list列表
INIT_LIST_HEAD(&asma->unpinned_list);
//对这个匿名共享内存命名
memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
//设置访问保护位
asma->prot_mask = PROT_MASK;
//将初始化成功的asma结构体放入设备文件的private_data字段。
file->private_data = asma;
return 0;
}
我们这里命名是使用了默认的名字 dev/ashmem,我们可以通过ioctl 命令 ASHMEM_SET_NAME来修改名字。
ashmem_ioctl方法用来响应ioctl命令。
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:
//最终调用set_name方法
ret = set_name(asma, (void __user *) arg);
break;
...
}
return ret;
}
static int set_name(struct ashmem_area *asma, void __user *name)
{
int len;
int ret = 0;
char local_name[ASHMEM_NAME_LEN];
//从参数中提取name,并且放入local_name中
len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
if (len < 0)
return len;
if (len == ASHMEM_NAME_LEN)
local_name[ASHMEM_NAME_LEN - 1] = '\0';
//
mutex_lock(&ashmem_mutex);
/* cannot change an existing mapping's name */
// 检查是否已经在tmpfs中创建临时文件(文件名就是这个匿名共享内存的名字),如果已经创建了,那么是不允许修改的
if (unlikely(asma->file))
ret = -EINVAL;
else
strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);
mutex_unlock(&ashmem_mutex);
return ret;
}
匿名共享内存设备文件内存映射过程
当应用程序打开设备后,都会调用mmap将设备文件映射到自己的进程地址空间,这是ashmem_mmap函数就会被调用。
static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
struct ashmem_area *asma = file->private_data;
int ret = 0;
mutex_lock(&ashmem_mutex);
/* user needs to SET_SIZE before mapping */
//用户在使用mmap进行映射之前需要先调用set_size方法设置大小,否则直接失败
if (unlikely(!asma->size)) {
ret = -EINVAL;
goto out;
}
/* requested protection bits must match our allowed protection mask */
//检测需要映射的虚拟内存vma的保护权限是否超过了匿名共享内存的保护权限
//比如vma除了允许读之外还允许写,但是asma只允许读,这就算超过了,会mmap失败,直接返回。
if (unlikely((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask)) &
calc_vm_prot_bits(PROT_MASK))) {
ret = -EPERM;
goto out;
}
vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
//如果还没有创建过临时文件,那么接下去就会创建一个临时文件
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 */
//在tmpfs中创建一个临时文件。
vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
if (unlikely(IS_ERR(vmfile))) {
ret = PTR_ERR(vmfile);
goto out;
}
asma->file = vmfile;
}
get_file(asma->file);
//判断映射虚拟内存vma是否需要在不同进程间共享,
if (vma->vm_flags & VM_SHARED)
//设置映射文件和内存操作方法
shmem_set_file(vma, asma->file);
else {
if (vma->vm_file)
fput(vma->vm_file);
vma->vm_file = asma->file;
}
out:
mutex_unlock(&ashmem_mutex);
return ret;
}
匿名共享内存锁定解锁操作
匿名共享内存在创建时是处于锁定状态的,用户可以根据需要将其切分成小块,当小块不再使用了之后,就通过将其解锁放入解锁内存列表。解锁列表内的内存可能在内存紧张时被回收,如果还没有被回收,那么可以通过锁定重新投入使用。
ashmem驱动通过两个ioctl命令来控制解锁和锁定,分别是ASHMEM_PIN 和 ASHMEM_UNPIN。他们需要一个ashmem_pin的结构体,前面已经有所介绍。
ashmem_ioctl方法中,这两个命令最终都会调用方法ashmem_pin_unpin来处理
static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
void __user *p)
{
struct ashmem_pin pin;
size_t pgstart, pgend;
int ret = -EINVAL;
if (unlikely(!asma->file))
return -EINVAL;
//获取参数存入 ashmem_pin
if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
return -EFAULT;
//检测锁定区域大小,如果是0,表示从pin.offset开始到共享内存末尾的所有空间
/* per custom, you can pass zero for len to mean "everything onward" */
if (!pin.len)
pin.len = PAGE_ALIGN(asma->size) - pin.offset;
//偏移地址和大小页对齐检测
if (unlikely((pin.offset | pin.len) & ~PAGE_MASK))
return -EINVAL;
//需要操作内存末尾地址是否超过无符号整数??
if (unlikely(((__u32) -1) - pin.offset < pin.len))
return -EINVAL;
//需要操作内存地址是否超过匿名共享内存末尾地址
if (unlikely(PAGE_ALIGN(asma->size) < pin.offset + pin.len))
return -EINVAL;
//需要操作内存的页地址
pgstart = pin.offset / PAGE_SIZE;
pgend = pgstart + (pin.len / PAGE_SIZE) - 1;
mutex_lock(&ashmem_mutex);
switch (cmd) {
case ASHMEM_PIN:
ret = ashmem_pin(asma, pgstart, pgend);
break;
case ASHMEM_UNPIN:
ret = ashmem_unpin(asma, pgstart, pgend);
break;
case ASHMEM_GET_PIN_STATUS:
ret = ashmem_get_pin_status(asma, pgstart, pgend);
break;
}
mutex_unlock(&ashmem_mutex);
return ret;
}
实际上这个方法最主要进行的是合法性检测,通过之后会再去分发命令。
ashmem_unpin用来解锁一块内存地址
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
unsigned int purged = ASHMEM_NOT_PURGED;
restart:
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
/* short circuit: this is our insertion point */
if (range_before_page(range, pgstart))
break;
/*
* The user can ask us to unpin pages that are already entirely
* or partially pinned. We handle those two cases here.
*/
if (page_range_subsumed_by_range(range, pgstart, pgend))
return 0;
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);
}
一块匿名共享内存中的所有解锁地址块都是按地址值从大到小的顺序保存在unpinned_list中的。在解锁一块内存块是,他需要先检测这块内存是否和已经解锁的内存块有地址相交的情况,如果存在相交,需要对齐进行合并,合并后存入unpinned_list中。
下面有个示意图,range表示已解锁内存块,pgstart和pgend标记了即将解锁内存块地址。
A,B,C三种情况需要对地址进行合并,D情况下直接忽略当前操作(因为已经标记了),E表示开开心心释放当前标记的内存块。
我们使用range_del和range_alloc 来从目标匿名共享内存asma中添加和删除一块解锁状态的内存块。
static void range_del(struct ashmem_range *range)
{
//将自己从宿主的unpinned_list中删除
list_del(&range->unpinned);
//如果处于全局列表ashmem_lru_list中
if (range_on_lru(range))
lru_del(range); //从ashmem_lru_list中删除当前解锁内存块
kmem_cache_free(ashmem_range_cachep, range);//将结构体range占用的内存重新放入slab中
}
static int range_alloc(struct ashmem_area *asma,
struct ashmem_range *prev_range, unsigned int purged,
size_t start, size_t end)
{
struct ashmem_range *range;
//创建一个range结构体
range = kmem_cache_zalloc(ashmem_range_cachep, GFP_KERNEL);
if (unlikely(!range))
return -ENOMEM;
range->asma = asma;
range->pgstart = start;
range->pgend = end;
range->purged = purged;
//添加到宿主unpinned_list中
list_add_tail(&range->unpinned, &prev_range->unpinned);
//如果还没有被系统回收
if (range_on_lru(range))
lru_add(range);//放入全局变量ashmem_lru_list中
return 0;
}
接下来就是ashmem_pin方法了,用来锁定一块内存。
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
int ret = ASHMEM_NOT_PURGED;
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
/* moved past last applicable page; we can short circuit */
if (range_before_page(range, pgstart))
break;
/*
* The user can ask us to pin pages that span multiple ranges,
* or to pin pages that aren't even unpinned, so this is messy.
*
* Four cases:
* 1. The requested range subsumes an existing range, so we
* just remove the entire matching range.
* 2. The requested range overlaps the start of an existing
* range, so we just update that range.
* 3. The requested range overlaps the end of an existing
* range, so we just update that range.
* 4. The requested range punches a hole in an existing range,
* so we have to update one side of the range and then
* create a new range for the other side.
*/
if (page_range_in_range(range, pgstart, pgend)) {
ret |= range->purged;
/* Case #1: Easy. Just nuke the whole thing. */
if (page_range_subsumes_range(range, pgstart, pgend)) {
range_del(range);
continue;
}
/* Case #2: We overlap from the start, so adjust it */
if (range->pgstart >= pgstart) {
range_shrink(range, pgend + 1, range->pgend);
continue;
}
/* Case #3: We overlap from the rear, so adjust it */
if (range->pgend <= pgend) {
range_shrink(range, range->pgstart, pgstart-1);
continue;
}
/*
* Case #4: We eat a chunk out of the middle. A bit
* more complicated, we allocate a new range for the
* second half and adjust the first chunk's endpoint.
*/
range_alloc(asma, range, range->purged,
pgend + 1, range->pgend);
range_shrink(range, range->pgstart, pgstart - 1);
break;
}
}
return ret;
}
一块匿名共享内存,最开始一定是处于锁定状态的,它被解锁之后会放入到unpinned_list中。该方法的作用就是遍历匿名共享内存中的unpinned_list,寻找和指定的pgstart和pgend相交或者包含的内存块,就会对这个内存块进行重新锁定。
这会存在如上的四种情况。
A情况处理相对简单,表示需要解锁的内存包含了当前内存块,只需要调用range_del移除当前内存块就行了。
B,C两种情况比较相似,但是处理方案可能和你想象得有些不一样,并是不删除range,重新分配range,添加入解锁列表中。而是直接修改range的范围(也就是修改range的大小)。
D情况就比较复杂了,需要解锁的内存包含在当前内存块中,这个时候就不能像B,C那样直接修改内存块大小了,而是需要先插入一块从pgend+1到range末尾的内存块,然后再将range的末尾改成pgstart。
其中方法range_shrink方法的作用就是修改当前range的大小。
static inline void range_shrink(struct ashmem_range *range,
size_t start, size_t end)
{
size_t pre = range_size(range);
range->pgstart = start;
range->pgend = end;
if (range_on_lru(range))
lru_count -= pre - range_size(range);
}