Android匿名共享内存(Ashmem)原理

写进程

int main()

{

void *shm = NULL;

struct shared_use_st *shared = NULL;

char buffer[BUFSIZ + 1];//用于保存输入的文本

int shmid;

//创建共享内存

shmid = shmget((key_t) 12345, sizeof(struct shared_use_st), 0666|IPC_CREAT);

//将共享内存连接到当前进程的地址空间

shm = shmat(shmid, (void*)0, 0);

printf(“Memory attached at %X\n”, (int)shm);

//设置共享内存

shared = (struct shared_use_st*)shm;

while(1)//向共享内存中写数据

{

//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本

while(shared->written == 1)

{

sleep(1);

}

//向共享内存中写入数据

fgets(buffer, BUFSIZ, stdin);

strncpy(shared->text, buffer, TEXT_SZ);

shared->written = 1;

if(strncmp(buffer, “end”, 3) == 0)

running = 0;

}

//把共享内存从当前进程中分离

if(shmdt(shm) == -1) { }

sleep(2);

exit(EXIT_SUCCESS);

}

可以看到,Linux共享内存通信效率非常高,进程间不需要传递数据,便可以直接访问,缺点也很明显,Linux共享内存没有提供同步的机制,在使用时,要借助其他的手段来处理进程间同步。Anroid本身在核心态是支持System V的功能,但是bionic库删除了glibc的shmget等函数,使得android无法采用shmget的方式实现有名共享内存,当然,它也没想着用那个,Android在此基础上,创建了自己的匿名共享内存方式。

Android的匿名共享内存

==============

Android可以使用Linux的一切IPC通信方式,包括共享内存,不过Android主要使用的方式是匿名共享内存Ashmem(Anonymous Shared Memory),跟原生的不太一样,比如它在自己的驱动中添加了互斥锁,另外通过fd的传递来实现共享内存的传递。MemoryFile是Android为匿名共享内存而封装的一个对象,这里通过使用MemoryFile来分析,Android中如何利用共享内存来实现大数据传递,同时MemoryFile也是进程间大数据传递的一个手段,开发的时候可以使用:

IMemoryAidlInterface.aidl

package com.snail.labaffinity;

import android.os.ParcelFileDescriptor;

interface IMemoryAidlInterface {

ParcelFileDescriptor getParcelFileDescriptor();

}

MemoryFetchService

public class MemoryFetchService extends Service {

@Nullable

@Override

public IBinder onBind(Intent intent) {

return new MemoryFetchStub();

}

static class MemoryFetchStub extends IMemoryAidlInterface.Stub {

@Override

public ParcelFileDescriptor getParcelFileDescriptor() throws RemoteException {

MemoryFile memoryFile = null;

try {

memoryFile = new MemoryFile(“test_memory”, 1024);

memoryFile.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});

Method method = MemoryFile.class.getDeclaredMethod(“getFileDescriptor”);

FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);

return ParcelFileDescriptor.dup(des);

} catch (Exception e) {}

return null;

}}}

TestActivity.java

Intent intent = new Intent(MainActivity.this, MemoryFetchService.class);

bindService(intent, new ServiceConnection() {

@Override

public void onServiceConnected(ComponentName name, IBinder service) {

byte[] content = new byte[10];

IMemoryAidlInterface iMemoryAidlInterface

= IMemoryAidlInterface.Stub.asInterface(service);

try {

ParcelFileDescriptor parcelFileDescriptor = iMemoryAidlInterface.getParcelFileDescriptor();

FileDescriptor descriptor = parcelFileDescriptor.getFileDescriptor();

FileInputStream fileInputStream = new FileInputStream(descriptor);

fileInputStream.read(content);

} catch (Exception e) {

}}

@Override

public void onServiceDisconnected(ComponentName name) {

}

}, Service.BIND_AUTO_CREATE);

以上是应用层使用匿名共享内存的方法,关键点就是文件描述符(FileDescriptor)的传递,文件描述符是Linux系统中访问与更新文件的主要方式。从MemoryFile字面上看出,共享内存被抽象成了文件,不过本质也是如此,就是在tmpfs临时文件系统中创建一个临时文件,(只是创建了节点,而没有看到实际的文件) 该文件与Ashmem驱动程序创建的匿名共享内存对应,可以直接去proc/pid下查看:

申请的共享内存在proc中的展示.jpg

下面就基于MemoryFile主要分析两点,共享内存的分配与传递,先看下MemoryFile的构造函数

public MemoryFile(String name, int length) throws IOException {

mLength = length;

mFD = native_open(name, length);

if (length > 0) {

mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);

} else {

mAddress = 0;

}

}

可以看到 Java层只是简单的封装,具体实现在native层 ,首先是通过native_open调用ashmem_create_region创建共享内存,

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);

}

接着通过native_mmap调用mmap将共享内存映射到当前进程空间,之后Java层就能利用FileDescriptor,像访问文件一样访问共享内存。

static jint android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor,

jint length, jint prot)

{

int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);

jint result = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0);

if (!result)

jniThrowException(env, “java/io/IOException”, “mmap failed”);

return result;

}

ashmem_create_region这个函数是如何向Linux申请一块共享内存的呢?

int ashmem_create_region(const char *name, size_t size)

{

int fd, ret;

fd = open(ASHMEM_DEVICE, O_RDWR);

if (fd < 0)

return fd;

if (name) {

char buf[ASHMEM_NAME_LEN];

strlcpy(buf, name, sizeof(buf));

ret = ioctl(fd, ASHMEM_SET_NAME, buf);

if (ret < 0)

goto error;

}

ret = ioctl(fd, ASHMEM_SET_SIZE, size);

if (ret < 0)

goto error;

return fd;

error:

close(fd);

return ret;

}

ASHMEM_DEVICE其实就是抽象的共享内存设备,它是一个杂项设备(字符设备的一种),在驱动加载之后,就会在/dev下穿件ashem文件,之后用户就能够访问该设备文件,同一般的设备文件不同,它仅仅是通过内存抽象的,同普通的磁盘设备文件、串行端口字段设备文件不一样:

#define ASHMEM_DEVICE “/dev/ashmem”

static struct miscdevice ashmem_misc = {

.minor = MISC_DYNAMIC_MINOR,

.name = “ashmem”,

.fops = &ashmem_fops,

};

接着进入驱动看一下,如何申请共享内存,open函数很普通,主要是创建一个ashmem_area对象

static int ashmem_open(struct inode *inode, struct file *file)

{

struct ashmem_area *asma;

int ret;

ret = nonseekable_open(inode, file);

if (unlikely(ret))

return ret;

asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);

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;

return 0;

}

接着利用ashmem_ioctl设置共享内存的大小,

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_SIZE:

ret = -EINVAL;

if (!asma->file) {

ret = 0;

asma->size = (size_t) arg;

}

break;

}

return ret;

}

可以看到,其实并未真正的分配内存,这也符合Linux的风格,只有等到真正的使用的时候,才会通过缺页中断分配内存,接着mmap函数,它会分配内存吗?

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)

{

struct ashmem_area *asma = file->private_data;

int ret = 0;

mutex_lock(&ashmem_mutex);

if (!asma->file) {

char *name = ASHMEM_NAME_DEF;

struct file *vmfile;

if (asma->name[ASHMEM_NAME_PREFIX_LEN] != ‘\0’)

name = asma->name;

// 这里创建的临时文件其实是备份用的临时文件,之类的临时文件有文章说只对内核态可见,用户态不可见,我们也没有办法通过命令查询到 ,可以看做是个隐藏文件,用户空间看不到!!

vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);

asma->file = vmfile;

}

get_file(asma->file);

if (vma->vm_flags & VM_SHARED)

shmem_set_file(vma, asma->file);

else {

if (vma->vm_file)

fput(vma->vm_file);

vma->vm_file = asma->file;

}

vma->vm_flags |= VM_CAN_NONLINEAR;

out:

mutex_unlock(&ashmem_mutex);

return ret;

}

其实这里就复用了Linux的共享内存机制,虽然说是匿名共享内存,但底层其实还是给共享内存设置了名称(前缀ASHMEM_NAME_PREFIX+名字),如果名字未设置,那就默认使用ASHMEM_NAME_PREFIX作为名称。不过,在这里没直接看到内存分配的函数。但是,有两个函数shmem_file_setup与shmem_set_file很重要,也是共享内存比较不好理解的地方,shmem_file_setup是原生linux的共享内存机制,不过Android也修改Linux共享内存的驱动代码,匿名共享内存其实就是在Linux共享内存的基础上做了改进,

struct file *shmem_file_setup(char *name, loff_t size, unsigned long flags)

{

int error;

struct file *file;

struct inode *inode;

struct dentry *dentry, *root;

struct qstr this;

error = -ENOMEM;

this.name = name;

this.len = strlen(name);

this.hash = 0; /* will go */

root = shm_mnt->mnt_root;

dentry = d_alloc(root, &this);//分配dentry cat/proc/pid/maps可以查到

error = -ENFILE;

file = get_empty_filp(); //分配file

error = -ENOSPC;

inode = shmem_get_inode(root->d_sb, S_IFREG | S_IRWXUGO, 0, flags);//分配inode,分配成功就好比建立了文件,也许并未存在真实文件映射

d_instantiate(dentry, inode);//绑定

inode->i_size = size;

inode->i_nlink = 0; /* It is unlinked */

// 文件操作符,这里似乎真的是不在内存里面创建什么东西???

init_file(file, shm_mnt, dentry, FMODE_WRITE | FMODE_READ,

&shmem_file_operations);//绑定,并指定该文件操作指针为shmem_file_operations

}

通过shmem_file_setup在tmpfs临时文件系统中创建一个临时文件(也许只是内核中的一个inode节点),该文件与Ashmem驱动程序创建的匿名共享内存对应,不过用户态并不能看到该临时文件,之后就能够使用该临时文件了,注意共享内存机制真正使用map的对象其实是这个临时文件,而不是ashmem设备文件,这里之所以是一次mmap,主要是通过vma->vm_file = asma->file完成map对象的替换,当映射的内存引起缺页中断的时候,就会调用shmem_file_setup创建的对象的函数,而不是ashmem的,看下临时文件的对应的hook函数,

void shmem_set_file(struct vm_area_struct *vma, struct file *file)

{

if (vma->vm_file)

fput(vma->vm_file);

vma->vm_file = file;

vma->vm_ops = &shmem_vm_ops;

}

到这里回到之前的MemoryFile,看一下写操作:

public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)

throws IOException {

if (isDeactivated()) {

throw new IOException(“Can’t write to deactivated memory file.”);

}

if (srcOffset < 0 || srcOffset > buffer.length || count < 0

|| count > buffer.length - srcOffset

|| destOffset < 0 || destOffset > mLength

|| count > mLength - destOffset) {

throw new IndexOutOfBoundsException();

}

native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);

}

进入native代码

static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,

jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,

jint count, jboolean unpinned)

{

int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);

if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {

ashmem_unpin_region(fd, 0, 0);

return -1;

}

env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);

if (unpinned) {

ashmem_unpin_region(fd, 0, 0);

}

return count;

}

在内核中,一块内存对应的数据结构是ashmem_area:

struct ashmem_area {

char name[ASHMEM_FULL_NAME_LEN];/* optional name for /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 prot_mask; /* allowed prot bits, as vm_flags */

};

当使用Ashmem分配了一块内存,部分不被使用时,就可以将这块内存unpin掉,内核可以将unpin对应的物理页面回收,回收后的内存还可以再次被获得(通过缺页handler),因为unpin操作并不会改变已经mmap的地址空间,不过,MemoryFile只会操作整个共享内存,而不会分块访问,所以pin与unpin对于它没多大意义,可以看做整个区域都是pin或者unpin的,首次通过env->GetByteArrayRegion访问会引发缺页中断,进而调用tmpfs 文件的相应操作,分配物理页,在Android现在的内核中,缺页中断对应的vm_operations_struct中的函数是fault,在共享内存实现中,对应的是shmem_fault如下,

static struct vm_operations_struct shmem_vm_ops = {

.fault = shmem_fault,

#ifdef CONFIG_NUMA

.set_policy = shmem_set_policy,

.get_policy = shmem_get_policy,

#endif

};

当mmap的tmpfs文件引发缺页中断时, 就会调用shmem_fault函数,

static int shmem_fault(struct vm_area_struct *vma, struct vm_fault *vmf)

{

struct inode *inode = vma->vm_file->f_path.dentry->d_inode;

int error;

int ret;

if (((loff_t)vmf->pgoff << PAGE_CACHE_SHIFT) >= i_size_read(inode))

return VM_FAULT_SIGBUS;

error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret);

if (error)

return ((error == -ENOMEM) ? VM_FAULT_OOM : VM_FAULT_SIGBUS);

return ret | VM_FAULT_LOCKED;

}

到这里,就可以看到会调用shmem_getpage函数分配真实的物理页,具体的分配策略比较复杂,不在分析。

Android匿名共享内存的pin与unpin

=======================

pin本身的意思是压住,定住,ashmem_pin_region和ashmem_unpin_region这两个函数从字面上来说,就是用来对匿名共享内存锁定和解锁,标识哪些内存正在使用需要锁定,哪些内存是不使用的,这样,ashmem驱动程序可以一定程度上辅助内存管理,提供一定的内存优化能力。匿名共享内存创建之初时,所有的内存都是pinned状态,只有用户主动申请,才会unpin一块内存,只有对于unpinned状态的内存块,用户才可以重新pin。现在仔细梳理一下驱动,看下pin与unpin的实现

static int __init ashmem_init(void)

{

int ret;

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);

register_shrinker(&ashmem_shrinker);

return 0;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android进阶资料

以下的资料是近年来,我和一些朋友面试收集整理了很多大厂的面试真题和资料,还有来自如阿里、小米、爱奇艺等一线大厂的大牛整理的架构进阶资料。希望可以帮助到大家。

Android进阶核心笔记

百万年薪必刷面试题

最全Android进阶学习视频

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-7GRGo2lh-1712215355435)]

[外链图片转存中…(img-4FFwoAWS-1712215355436)]

[外链图片转存中…(img-5w5F5cN4-1712215355436)]

[外链图片转存中…(img-sJQhMA1S-1712215355436)]

[外链图片转存中…(img-FuQZQIlU-1712215355437)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android进阶资料

以下的资料是近年来,我和一些朋友面试收集整理了很多大厂的面试真题和资料,还有来自如阿里、小米、爱奇艺等一线大厂的大牛整理的架构进阶资料。希望可以帮助到大家。

Android进阶核心笔记

[外链图片转存中…(img-j949WXlQ-1712215355437)]

百万年薪必刷面试题

[外链图片转存中…(img-7Wdurqk7-1712215355437)]

最全Android进阶学习视频

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值