技术开发 频道

技术内幕:Android对Linux内核的增强

  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所示。

Ashmem(匿名内存共享)
▲图2-1 Ashmem实现函数列表

  可以看到Ashmem是通过以下代码来管理其初始化和退出操作的,我们分别需要实现其初始化函数ashmem_init和退出函数ashmem_exit。

  module_init(ashmem_init);

  module_exit(ashmem_exit);

  ashmem_init的实现很简单,首先,定义一个结构体ashmem_area代表匿名共享内存区;然后,定义一个结构体ashmem_range代表unpinned页面的区域,代码如下:

struct ashmem_area {
    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。到这里,初始化操作则完成了,实现代码如下:

static int __init ashmem_init(void)
{
    
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函数的具体实现:

static void __exit ashmem_exit(void)
{
    
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设备,其中包含的相关方法及其作用如下面的代码所示。

static struct file_operations ashmem_fops = {
    .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函数的实现如下:

static int ashmem_release(struct inode *ignored, struct file *file)
{
    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分配算法用于页面回收的回调函数。具体实现如下:

static int ashmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    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驱动的分析已经结束了。因为我们讲述的是实现的原理和机制,所以没有将代码全部贴出来,建议大家参考源代码进行理解。

0
相关文章