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/"

如果应用程序为创建的匿名共享内存指定名称,名字的长度不能大于256字节,因为在代码中定义了匿名共享内存全名的长度为指定名称长度加上前缀长度,指定名称长度定义为256字节

#define ASHMEM_FULL_NAME_LEN (ASHMEM_NAME_LEN + ASHMEM_NAME_PREFIX_LEN)
#define ASHMEM_NAME_LEN		256
#define ASHMEM_NAME_PREFIX_LEN (sizeof(ASHMEM_NAME_PREFIX) - 1)

每一块匿名共享内存的名字会显示/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列表中的内存块

驱动初始化

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:
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;
}

4.锁定或解锁匿名共享内存

匿名共享内存是以分块的形式来管理的,应用程序可以对这些小块内存进行锁定或解锁操作,处于解锁状态的内存在系统内存不足时,可以被内存管理系统回收的。匿名共享内存在创建时就被设置为锁定状态,当某块内存不在使用时,需要将其设置为解锁状态,从而允许内存管理系统回收该块内存。通过ASHMEM_PIN和ASHMEM_UNPIN两个ioctl操作来实现匿名共享内存的锁定和解锁操作。
ASHMEM_PIN和ASHMEM_UNPIN这两个命令号的定义:
#define ASHMEM_PIN		_IOW(__ASHMEMIOC, 7, struct ashmem_pin)
#define ASHMEM_UNPIN		_IOW(__ASHMEMIOC, 8, struct ashmem_pin)
它们的参数类型为struct ashmem_pin:
struct ashmem_pin {
	__u32 offset;	/* 要锁定或者要解锁的内块在宿主内存快中的偏移 */
	__u32 len;	/* 要锁定或者要解锁的内块块的大小 */
};
锁定或解锁指定内存块的过程如下:
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;
    //从用户空间将参数p拷贝到内核空间
	if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
		return -EFAULT;
	//如果要锁定或解锁的内存大小为0,则表示要锁定或解锁的内存块从pin所描述的偏移地址开始,一直到匿名共享内存的末尾
	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;
}
首先是获得用户空间传进来的struct ashmem_pin参数,并保存在本地变量pin中,它包括了要pin/unpin的内存块的起始地址和大小,这里的起始地址和大小都是以字节为单位的,因此,通过转换把它们换成以页面为单位的,并且保存在本地变量pgstart和pgend中。这里除了要对参数作一个安全性检查外,还要一个处理逻辑是,如果从用户空间传进来的内块块的大小值为0 ,则认为是要pin/unpin整个匿名共享内存。
函数最后根据当前要执行的是ASHMEM_PIN操作还是ASHMEM_UNPIN操作来分别执行ashmem_pin和ashmem_unpin来进一步处理。创建匿名共享内存时,默认所有的内存都是pinned状态的,只有用户告诉Ashmem驱动程序要unpin某一块内存时,Ashmem驱动程序才会把这块内存unpin,之后,用户可以再告诉Ashmem驱动程序要重新pin某一块之前被unpin过的内块,从而把这块内存从unpinned状态改为pinned状态。ashmem_pin_unpin函数根据命令操作码分为内存块锁定,内存块解锁,查看内存块锁定状态三个操作。前面介绍到所有解锁内存块按照地址值从大到小的顺序保存到该宿主共享内存的unpinned_list成员中,每一块解锁的内存地址相互独立,在解锁指定内存块时,需要检查解锁的内存块是否与已经解锁的内存块相互重合,如果重合,就需要进行合并,然后将解锁后的内存块保存到其宿主内存块unpinned_list的指定位置。

     |-------range-----|        |-------range------|       |--------range---------|                 |----range---|                   

       |-start----end-|       |-start-----end-|                          |-start-------end-|        |-start-----------end-|      
                 (1)                                (2)                                  (3)                                            (4)               

系统定义了以下宏来判断两块内存块的位置关系:
#define page_range_subsumed_by_range(range, start, end) \
  (((range)->pgstart <= (start)) && ((range)->pgend >= (end)))
page_range_subsumed_by_range宏描述了位置1,用来判断内存块range是否包含内存[start,end]
#define page_range_subsumes_range(range, start, end) \
  (((range)->pgstart >= (start)) && ((range)->pgend <= (end)))
page_range_subsumes_range宏描述了位置4,用来判断内存块[start,end]是否包含内存块range
#define page_range_in_range(range, start, end) \
  (page_in_range(range, start) || page_in_range(range, end) || \
   page_range_subsumes_range(range, start, end))
page_range_in_range宏描述了位置2和3,4
#define range_before_page(range, page) ((range)->pgend < (page))
#define page_in_range(range, page) (((range)->pgstart <= (page)) && ((range)->pgend >= (page)))

1)内存锁定

每一块内存默认情况下处于锁定状态,当它被解锁后,就会被插入到宿主匿名共享内存的unpinned_list链表中,只有位于这个链表中的内存块才可以执行锁定操作
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
	struct ashmem_range *range, *next;
	int ret = ASHMEM_NOT_PURGED;
	//遍历宿主匿名共享内存的成员unpinned_list链表
	list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
	    //如果解锁内存块在当前解锁内存块的前面,位置如下,即不和已解锁内存块相交,只有位于unpinned_list的内存块才可以执行锁定操作
		// |----range---|       
		//               |-pgstart-----------pgend-|      
		if (range_before_page(range, pgstart))
			break;
		//      位置3               |           位置2            |       位置4
		//|----range---|            |          |----range---|    |     |----range---|
		//   |-pgstart---pgend-|    |  |-pgstart---pgend-|       |  |-pgstart---pgend-|
		if (page_range_in_range(range, pgstart, pgend)) {
			ret |= range->purged;
			//该位置下的当前内存块已经解锁,因此需要将range从链表中删除,重新锁定
			// |----------range---------|  
			//   |-pgstart---pgend-|  
			if (page_range_subsumes_range(range, pgstart, pgend)) {
				range_del(range);
				continue;
			}
			//        |------range-----|  |                     |---range--|
			//   |-pgstart---pgend-|      |  |-pgstart---pgend-|
			if (range->pgstart >= pgstart) {
				range_shrink(range, pgend + 1, range->pgend);
				continue;
			}
			// |----range---|             |         |---range--|
			//        |-pgstart---pgend-| |                     |-pgstart---pgend-|
			if (range->pgend <= pgend) {
				range_shrink(range, range->pgstart, pgstart-1);
				continue;
			}
			// |--------range-------|   
			//    |pgstart--pgend|
			range_alloc(asma, range, range->purged,pgend + 1, range->pgend);
			range_shrink(range, range->pgstart, pgstart - 1);
			break;
		}
	}
	return ret;
}
指定要锁定的内存块[pgstart,pgend]和处于解锁状态的内存块range位置分以下4中情况:
一  指定要锁定的内存块[pgstart,pgend]包含了处于解锁状态的内存块range,这时候只需要将处于解锁状态的内存块range的开始地址修改为指定要锁定的内存块的末尾地址的下一个页面地址;
二  指定要锁定的内存块[pgstart,pgend]的后半部分与处于解锁状态的内存块range的前半部分相交,这时候只需要将处于解锁状态的内存块range的开始地址修改为指定要锁定的内存块的末尾地址的下一个页面地址;
三  指定要锁定的内存块[pgstart,pgend]的前半部分与处于解锁状态的内存块range的后半部分相交,这时候只需要将处于解锁状态的内存块range的末尾地址修改为指定要锁定的内存块的开始地址的上一个页面地址;
四  指定要锁定的内存块[pgstart,pgend]包含在解锁状态的内存块range中,这时候就将处于解锁状态的内存块划分为两部分,并调整划分后的解锁状态的内存块地址;
range_shrink函数用于修改一个处于解锁状态的内存块的开始地址或末尾地址
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);
}

2)内存解锁

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:
	//遍历宿主匿名共享内存的成员unpinned_list链表中的所有已解锁的小内存块range
	list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
		//如果解锁内存块在当前解锁内存块的前面,位置如下,即不和已解锁内存块相交,因此不需要继续遍历,
		// |----range---|       
		//               |-pgstart-----------pgend-|      
		if (range_before_page(range, pgstart))
			break;
		//该位置下已经解锁
		// |----------range---------|  
		//   |-pgstart---pgend-|  
		if (page_range_subsumed_by_range(range, pgstart, pgend))
			return 0;
		//      位置3               |           位置2            |       位置4
		//|----range---|            |          |----range---|    |     |----range---|
		//   |-pgstart---pgend-|    |  |-pgstart---pgend-|       |  |-pgstart---pgend-|
		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;
			//从宿主匿名共享内存的成员unpinned_list链表中删除当前这块已解锁的内存块
			range_del(range);
			goto restart;
		}
	}
    //解锁当前内存块,并且插入到宿主匿名共享内存的成员unpinned_list链表中
	return range_alloc(asma, range, purged, pgstart, pgend);
}

通过查询链表unpinned_list并调整解锁内存块的地址后,通过函数range_alloc完成解锁任务,并加入到unpinned_list链表中

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;
	//分配一个ashmem_range
	range = kmem_cache_zalloc(ashmem_range_cachep, GFP_KERNEL);
	if (unlikely(!range))
		return -ENOMEM;
	//初始化该ashmem_range
	range->asma = asma;
	range->pgstart = start;
	range->pgend = end;
	range->purged = purged;
	list_add_tail(&range->unpinned, &prev_range->unpinned);//插入宿主内存块的链表unpinned_list中
	//判断该ashmem_range是否已经回收
	if (range_on_lru(range))
		lru_add(range);//添加到全局链表ashmem_lru_list中
	return 0;
}
宏range_on_lru用于判断内存块是否被回收

#define range_on_lru(range) \
  ((range)->purged == ASHMEM_NOT_PURGED)
函数lru_add用于向全局链表ashmem_lru_list添加解锁的内存块
static inline void lru_add(struct ashmem_range *range)
{
	//添加到全局链表ashmem_lru_list中
	list_add_tail(&range->lru, &ashmem_lru_list);
	lru_count += range_size(range);//统计已解锁内存大小
}

匿名共享内存驱动程序使用全局变量lru_count来统计保存在全局变量ashmem_lru_list中的处于解锁状态的内存块总大小,用来表示可以被内存管理系统回收的匿名共享内存的容量。宏range_size用于计算解锁内存块的大小:

#define range_size(range) \
  ((range)->pgend - (range)->pgstart + 1)

匿名共享内存块回收

在匿名共享内存驱动程序初始化时,注册了内存回收操作函数
register_shrinker(&ashmem_shrinker);
static struct shrinker ashmem_shrinker = {
	.shrink = ashmem_shrink,
	.seeks = DEFAULT_SEEKS * 4,
};
当系统内存不足时,函数ashmem_shrink将被调用,用于回收处于解锁状态的匿名共享内存
static int ashmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
	struct ashmem_range *range, *next;
	/* We might recurse into filesystem code, so bail out if necessary */
	if (sc->nr_to_scan && !(sc->gfp_mask & __GFP_FS))
		return -1;
	//nr_to_scan表示内存管理系统回收的内存页数,如果等于0,表示查询处于解锁状态的内存大小
	if (!sc->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);
		//调整内存管理系统要回收的内存页数
		sc->nr_to_scan -= range_size(range);
		//如果完成内存管理系统要求回收的页数,则停止回收
		if (sc->nr_to_scan <= 0)
			break;
	}
	mutex_unlock(&ashmem_mutex);
	//返回剩下可以回收的内存大小
	return lru_count;
}

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐