在Android平台上,提供了一种共享内存的机制——Ashmem。这种机制内部其实复用了Linux共享内存机制。Ashmem机制使用Linux的mmap系统调用,可以将同一段物理内存映射到不通进城各自的虚拟地址空间,从而实现高效的进程间共享。
Linux上“一切皆文件”,一块共享内存当然也不例外。因此,在用户态我们能看到的重要概念就是共享内存的“文件描述符”,文件描述符可以对应一个内核态的ashmem file。file中又可以管理自己的逻辑数据(ashmem_area)。不同进程里的不同文件描述符可以对应同一个内核态的file,这就是跨进程共享的基础。当我们对这个文件描述符做完mmap操作后,一般都会记下映射好的起始地址,这是后续进行读取、写入操作的一个基准值,在后文要说的MemoryFile里,这个基准值会记在mAddress成员变量里。
我们先画一张示意图对ashmem有个大提升的了解:
1. 以MemoryFile为切入点
我们不大可能直接使用Ashmem,为此Android提供了一个MemoryFile类,其内部实现就是基于ashmem的。MemoryFile本身虽不太常用,但我们可以以这个类为切入点,来看看ashmem的细节。
// frameworks/base/core/java/android/os/MemoryFile.java
public class MemoryFile
{
. . . . . .
private static native FileDescriptor native_open(String name, int length) throws IOException;
private static native long native_mmap(FileDescriptor fd, int length, int mode)
throws IOException;
. . . . . .
public MemoryFile(String name, int length) throws IOException {
mLength = length;
if (length >= 0) {
mFD = native_open(name, length);
} else {
throw new IOException("Invalid length: " + length);
}
if (length > 0) {
mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);
} else {
mAddress = 0;
}
}
在其构造的时候,主要就是调用了native_open()和native_mmap()。这两个函数对应着C++层的android_os_MemoryFile_open()和android_os_MemoryFile_mmap(),"open"用于创建一个共享内存区域,"mmap"用于进行内存映射。但是具体的创建和映射动作其实都是在内核态完成的,这就涉及到Ashmem驱动程序的内容。
在Android平台上,Ashmem是作为一个驱动程序存在的。我们在Ashmem.c文件中,可以看到这个驱动的入口函数ashmem_init():【kernel/drivers/staging/android/Ashmem.c】
module_init(ashmem_init); // 初始化动作
module_exit(ashmem_exit); // 退出动作
ashmem驱动的初始化动作如下:
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
static int __init ashmem_init(void)
{
. . . . . .
ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
sizeof(struct ashmem_area),
0, 0, NULL);
. . . . . .
ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
sizeof(struct ashmem_range),
0, 0, NULL);
. . . . . .
ret = misc_register(&ashmem_misc); // 注册file_operations
. . . . . .
}
其中,ashmem_misc的定义如下:
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
static const 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,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_ashmem_ioctl,
#endif
};
static struct miscdevice ashmem_misc = { // 用于注册杂项从设备
.minor = MISC_DYNAMIC_MINOR,
.name = "ashmem",
.fops = &ashmem_fops,
};
其实是向系统内部注册了一个“ashmem杂项从设备”。在linux系统中,杂项设备(misc device)其实是个特殊的字符设备。我们可以为杂项设备注册多个“从设备”,“ashmem杂项从设备”只是其中之一。
要使用一块ashmem内存,其实是所到底就是要操作“ashmem杂项从设备”,而操作设备的动作主要就体现在对设备执行注入open、read、write、mmap、ioctl等文件操作。这些文件操作在ashmem驱动层就对应为上面代码中的ashmem_open、ashmem_mmap等函数。
2.创建共享内存区域
我们回过头说前文的android_os_MemoryFile_open()函数:
【frameworks/base/core/jni/android_os_MemoryFile.cpp】
static jobject android_os_MemoryFile_open(JNIEnv* env, jobject clazz, jstring name,
jint length)
{
const char* namestr = (name ? env->GetStringUTFChars(name, NULL) : NULL);
int result = ashmem_create_region(namestr, length);
if (name)
env->ReleaseStringUTFChars(name, namestr);
if (result < 0) {
jniThrowException(env, "java/io/IOException", "ashmem_create_region failed");
return NULL;
}
return jniCreateFileDescriptor(env, result);
}
其中最重要的一句是调佣ashmem_create_region(),它的返回值如果大于等于0,就说明返回的是一个合法的文件描述符,这种描述符还得进一步包装成Java层的FileDescriptor,所以需要在最后调用叫你CreateFileDescriptor()。
当我们要创建一块共享内存区域时,我们需要指明这个区域的名字以及该区域的大小。可以参考一下system/core/libcutils/Ashmem-dev.c文件里的ashmem_create_region()函数的调用关系,可以绘制出下图:
可以看到,在创建一块共享内存区域时,我们用到了open()、ioctl()等操作,它们分别对应着前文ashmem_fops里的ashmem_open()、ashmem_ioctl()函数。
ashmem_create_region()所返回的指代匿名共享内存的文件描述符,可以在手机等设备的/proc/[pid]/maps里看到,同时还能看到这块共享内存对应的名字。当然,我们也可以创建多个ashmem共享内存区域,它们会对应不同的inode和file。
2.1 ashmem_open()操作
ashmem驱动程序一般位于Ashmem.c文件中,其中ashmem_open()的实现代码如下:
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
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); // 申请一块ashmem_area内存
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; // 将申请的ashmem_area记入file->private_data
return 0;
}
上面代码说明,当我们创建一块ashmem共享内存时,其实是在内核层打开了一个ashmem file,而且这个file的private_data里记录了一块ashmem_area。如图所示:
ashmem_area的定义如下:
struct ashmem_area {
char name[ASHMEM_FULL_NAME_LEN]; /* optional name in /proc/pid/maps */
struct list_head unpinned_list; /* list of all ashmem areas */
struct file *file; /* the shmem-based backing file */
size_t size; /* size of the mapping, in bytes */
unsigned long vm_start; /* Start address of vm_area
* which maps this ashmem */
unsigned long prot_mask; /* allowed prot bits, as vm_flags */
};
其中最重要的当然是file域,一开始这个域的值为null。
2.2 ashmem_ioctl()操作
ashmem_open()之后,紧接着要设置刚打开的共享内存文件的一些属性,于是调用到ioctl()对一个的ashmem_ioctl()。主要的设置动作其实就是向ashmem_area里写入一些数据。ashmem_ioctl()函数的定义如下:
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
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_SET_SIZE:
ret = -EINVAL;
if (!asma->file) {
ret = 0;
asma->size = (size_t) arg;
}
break;
. . . . . .
. . . . . .
}
return ret;
}
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
static int set_name(struct ashmem_area *asma, void __user *name)
{
. . . . . .
len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
. . . . . .
mutex_lock(&ashmem_mutex);
. . . . . .
strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);
mutex_unlock(&ashmem_mutex);
. . . . . .
}
实际设置的区域名是:“dev/ashmem/” + “传入的名字”。这样,我们就可以得到下面这张图:
3. 映射共享内存区域
在上图这种“file里面套file”的结构中,内层那个file究竟是什么时候打开的呢?简单地说,就是在我们针对这块共享内存做mmap操作的时候。mmap操作在驱动层对应的是ashmem_mmap()函数。
3.1 ashmem_mmap()操作
【kernel/msm-3.18/drivers/staging/android/Ashmem.c】
static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
struct ashmem_area *asma = file->private_data;
. . . . . .
vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
if (!asma->file) {
. . . . . .
vmfile = shmem_file_setup(name, asma->size, vma->vm_flags); // 创建文件节点
. . . . . .
asma->file = vmfile; // ashmem_area里的file终于有值了!
}
get_file(asma->file);
if (vma->vm_flags & VM_SHARED)
shmem_set_file(vma, asma->file);
else {
. . . . . .
}
asma->vm_start = vma->vm_start; // vm_area_struct里的必要信息,复制到ashmem_area中
. . . . . .
return ret;
}
最关键的一步是vmfile = shmem_file_setup(…),调用的其实是linux系统的接口,在linux“内存文件系统”里创建一个文件节点。shmem_file_setup()的代码如下:
【kernel/msm-3.18/mm/Shmem.c】
struct file *shmem_file_setup(const char *name, loff_t size, unsigned long flags)
{
return __shmem_file_setup(name, size, flags, 0);
}
static struct file *__shmem_file_setup(const char *name, loff_t size,
unsigned long flags, unsigned int i_flags)
{
struct file *res;
struct inode *inode;
. . . . . .
. . . . . .
inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
. . . . . .
d_instantiate(path.dentry, inode);
inode->i_size = size;
. . . . . .
res = alloc_file(&path, FMODE_WRITE | FMODE_READ,
&shmem_file_operations);
. . . . . .
return res;
. . . . . .
}
shmem_file_setup()建立的file对应的文件操作(shmem_file_operations)如下:
【kernel/msm-3.18/mm/Shmem.c】
static const struct file_operations shmem_file_operations = {
.mmap = shmem_mmap,
#ifdef CONFIG_TMPFS
.llseek = shmem_file_llseek,
.read = new_sync_read,
.write = new_sync_write,
.read_iter = shmem_file_read_iter,
.write_iter = generic_file_write_iter,
.fsync = noop_fsync,
.splice_read = shmem_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = shmem_fallocate,
#endif
};
ashmem_mmap()的调用关系图如下:
经过mmap操作,file里面套file的结构就完成了,示意图如下:
注意,上图中的两个file对应的文件操作是不一样的。第一个是ashmem层次的file,第二个是tmpfs虚拟文件系统里的file。补充说明一下,tmpfs(temporary filesystem)是Linux特有的文件系统,标准挂载点是/dev/shm。其内部使用物理内存或swap交换空间实现了一套独立的文件系统。该系统不是块设备系统,所以不需要格式化操作,只要成功挂载,就可以立即使用。
现在我们把“创建共享内存区域”和“映射共享内存区域”两个小节的内容汇整成一张示意图,图中表示了两个步骤,创建和映射,最终完成双file结构:
3.2 munmap()操作
ashmem共享内存只有在成功mmap以后,才能够读写。不过MemoryFile允许用户在需要时收回命令,取消mmap。为此,它提供了deactivate()函数。该函数的代码如下:
【frameworks/base/core/java/android/os/MemoryFile.java】
void deactivate() {
if (!isDeactivated()) {
try {
native_munmap(mAddress, mLength); // 其实就是在做munmap动作
mAddress = 0; // 一旦销毁了映射,mAddress也必须设为0
} catch (IOException ex) {
Log.e(TAG, ex.toString());
}
}
}
native_munmap()对应的C++层函数是android_os_MemoryFile_munmap(),其定义如下:【frameworks/base/core/jni/android_os_MemoryFile.cpp】
static void android_os_MemoryFile_munmap(JNIEnv* env, jobject clazz, jlong addr, jint length)
{
int result = munmap(reinterpret_cast<void *>(addr), length);
if (result < 0)
jniThrowException(env, "java/io/IOException", "munmap failed");
}
可以看到其实就是在调用munmap()操作。
munmap()并不像mmap()那样有对应的ashmem_mmap(),也就是说不存在ashmem_munmap()。munmap()的工作完全由系统内核完成。注意,munmap()只会解除内存映射关系,却不会关闭共享内存。这也就是说,此时的读写操作虽然会失败,但调用getFileDescriptor()还是可以拿到一个合法的文件描述符的。