Ashmem机制

在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()还是可以拿到一个合法的文件描述符的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值