Android系统的匿名共享内存Ashmem驱动程序利用了Linux的共享内存子系统导出的接口来实现,本文通过源码分析方式详细介绍Android系统的匿名共享内存机制。在Android系统中,匿名共享内存也是进程间通信方式的一种。相比于malloc和anonymous/named mmap等传统的内存分配机制,Ashmem的优势是通过内核驱动提供了辅助内核的内存回收算法机制(pin/unpin)。内存回收算法机制就是当你使用Ashmem分配了一块内存,但是其中某些部分却不会被使用时,那么就可以将这块内存unpin掉。unpin后,内核可以将它对应的物理页面回收,以作他用。你也不用担心进程无法对unpin掉的内存进行再次访问,因为回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经 mmap的地址空间。
实现代码文件:
kernelmmashmem.c
kernelincludelinuxashmem.h
ashmem_area用来描述一块匿名共享内存,域name表示这块共享内存的名字,每一块匿名共享内存的名字以ASHMEM_NAME_PREFIX为前缀,如果应用程序在创建匿名共享内存时没有指定名称,则这块匿名共享内存的名称默认为ASHMEM_NAME_PREFIX
如果应用程序为创建的匿名共享内存指定名称,名字的长度不能大于256字节,因为在代码中定义了匿名共享内存全名的长度为指定名称长度加上前缀长度,指定名称长度定义为256字节
每一块匿名共享内存的名字会显示/proc/<pid>/maps文件中,<pid>表示打开这个共享内存文件的进程ID;
域unpinned_list是一个列表头,它把这块共享内存中所有被解锁的内存块连接在一起。一块匿名共享内存可以划分为若干小块,unpinned_list就是用于管理这些处于解锁状态下的小块内存,解锁内存块的地址相互独立,在unpinned_list链表中按地址值从大到小的顺序排列。
域file表示这个共享内存在临时文件系统tmpfs中对应的文件,在内核决定要把这块共享内存对应的物理页面回收时,就会把它的内容交换到这个临时文件中去,匿名共享内存是基于Linux内核的临时文件系统tmpfs实现的,每一块匿名共享内存在临时文件系统tmpfs中都有一个对应的文件。
域size表示这块共享内存的大小;域prot_mask表示这块共享内存的访问保护位。
ashmem_range数据结构就是用来表示某一块被解锁(unpinnd)的小块匿名共享内存,这些解锁的小块内存都是从一块匿名共享内存中划分出来的。域asma表示这块被解锁的内存所属于的匿名共享内存,它通过域unpinned连接在asma->unpinned_list表示的列表中;域pgstart和paend表示这个内存块的开始和结束页面号,它们表示一个前后闭合的区间;域purged表示这个内存块占用的物理内存是否已经被回收,如果被回收,它的值为ASHMEM_WAS_PURGED,否则为ASHMEM_NOT_PURGED
这块被解锁的内存块除了保存在它所属的匿名共享内存asma的解锁列表unpinned_list之外,还通过域lru保存在一个全局的最近最少使用列表ashmem_lru_list列表中,处于解锁状态的内存块都是可以回收的,因此当系统内存不足时,内存管理系统就会回收ashmem_lru_list列表中的内存块
在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分别定义成全局变量
它的类型是struct kmem_cache,表示这是一个slab缓冲区,由内核中的内存管理系统进行管理。
创建完成之后,通过misc_register函数将Ashmem注册为misc设备。这里需要注意,我们对所创建的这些cache都需要进行回收,因此,再紧接着需调用register_shrinker注册回收函数ashmem_shrinker。注册名为ashmem的字符设备:
注册的字符操作函数集为ashmem_fops:
Ahshmem驱动程序在加载时,会创建一个/dev/ashmem的设备文件,这是一个misc类型的设备。注册misc设备是通过misc_register函数进行的调用这个函数成功后,就会在/dev目录下生成一个ashmem设备文件了。这个设备文件提供了open、mmap、release和ioctl四种操作,但没有写操作接口,这是因为写共享内存的方法是通过内存映射地址来进行的,即通过mmap系统调用把这个设备文件映射到进程地址空间中,然后就直接对内存进行读写了。
当应用程序使用open函数打开/dev/ashmem时,Ashmem驱动程序中的ashmem_open函数就会被调用,用来为应用程序创建一块匿名共享内存。
首先是通过nonseekable_open函数来设备这个文件不可以执行定位操作,即不可以执行seek文件操作。接着就是通过kmem_cache_zalloc函数从刚才我们创建的slab缓冲区ashmem_area_cachep来创建一个ashmem_area结构体了,并且保存在本地变量asma中。再接下去就是初始化变量asma的其它域,其中,域name初始为ASHMEM_NAME_PREFIX,这是一个宏,定义为:
函数的最后是把这个ashmem_area结构保存在打开文件结构体的private_data域中,这样,Ashmem驱动程序就可以在其它地方通过这个private_data域来取回这个ashmem_area结构了。到这里,设备文件/dev/ashmem的打开操作就完成了,它实际上就是在Ashmem驱动程序中创建了一个ashmem_area结构,表示一块新的共享内存,在不修改该匿名共享内存的名称情况下,默认名称为dev/ashmem。
当应用程序调用mmap函数将匿名共享内存设备文件映射到进程的地址空间时,ashmem_mmap被调用,在映射的过程中为该匿名共享内存块创建一个临时文件。
通过以上的ashmem_mmap函数就可以将创建的匿名共享内存映射到进程的虚拟地址空间中,对该匿名共享内存的操作就变成了直接操作进程的某块地址空间了。
设备文件/dev/ashmem是驱动程序提供给上层应用程序访问匿名共享内存的通道,通过该设备文件可以执行IOCTL操作。
函数首先从设备文件描述符的private_data域中取出打开匿名共享内存设备时创建的一块匿名共享内存区域的指针,然后根据IO命令,作不同的处理。
1.设置匿名共享内存名称
匿名共享内存名称设置过程很简单,就是将用户空间传过来的名称拼接到"dev/ashmem/"字符串后面。每一个匿名共享内存在临时文件系统tmpfs中都有相应的文件,文件名称为该匿名共享内存的名称,当匿名共享内存驱动程序为某一块匿名共享内存创建了临时文件,就不可以更改该文件名称了,因此也就无法更改该匿名共享内存的名称了,因此在修改匿名共享内存块的名称时,会首先判断其成员变量file是否已经指向了某个文件,如果是,就表示已经为该匿名共享内存块创建了对应的临时文件,修改名称操作就直接返回,无法更改匿名共享内存块的名称了。
2.获取匿名共享内存名称
获取匿名共享内存块名称很简单,就是将匿名内存块的名称返回到用户空间而已
3.设置匿名共享掩码
4.锁定或解锁匿名共享内存
匿名共享内存是以分块的形式来管理的,应用程序可以对这些小块内存进行锁定或解锁操作,处于解锁状态的内存在系统内存不足时,可以被内存管理系统回收的。匿名共享内存在创建时就被设置为锁定状态,当某块内存不在使用时,需要将其设置为解锁状态,从而允许内存管理系统回收该块内存。通过ASHMEM_PIN和ASHMEM_UNPIN两个ioctl操作来实现匿名共享内存的锁定和解锁操作。
ASHMEM_PIN和ASHMEM_UNPIN这两个命令号的定义:
它们的参数类型为struct ashmem_pin:
锁定或解锁指定内存块的过程如下:
首先是获得用户空间传进来的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)
page_range_subsumed_by_range宏描述了位置1,用来判断内存块range是否包含内存[start,end]
page_range_subsumes_range宏描述了位置4,用来判断内存块[start,end]是否包含内存块range
page_range_in_range宏描述了位置2和3,4
1)内存锁定
每一块内存默认情况下处于锁定状态,当它被解锁后,就会被插入到宿主匿名共享内存的unpinned_list链表中,只有位于这个链表中的内存块才可以执行锁定操作
指定要锁定的内存块[pgstart,pgend]和处于解锁状态的内存块range位置分以下4中情况:
一 指定要锁定的内存块[pgstart,pgend]包含了处于解锁状态的内存块range,这时候只需要将处于解锁状态的内存块range的开始地址修改为指定要锁定的内存块的末尾地址的下一个页面地址;
二 指定要锁定的内存块[pgstart,pgend]的后半部分与处于解锁状态的内存块range的前半部分相交,这时候只需要将处于解锁状态的内存块range的开始地址修改为指定要锁定的内存块的末尾地址的下一个页面地址;
三 指定要锁定的内存块[pgstart,pgend]的前半部分与处于解锁状态的内存块range的后半部分相交,这时候只需要将处于解锁状态的内存块range的末尾地址修改为指定要锁定的内存块的开始地址的上一个页面地址;
四 指定要锁定的内存块[pgstart,pgend]包含在解锁状态的内存块range中,这时候就将处于解锁状态的内存块划分为两部分,并调整划分后的解锁状态的内存块地址;
range_shrink函数用于修改一个处于解锁状态的内存块的开始地址或末尾地址
2)内存解锁
通过查询链表unpinned_list并调整解锁内存块的地址后,通过函数range_alloc完成解锁任务,并加入到unpinned_list链表中
宏range_on_lru用于判断内存块是否被回收
函数lru_add用于向全局链表ashmem_lru_list添加解锁的内存块
匿名共享内存驱动程序使用全局变量lru_count来统计保存在全局变量ashmem_lru_list中的处于解锁状态的内存块总大小,用来表示可以被内存管理系统回收的匿名共享内存的容量。宏range_size用于计算解锁内存块的大小:
匿名共享内存块回收
在匿名共享内存驱动程序初始化时,注册了内存回收操作函数
当系统内存不足时,函数ashmem_shrink将被调用,用于回收处于解锁状态的匿名共享内存