前言
本文讓我們來聊聊匿名共享內(nèi)存Ashmem棍潘。Ashmem為什么會(huì)誕生恃鞋?共享內(nèi)存本質(zhì)上還是為了方便跨進(jìn)程通信崖媚,減少拷貝次數(shù),提高性能恤浪。
遇到問題可以來本文討論http://www.reibang.com/p/6a8513fdb792
但是我們Android不是已經(jīng)有了Binder這個(gè)跨進(jìn)程通信利器嗎畅哑?為什么還需要匿名共享內(nèi)存?讓我們先看看Binder初始化時(shí)候這行代碼水由。
文件:/frameworks/native/libs/binder/ProcessState.cpp
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
ProcessState::ProcessState(const char *driver)
: mDriverName(String8(driver))
, mDriverFD(open_driver(driver))
, mVMStart(MAP_FAILED)
, mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
, mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
, mExecutingThreadsCount(0)
, mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
, mStarvationStartTimeMs(0)
, mManagesContexts(false)
, mBinderContextCheckFunc(NULL)
, mBinderContextUserData(NULL)
, mThreadPoolStarted(false)
, mThreadPoolSeq(1)
{
if (mDriverFD >= 0) {
// mmap the binder, providing a chunk of virtual address space to receive transactions.
mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
...
}
}
能看到應(yīng)用在初始化Binder的時(shí)候荠呐,已經(jīng)限制了大小為1M-2頁(1頁=4k)的大小也就是1016k大小,如果只是傳輸命令的話還可以砂客,但是要傳輸圖像數(shù)據(jù)這個(gè)大小根本不夠泥张。
加上Binder內(nèi)部有對(duì)每一個(gè)Binder內(nèi)核緩沖區(qū)有自己的調(diào)度算法,沒辦法滿足以最快的速度傳輸?shù)絊F進(jìn)程中鞠值。也因此媚创,Android選擇使用共享內(nèi)存的方式傳遞數(shù)據(jù)袱饭,也就是Ashmem匿名內(nèi)存荤西。
正文
其實(shí)Ashmem不僅僅只是內(nèi)核中能夠使用,其實(shí)在Java層Android也提供了一個(gè)名為MemoryFile的類提供方便使用匿名共享內(nèi)存刊侯,本次就以MemoryFile為切口声离,來聊聊Ashmem匿名內(nèi)存的使用芒炼。
老規(guī)矩,先來看看MemoryFile是如何使用的术徊。
MemoryFile memoryFile = null;
try{
//構(gòu)建一個(gè)共享內(nèi)存
memoryFile = new MemoryFile("test",1024*5);
OutputStream o = memoryFile.getOutputStream();
byte[] bs = new byte[1024];
bs[0] = 1;
//寫入
o.write(bs,0,1);
o.flush();
//讀出
InputStream in = memoryFile.getInputStream();
int r = in.read(bs,0,1);
Log.e("r","r:"+bs[0]);
}catch(Exception e){
e.printStackTrace();
}finally {
if(memoryFile != null){
memoryFile.close();
}
}
能看到操作和普通的File操作一模一樣焕议,好像根本沒有什么區(qū)別。File本身也可以作為數(shù)據(jù)中轉(zhuǎn)站做傳遞信息弧关。那么MemoryFile比起普通的File優(yōu)勢(shì)強(qiáng)在哪里呢盅安?接下來,讓我們剖析一下源碼世囊,來比較看看匿名內(nèi)存和File相比有什么區(qū)別别瞭,和Binder驅(qū)動(dòng)又有什么區(qū)別。
MemoryFile源碼解析
MemoryFile的創(chuàng)建
文件:/frameworks/base/core/java/android/os/MemoryFile.java
public MemoryFile(String name, int length) throws IOException {
try {
mSharedMemory = SharedMemory.create(name, length);
mMapping = mSharedMemory.mapReadWrite();
} catch (ErrnoException ex) {
ex.rethrowAsIOException();
}
}
能看到實(shí)際上MemoryFile內(nèi)部有一個(gè)核心的類SharedMemory作為核心操作類株憾。我們?nèi)タ纯碨haredMemory創(chuàng)建了什么東西蝙寨。
文件:/frameworks/base/core/java/android/os/SharedMemory.java
public static @NonNull SharedMemory create(@Nullable String name, int size)
throws ErrnoException {
...
return new SharedMemory(nCreate(name, size));
}
private SharedMemory(FileDescriptor fd) {
...
mFileDescriptor = fd;
mSize = nGetSize(mFileDescriptor);
..
mMemoryRegistration = new MemoryRegistration(mSize);
mCleaner = Cleaner.create(mFileDescriptor,
new Closer(mFileDescriptor, mMemoryRegistration));
}
SharedMemory首先通過nCreate在native下創(chuàng)建一個(gè)文件描述符,并且關(guān)聯(lián)到到SharedMemory嗤瞎,通過nGetSize獲取當(dāng)前共享內(nèi)存大小墙歪,最后通過MemoryRegistration把當(dāng)前大小注冊(cè)到Java 虛擬機(jī)中的native堆棧大小中,初始化Cleaner等到合適的時(shí)候通過gc聯(lián)動(dòng)Cleaner銷毀native下的對(duì)象贝奇。
private static final class MemoryRegistration {
private int mSize;
private int mReferenceCount;
private MemoryRegistration(int size) {
mSize = size;
mReferenceCount = 1;
VMRuntime.getRuntime().registerNativeAllocation(mSize);
}
public synchronized MemoryRegistration acquire() {
mReferenceCount++;
return this;
}
public synchronized void release() {
mReferenceCount--;
if (mReferenceCount == 0) {
VMRuntime.getRuntime().registerNativeFree(mSize);
}
}
}
MemoryRegistration 本質(zhì)上就是注冊(cè)了Java虛擬機(jī)中native堆的大小虹菲,每一次一個(gè)引用都有一次計(jì)數(shù),只有減到0才銷毀掉瞳,畢竟這是共享內(nèi)存毕源,不應(yīng)該完全由Java虛擬機(jī)的GC機(jī)制決定
那么其核心畢竟就是nCreate這個(gè)native方法浪漠,接著會(huì)通過mapReadWrite
nCreate構(gòu)建native下層的共享內(nèi)存
文件: /frameworks/base/core/jni/android_os_SharedMemory.cpp
static jobject SharedMemory_create(JNIEnv* env, jobject, jstring jname, jint size) {
const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;
int fd = ashmem_create_region(name, size);
int err = fd < 0 ? errno : 0;
if (name) {
env->ReleaseStringUTFChars(jname, name);
}
....
return jniCreateFileDescriptor(env, fd);
}
終于看到了,匿名共享內(nèi)存相關(guān)的字眼霎褐,通過ashmem_create_region址愿,創(chuàng)建一個(gè)共享內(nèi)存的區(qū)域。還記得Linux中那句話冻璃,一切皆為文件响谓,實(shí)際上匿名共享內(nèi)存創(chuàng)建出來也是一個(gè)文件,不過因?yàn)槭窃趖mpfs臨時(shí)文件系統(tǒng)才叫做匿名的省艳。最后創(chuàng)建java的文件描述符對(duì)象并和fd關(guān)聯(lián)起來歌粥。
接下來讓我們看看cutils中ashmem_create_region做了什么封裝。
文件:/system/core/libcutils/ashmem-dev.cpp
int ashmem_create_region(const char *name, size_t size)
{
int ret, save_errno;
int fd = __ashmem_open();
if (fd < 0) {
return fd;
}
if (name) {
char buf[ASHMEM_NAME_LEN] = {0};
strlcpy(buf, name, sizeof(buf));
ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));
...
}
ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));
...
return fd;
...
}
創(chuàng)建匿名共享內(nèi)存分為三個(gè)步驟:
1.__ashmem_open 創(chuàng)建匿名共享內(nèi)存
2.通過ioctl 給匿名共享內(nèi)存命名拍埠,只有命名了才能通過命名找到對(duì)應(yīng)的匿名共享內(nèi)存。
3.ioctl通過ASHMEM_SET_SIZE命令設(shè)置匿名共享內(nèi)存的大小
__ashmem_open
這個(gè)方法最終會(huì)調(diào)用到如下的方法:
#define ASHMEM_DEVICE "/dev/ashmem"
static int __ashmem_open_locked()
{
int ret;
struct stat st;
int fd = TEMP_FAILURE_RETRY(open(ASHMEM_DEVICE, O_RDWR | O_CLOEXEC));
if (fd < 0) {
return fd;
}
ret = TEMP_FAILURE_RETRY(fstat(fd, &st));
if (ret < 0) {
int save_errno = errno;
close(fd);
errno = save_errno;
return ret;
}
if (!S_ISCHR(st.st_mode) || !st.st_rdev) {
close(fd);
errno = ENOTTY;
return -1;
}
__ashmem_rdev = st.st_rdev;
return fd;
}
終于看到了土居,類似于Binder驅(qū)動(dòng)的打開方式一樣枣购,通過/dev/ashmem的方式訪問ashmem驅(qū)動(dòng)的file_operation的open方法,最后獲得對(duì)應(yīng)的文件描述符fd擦耀。
在這這里先停下來棉圈,只要記住Ashmem創(chuàng)建三個(gè)步驟:
- 1.open /dev/ashmem驅(qū)動(dòng)連通ashmem驅(qū)動(dòng)
- 2.ioctl 發(fā)送ASHMEM_SET_NAME命令為該ashmem創(chuàng)建名字
- 3.ioctl通過ASHMEM_SET_SIZE命令設(shè)置匿名共享內(nèi)存的大小
ShareMemory.mapReadWrite創(chuàng)建內(nèi)存映射緩存區(qū)
ShareMemory當(dāng)創(chuàng)建好ashmem匿名共享內(nèi)存之后,將會(huì)調(diào)用mapReadWrite
public @NonNull ByteBuffer mapReadWrite() throws ErrnoException {
return map(OsConstants.PROT_READ | OsConstants.PROT_WRITE, 0, mSize);
}
public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
checkOpen();
validateProt(prot);
...
long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset);
boolean readOnly = (prot & OsConstants.PROT_WRITE) == 0;
Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire());
return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly);
}
能看到map方法最終會(huì)調(diào)用 Os.mmap眷蜓。其實(shí)這個(gè)方法的本質(zhì)就是調(diào)用系統(tǒng)調(diào)用mmap分瘾。這里面的意思就是調(diào)用Ashmem對(duì)應(yīng)的文件描述符mmap方法,也就是會(huì)調(diào)用Ashmem驅(qū)動(dòng)中file_ops中的mmap方法吁系,最后會(huì)直接映射一段邏輯上的虛擬內(nèi)存和文件file關(guān)聯(lián)起來德召。當(dāng)系統(tǒng)正式訪問這一段虛擬內(nèi)存,如果找不到就會(huì)觸發(fā)缺頁中斷(或者嘗試的從磁盤執(zhí)行物理頁的換入換出)汽纤,此時(shí)就會(huì)把這一段邏輯綁定的虛擬內(nèi)存和file正式映射到物理內(nèi)存上岗。
通過這種常規(guī)的mmap,讓用戶態(tài)的虛擬內(nèi)存直接和物理內(nèi)存映射起來蕴坪,就能通過0次拷貝的方式映射起來肴掷。是否是這樣,我們稍后來看看背传。
同時(shí)在DirectByteBuffer設(shè)置解開映射的回調(diào)Unmapper
private static final class Unmapper implements Runnable {
private long mAddress;
private int mSize;
private MemoryRegistration mMemoryReference;
private Unmapper(long address, int size, MemoryRegistration memoryReference) {
mAddress = address;
mSize = size;
mMemoryReference = memoryReference;
}
@Override
public void run() {
try {
Os.munmap(mAddress, mSize);
} catch (ErrnoException e) { /* swallow exception */ }
mMemoryReference.release();
mMemoryReference = null;
}
}
能看到如果通過mMemoryRegistration察覺到引用計(jì)數(shù)為0呆瞻,就會(huì)調(diào)用munmap解映射。因此我們可以推敲出径玖,MemoryFile將會(huì)以mapReadWrite產(chǎn)生出來的mMapping為基準(zhǔn)痴脾,不斷的從這一段虛擬內(nèi)存讀寫。讓我們來看看MemoryFile的讀寫方法梳星。
MemoryFile寫入數(shù)據(jù)
寫入操作能看到就是獲取MemoryFile的OutputStream對(duì)象進(jì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);
}
}
能看到在write方法中买窟,本質(zhì)上還是調(diào)用writeBytes作為核心寫入方法。
private void beginAccess() throws IOException {
checkActive();
if (mAllowPurging) {
if (native_pin(mSharedMemory.getFileDescriptor(), true)) {
throw new IOException("MemoryFile has been purged");
}
}
}
private void endAccess() throws IOException {
if (mAllowPurging) {
native_pin(mSharedMemory.getFileDescriptor(), false);
}
}
public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
throws IOException {
beginAccess();
try {
mMapping.position(destOffset);
mMapping.put(buffer, srcOffset, count);
} finally {
endAccess();
}
}
能看到在這個(gè)過程中會(huì)先調(diào)用native_pin進(jìn)行鎖定這一塊大小的虛擬內(nèi)存薯定,避免被系統(tǒng)回收始绍,最后才調(diào)用mMapping的position記錄寫完后的位置,并且把buffer數(shù)據(jù)寫入到mMapping中.
等一下怎么回事话侄,為什么不調(diào)用write系統(tǒng)調(diào)用亏推?如果閱讀過我之前文章就知道m(xù)map的核心原理就是把物理頁和虛擬內(nèi)存頁映射起來。
public ByteBuffer put(byte[] src, int srcOffset, int length) {
...
checkBounds(srcOffset, length, src.length);
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
...
Memory.pokeByteArray(ix(pos),
src, srcOffset, length);
position = pos + length;
return this;
}
put首先會(huì)根據(jù)設(shè)置進(jìn)來的position設(shè)定已經(jīng)寫入了多少數(shù)據(jù)年堆,從哪里開始寫入吞杭。接著會(huì)通過傳進(jìn)來的數(shù)據(jù)長度以及要寫入的偏移量來確定要寫入哪一塊內(nèi)存。
此時(shí)put會(huì)調(diào)用Memory.pokeByteArray方法变丧,把內(nèi)容寫到虛擬地址偏移量的起點(diǎn)到數(shù)據(jù)長度結(jié)束中芽狗,也就是寫入到對(duì)應(yīng)位置的物理頁中。
文件:/libcore/luni/src/main/native/libcore_io_Memory.cpp
static void Memory_pokeByteArray(JNIEnv* env, jclass, jlong dstAddress, jbyteArray src, jint offset, jint length) {
env->GetByteArrayRegion(src, offset, length, cast<jbyte*>(dstAddress));
}
如下就是示意圖:
MemoryFile讀取數(shù)據(jù)
同理痒蓬,MemoryFile讀取數(shù)據(jù)的核心方法也是類似的.
文件:/frameworks/base/core/java/android/os/MemoryFile.java
private class MemoryInputStream extends InputStream {
private int mMark = 0;
private int mOffset = 0;
private byte[] mSingleByte;
....
@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;
}
...
}
能看到MemoryInputStream的read核心方法還是使用readBytes方法童擎。
public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)
throws IOException {
beginAccess();
try {
mMapping.position(srcOffset);
mMapping.get(buffer, destOffset, count);
} finally {
endAccess();
}
return count;
}
能看到核心還是獲取mMapping這一塊DirectByteBuffer中的數(shù)據(jù),其調(diào)用核心調(diào)用攻晒,
文件:/libcore/ojluni/src/main/java/java/nio/DirectByteBuffer.java
public ByteBuffer get(byte[] dst, int dstOffset, int length) {
...
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
...
Memory.peekByteArray(ix(pos),
dst, dstOffset, length);
position = pos + length;
return this;
}
一樣也是獲取當(dāng)前已經(jīng)寫入的位置顾复,從該位置+偏移量作為讀取數(shù)據(jù)的起點(diǎn),讀取數(shù)據(jù)的長度即為所得鲁捏。
static void Memory_peekByteArray(JNIEnv* env, jclass, jlong srcAddress, jbyteArray dst, jint dstOffset, jint byteCount) {
env->SetByteArrayRegion(dst, dstOffset, byteCount, cast<const jbyte*>(srcAddress));
}
能看到此時(shí)就是獲取目標(biāo)區(qū)域內(nèi)存的數(shù)據(jù)芯砸,設(shè)置到srcAddress中。
小結(jié)
經(jīng)過對(duì)MemoryFile的解析给梅,能夠弄清楚假丧,Ashmem匿名共享內(nèi)存使用的步驟可以分為4步:
- 1.open /dev/ashmem驅(qū)動(dòng)連通ashmem驅(qū)動(dòng)。
- 2.ioctl 發(fā)送ASHMEM_SET_NAME命令為該ashmem創(chuàng)建名字动羽。
- 3.ioctl 發(fā)送ASHMEM_SET_SIZE命令為ashmem設(shè)置大小
- 4.mmap 做內(nèi)存映射虎谢。
- 5.對(duì)該文件描述符進(jìn)行讀寫即可。
只要進(jìn)行了前三步驟曹质,算作是進(jìn)程初始化了為在ashmem驅(qū)動(dòng)內(nèi)創(chuàng)建一個(gè)文件描述符用于共享內(nèi)存婴噩,但是此時(shí)還沒有關(guān)聯(lián)起來相當(dāng)于有了一個(gè)該名字的匿名內(nèi)存標(biāo)識(shí);同時(shí)設(shè)置了共享內(nèi)存的大小區(qū)域
第三步羽德,調(diào)用mmap才正式把file和虛擬內(nèi)存在邏輯上關(guān)聯(lián)起來几莽;
第四步,讀寫才會(huì)觸發(fā)缺頁中斷宅静,申請(qǐng)物理頁并且綁定起來章蚣。
從這里我想起一些網(wǎng)上可笑的言論,在做性能優(yōu)化的內(nèi)存優(yōu)化的時(shí)候,為了減少Java堆中的大小而把部分?jǐn)?shù)據(jù)通過共享內(nèi)存?zhèn)鬟f纤垂,這樣就規(guī)避了Java的內(nèi)存檢測矾策。這是優(yōu)化?這僅僅只是使用了Android老版本檢測內(nèi)存的漏洞而已峭沦。如果熟知Linux內(nèi)核的朋友就知道贾虽,這只是障眼法,Linux用戶態(tài)和內(nèi)核態(tài)使用的都是虛擬內(nèi)存(內(nèi)存管理系統(tǒng)分配物理頁流程除外)吼鱼,且有大小限制蓬豁。
而Java虛擬機(jī)對(duì)Java堆棧和Java的Native堆棧做了大小的限制,就是因?yàn)槊恳粋€(gè)進(jìn)程本身能申請(qǐng)的虛擬內(nèi)存就是有限的菇肃。壓根就沒有真正的做到內(nèi)存優(yōu)化地粪。
Ashmem驅(qū)動(dòng)
我們了解如何使用Ashmem驅(qū)動(dòng)之后,我們就根據(jù)著使用流程琐谤,從初始化到使用閱讀一下Ashmem究竟在內(nèi)核中做了什么蟆技。
Ashmem 初始化
文件:/drivers/staging/android/ashmem.c
先來看看Ashmem的初始化
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);
pr_info("initialized\n");
return 0;
}
能看到在這個(gè)過程中,在slab高速緩存開辟了ashmem_area斗忌,以及ashmem_range兩個(gè)結(jié)構(gòu)體的cache质礼,方便之后的申請(qǐng)。ashmem_area結(jié)構(gòu)體的作用為切割出來給用戶態(tài)的內(nèi)存飞蹂,ashmem_range為非鎖定的內(nèi)存塊的鏈表結(jié)構(gòu),里面的內(nèi)存塊會(huì)在內(nèi)核需要的時(shí)候被回收翻屈。
最后通過register_shrinker向內(nèi)存管理系統(tǒng)注冊(cè)Ashmem回收函數(shù)陈哑。
Ashmem 的file_operation
了解驅(qū)動(dòng)最快的方式就要看這個(gè)驅(qū)動(dòng)復(fù)寫的file_operation 結(jié)構(gòu)體中有多少操作,每個(gè)操作指向哪一個(gè)方法:
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
};
能看到里面有open伸眶,read惊窖,mmap,unlocked_ioctl這四個(gè)核心的方法厘贼,我們只要分析這四個(gè)在Ashmem下的方法就能清除Ashmem做了什么界酒。
Ashmem open
#define ASHMEM_NAME_PREFIX "dev/ashmem/"
#define ASHMEM_NAME_PREFIX_LEN (sizeof(ASHMEM_NAME_PREFIX) - 1)
#define ASHMEM_FULL_NAME_LEN (ASHMEM_NAME_LEN + ASHMEM_NAME_PREFIX_LEN)
static int ashmem_open(struct inode *inode, struct file *file)
{
struct ashmem_area *asma;
int ret;
ret = generic_file_open(inode, file);
...
asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
...
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_area_cachep申請(qǐng)slab緩沖區(qū)中一塊ashmem_area區(qū)域,并且初始化ashmem_area中unpinned_list解鎖內(nèi)存塊列表的隊(duì)列頭嘴秸,并且把a(bǔ)sma這一塊匿名區(qū)域設(shè)置名字為/dev/ashmem毁欣。
最后把當(dāng)前的ashmem_area設(shè)置為file的私有數(shù)據(jù)。
Ashmem ioctl設(shè)置名字與大小
接下來會(huì)通過ioctl設(shè)置名字岳掐,調(diào)用的命令是ASHMEM_SET_NAME凭疮。
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_GET_NAME:
ret = get_name(asma, (void __user *) arg);
break;
case ASHMEM_SET_SIZE:
ret = -EINVAL;
if (!asma->file) {
ret = 0;
asma->size = (size_t) arg;
}
break;
case ASHMEM_GET_SIZE:
ret = asma->size;
break;
...
return ret;
}
首先來看看幾個(gè)簡單的命令,ASHMEM_SET_NAME通過set_name設(shè)置名字串述;ASHMEM_GET_NAME通過get_name獲取名字执解;ASHMEM_SET_SIZE設(shè)置區(qū)域大小。設(shè)置大小本質(zhì)上就是設(shè)置asma中的size屬性纲酗。我們來看看設(shè)置名字的set_name的邏輯衰腌。
static int set_name(struct ashmem_area *asma, void __user *name)
{
int len;
int ret = 0;
char local_name[ASHMEM_NAME_LEN];
len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
...
if (len == ASHMEM_NAME_LEN)
local_name[ASHMEM_NAME_LEN - 1] = '\0';
mutex_lock(&ashmem_mutex);
/* cannot change an existing mapping's name */
if (unlikely(asma->file))
ret = -EINVAL;
else
strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);
mutex_unlock(&ashmem_mutex);
return ret;
}
這里面的邏輯很簡單新蟆,實(shí)際上就是獲取asma中那么屬性,之前是/dev/ashmem,現(xiàn)在在末尾追加文件名字右蕊,如/dev/ashmem/<filename>,這樣驅(qū)動(dòng)程序?yàn)槊恳粋€(gè)匿名共享內(nèi)存創(chuàng)建自己獨(dú)有的名字琼稻,當(dāng)然一旦判斷這個(gè)asma已經(jīng)映射了file就拒絕再次命名。
Ashmem的mmap映射內(nèi)存
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);
/* user needs to SET_SIZE before mapping */
if (unlikely(!asma->size)) {
ret = -EINVAL;
goto out;
}
/* requested protection bits must match our allowed protection mask */
if (unlikely((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask)) &
calc_vm_prot_bits(PROT_MASK))) {
ret = -EPERM;
goto out;
}
vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
if (!asma->file) {
char *name = ASHMEM_NAME_DEF;
struct file *vmfile;
if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')
name = asma->name;
/* ... and allocate the backing shmem file */
vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
if (unlikely(IS_ERR(vmfile))) {
ret = PTR_ERR(vmfile);
goto out;
}
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;
}
out:
mutex_unlock(&ashmem_mutex);
return ret;
}
能看到這個(gè)過程校驗(yàn)了尤泽,必須要設(shè)置asma的size欣簇,不然會(huì)拋異常。檢測需要映射的vma虛擬內(nèi)存是否符合權(quán)限坯约,否則拋異常熊咽。
接著檢查asma中的file文件結(jié)構(gòu)體是否創(chuàng)建,沒有則獲取asma名字和大小通過shmem_file_setup創(chuàng)建一個(gè)文件描述符闹丐。
檢查如果當(dāng)前的vma虛擬內(nèi)存允許共享則調(diào)用shmem_set_file映射文件横殴。
我們就來看看shmem_file_setup和shmem_set_file。
shmem_file_setup
最后會(huì)調(diào)用如下核心代碼
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;
struct path path;
struct super_block *sb;
struct qstr this;
...
res = ERR_PTR(-ENOMEM);
this.name = name;
this.len = strlen(name);
this.hash = 0; /* will go */
sb = shm_mnt->mnt_sb;
path.mnt = mntget(shm_mnt);
path.dentry = d_alloc_pseudo(sb, &this);
...
d_set_d_op(path.dentry, &anon_ops);
res = ERR_PTR(-ENOSPC);
inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
...
inode->i_flags |= i_flags;
d_instantiate(path.dentry, inode);
inode->i_size = size;
clear_nlink(inode); /* It is unlinked */
res = ERR_PTR(ramfs_nommu_expand_for_mapping(inode, size));
if (IS_ERR(res))
goto put_path;
res = alloc_file(&path, FMODE_WRITE | FMODE_READ,
&shmem_file_operations);
...
return res;
put_memory:
shmem_unacct_size(flags, size);
put_path:
path_put(&path);
return res;
}
在__shmem_file_setup中做了如下幾個(gè)十分重要的事情:
- 1.d_instantiate設(shè)置目錄結(jié)構(gòu)體
- 2.通過shmem_get_inode設(shè)置共享的inode卿拴,inode是Linux訪問硬盤文件系統(tǒng)的基本單位衫仑,里面包含如superblock等元數(shù)據(jù)。
- 3.alloc_file申請(qǐng)一個(gè)file結(jié)構(gòu)體堕花,同時(shí)復(fù)寫file的結(jié)構(gòu)中的file_operation文件操作.
讓我們看看shmem_file_operations有具體操作:
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,//預(yù)分配物理內(nèi)存
#endif
};
通過shmem_file_setup文狱,ashmem驅(qū)動(dòng)程序就把vma中的file文件結(jié)構(gòu)體轉(zhuǎn)化為共享內(nèi)存了。
不過看到shmem這個(gè)名字就應(yīng)該知道其實(shí)這就是Linux中的共享內(nèi)存缘挽。
shmem_set_file
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;
}
能看到本質(zhì)上這個(gè)方法就是把vm_file和file結(jié)構(gòu)體關(guān)聯(lián)起來瞄崇,同時(shí)設(shè)置了虛擬內(nèi)存的操作函數(shù):
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
.map_pages = filemap_map_pages,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
#endif
.remap_pages = generic_file_remap_pages,
};
這個(gè)結(jié)構(gòu)體尤為的重要其中fault操作函數(shù)shmem_fault,是指當(dāng)接收到缺頁中斷時(shí)候壕曼,共享內(nèi)存該如何綁定物理頁苏研。
因?yàn)榇藭r(shí)只是從邏輯上把vma和匿名共享內(nèi)存對(duì)應(yīng)的file文件在邏輯上關(guān)聯(lián)起來,當(dāng)我們嘗試讀寫這一段虛擬內(nèi)存的時(shí)候腮郊,發(fā)現(xiàn)并沒有映射摹蘑,也沒有在硬盤上保存相應(yīng)的數(shù)據(jù)進(jìn)行換入,就會(huì)綁定一段物理內(nèi)存轧飞。
先不關(guān)心是怎么調(diào)用到shmem_fault之后有機(jī)會(huì)會(huì)聊到的衅鹿,先看看下面這個(gè)方法做了什么。
static int shmem_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
struct inode *inode = file_inode(vma->vm_file);
int error;
int ret = VM_FAULT_LOCKED;
if (unlikely(inode->i_private)) {
struct shmem_falloc *shmem_falloc;
spin_lock(&inode->i_lock);
shmem_falloc = inode->i_private;
if (shmem_falloc &&
shmem_falloc->waitq &&
vmf->pgoff >= shmem_falloc->start &&
vmf->pgoff < shmem_falloc->next) {
...
}
spin_unlock(&inode->i_lock);
}
error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret);
...
return ret;
}
當(dāng)觸發(fā)了缺頁中斷之后过咬,就會(huì)查找預(yù)分配的物理內(nèi)存塘安,此時(shí)沒有,會(huì)直接調(diào)用shmem_getpage援奢,綁定vmf中的物理頁和虛擬內(nèi)存頁兼犯。
static int shmem_getpage_gfp(struct inode *inode, pgoff_t index,
struct page **pagep, enum sgp_type sgp, gfp_t gfp, int *fault_type)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info;
struct shmem_sb_info *sbinfo;
struct mem_cgroup *memcg;
struct page *page;
swp_entry_t swap;
int error;
int once = 0;
int alloced = 0;
if (index > (MAX_LFS_FILESIZE >> PAGE_CACHE_SHIFT))
return -EFBIG;
repeat:
swap.val = 0;
page = find_lock_entry(mapping, index);
....
/*
* Fast cache lookup did not find it:
* bring it back from swap or allocate.
*/
info = SHMEM_I(inode);
sbinfo = SHMEM_SB(inode->i_sb);
if (swap.val) {
....
} else {
if (shmem_acct_block(info->flags)) {
error = -ENOSPC;
goto failed;
}
if (sbinfo->max_blocks) {
if (percpu_counter_compare(&sbinfo->used_blocks,
sbinfo->max_blocks) >= 0) {
error = -ENOSPC;
goto unacct;
}
percpu_counter_inc(&sbinfo->used_blocks);
}
page = shmem_alloc_page(gfp, info, index);
if (!page) {
error = -ENOMEM;
goto decused;
}
__SetPageSwapBacked(page);
__set_page_locked(page);
if (sgp == SGP_WRITE)
__SetPageReferenced(page);
error = mem_cgroup_try_charge(page, current->mm, gfp, &memcg);
if (error)
goto decused;
error = radix_tree_maybe_preload(gfp & GFP_RECLAIM_MASK);
if (!error) {
error = shmem_add_to_page_cache(page, mapping, index,
NULL);
radix_tree_preload_end();
}
if (error) {
mem_cgroup_cancel_charge(page, memcg);
goto decused;
}
mem_cgroup_commit_charge(page, memcg, false);
lru_cache_add_anon(page);
spin_lock(&info->lock);
info->alloced++;
inode->i_blocks += BLOCKS_PER_PAGE;
shmem_recalc_inode(inode);
spin_unlock(&info->lock);
alloced = true;
/*
* Let SGP_FALLOC use the SGP_WRITE optimization on a new page.
*/
if (sgp == SGP_FALLOC)
sgp = SGP_WRITE;
clear:
/*
* Let SGP_WRITE caller clear ends if write does not fill page;
* but SGP_FALLOC on a page fallocated earlier must initialize
* it now, lest undo on failure cancel our earlier guarantee.
*/
if (sgp != SGP_WRITE) {
clear_highpage(page);
flush_dcache_page(page);
SetPageUptodate(page);
}
if (sgp == SGP_DIRTY)
set_page_dirty(page);
}
/* Perhaps the file has been truncated since we checked */
...
*pagep = page;
return 0;
...
}
這里面做的事情有如下幾件:
- 1.首先拿到頁內(nèi)偏移,嘗試的查找是否虛擬地址中保存著物理內(nèi)存頁
- 2.檢查inode中的superblock的標(biāo)志位或者容量是否已經(jīng)超出原來預(yù)設(shè)的
- 3.shmem_alloc_page通過alloc_page通過伙伴系統(tǒng)申請(qǐng)綁定物理頁
- 4.mem_cgroup_try_charge 記錄當(dāng)前page的緩存,mem_cgroup_commit_charge提交到linux中切黔。cgroup的機(jī)制就是為了檢測申請(qǐng)的內(nèi)存砸脊,以及尋找時(shí)機(jī)回收。
- 5.最后把page掛在到address_space這個(gè)結(jié)構(gòu)體的maping的基數(shù)樹中纬霞。
這里提一句凌埂,address_space這個(gè)結(jié)構(gòu)體是用于記錄文件和內(nèi)存的關(guān)聯(lián)的∈撸基數(shù)樹實(shí)際上就是以bit為key瞳抓,生成多個(gè)分支的樹。有點(diǎn)像哈夫曼樹一樣伏恐,把一個(gè)key的bit位全部讀出來孩哑,取出key中一位一位或者多位的生成多個(gè)階段的樹,只要把這個(gè)key的bit位全部讀取完畢就能找到內(nèi)容翠桦。
是一個(gè)十分快速的映射數(shù)據(jù)結(jié)構(gòu),借用網(wǎng)上的一個(gè)圖:
這樣就完成了映射横蜒,而這種機(jī)制其實(shí)是比起B(yǎng)inder的mmap很接近ext4文件系統(tǒng)的方式。
Ashmem驅(qū)動(dòng)讀寫
還記得讀寫操作此時(shí)不會(huì)對(duì)ashmem生成的文件進(jìn)行讀寫销凑,而是對(duì)映射的區(qū)域進(jìn)行讀寫丛晌,換句話說就是對(duì)共享內(nèi)存這段地址區(qū)域直接進(jìn)行讀寫,沒有經(jīng)過write斗幼,read的系統(tǒng)調(diào)用澎蛛,也就不會(huì)走到他們對(duì)應(yīng)的file_operation.
Ashmem鎖定與解鎖
還記得,在這Ashmem初始化一節(jié)就說過的另一個(gè)數(shù)據(jù)結(jié)構(gòu)ashmem_range嗎蜕窿?這里就涉及到了谋逻。那么Ashmem申請(qǐng)的共享匿名共享本質(zhì)上還是借助shmem共享內(nèi)存函數(shù)實(shí)現(xiàn)的,那么Ashmem和shmem有什么區(qū)別渠羞,其實(shí)區(qū)別就在這個(gè)映射區(qū)域的鎖定與解鎖中斤贰。
讓我們把目光回顧到MemoryFile中,能夠發(fā)現(xiàn)每一次調(diào)用讀寫都會(huì)調(diào)用一次native_pin方法:
native_pin(mSharedMemory.getFileDescriptor(), true)//鎖定
native_pin(mSharedMemory.getFileDescriptor(), false)//解鎖
在native層調(diào)用方式如下:
static jboolean 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);
}
return result == ASHMEM_WAS_PURGED;
}
文件:/system/core/libcutils/ashmem-dev.cpp
int ashmem_pin_region(int fd, size_t offset, size_t len)
{
// TODO: should LP64 reject too-large offset/len?
ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };
int ret = __ashmem_is_ashmem(fd, 1);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin));
}
int ashmem_unpin_region(int fd, size_t offset, size_t len)
{
// TODO: should LP64 reject too-large offset/len?
ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };
int ret = __ashmem_is_ashmem(fd, 1);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_UNPIN, &pin));
}
能看到本質(zhì)上就是把offset偏移量和len要寫入的長度封裝為一個(gè)ashmem_pin結(jié)構(gòu)體中智哀。此時(shí)全是0.
而這個(gè)方式本質(zhì)上是調(diào)用ioctl如下命令A(yù)SHMEM_PIN和ASHMEM_UNPIN:
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_PIN:
case ASHMEM_UNPIN:
case ASHMEM_GET_PIN_STATUS:
ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
break;
...
return ret;
}
static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
void __user *p)
{
struct ashmem_pin pin;
size_t pgstart, pgend;
int ret = -EINVAL;
...
if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
return -EFAULT;
...
pgstart = pin.offset / PAGE_SIZE;
pgend = pgstart + (pin.len / PAGE_SIZE) - 1;
mutex_lock(&ashmem_mutex);
switch (cmd) {
case ASHMEM_PIN:
ret = ashmem_pin(asma, pgstart, pgend);
break;
case ASHMEM_UNPIN:
ret = ashmem_unpin(asma, pgstart, pgend);
break;
case ASHMEM_GET_PIN_STATUS:
ret = ashmem_get_pin_status(asma, pgstart, pgend);
break;
}
mutex_unlock(&ashmem_mutex);
return ret;
}
此時(shí)會(huì)計(jì)算要鎖定或者解鎖的區(qū)域開始和結(jié)束地址次询。pgstart計(jì)算方式就是除去一頁大小4kb,這樣就能拿到偏移量是第幾頁瓷叫,由于是除沒有余數(shù)就能拿到當(dāng)前頁的起點(diǎn)屯吊。pgend就是pgstart加上長度占用的頁數(shù)減1.
其實(shí)從計(jì)算就能知道,ashmem的鎖定區(qū)域必定是按照頁為最基本單位鎖定和解鎖的摹菠。
在閱讀源碼之前首先要明白盒卸,默認(rèn)mmap出來的地址都是鎖定好的。
Ashmem的解鎖ashmem_unpin
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
unsigned int purged = ASHMEM_NOT_PURGED;
restart:
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
/* short circuit: this is our insertion point */
if (range_before_page(range, pgstart))
break;
/*
* The user can ask us to unpin pages that are already entirely
* or partially pinned. We handle those two cases here.
*/
if (page_range_subsumed_by_range(range, pgstart, pgend))
return 0;
if (page_range_in_range(range, pgstart, pgend)) {
pgstart = min_t(size_t, range->pgstart, pgstart),
pgend = max_t(size_t, range->pgend, pgend);
purged |= range->purged;
range_del(range);
goto restart;
}
}
return range_alloc(asma, range, purged, pgstart, pgend);
}
在這個(gè)過程中需要比較從unpinned_list鏈表種每一項(xiàng)已經(jīng)解鎖的range次氨。大致會(huì)分為5種情況:
在情況1蔽介,2,3種當(dāng)解鎖的range在這一次pagestart和pageend之間有交集,則會(huì)合并起來.
在第4種情況虹蓄,由于range已經(jīng)包含了pagestart和pageend就沒必要處理
第5種情況犀呼,由于range和即將解鎖的區(qū)域不相交,并且range在即將解鎖區(qū)域的前方則不需要遍歷薇组,沒必要做合并操作外臂。
range_alloc
static int range_alloc(struct ashmem_area *asma,
struct ashmem_range *prev_range, unsigned int purged,
size_t start, size_t end)
{
struct ashmem_range *range;
range = kmem_cache_zalloc(ashmem_range_cachep, GFP_KERNEL);
...
range->asma = asma;
range->pgstart = start;
range->pgend = end;
range->purged = purged;
list_add_tail(&range->unpinned, &prev_range->unpinned);
if (range_on_lru(range))
lru_add(range);
return 0;
}
此時(shí)會(huì)從ashmem_range_cachep創(chuàng)建一個(gè)asma_range,設(shè)置好其起始和結(jié)束地址律胀,添加amsa的unpinnedlist末尾中宋光。
static inline void lru_add(struct ashmem_range *range)
{
list_add_tail(&range->lru, &ashmem_lru_list);
lru_count += range_size(range);
}
最后把當(dāng)前range設(shè)置到全局變量ashmem_lru_list鏈表的末尾,并且記錄當(dāng)前解鎖的總大小炭菌。
Ashmem的鎖定ashmem_pin
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
struct ashmem_range *range, *next;
int ret = ASHMEM_NOT_PURGED;
list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
/* moved past last applicable page; we can short circuit */
if (range_before_page(range, pgstart))
break;
/*
* The user can ask us to pin pages that span multiple ranges,
* or to pin pages that aren't even unpinned, so this is messy.
*
* Four cases:
* 1. The requested range subsumes an existing range, so we
* just remove the entire matching range.
* 2. The requested range overlaps the start of an existing
* range, so we just update that range.
* 3. The requested range overlaps the end of an existing
* range, so we just update that range.
* 4. The requested range punches a hole in an existing range,
* so we have to update one side of the range and then
* create a new range for the other side.
*/
if (page_range_in_range(range, pgstart, pgend)) {
ret |= range->purged;
/* Case #1: Easy. Just nuke the whole thing. */
if (page_range_subsumes_range(range, pgstart, pgend)) {
range_del(range);
continue;
}
/* Case #2: We overlap from the start, so adjust it */
if (range->pgstart >= pgstart) {
range_shrink(range, pgend + 1, range->pgend);
continue;
}
/* Case #3: We overlap from the rear, so adjust it */
if (range->pgend <= pgend) {
range_shrink(range, range->pgstart, pgstart-1);
continue;
}
/*
* Case #4: We eat a chunk out of the middle. A bit
* more complicated, we allocate a new range for the
* second half and adjust the first chunk's endpoint.
*/
range_alloc(asma, range, range->purged,
pgend + 1, range->pgend);
range_shrink(range, range->pgstart, pgstart - 1);
break;
}
}
return ret;
}
同理罪佳,在上鎖的情況也可以依照上面解鎖的圖中的4種情況:
- 1.情況1如果內(nèi)存塊start和end包含了range區(qū)域,那么直接把range從unpinned_list中移除
- 2.如果是內(nèi)存塊start和end后半部分和讓相交娃兽,則直接修改要鎖定的unpinned_list中range的起始地址是內(nèi)存塊的末尾地址
- 3.如果是內(nèi)存塊start和end前半部分和讓相交菇民,則直接修改要鎖定的unpinned_list中range的末尾地址是內(nèi)存塊的起始地址
- 4.如果range包含了要鎖定的內(nèi)存塊,則要挖一個(gè)洞投储,移除range中間部分第练,把前后兩部分生成兩個(gè)新的range加入到unpinned_list
第5種情況不相交就不用管了。
Ashmem的內(nèi)存回收
既然存在了unpinned_list解鎖定的內(nèi)存區(qū)域玛荞,那么什么時(shí)候回收呢娇掏?這個(gè)操作結(jié)構(gòu)體在初始化的時(shí)候就提到過,我們來直接看看這個(gè)結(jié)構(gòu)體做了什么勋眯。
static struct shrinker ashmem_shrinker = {
.count_objects = ashmem_shrink_count,
.scan_objects = ashmem_shrink_scan,
.seeks = DEFAULT_SEEKS * 4,
};
核心是這個(gè)掃描函數(shù)ashmem_shrink_scan
static unsigned long
ashmem_shrink_scan(struct shrinker *shrink, struct shrink_control *sc)
{
struct ashmem_range *range, *next;
unsigned long freed = 0;
/* We might recurse into filesystem code, so bail out if necessary */
...
list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
loff_t start = range->pgstart * PAGE_SIZE;
loff_t end = (range->pgend + 1) * PAGE_SIZE;
range->asma->file->f_op->fallocate(range->asma->file,
FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
start, end - start);
range->purged = ASHMEM_WAS_PURGED;
lru_del(range);
freed += range_size(range);
if (--sc->nr_to_scan <= 0)
break;
}
mutex_unlock(&ashmem_mutex);
return freed;
}
能看到婴梧,在這個(gè)過程會(huì)循環(huán)之前解鎖放進(jìn)來的全局鏈表,并且不斷的調(diào)用lru_del刪除rang項(xiàng)客蹋,重新計(jì)算剩余的空間大小塞蹭。還有一個(gè)更加重要的是調(diào)用了fallocate文件操作調(diào)用。
還記得這個(gè)文件操作是在__shmem_file_setup中設(shè)置的嗎讶坯?我們?nèi)hmem中一看shmem_fallocate方法中解映射的邏輯
文件:/mm/shmem.c
static long shmem_fallocate(struct file *file, int mode, loff_t offset,
loff_t len)
{
struct inode *inode = file_inode(file);
struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
struct shmem_inode_info *info = SHMEM_I(inode);
struct shmem_falloc shmem_falloc;
pgoff_t start, index, end;
int error;
if (mode & ~(FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE))
return -EOPNOTSUPP;
mutex_lock(&inode->i_mutex);
if (mode & FALLOC_FL_PUNCH_HOLE) {
struct address_space *mapping = file->f_mapping;
loff_t unmap_start = round_up(offset, PAGE_SIZE);
loff_t unmap_end = round_down(offset + len, PAGE_SIZE) - 1;
DECLARE_WAIT_QUEUE_HEAD_ONSTACK(shmem_falloc_waitq);
/* protected by i_mutex */
if (info->seals & F_SEAL_WRITE) {
error = -EPERM;
goto out;
}
shmem_falloc.waitq = &shmem_falloc_waitq;
shmem_falloc.start = unmap_start >> PAGE_SHIFT;
shmem_falloc.next = (unmap_end + 1) >> PAGE_SHIFT;
spin_lock(&inode->i_lock);
inode->i_private = &shmem_falloc;
spin_unlock(&inode->i_lock);
if ((u64)unmap_end > (u64)unmap_start)
unmap_mapping_range(mapping, unmap_start,
1 + unmap_end - unmap_start, 0);
shmem_truncate_range(inode, offset, offset + len - 1);
/* No need to unmap again: hole-punching leaves COWed pages */
spin_lock(&inode->i_lock);
inode->i_private = NULL;
wake_up_all(&shmem_falloc_waitq);
spin_unlock(&inode->i_lock);
error = 0;
goto out;
}
...
out:
mutex_unlock(&inode->i_mutex);
return error;
}
因?yàn)檎{(diào)用時(shí)候設(shè)置了FALLOC_FL_PUNCH_HOLE標(biāo)志番电,因此會(huì)走到這里面,能看到最核心的方法辆琅,調(diào)用了unmap_mapping_range解開了物理內(nèi)存和虛擬內(nèi)存的映射關(guān)系漱办。同時(shí)調(diào)用shmem_truncate_range釋放保存在address_space的mapping的映射區(qū)域。并且設(shè)置等大小的文件空洞婉烟。這個(gè)這個(gè)空洞的意思娩井,可以訪問超出當(dāng)前的文件大小,當(dāng)然要在預(yù)留的空洞大小內(nèi)似袁,當(dāng)寫入的時(shí)候就會(huì)把該文件撐大洞辣。
總結(jié)
ashmem的使用流程如下:
- ashmem_create_region創(chuàng)建匿名共享內(nèi)存區(qū)域咐刨,本質(zhì)是調(diào)用open系統(tǒng)調(diào)用
- 2.ioctl設(shè)置共享內(nèi)存的名字和大小,設(shè)置的名字為/dev/ashmem/<filename>扬霜,名字的存在就為了能夠讓其他人找到目標(biāo)
- 3.mmap映射文件中的虛擬內(nèi)存以及物理內(nèi)存
- 4.直接對(duì)著這一塊地址區(qū)域讀寫所宰。
其中ioctl必須設(shè)置名字和大小,不然沒辦法進(jìn)行映射畜挥,因?yàn)樵谟成渲斑M(jìn)行了校驗(yàn)仔粥。
而mmap步驟才會(huì)真正的把匿名共享內(nèi)存的區(qū)域和file結(jié)構(gòu)體關(guān)聯(lián)起來,并且設(shè)置上shmem共享內(nèi)存的文件操作符以及共享內(nèi)存的vma操作蟹但。到這一步開始ashmem把工作交給了shmem中躯泰,最后通過alloc_page的方法把虛擬內(nèi)存和物理頁映射起來。
因此ashmem本質(zhì)上還是依靠這shmem共享內(nèi)存進(jìn)行工作华糖。那么ashmem和shmem有什么關(guān)系嗎麦向。從名字上就能知道a是指auto,也就是能夠通過內(nèi)存系統(tǒng)自動(dòng)回收需要的內(nèi)存客叉。
因此在ashmem中有比較重要的機(jī)制鎖定和解鎖的機(jī)制诵竭。一般所有解鎖都會(huì)放到當(dāng)前amsa的unpinned_list中管理,同時(shí)會(huì)記錄在全局變量ashmem_lru_list中兼搏。上鎖就是把添加到unpinned_list和ashmem_lru_list的內(nèi)存塊記錄移除卵慰。
因此,mmap誕生出來的整個(gè)內(nèi)存塊默認(rèn)是解鎖的佛呻。也正因?yàn)樘砑拥饺肿兞縜shmem_lru_list中裳朋,也就讓內(nèi)存管理系統(tǒng)遍歷ashmem_lru_list通過shmem_fallocate文件操作對(duì)這些內(nèi)存進(jìn)行解開映射,并且留下文件空洞(其實(shí)就是想辦法通過頁緩存,重新申請(qǐng)等手段重新把這一塊內(nèi)存重新填補(bǔ)上來)吓著,
原理圖如下:
思考
那么ashmem和Binder有什么區(qū)別呢鲤嫡?先放上Binder的mmap 的文章:
Android 重學(xué)系列 Binder驅(qū)動(dòng)的初始化 映射原理(二)
其實(shí)最主要的區(qū)別就是Binder的mmap時(shí)候已經(jīng)通過伙伴系統(tǒng)綁定了物理頁和虛擬內(nèi)存之間的聯(lián)系,而Ashmem則是通過缺頁中斷绑莺,調(diào)用相關(guān)的函數(shù)才進(jìn)行綁定暖眼。換句話說Ashmem是按需加載,而Binder則是一開始就通過mmap就分配好纺裁。當(dāng)然這也是Binder的機(jī)制相關(guān)诫肠,因?yàn)锽inder一旦在一個(gè)Android啟動(dòng)之后就要開始通信,同時(shí)Binder需要通過mmap的方式对扶,在Binder驅(qū)動(dòng)程序中設(shè)置一個(gè)象征進(jìn)程的內(nèi)核緩沖區(qū)区赵,方便一開始通信惭缰,沒必要等到中斷來了再申請(qǐng)物理頁浪南,從設(shè)計(jì)的角度來看Binder這么做更加合理。
兩者之間的設(shè)計(jì)有什么優(yōu)劣呢漱受?很明顯络凿,Ashmem就是打通一塊大的內(nèi)存通道方便進(jìn)程之間通信大數(shù)據(jù)骡送。而Binder更加傾向小規(guī)模的指令,并且這種指令有明確的方向和順序絮记,保證每一個(gè)指令的可靠性摔踱。從功能上看起來差不多的東西,但是由于設(shè)計(jì)出發(fā)的角度看來怨愤,Binder為了保證每一個(gè)指令的可靠做了極其復(fù)雜的數(shù)據(jù)結(jié)構(gòu)進(jìn)行管理派敷。
對(duì)了,稍后還有一篇關(guān)于Linux內(nèi)存管理的筆記撰洗,如果對(duì)內(nèi)存管理系統(tǒng)比較吃力篮愉,不妨先去看那一篇再回來看Binder和Ashmem吧。