四. 匿名共享内存的锁定和解锁操作
前面提到,Android系统的运行时库提到了执行匿名共享内存的锁定和解锁操作的两个函数ashmem_pin_region和ashmem_unpin_region,它们实现在system/core/libcutils/ashmem-dev.c文件中:
- int ashmem_pin_region(int fd, size_t offset, size_t len)
- {
- struct ashmem_pin pin = { offset, len };
- return ioctl(fd, ASHMEM_PIN, &pin);
- }
- int ashmem_unpin_region(int fd, size_t offset, size_t len)
- {
- struct ashmem_pin pin = { offset, len };
- return ioctl(fd, ASHMEM_UNPIN, &pin);
- }
它们的实现很简单,通过ASHMEM_PIN和ASHMEM_UNPIN两个ioctl操作来实现匿名共享内存的锁定和解锁操作。
我们先看来一下ASHMEM_PIN和ASHMEM_UNPIN这两个命令号的定义,它们的定义可以在kernel/common/include/linux/ashmem.h文件中找到:
- #define __ASHMEMIOC 0x77
- #define ASHMEM_PIN _IOW(__ASHMEMIOC, 7, struct ashmem_pin)
- #define ASHMEM_UNPIN _IOW(__ASHMEMIOC, 8, struct ashmem_pin)
它们的参数类型为struct ashmem_pin,它也是定义在kernel/common/include/linux/ashmem.h文件中:
- struct ashmem_pin {
- __u32 offset; /* offset into region, in bytes, page-aligned */
- __u32 len; /* length forward from offset, in bytes, page-aligned */
- };
这个结构体只有两个域,分别表示要锁定或者要解锁的内块块的起始大小以及大小。
在分析这两个操作之前,我们先来看一下Ashmem驱动程序中的一个数据结构struct ashmem_range,这个数据结构就是用来表示某一块被解锁(unpinnd)的内存:
- /*
- * ashmem_range - represents an interval of unpinned (evictable) pages
- * Lifecycle: From unpin to pin
- * Locking: Protected by `ashmem_mutex'
- */
- struct ashmem_range {
- struct list_head lru; /* entry in LRU list */
- struct list_head unpinned; /* entry in its area's unpinned list */
- struct ashmem_area *asma; /* associated area */
- size_t pgstart; /* starting page, inclusive */
- size_t pgend; /* ending page, inclusive */
- unsigned int purged; /* ASHMEM_NOT or ASHMEM_WAS_PURGED */
- };
域asma表示这块被解锁的内存所属于的匿名共享内存,它通过域unpinned连接在asma->unpinned_list表示的列表中;域pgstart和paend表示这个内存块的开始和结束页面号,它们表示一个前后闭合的区间;域purged表示这个内存块占用的物理内存是否已经被回收;这块被解锁的内存块除了保存在它所属的匿名共享内存asma的解锁列表unpinned_list之外,还通过域lru保存在一个全局的最近最少使用列表ashmem_lru_list列表中,它的定义如下:
- /* LRU list of unpinned pages, protected by ashmem_mutex */
- static LIST_HEAD(ashmem_lru_list);
了解了这个数据结构之后,我们就可以来看ashmem_ioctl函数中关于ASHMEM_PIN和ASHMEM_UNPIN的操作了:
- 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_PIN:
- case ASHMEM_UNPIN:
- ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
- break;
- ......
- }
- return ret;
- }
它们都是通过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;
- if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
- return -EFAULT;
- /* 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;
- ......
- }
- mutex_unlock(&ashmem_mutex);
- return ret;
- }
首先是获得用户空间传进来的参数,并保存在本地变量pin中,这是一个struct ashmem_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操作时,目标对象必须是一块当前处于unpinned状态的内存块。
我们先来看一下ASHMEM_UNPIN操作,进入到ashmem_unpin函数:
- /*
- * ashmem_unpin - unpin the given range of pages. Returns zero on success.
- *
- * Caller must hold ashmem_mutex.
- */
- 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);
- }
这个函数的主体就是在遍历asma->unpinned_list列表,从中查找当前处于unpinned状态的内存块是否与将要unpin的内存块[pgstart, pgend]是否相交,如果相交,则要执行合并操作,即调整pgstart和pgend的大小,然后通过调用range_del函数删掉原来的已经被unpinned过的内存块,最后再通过range_alloc函数来重新unpinned这块调整过后的内存块[pgstart, pgend],这里新的内存块[pgstart, pgend]已经包含了刚才所有被删掉的unpinned状态的内存。注意,这里如果找到一块相并的内存块,并且调整了pgstart和pgend的大小之后,要重新再扫描一遍asma->unpinned_list列表,因为新的内存块[pgstart, pgend]可能还会与前后的处于unpinned状态的内存块发生相交。
我们来看一下range_before_page的操作,这是一个宏定义:
- #define range_before_page(range, page) \
- ((range)->pgend < (page))
表示range描述的内存块是否在page页面之前,如果是,则整个描述就结束了。从这里我们可以看出asma->unpinned_list列表是按照页面号从大到小进行排列的,并且每一块被unpin的内存都是不相交的。
再来看一下page_range_subsumed_by_range的操作,这也是一个宏定义:
- #define page_range_subsumed_by_range(range, start, end) \
- (((range)->pgstart <= (start)) && ((range)->pgend >= (end)))
表示range描述的内存块是不是包含了[start, end]这个内存块,如果包含了,则说明当前要unpin的内存块已经处于unpinned状态,什么也不用操作,直接返回即可。
再看page_range_in_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))
它用到的其它两个宏分别定义为:
- #define page_range_subsumed_by_range(range, start, end) \
- (((range)->pgstart <= (start)) && ((range)->pgend >= (end)))
- #define page_in_range(range, page) \
- (((range)->pgstart <= (page)) && ((range)->pgend >= (page)))
它们都是用来判断两个内存区间是否相交的。
两个内存块相交分为四种情况:
|-------range-----| |-------range------| |--------range---------| |----range---|
|-start----end-| |-start-----end-| |-start-------end-| |-start-----------end-|
(1) (2) (3) (4)
第一种情况,前面已经讨论过了,对于第二到第四种情况,都是需要执行合并操作的。
再来看从asma->unpinned_list中删掉内存块的range_del函数:
- static void range_del(struct ashmem_range *range)
- {
- list_del(&range->unpinned);
- if (range_on_lru(range))
- lru_del(range);
- kmem_cache_free(ashmem_range_cachep, range);
- }
这个函数首先把range从相应的unpinned_list列表中删除,然后判断它是否在lru列表中:
- #define range_on_lru(range) \
- ((range)->purged == ASHMEM_NOT_PURGED)
如果它的状态purged等于ASHMEM_NOT_PURGED,即对应的物理页面尚未被回收,它就位于lru列表中,通过调用lru_del函数进行删除:
- static inline void lru_del(struct ashmem_range *range)
- {
- list_del(&range->lru);
- lru_count -= range_size(range);
- }
最后调用kmem_cache_free将它从slab缓冲区ashmem_range_cachep中释放。
这里的slab缓冲区ashmem_range_cachep定义如下:
- static struct kmem_cache *ashmem_range_cachep __read_mostly;
它和前面介绍的slab缓冲区ashmem_area_cachep一样,是在Ashmem驱动程序模块初始化函数ashmem_init进行初始化的:
- static int __init ashmem_init(void)
- {
- int ret;
- ......
- 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;
- }
- ......
- printk(KERN_INFO "ashmem: initialized\n");
- return 0;
- }
回到ashmem_unpin函数中,我们再来看看range_alloc函数的实现:
- /*
- * range_alloc - allocate and initialize a new ashmem_range structure
- *
- * 'asma' - associated ashmem_area
- * 'prev_range' - the previous ashmem_range in the sorted asma->unpinned list
- * 'purged' - initial purge value (ASMEM_NOT_PURGED or ASHMEM_WAS_PURGED)
- * 'start' - starting page, inclusive
- * 'end' - ending page, inclusive
- *
- * Caller must hold ashmem_mutex.
- */
- 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 = 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;
- list_add_tail(&range->unpinned, &prev_range->unpinned);
- if (range_on_lru(range))
- lru_add(range);
- return 0;
- }
这个函数的作用是从slab 缓冲区中ashmem_range_cachep分配一个ashmem_range,然后对它作相应的初始化,放在相应的ashmem_area->unpinned_list列表中,并且还要判断这个range的purged是否是ASHMEM_NOT_PURGED状态,如果是,还要把它放在lru列表中:
- static inline void lru_add(struct ashmem_range *range)
- {
- list_add_tail(&range->lru, &ashmem_lru_list);
- lru_count += range_size(range);
- }
这样,ashmem_unpin的源代码我们就分析完了。
接着,我们再来看一下ASHMEM_PIN操作,进入到ashmem_pin函数:
- /*
- * ashmem_pin - pin the given ashmem region, returning whether it was
- * previously purged (ASHMEM_WAS_PURGED) or not (ASHMEM_NOT_PURGED).
- *
- * Caller must hold ashmem_mutex.
- */
- 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;
- }
前面我们说过,被pin的内存块,必须是在unpinned_list列表中的,如果不在,就什么都不用做。要判断要pin的内存块是否在unpinned_list列表中,又要通过遍历相应的asma->unpinned_list列表来找出与之相交的内存块了。这个函数的处理方法大体与前面的ashmem_unpin函数是一致的,也是要考虑四种不同的相交情况,这里就不详述了,读者可以自己分析一下。
这里我们只看一下range_shrink函数的实现:
- /*
- * range_shrink - shrinks a range
- *
- * Caller must hold ashmem_mutex.
- */
- 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);
- }
这个函数的实现很简单,只是调整一下range描述的内存块的起始页面号,如果它是位于lru列表中,还要调整一下在lru列表中的总页面数大小。
这样,匿名共享内存的ASHMEM_PIN和ASHMEM_UNPIN操作就介绍完了,但是,我们还看不出来Ashmem驱动程序是怎么样辅助内存管理系统来有效管理内存的。有了前面这些unpinned的内存块列表之后,下面我们就看一下Ashmem驱动程序是怎么样辅助内存管理系统来有效管理内存的。
首先看一下Ashmem驱动程序模块初始化函数ashmem_init:
- static struct shrinker ashmem_shrinker = {
- .shrink = ashmem_shrink,
- .seeks = DEFAULT_SEEKS * 4,
- };
- static int __init ashmem_init(void)
- {
- int ret;
- ......
- register_shrinker(&ashmem_shrinker);
- printk(KERN_INFO "ashmem: initialized\n");
- return 0;
- }
这里通过调用register_shrinker函数向内存管理系统注册一个内存回收算法函数。在Linux内核中,当系统内存紧张时,内存管理系统就会进行内存回收算法,将一些最近没有用过的内存换出物理内存去,这样可以增加物理内存的供应。因此,当内存管理系统进行内存回收时,就会调用到这里的ashmem_shrink函数,让Ashmem驱动程序执行内存回收操作:
- /*
- * ashmem_shrink - our cache shrinker, called from mm/vmscan.c :: shrink_slab
- *
- * 'nr_to_scan' is the number of objects (pages) to prune, or 0 to query how
- * many objects (pages) we have in total.
- *
- * 'gfp_mask' is the mask of the allocation that got us into this mess.
- *
- * Return value is the number of objects (pages) remaining, or -1 if we cannot
- * proceed without risk of deadlock (due to gfp_mask).
- *
- * We approximate LRU via least-recently-unpinned, jettisoning unpinned partial
- * chunks of ashmem regions LRU-wise one-at-a-time until we hit 'nr_to_scan'
- * pages freed.
- */
- static int ashmem_shrink(int nr_to_scan, gfp_t gfp_mask)
- {
- struct ashmem_range *range, *next;
- /* We might recurse into filesystem code, so bail out if necessary */
- 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;
- }
这里的参数nr_to_scan表示要扫描的页数,如果是0,则表示要查询一下,当前Ashmem驱动程序有多少页面可以回收,这里就等于挂在lru列表的内块页面的总数了,即lru_count;否则,就要开始扫描lru列表,从中回收内存了,直到回收的内存页数等于nr_to_scan,或者已经没有内存可回收为止。回收内存页面是通过vm_truncate_range函数进行的,这个函数定义在kernel/common/mm/memory.c文件中,它是Linux内核内存管理系统实现的,有兴趣的读者可以研究一下。
这样,Android系统匿名共享内存Ashmem驱动程序源代码就分析完了,在下一篇文章中,我们将继续分析Android系统的匿名共享内存机制,研究它是如何通过Binder进程间通信机制实现在不同进程程进行内存共享的,敬请关注。