Android匿名共享内存
在Android中我们熟知的IPC方式有Socket
、文件
、ContentProvider
、Binder
、共享内存
。其中共享内存
的效率最高,可以做到0拷贝,在跨进程进行大数据传输,日志收集等场景下非常有用。共享内存
是Linux自带的一种IPC机制,Android直接使用使用了该模型,不过做出了自己的改进,进而形成了Android的匿名共享内存。
本文将会通过android提供的MemoryFile
源码来分析如何使用匿名共享内存,并使用native层代码实现一个简易版的MemoryFile。
MemoryFile简单使用
//MainActivity.kt 进程1
class MainActivity : AppCompatActivity() {
var mBinder: Binder? = null
val memoryFile:MemoryFile? = null
private var mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
mBinder = service as Binder
}
override fun onServiceDisconnected(className: ComponentName) {
mBinder = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val intent = Intent(this, TestShareMemoryService::class.java)
startService(intent)
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
//1.创建共享内存,并通过binder传递文件描述符
fun createMemoryFile(view: View) {
//参数1文件名可为null,参数2文件大小
memoryFile = MemoryFile("test", 1024)
memoryFile?.apply {
mBinder?.apply {
val data = Parcel.obtain()
val reply = Parcel.obtain()
val getFileDescriptorMethod: Method =
memoryFile.getClass().getDeclaredMethod("getFileDescriptor")
val fileDescriptor = getFileDescriptorMethod.invoke(memoryFile)
// 序列化,才可传送
val pfd = ParcelFileDescriptor.dup(fileDescriptor)
data.writeFileDescriptor(fileDescriptor)
transact(TestShareMemoryService.TRANS_CODE_SET_FD, data, reply, 0)
}
}
}
//2.写入数据
fun write(data:ByteArray) {
memoryFile.write(data, 0, 0, data.size);
}
}
复制代码
//MainActivity2.kt 进程2
class MainActivity2 : AppCompatActivity() {
var mBinder: IBinder? = null
private var mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
mBinder = service
}
override fun onServiceDisconnected(className: ComponentName) {
mBinder = null
}
}
fun read(view: View) {
val data = Parcel.obtain()
val reply = Parcel.obtain()
mBinder?.apply {
//从服务端获取MainActivity传递的文件描述符
transact(TestShareMemoryService.TRANS_CODE_GET_FD, data, reply, 0)
var fi: FileInputStream? = null
var fileDescriptor: FileDescriptor? = null
try {
val pfd = reply.readFileDescriptor()
if (pfd == null) {
return
}
fileDescriptor = pfd.fileDescriptor
fi = FileInputStream(fileDescriptor)
//读取数据
fi.read(buffer)
}
} catch (e: RemoteException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (fileDescriptor != null) {
try {
fi.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
val intent = Intent(this, TestShareMemoryService::class.java)
startService(intent)
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
}
复制代码
//TestShareMemoryService.kt
class TestShareMemoryService : Service() {
lateinit var fd: ParcelFileDescriptor
companion object {
const val TRANS_CODE_GET_FD = 0x0000
const val TRANS_CODE_SET_FD = 0x0001
}
override fun onBind(intent: Intent?): IBinder {
return TestBinder()
}
inner class TestBinder : Binder() {
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
when (code) {
TRANS_CODE_SET_FD -> {
//保存创建共享内存进程传递过来的文件描述符
fd = data.readFileDescriptor()
}
TRANS_CODE_GET_FD -> {
//将文件描述符传递给请求的进程
reply?.writeFileDescriptor(fd.fileDescriptor)
}
}
return true
}
}
}
复制代码
梳理一下流程
- 1、进程1创建MemoryFile并写入数据
- 2、通过Binder将MemoryFile的文件描述符传递到进程2
- 3、进程2通过获取到的文件描述符进行数据的读写
这里流程中的第二步有一个问题,从进程1将文件描述符
传递到进程2,那么这两个进程的文件描述符
是同一个吗?
答案是这两个文件描述符
并不是同一个,只不过他们都指向了内核中的同一个文件。
文件描述符
linux系统中的文件描述符是什么?在回答这个问题前先来看一下linux系统中进程是什么?
在linux系统中进程实际上就是一个结构体,而且线程和进程使用的是同一个结构体,其部分源码如下:
struct task_struct {
// 进程状态
long state;
// 虚拟内存结构体
struct mm_struct *mm;
// 进程号
pid_t pid;
// 指向父进程的指针
struct task_struct __rcu *parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct *fs;
// 一个数组,包含该进程打开的文件指针
struct files_struct *files;
};
复制代码
可以看到在结构体中有一个files
字段,它记录着该进程打开的文件指针,而我们所说的文件描述符
实际上就是这个files数组的索引,他们的关系如下图所示:
[图片上传中...(image-348894-1608187646036-1)]
为了画图方便,这里将fd1
和fd2
都写成了1,实际上每个进程被创建时,files
的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。所以进程的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。
从图中可以看出fd1
和fd2
其实并没有直接的关系,那么进程2是如何通过进程1的fd1
来生成一个同fd1
指向同一个 文件呢?
回想一下我们是怎么把fd1
转成fd2
的,是通过Binder#transact
方法实现的,因此我们来看一下Binder
的源码是如何做的
//Binder.c
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply) {
switch(fp->type) {
case BINDER_TYPE_FD: {
int target_fd;
struct file *file;
// 通过进程1的fp->handle获取到真正的文件,在内核中是唯一的fd指向它
file = fget(fp->handle);
// 获取目标进程中未使用的文件描述符fd
target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
// 将目标进程的文件描述符fd和该file进行配对,这样目标进程就能通过target_fd找到file
task_fd_install(target_proc, target_fd, file);
} break;
}
}
复制代码
看了源码我们发现原理非常简单,其实就是通过内核中的Binder帮我们进行转换的,因为内核是有所有用户进程信息,所以它可以轻松的做到这一点。
还有一点需要说明的是,在上图中的file1,file2,file3并不一定是存在磁盘上的物理文件,也有可能是抽象的文件(虚拟文件),而本篇文章说的匿名共享内存
实际上就是映射到一个虚拟的文件,至于这块的内容可以看一下Linux的tmpfs文件系统 。
MemoryFile源码解析
共享内存的基础知识上面做了简单的介绍,现在来看看Android是如何做的。MemoryFile
是Android提供的java层匿名共享内存工具,通过它的源码来跟踪整个流程。
相关文件列表:
frameworks/base/core/java/android/os/
- MemoryFile.java
- SharedMemory.java
frameworks/base/core/jni/android_os_SharedMemory.cpp
system/core/libcutils/ashmem-dev.cpp
复制代码
//MemoryFile.java
public MemoryFile(String name, int length) throws IOException {
//通过SharedMemory创建匿名共享内存
mSharedMemory = SharedMemory.create(name, length);
//映射
mMapping = mSharedMemory.mapReadWrite();
}
复制代码
//SharedMemory
public static @NonNull SharedMemory create(@Nullable String name, int size)
throws ErrnoException {
//实际上调用了native层去创建匿名共享内存,并返回文件描述符
return new SharedMemory(nCreate(name, size));
}
private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;
复制代码
//android_os_SharedMemory.cpp
jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) {
const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;
//调用ashmem_create_region来创建匿名共享内存
int fd = ashmem_create_region(name, size);
//...
jobject jifd = jniCreateFileDescriptor(env, fd);
if (jifd == nullptr) {
close(fd);
}
return jifd;
}
复制代码
// ashmem-dev.cpp
int ashmem_create_region(const char *name, size_t size)
{
int ret, save_errno;
//打开匿名共享内存对应的虚拟文件,最终调用到 __ashmem_open_locked()
int fd = __ashmem_open();
if (fd < 0) {
return fd;
}
if (name) {
char buf[ASHMEM_NAME_LEN] = {0};
strlcpy(buf, name, sizeof(buf));
//通过ioctl设置名字,TEMP_FAILURE_RETRY宏定义会让返回的结果为false时一直重试
//ioctl是系统调用,用户进程和内存进行交互,内部调用copy_from_user获取到用户进程传递的数据
ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));
if (ret < 0) {
goto error;
}
}
//设置匿名共享文件大小
ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));
if (ret < 0) {
goto error;
}
return fd;
error:
save_errno = errno;
close(fd);
errno = save_errno;
return ret;
}
static std::string get_ashmem_device_path() {
static const std::string boot_id_path = "/proc/sys/kernel/random/boot_id";
std::string boot_id;
if (!android::base::ReadFileToString(boot_id_path, &boot_id)) {
ALOGE("Failed to read %s: %s.\n", boot_id_path.c_str(), strerror(errno));
return "";
};
boot_id = android::base::Trim(boot_id);
return "/dev/ashmem" + boot_id;
}
static int __ashmem_open_locked()
{
//获取匿名共享内存路径,Android Q之后使用这个方式获取
static const std::string ashmem_device_path = get_ashmem_device_path();
if (ashmem_device_path.empty()) {
return -1;
}
//打开匿名共享内存使用的虚拟文件
int fd = TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC));
// Android Q之前的设备这里fd < 0,使用原来的路径"/dev/ashmem"
if (fd < 0) {
int saved_errno = errno;
//打开匿名共享内存使用的虚拟文件
fd = TEMP_FAILURE_RETRY(open("/dev/ashmem", O_RDWR | O_CLOEXEC));
if (fd < 0) {
return fd;
}
}
//...
return fd;
}
复制代码
以上是获取匿名共享内存的文件描述符流程,总结一下核心的部分,只例举Android Q之前:
- 1、open("/dev/ashmem", O_RDWR | O_CLOEXEC),打开虚拟文件
- 2、ioctl(fd, ASHMEM_SET_NAME, buf),设置名字
- 3、ioctl(fd, ASHMEM_SET_SIZE, size),设置大小
接下来来看一下如何通过文件描述符
映射到共享内存中
如上面分析的代码,在MemoryFile的构造函数中先调用了SharedMemory#create(name, size)
方法创建了匿名文件,之后调用SharedMemory.mapReadOnly()
来将匿名文件映射到共享内存中,最终调用到了如下方法中:
//SharedMemory.java
public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
//通过mFileDescriptor文件描述符进行内存映射,并返回内存地址
long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset);
boolean readOnly = (prot & OsConstants.PROT_WRITE) == 0;
//取消内存映射的Runnable,run方法中会调用Os.munmap(mAddress, mSize);
Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire());
//使用DirectByteBuffer直接对内存进行读写操作
return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly);
}
复制代码
如果对linux系统熟悉的话看到Os.mmap()
和Os.munmap()
方法应该能知道内存映射实际上就是调用的linux系统函数mmap
和munmap
函数,看一下man手册中的介绍
mmap, munmap - map or unmap files or devices into memory
- mmap,映射文件或设备到内存中
- munmap ,取消文件或设备到内存的映射
实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建 立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回 文件的。因此,采用共享内存的通信方式效率是非常高的。
看一下官方的注释,放了个连接,打开一看果然调用的mmap
//Os.java
/**
* See <a href="http://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>.
*/
public static long mmap(long address, long byteCount, int prot, int flags, FileDescriptor fd, long offset) throws ErrnoException { return Libcore.os.mmap(address, byteCount, prot, flags, fd, offset); }
复制代码
至此,MemoryFile
源码的核心可以说是分析完了。
最后,稍微提一下linux内存映射的原理:
linux下,内存采用分页存储,一个物理页的大小是4k(即你理解的内存块),物理页有页号,如果a,b两个进程共享了8k的内存,比如代码区相同,则在双方进程的页表中(线性地址到物理地址的转换表,linux下逻辑地址和线性地址相同)会将各自的线性地址映射到那两个相同的物理页面上去。实际上内存就只有一份数据。
[图片上传中...(image-f7cbfe-1608187646035-0)]
native实现一个简易版MemoryFile
现在来自定义一个MemoryFile,用到核心方法:
open(ASHMEM_NAME_DEF, O_RDWR);
mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
ioctl(fd, ASHMEM_SET_NAME, name);
ioctl(fd, ASHMEM_SET_SIZE, size);
munmap((void *) addr, size);
复制代码
第一步我们先把api接口定义出来,代码如下
class MyShareMemory(fd: Int) {
private val mFd: Int = fd
private val mSize: Int
init {
mSize = nGetSize(mFd)
require(mSize > 0) { "FileDescriptor is not a valid ashmem fd" }
}
//获取可以使用Binder传输文件描述符的对象,用于跨进程传输文件描述符
fun getFileDescriptor(): FileDescriptor {
return ParcelFileDescriptor.fromFd(mFd).fileDescriptor;
}
companion object {
init {
System.loadLibrary("mysharememory-lib")
}
fun create(name: String, size: Int): MyShareMemory {
require(size > 0) { "Size must be greater than zero" }
return MyShareMemory(nCreate(name, size))
}
//创建需要映射的匿名文件
@JvmStatic
private external fun nCreate(name: String, size: Int): Int
//获取大小
@JvmStatic
private external fun nGetSize(fd: Int): Int
//关闭文件并解除映射
@JvmStatic
private external fun nClose(fd: Int)
//写数据,这里的offset只设置了destOffset,没有写srcOffset,可以完善,nRead同理
@JvmStatic
private external fun nWrite(fd: Int, size: Int, offset: Int, data: ByteArray): Int
//读数据
@JvmStatic
private external fun nRead(fd: Int, size: Int, offset: Int, data: ByteArray): Int
}
}
复制代码
接下来来实现这5个jni方法
extern "C"
JNIEXPORT jint JNICALL
Java_com_baidu_ryujin_ktc_MyShareMemory_nCreate(JNIEnv *env, jclass clazz, jstring name,
jint size) {
char *addr;
int64_t ufd = 0;
const char *_name = env->GetStringUTFChars(name, 0);
//打开匿名文件并进行映射,addr为映射内存的地址,ufd为文件描述符
int ret = create_shared_memory(_name, size, addr, ufd);
env->ReleaseStringUTFChars(name, _name);
return ufd;
}extern "C"
JNIEXPORT jint JNICALL
Java_com_baidu_ryujin_ktc_MyShareMemory_nGetSize(JNIEnv *env, jclass clazz,
jint fd) {
return get_shared_memory_size(fd);
}extern "C"
JNIEXPORT void JNICALL
Java_com_baidu_ryujin_ktc_MyShareMemory_nClose(JNIEnv *env, jclass clazz, jint fd) {
char *addr;
//这里调用open去映射内存是为了获取addr,因为取消映射需要用到,这里是为了方便这么做,实际使用中可以保存起来
open_shared_memory(addr, fd);
close_shared_memory(fd, addr);
}extern "C"
JNIEXPORT jint JNICALL
Java_com_baidu_ryujin_ktc_MyShareMemory_nWrite(JNIEnv *env, jclass clazz, jint fd,
jint size, jint offset, jbyteArray data_) {
char *addr;
int space = get_shared_memory_size(fd) - offset;
if (size - space > 0) {
return -1;
}
//同close一样,这里也是为了获取addr
open_shared_memory(addr, fd);
jbyte *data = env->GetByteArrayElements(data_, 0);
//获取到共享内存地址后直接往里面写数据就行了
memcpy(addr + offset, data, size);
env->ReleaseByteArrayElements(data_, data, 0);
return 0;
}extern "C"
JNIEXPORT jint JNICALL
Java_com_baidu_ryujin_ktc_MyShareMemory_nRead(JNIEnv *env, jclass clazz, jint fd, jint size,
jint offset, jbyteArray data_) {
//...
return 0;
}
复制代码
核心实现代码
int create_shared_memory(const char *name, int64_t size, char *&addr, int64_t &fd) {
fd = open(ASHMEM_NAME_DEF, O_RDWR);//#define ASHMEM_NAME_DEF "dev/ashmem"
if (fd < 0) {
return -1;
}
int len = get_shared_memory_size(fd);
if (len > 0) {//改fd已经映射,直接获取地址
addr = (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
return 1;
} else {//未映射
int ret = ioctl(fd, ASHMEM_SET_NAME, name);//设置名称
if (ret < 0) {
close(fd);
return -1;
}
ret = ioctl(fd, ASHMEM_SET_SIZE, size);//设置大小
if (ret < 0) {
close(fd);
return -1;
}
//内存映射
addr = (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
}
return 0;
}
int open_shared_memory(char *&addr, int64_t fd) {
int size = get_shared_memory_size(fd);
if (size > 0) {
addr = (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
} else {
return -1;
}
return 0;
}
int close_shared_memory(int64_t fd, char *&addr) {
int size = get_shared_memory_size(fd);
if (size < 0) {
return -1;
}
//取消映射
int ret = munmap((void *) addr, size);
if (ret == -1) {
return -1;
}
ret = close(fd);
if (ret == -1) {
return -1;
}
return 0;
}
int get_shared_memory_size(int64_t fd) {
return ioctl(fd, ASHMEM_GET_SIZE, NULL);
}
复制代码
现在就可以像MemoryFile一样使用自定义的MemoryFile进行跨进程数据传输了,具体的可以github上的demo,CustomAnroidShareMemory 。
最后讨论一下两个问题,以下仅为个人思考,欢迎补充和指正:
一、Android为什么设计一个匿名共享内存,共享内存不能满足需求吗?
首先我们来思考一下共享内存和Android匿名共享内存最大的区别,那就是共享内存往往映射的是一个硬盘中真实存在的文件,而Android的匿名共享内存映射的一个虚拟文件。这说明Android又想使用共享内存进行跨进程通信,又不想留下文件,同时也不想被其它的进程不小心打开了自己进程的文件,因此使用匿名共享内存的好处就是:
- 不用担心共享内存映射的文件被其它进程打开导致数据异常。
- 不会在硬盘中生成文件,使用匿名共享内存的方式主要是为了通信,而且通信是很频繁的,不希望因为通信而生成很多的文件,或者留下文件。
二、为什么叫匿名共享内存?明明通过iotc
设置了名字的?
这个问题在我看来是我之前对匿名这个词有些误解,其实匿名并不是没有名字,而是无法通过这些明面上的信息找到实际的对象,就像马甲一样。匿名共享内存也正是如此,虽然我们设置了名字,但是另外的进程通过同样的名字创建匿名共享内存却并不指向同一个内存了(代码验证过),虽然名字相同,但是背后的人却已经换了。这同时也回答上个问题,为什么匿名共享内存不用担心被其它进程映射进行数据读写(除非经过自己的同意,也就是通过binder传递了文件描述符给另一个进程)。
作者:进击的鱼儿
链接:https://juejin.cn/post/6906444643089514510
来源:掘金
网友评论