声明
- 其实对于Android系统的Ashmem匿名共享内存系统早就有分析的想法,记得2019年6、7月份Mr.Deng离职期间约定一起对其进行研究的,但因为我个人问题没能实施这个计划,留下些许遗憾…
- 文中参考了很多书籍及博客内容,可能涉及的比较多先不具体列出来了;
- 本文使用的代码是LineageOS的cm-14.1,对应Android 7.1.2,可以参考我的另一篇博客:cm-14.1 Android系统启动过程分析(1)-如何下载Nexus5的LineageOS14.1(cm-14.1)系统源码并编译、刷机
- 若对JNI不熟悉,可参考此专栏:Androd系统的JNI与NDK
1 Ashmem的架构
Android 系统实现的 Ashmem 匿名共享内存子系统,用来在应用程序之间共享数据。Ashmem 与传统的Linux系统实现的共享内存一样,都是基于内核提供的临时文件系统tmpfs实现的,但是 Ashmem 对内存块进行了更为精细化的管理。应用程序可以动态地将一块匿名共享内存划分为若干个小块,当这些小块内存不再需要使用时,它们就可以被内存管理系统回收。通过这种动态的、分而治之的内存管理方式,Android系统就能够有效地使用系统内存,适应内存较小的移动设备环境。
匿名共享内存系统是以Ashmem驱动程序为基础的,系统中所有的匿名共享内存都由Ashmem驱动程序负责分配和管理。Android系统在 Native 层提供了 C/C++ 调用接口和 Framework 层提供了 Java 调用接口。
- 在Framework 层中,提供了两个C++类 MemoryBase 和 MemoryHeapBase,以及一个 Java 类 MemoryFile 来使用匿名共享内存。
- 在运行时库 cutils 中,主要提供了三个C函数 ashmem_create_region、ashmem_pin_region 和 ashmem_unpin_region 来访问 Ashmem 驱动程序。
Ashmem驱动程序在启动时,会创建一个 /dev/ashmem 设备文件,这样,运行时库 cutils 中的匿名共享内存接口就可以通过文件操作函数 open 和 ioctl 等来访问 Ashmem 驱动程序。
传统的 Linux 系统使用一个整数来标志一块共享内存,而 Android 系统则使用一个文件描述符来标志一块匿名共享内存。使用文件描述符来描述一块匿名共享内存有两个好处:
- 可以方便地将它映射到进程的地址空间,从而可以直接访问它的内容;
- 可以使用 Binder 进程间通信机制来传输这个文件描述符,从而实现在不同的应用程序之间共享一块匿名内存。
Binder 进程间通信机制使用一个类型为 BINDER_TYPE_FD 的 Binder 对象来描述一个文件描述符,当 Binder 驱动程序发现进程间通信数据中包含有这种 Binder 对象时,就会将对应的文件描述符复制到目标进程中,从而实现在两个进程中共享同一个文件。
2 Ashmem子系统 Java 访问接口分析
在Android的 Framework 层中通过使用接口 MemoryFile 来封装匿名共享内存文件的创建和使用接口 MemoryFile 在:frameworks/base/core/java/android/os/MemoryFile.java,具体实现代码如下所示:
public class MemoryFile
{
private static String TAG = "MemoryFile";
// mmap(2) protection flags from <sys/mman.h>
private static final int PROT_READ = 0x1;
private static final int PROT_WRITE = 0x2;
private static native FileDescriptor native_open(String name, int length) throws IOException;
// returns memory address for ashmem region
private static native long native_mmap(FileDescriptor fd, int length, int mode)
throws IOException;
private static native void native_munmap(long addr, int length) throws IOException;
private static native void native_close(FileDescriptor fd);
private static native int native_read(FileDescriptor fd, long address, byte[] buffer,
int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;
private static native void native_write(FileDescriptor fd, long address, byte[] buffer,
int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException;
private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException;
private static native int native_get_size(FileDescriptor fd) throws IOException;
private FileDescriptor mFD; // ashmem file descriptor
private long mAddress; // address of ashmem memory
private int mLength; // total length of our ashmem region
private boolean mAllowPurging = false; // true if our ashmem region is unpinned
/**
* Allocates a new ashmem region. The region is initially not purgable.
*
* @param name optional name for the file (can be null).
* @param length of the memory file in bytes, must be non-negative.
* @throws IOException if the memory file could not be created.
*/
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;
}
}
/**
* Closes the memory file. If there are no other open references to the memory
* file, it will be deleted.
*/
public void close() {
deactivate();
if (!isClosed()) {
native_close(mFD);
}
}
/**
* Unmaps the memory file from the process's memory space, but does not close it.
* After this method has been called, read and write operations through this object
* will fail, but {@link #getFileDescriptor()} will still return a valid file descriptor.
*
* @hide
*/
void deactivate() {
if (!isDeactivated()) {
try {
native_munmap(mAddress, mLength);
mAddress = 0;
} catch (IOException ex) {
Log.e(TAG, ex.toString());
}
}
}
/**
* Checks whether the memory file has been deactivated.
*/
private boolean isDeactivated() {
return mAddress == 0;
}
/**
* Checks whether the memory file has been closed.
*/
private boolean isClosed() {
return !mFD.valid();
}
@Override
protected void finalize() {
if (!isClosed()) {
Log.e(TAG, "MemoryFile.finalize() called while ashmem still open");
close();
}
}
/**
* Returns the length of the memory file.
*
* @return file length.
*/
public int length() {
return mLength;
}
/**
* Is memory file purging enabled?
*
* @return true if the file may be purged.
*/
public boolean isPurgingAllowed() {
return mAllowPurging;
}
/**
* Enables or disables purging of the memory file.
*
* @param allowPurging true if the operating system can purge the contents
* of the file in low memory situations
* @return previous value of allowPurging
*/
synchronized public boolean allowPurging(boolean allowPurging) throws IOException {
boolean oldValue = mAllowPurging;
if (oldValue != allowPurging) {
native_pin(mFD, !allowPurging);
mAllowPurging = allowPurging;
}
return oldValue;
}
/**
* Creates a new InputStream for reading from the memory file.
*
@return InputStream
*/
public InputStream getInputStream() {
return new MemoryInputStream();
}
/**
* Creates a new OutputStream for writing to the memory file.
*
@return OutputStream
*/
public OutputStream getOutputStream() {
return new MemoryOutputStream();
}
/**
* Reads bytes from the memory file.
* Will throw an IOException if the file has been purged.
*
* @param buffer byte array to read bytes into.
* @param srcOffset offset into the memory file to read from.
* @param destOffset offset into the byte array buffer to read into.
* @param count number of bytes to read.
* @return number of bytes read.
* @throws IOException if the memory file has been purged or deactivated.
*/
public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)
throws IOException {
if (isDeactivated()) {
throw new IOException("Can't read from deactivated memory file.");
}
if (destOffset < 0 || destOffset > buffer.length || count < 0
|| count > buffer.length - destOffset
|| srcOffset < 0 || srcOffset > mLength
|| count > mLength - srcOffset) {
throw new IndexOutOfBoundsException();
}
return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
}
/**
* Write bytes to the memory file.
* Will throw an IOException if the file has been purged.
*
* @param buffer byte array to write bytes from.
* @param srcOffset offset into the byte array buffer to write from.
* @param destOffset offset into the memory file to write to.
* @param count number of bytes to write.
* @throws IOException if the memory file has been purged or deactivated.
*/
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);
}
/**
* Gets a FileDescriptor for the memory file.
*
* The returned file descriptor is not duplicated.
*
* @throws IOException If the memory file has been closed.
*
* @hide
*/
public FileDescriptor getFileDescriptor() throws IOException {
return mFD;
}
/**
* Returns the size of the memory file that the file descriptor refers to,
* or -1 if the file descriptor does not refer to a memory file.
*
* @throws IOException If <code>fd</code> is not a valid file descriptor.
*
* @hide
*/
public static int getSize(FileDescriptor fd) throws IOException {
return native_get_size(fd);
}
private class MemoryInputStream extends InputStream {
private int mMark = 0;
private int mOffset = 0;
private byte[] mSingleByte;
@Override
public int available() throws IOException {
if (mOffset >= mLength) {
return 0;
}
return mLength - mOffset;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public void mark(int readlimit) {
mMark = mOffset;
}
@Override
public void reset() throws IOException {
mOffset = mMark;
}
@Override
public int read() throws IOException {
if (mSingleByte == null) {
mSingleByte = new byte[1];
}
int result = read(mSingleByte, 0, 1);
if (result != 1) {
return -1;
}
return mSingleByte[0];
}
@Override
public int read(byte buffer[], int offset, int count) throws IOException {
if (offset < 0 || count < 0 || offset + count > buffer.length) {
// readBytes() also does this check, but we need to do it before
// changing count.
throw new IndexOutOfBoundsException();
}
count = Math.min(count, available());
if (count < 1) {
return -1;
}
int result = readBytes(buffer, mOffset, offset, count);
if (result > 0) {
mOffset += result;
}
return result;
}
@Override
public long skip(long n) throws IOException {
if (mOffset + n > mLength) {
n = mLength - mOffset;
}
mOffset += n;
return n;
}
}
private class MemoryOutputStream extends OutputStream {
private int mOffset = 0;
private byte[] mSingleByte;
@Override
public void write(byte buffer[], int offset, int count) throws IOException {
writeBytes(buffer, offset, mOffset, count);
mOffset += count;
}
@Override
public void write(int oneByte) throws IOException {
if (mSingleByte == null) {
mSingleByte = new byte[1];
}
mSingleByte[0] = (byte)oneByte;
write(mSingleByte, 0, 1);
}
}
}
构造方法 MemoryFile 以指定的字符串调用了JNI方法 native_open,目的是建立一个匿名共享内存文件,这样可以得到一个文件描述符。然后使用这个文件描述符为参数调用JNI方法natvie_mmap,并把匿名共享内存文件映射到进程空间中这样就可以通过映射得到地址空间的方式直接访问内存数据。
成员函数 readBytes 用于读取某一块匿名共享内存的内容,成员函数 writeBytes 用于写入某一块匿名共享内存的内容,成员函数 isDeactivated 用于保证匿名共享内存已经被映射到进程的地址空间中。
以上native方法对应实现在源码文件:frameworkslbaselcorejniandroid_os_MemoryFile.cpp 中,具体实现代码如下所示:
#define LOG_TAG "MemoryFile"
#include <utils/Log.h>
#include <cutils/ashmem.h>
#include "core_jni_helpers.h"
#include "JNIHelp.h"
#include <unistd.h>
#include <sys/mman.h>
namespace android {
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);
}
static jlong android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor,
jint length, jint prot)
{
int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
void* result = mmap(NULL, length, prot, MAP_SHARED, fd, 0);
if (result == MAP_FAILED) {
jniThrowException(env, "java/io/IOException", "mmap failed");
}
return reinterpret_cast<jlong>(result);
}
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");
}
static void android_os_MemoryFile_close(JNIEnv* env, jobject clazz, jobject fileDescriptor)
{
int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
if (fd >= 0) {
jniSetFileDescriptorOfFD(env, fileDescriptor, -1);
close(fd);
}
}
static jint android_os_MemoryFile_read(JNIEnv* env, jobject clazz,
jobject fileDescriptor, jlong 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);
jniThrowException(env, "java/io/IOException", "ashmem region was purged");
return -1;
}
env->SetByteArrayRegion(buffer, destOffset, count, (const jbyte *)address + srcOffset);
if (unpinned) {
ashmem_unpin_region(fd, 0, 0);
}
return count;
}
static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,
jobject fileDescriptor, jlong 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);
jniThrowException(env, "java/io/IOException", "ashmem region was purged");
return -1;
}
env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);
if (unpinned) {
ashmem_unpin_region(fd, 0, 0);
}
return count;
}
static void android_os_MemoryFile_pin(JNIEnv* env, jobject clazz, jobject fileDescriptor, jboolean pin)
{
int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));
if (result < 0) {
jniThrowException(env, "java/io/IOException", NULL);
}
}
static jint android_os_MemoryFile_get_size(JNIEnv* env, jobject clazz,
jobject fileDescriptor) {
int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
// Use ASHMEM_GET_SIZE to find out if the fd refers to an ashmem region.
// ASHMEM_GET_SIZE should succeed for all ashmem regions, and the kernel
// should return ENOTTY for all other valid file descriptors
int result = ashmem_get_size_region(fd);
if (result < 0) {
if (errno == ENOTTY) {
// ENOTTY means that the ioctl does not apply to this object,
// i.e., it is not an ashmem region.
return (jint) -1;
}
// Some other error, throw exception
jniThrowIOException(env, errno);
return (jint) -1;
}
return (jint) result;
}
static const JNINativeMethod methods[] = {
{"native_open", "(Ljava/lang/String;I)Ljava/io/FileDescriptor;", (void*)android_os_MemoryFile_open},
{"native_mmap", "(Ljava/io/FileDescriptor;II)J", (void*)android_os_MemoryFile_mmap},
{"native_munmap", "(JI)V", (void*)android_os_MemoryFile_munmap},
{"native_close", "(Ljava/io/FileDescriptor;)V", (void*)android_os_MemoryFile_close},
{"native_read", "(Ljava/io/FileDescriptor;J[BIIIZ)I", (void*)android_os_MemoryFile_read},
{"native_write", "(Ljava/io/FileDescriptor;J[BIIIZ)V", (void*)android_os_MemoryFile_write},
{"native_pin", "(Ljava/io/FileDescriptor;Z)V", (void*)android_os_MemoryFile_pin},
{"native_get_size", "(Ljava/io/FileDescriptor;)I",
(void*)android_os_MemoryFile_get_size}
};
int register_android_os_MemoryFile(JNIEnv* env)
{
return RegisterMethodsOrDie(env, "android/os/MemoryFile", methods, NELEM(methods));
}
}