安卓ashmem学习内核解析

Android系统的匿名共享内存Ashmem驱动程序利用了Linux的共享内存子系统导出的接口来实现,本文通过源码分析方式详细介绍Android系统的匿名共享内存机制。在Android系统中,匿名共享内存也是进程间通信方式的一种。相比于malloc和anonymous/named mmap等传统的内存分配机制,Ashmem的优势是通过内核驱动提供了辅助内核的内存回收算法机制(pin/unpin)。内存回收算法机制就是当你使用Ashmem分配了一块内存,但是其中某些部分却不会被使用时,那么就可以将这块内存unpin掉。unpin后,内核可以将它对应的物理页面回收,以作他用。你也不用担心进程无法对unpin掉的内存进行再次访问,因为回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经 mmap的地址空间。

实现代码文件:

kernel\mm\ashmem.c

kernel\include\linux\ashmem.h

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 */  
}; 

ashmem_area用来描述一块匿名共享内存,域 name 表示这块共享内存的名字,每一块匿名共享内存的名字以ASHMEM_NAME_PREFIX为前缀,如果应用程序在创建匿名共享内存时没有指定名称,则这块匿名共享内存的名称默认为ASHMEM_NAME_PREFIX

#define ASHMEM_NAME_PREFIX "dev/ashmem/"  


每一块匿名共享内存的名字会显示/proc/<pid>/maps文件中,<pid>表示打开这个共享内存文件的进程ID;

域unpinned_list是一个列表头,它把这块共享内存中所有被解锁的内存块连接在一起。一块匿名共享内存可以划分为若干小块,unpinned_list就是用于管理这些处于解锁状态下的小块内存,解锁内存块的地址相互独立,在unpinned_list链表中按地址值从大到小的顺序排列。

域file表示这个共享内存在临时文件系统tmpfs中对应的文件,在内核决定要把这块共享内存对应的物理页面回收时,就会把它的内容交换到这个临时文件中去,匿名共享内存是基于Linux内核的临时文件系统tmpfs实现的,每一块匿名共享内存在临时文件系统tmpfs中都有一个对应的文件。

域size表示这块共享内存的大小;域prot_mask表示这块共享内存的访问保护位。

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_range数据结构就是用来表示某一块被解锁(unpinnd)的小块匿名共享内存,这些解锁的小块内存都是从一块匿名共享内存中划分出来的。域asma表示这块被解锁的内存所属于的匿名共享内存,它通过域unpinned连接在asma->unpinned_list表示的列表中;域pgstartpaend表示这个内存块的开始和结束页面号,它们表示一个前后闭合的区间;域purged表示这个内存块占用的物理内存是否已经被回收,如果被回收,它的值为ASHMEM_WAS_PURGED,否则为ASHMEM_NOT_PURGED

#define ASHMEM_NOT_PURGED   0  

#define ASHMEM_WAS_PURGED   1  

这块被解锁的内存块除了保存在它所属的匿名共享内存asma的解锁列表unpinned_list之外,还通过域lru保存在一个全局的最近最少使用列表ashmem_lru_list列表中,处于解锁状态的内存块都是可以回收的,因此当系统内存不足时,内存管理系统就会回收ashmem_lru_list列表中的内存块


址空间ashmem_range的成员变量lru插入全局链表ashmem_lru_list表中。

驱动初始化:

module_init(ashmem_init);  
static int __init ashmem_init(void)  
{  
    int ret;  
    //通过kmem_cache_create函数来创建一个名为"ashmem_area_cache"、对象大小为sizeof(struct ashmem_area)的缓冲区  
    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;  
    }  
    //通过kmem_cache_create函数来创建一个名为"ashmem_range_cache"、对象大小为sizeof(struct ashmem_range)的缓冲区  
    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_area实例都是从自定义的一个slab缓冲区创建的。这个slab缓冲区是在驱动程序模块初始化函数创建的,ashmem_area的生命周期为文件的open()和release()操作之间,而ashmem_range的生命周期则是从unpin到pin,初始化时首先通过kmem_cache_create创建一个高速缓存cache。如果创建成功,则返回指向cache的指针;如果创建失败,则返回NULL。然后采用unlikely来对其创建结果进行判断。如果成功,就接着创建ashmem_range的cache。
ashmem_area_cachep和ashmem_range_cachep分别定义成全局变量
static struct kmem_cache *ashmem_area_cachep __read_mostly;  
static struct kmem_cache *ashmem_range_cachep __read_mostly;

它的类型是struct kmem_cache,表示这是一个slab缓冲区,由内核中的内存管理系统进行管理。
创建完成之后,通过misc_register函数将Ashmem注册为misc设备。这里需要注意,我们对所创建的这些cache都需要进行回收,因此,再紧接着需调用register_shrinker注册回收函数ashmem_shrinker。注册名为ashmem的字符设备:

static struct miscdevice ashmem_misc = {  
    .minor = MISC_DYNAMIC_MINOR,  
    .name = "ashmem",  
    .fops = &ashmem_fops,  
};  
注册的字符操作函数集为ashmem_fops:
[cpp] view plaincopy
static 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,  
}; 

Ahshmem驱动程序在加载时,会创建一个/dev/ashmem的设备文件,这是一个misc类型的设备。注册misc设备是通过misc_register函数进行的调用这个函数成功后,就会在/dev目录下生成一个ashmem设备文件了。这个设备文件提供了open、mmap、release和ioctl四种操作,但没有写操作接口,这是因为写共享内存的方法是通过内存映射地址来进行的,即通过mmap系统调用把这个设备文件映射到进程地址空间中,然后就直接对内存进行读写了。
注册的回收函数集为:

static struct shrinker ashmem_shrinker = {  
    .shrink = ashmem_shrink,  
    .seeks = DEFAULT_SEEKS * 4,  
};  

卸载设备驱动

当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");  
}  

打开设备驱动

当应用程序使用open函数打开/dev/ashmem时,Ashmem驱动程序中的ashmem_open函数就会被调用,用来为应用程序创建一块匿名共享内存。
static int ashmem_open(struct inode *inode, struct file *file)  
{  
    struct ashmem_area *asma;  
    int ret;  
     
    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;  
  
    INIT_LIST_HEAD(&asma->unpinned_list);  
    memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);  
    asma->prot_mask = PROT_MASK;  
    file->private_data = asma;  
  
    return 0;  
}  

首先是通过nonseekable_open函数来设备这个文件不可以执行定位操作,即不可以执行seek文件操作。接着就是通过kmem_cache_zalloc函数从刚才我们创建的slab缓冲区ashmem_area_cachep来创建一个ashmem_area结构体了,并且保存在本地变量asma中。再接下去就是初始化变量asma的其它域,其中,域name初始为ASHMEM_NAME_PREFIX,这是一个宏,定义为:

#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)  

函数的最后是把这个ashmem_area结构保存在打开文件结构体的private_data域中,这样,Ashmem驱动程序就可以在其它地方通过这个private_data域来取回这个ashmem_area结构了。到这里,设备文件/dev/ashmem的打开操作就完成了,它实际上就是在Ashmem驱动程序中创建了一个ashmem_area结构,表示一块新的共享内存,在不修改该匿名共享内存的名称情况下,默认名称为dev/ashmem。

进程地址空间映射

当应用程序调用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);//加锁  
    //如果该匿名共享内存大小为0,返回  
    if (unlikely(!asma->size)) {  
        ret = -EINVAL;  
        goto out;  
    }  
    /* 检查虚拟地址空间的保护位是否和匿名共享内存块的保护位匹配*/  
    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;  
        //s设置临时文件的文件名称为匿名共享内存块的名称  
        if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')  
            name = asma->name;  
        //为匿名共享内存创建临时文件,文件大小为该匿名共享内存大小  
        vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);  
        if (unlikely(IS_ERR(vmfile))) {  
            ret = PTR_ERR(vmfile);  
            goto out;  
        }  
        //将创建的临时文件指针保存到该匿名共享内存块的file成员变量中,这样就将匿名共享内存和它的临时文件关联起来了  
        asma->file = vmfile;  
    }  
    get_file(asma->file);  
    //判断该匿名共享内存映射的虚拟地址空间是否允许进程间共享,如果允许共享,则为该虚拟地址空间设置内存操作方法  
    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;  
    }  
    vma->vm_flags |= VM_CAN_NONLINEAR;  
  
out:  
    mutex_unlock(&ashmem_mutex);//解锁  
    return ret;  
}  

通过以上的ashmem_mmap函数就可以将创建的匿名共享内存映射到进程的虚拟地址空间中,对该匿名共享内存的操作就变成了直接操作进程的某块地址空间了。

IO命令控制

设备文件/dev/ashmem是驱动程序提供给上层应用程序访问匿名共享内存的通道,通过该设备文件可以执行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:  
        ret = set_name(asma, (void __user *) arg);  
        break;  
    case ASHMEM_GET_NAME:  
        ret = get_name(asma, (void __user *) arg);  
        break;  
    case ASHMEM_SET_SIZE:  
        ret = -EINVAL;  
        if (!asma->file) {  
            ret = 0;  
            asma->size = (size_t) arg;  
        }  
        break;  
    case ASHMEM_GET_SIZE:  
        ret = asma->size;  
        break;  
    case ASHMEM_SET_PROT_MASK:  
        ret = set_prot_mask(asma, arg);  
        break;  
    case ASHMEM_GET_PROT_MASK:  
        ret = asma->prot_mask;  
        break;  
    case ASHMEM_PIN:  
    case ASHMEM_UNPIN:  
    case ASHMEM_GET_PIN_STATUS:  
        ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);  
        break;  
    case ASHMEM_PURGE_ALL_CACHES:  
        ret = -EPERM;  
        if (capable(CAP_SYS_ADMIN)) {  
            struct shrink_control sc = {  
                .gfp_mask = GFP_KERNEL,  
                .nr_to_scan = 0,  
            };  
            ret = ashmem_shrink(&ashmem_shrinker, &sc);  
            sc.nr_to_scan = ret;  
            ashmem_shrink(&ashmem_shrinker, &sc);  
        }  
        break;  
    }  
  
    return ret;  
}  

函数首先从设备文件描述符的private_data域中取出打开匿名共享内存设备时创建的一块匿名共享内存区域的指针,然后根据IO命令,作不同的处理。

1.设置匿名共享内存名称

static int set_name(struct ashmem_area *asma, void __user *name)  
{  
    int ret = 0;  
    mutex_lock(&ashmem_mutex);  
    /* cannot change an existing mapping's name */  
    if (unlikely(asma->file)) {  
        ret = -EINVAL;  
        goto out;  
    }  
    if (unlikely(copy_from_user(asma->name + ASHMEM_NAME_PREFIX_LEN,name, ASHMEM_NAME_LEN)))  
        ret = -EFAULT;  
    asma->name[ASHMEM_FULL_NAME_LEN-1] = '\0';  
  
out:  
    mutex_unlock(&ashmem_mutex);  
  
    return ret;  
}  

匿名共享内存名称设置过程很简单,就是将用户空间传过来的名称拼接到"dev/ashmem/"字符串后面。每一个匿名共享内存在临时文件系统tmpfs中都有相应的文件,文件名称为该匿名共享内存的名称,当匿名共享内存驱动程序为某一块匿名共享内存创建了临时文件,就不可以更改该文件名称了,因此也就无法更改该匿名共享内存的名称了,因此在修改匿名共享内存块的名称时,会首先判断其成员变量file是否已经指向了某个文件,如果是,就表示已经为该匿名共享内存块创建了对应的临时文件,修改名称操作就直接返回,无法更改匿名共享内存块的名称了。

2.获取匿名共享内存名称

static int get_name(struct ashmem_area *asma, void __user *name)  
{  
    int ret = 0;  
  
    mutex_lock(&ashmem_mutex);  
    if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0') {  
        size_t len;  
        len = strlen(asma->name + ASHMEM_NAME_PREFIX_LEN) + 1;  
        //拷贝"dev/ashmem"字符串的后缀到用户空间  
        if (unlikely(copy_to_user(name,asma->name + ASHMEM_NAME_PREFIX_LEN, len)))  
            ret = -EFAULT;  
    } else {  
        //拷贝字符串"dev/ashmem"到用户空间  
        if (unlikely(copy_to_user(name, ASHMEM_NAME_DEF,sizeof(ASHMEM_NAME_DEF))))  
            ret = -EFAULT;  
    }  
    mutex_unlock(&ashmem_mutex);  
  
    return ret;  
}  

获取匿名共享内存块名称很简单,就是将匿名内存块的名称返回到用户空间而已

3.设置匿名共享掩码

static int set_prot_mask(struct ashmem_area *asma, unsigned long prot)  
{  
    int ret = 0;  
    mutex_lock(&ashmem_mutex);  
    /* the user can only remove, not add, protection bits */  
    if (unlikely((asma->prot_mask & prot) != prot)) {  
        ret = -EINVAL;  
        goto out;  
    }  
  
    /* does the application expect PROT_READ to imply PROT_EXEC? */  
    if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))  
        prot |= PROT_EXEC;  
  
    asma->prot_mask = prot;  
  
out:  
    mutex_unlock(&ashmem_mutex);  
    return ret;  
} 


你可能感兴趣的:(buffer,android应用)