四. 匿名共享内存的锁定和解锁操作
前面提到,Android系统的运行时库提到了执行匿名共享内存的锁定和解锁操作的两个函数ashmem_pin_region和ashmem_unpin_region,它们实现在system/core/libcutils/ashmem-dev.c文件中:
intashmem_pin_region(intfd, size_t offset, size_t len)
{
struct ashmem_pin pin = { offset, len };
returnioctl(fd, ASHMEM_PIN, &pin);
}
intashmem_unpin_region(intfd, size_t offset, size_t len)
{
struct ashmem_pin pin = { offset, len };
returnioctl(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; /* offsetintoregion,inbytes, page-aligned */
__u32 len; /* lengthforwardfromoffset,inbytes, page-aligned */
};
这个结构体只有两个域,分别表示要锁定或者要解锁的内块块的起始大小以及大小。
在分析这两个操作之前,我们先来看一下Ashmem驱动程序中的一个数据结构struct ashmem_range,这个数据结构就是用来表示某一块被解锁(unpinnd)的内存:
/*
* ashmem_range - represents an intervalofunpinned (evictable) pages
* Lifecycle:Fromunpintopin
* Locking: Protectedby`ashmem_mutex'
*/
struct ashmem_range {
struct list_head lru; /* entryinLRU list */
struct list_head unpinned; /* entryinits area's unpinned list */
struct ashmem_area *asma; /* associated area */
size_t pgstart; /* starting page, inclusive */
size_t pgend; /* ending page, inclusive */
unsignedintpurged; /* ASHMEM_NOTorASHMEM_WAS_PURGED */
};
域asma表示这块被解锁的内存所属于的匿名共享内存,它通过域unpinned连接在asma->unpinned_list表示的列表中;域pgstart和paend表示这个内存块的开始和结束页面号,它们表示一个前后闭合的区间;域purged表示这个内存块占用的物理内存是否已经被回收;这块被解锁的内存块除了保存在它所属的匿名共享内存asma的解锁列表unpinned_list之外,还通过域lru保存在一个全局的最近最少使用列表ashmem_lru_list列表中,它的定义如下:
/* LRU listofunpinned pages, protectedbyashmem_mutex */
staticLIST_HEAD(ashmem_lru_list);
了解了这个数据结构之后,我们就可以来看ashmem_ioctl函数中关于ASHMEM_PIN和ASHMEM_UNPIN的操作了:
staticlong ashmem_ioctl(struct file *file, unsignedintcmd, unsigned long arg)
{
struct ashmem_area *asma = file->private_data;
long ret = -ENOTTY;
switch (cmd) {
......
caseASHMEM_PIN:
caseASHMEM_UNPIN:
ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
break;
......
}
returnret;
}
它们都是通过ashmem_pin_unpin来进一步处理:
staticintashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
void __user *p)
{
struct ashmem_pin pin;
size_t pgstart, pgend;
intret = -EINVAL;
if (unlikely(!asma->file))
return-EINVAL;
if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
return-EFAULT;
/* per custom, you can pass zeroforlentomean"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
return-EINVAL;
if (unlikely(PAGE_ALIGN(asma->size)
return-EINVAL;
pgstart = pin.offset / PAGE_SIZE;
pgend = pgstart + (pin.len / PAGE_SIZE) - 1;
mutex_lock(&ashmem_mutex);
switch (cmd) {
caseASHMEM_PIN:
ret = ashmem_pin(asma, pgstart, pgend);
break;
caseASHMEM_UNPIN:
ret = ashmem_unpin(asma, pgstart, pgend);
break;
......
}
mutex_unlock(&ashmem_mutex);
returnret;
}
首先是获得用户空间传进来的参数,并保存在本地变量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 rangeofpages.Returnszeroonsuccess.
*
* Caller must hold ashmem_mutex.
*/
staticintashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
unsignedintpurged = ASHMEM_NOT_PURGED;
restart:
list_for_each_entry_safe(range,next, &asma->unpinned_list, unpinned) {
/* short circuit: thisisour insertion point */
if (range_before_page(range, pgstart))
break;
/*
* Theusercan ask ustounpin pages that are already entirely
*orpartially pinned. We handle those two cases here.
*/
if (page_range_subsumed_by_range(range, pgstart, pgend))
return0;
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);
gotorestart;
}
}
returnrange_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
表示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函数:
staticvoid 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函数进行删除:
staticinline 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定义如下:
staticstruct kmem_cache *ashmem_range_cachep __read_mostly;
它和前面介绍的slab缓冲区ashmem_area_cachep一样,是在Ashmem驱动程序模块初始化函数ashmem_init进行初始化的:
staticint__init ashmem_init(void)
{
intret;
......
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");
return0;
}
回到ashmem_unpin函数中,我们再来看看range_alloc函数的实现:
/*
* range_alloc - allocateandinitialize a new ashmem_range structure
*
*'asma'- associated ashmem_area
*'prev_range'- the previous ashmem_rangeinthe sorted asma->unpinned list
*'purged'- initial purge value (ASMEM_NOT_PURGEDorASHMEM_WAS_PURGED)
*'start'- starting page, inclusive
*'end'- ending page, inclusive
*
* Caller must hold ashmem_mutex.
*/
staticintrange_alloc(struct ashmem_area *asma,
struct ashmem_range *prev_range, unsignedintpurged,
size_t start, size_tend)
{
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);
return0;
}
这个函数的作用是从slab 缓冲区中ashmem_range_cachep分配一个ashmem_range,然后对它作相应的初始化,放在相应的ashmem_area->unpinned_list列表中,并且还要判断这个range的purged是否是ASHMEM_NOT_PURGED状态,如果是,还要把它放在lru列表中:
staticinline 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)ornot(ASHMEM_NOT_PURGED).
*
* Caller must hold ashmem_mutex.
*/
staticintashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
intret = ASHMEM_NOT_PURGED;
list_for_each_entry_safe(range,next, &asma->unpinned_list, unpinned) {
/* moved pastlastapplicable page; we can short circuit */
if (range_before_page(range, pgstart))
break;
/*
* Theusercan ask ustopin pages that span multiple ranges,
*ortopin pages that aren't even unpinned, so thisismessy.
*
* Four cases:
* 1. The requested range subsumes an existing range, so we
* just remove the entire matching range.
* 2. The requested range overlaps the startofan existing
* range, so we justupdatethat range.
* 3. The requested range overlaps theendofan existing
* range, so we justupdatethat range.
* 4. The requested range punches a holeinan existing range,
* so we havetoupdateone sideofthe rangeandthen
*createa new rangeforthe 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 overlapfromthe start, so adjust it */
if (range->pgstart >= pgstart) {
range_shrink(range, pgend + 1, range->pgend);
continue;
}
/*Case#3: We overlapfromthe rear, so adjust it */
if (range->pgend <= pgend) {
range_shrink(range, range->pgstart, pgstart-1);
continue;
}
/*
*Case#4: We eat a chunkoutofthe middle. Abit
* more complicated, we allocate a new rangeforthe
*secondhalfandadjust thefirstchunk's endpoint.
*/
range_alloc(asma, range, range->purged,
pgend + 1, range->pgend);
range_shrink(range, range->pgstart, pgstart - 1);
break;
}
}
returnret;
}
前面我们说过,被pin的内存块,必须是在unpinned_list列表中的,如果不在,就什么都不用做。要判断要pin的内存块是否在unpinned_list列表中,又要通过遍历相应的asma->unpinned_list列表来找出与之相交的内存块了。这个函数的处理方法大体与前面的ashmem_unpin函数是一致的,也是要考虑四种不同的相交情况,这里就不详述了,读者可以自己分析一下。
这里我们只看一下range_shrink函数的实现:
/*
* range_shrink - shrinks a range
*
* Caller must hold ashmem_mutex.
*/
staticinline void range_shrink(struct ashmem_range *range,
size_t start, size_tend)
{
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:
staticstruct shrinker ashmem_shrinker = {
.shrink = ashmem_shrink,
.seeks = DEFAULT_SEEKS * 4,
};
staticint__init ashmem_init(void)
{
intret;
......
register_shrinker(&ashmem_shrinker);
printk(KERN_INFO"ashmem: initialized\n");
return0;
}
这里通过调用register_shrinker函数向内存管理系统注册一个内存回收算法函数。在Linux内核中,当系统内存紧张时,内存管理系统就会进行内存回收算法,将一些最近没有用过的内存换出物理内存去,这样可以增加物理内存的供应。因此,当内存管理系统进行内存回收时,就会调用到这里的ashmem_shrink函数,让Ashmem驱动程序执行内存回收操作:
/*
* ashmem_shrink - our cache shrinker, calledfrommm/vmscan.c :: shrink_slab
*
*'nr_to_scan'isthe numberofobjects (pages)toprune,or0toquery how
* many objects (pages) we haveintotal.
*
*'gfp_mask'isthe maskofthe allocation that got usintothis mess.
*
*Returnvalueisthe numberofobjects (pages) remaining,or-1 if we cannot
* proceed without riskofdeadlock (duetogfp_mask).
*
* We approximate LRU via least-recently-unpinned, jettisoning unpinnedpartial
* chunksofashmem regions LRU-wise one-at-a-timeuntil we hit'nr_to_scan'
* pages freed.
*/
staticintashmem_shrink(intnr_to_scan, gfp_t gfp_mask)
{
struct ashmem_range *range, *next;
/* We might recurseintofilesystem code, so bailoutif necessary */
if (nr_to_scan && !(gfp_mask & __GFP_FS))
return-1;
if (!nr_to_scan)
returnlru_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_tend= (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);
returnlru_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进程间通信机制实现在不同进程程进行内存共享的,敬请关注。