Android mmap學(xué)習(xí)筆記

Android日志收集:

日志的收集一直有個痛點(diǎn),就是性能與日志完整性無法兼得。

保證性能:

要實(shí)現(xiàn)高性能的日志收集,勢必要使用大量內(nèi)存,先將日志寫入內(nèi)存中,然后在合適的時機(jī)將內(nèi)存里的日志寫入到文件系統(tǒng)中(flush), 如果在 flush 之前用戶強(qiáng)殺了進(jìn)程,那么內(nèi)存里的內(nèi)容會因此而丟失 。

保證完整:

日志實(shí)時寫入文件可以保證日志的完整性,但是寫文件是 IO 操作,涉及到用戶態(tài)與內(nèi)核態(tài)的切換,而且這種開銷是開啟線程都無法避免的,也就是說即使開啟一個新線程實(shí)時寫入也是相對耗時的。

mmap概念

mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。
特點(diǎn):實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。


image.png

mmap 內(nèi)存映射文件之后,可以直接通過操作內(nèi)存來讀寫文件,性能上接近直接讀寫內(nèi)存。針對一次寫文件,節(jié)省了用戶態(tài)到內(nèi)核態(tài)切換的開銷,也減少了數(shù)據(jù)拷貝的次數(shù)。

mmap代碼

地址:android/platform/bionic/libc/bionic/mmap.cpp:

mmap原型函數(shù):

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
  • 參數(shù)start:指向欲映射的內(nèi)存起始地址,通常設(shè)為 NULL,代表讓系統(tǒng)自動選定地址,映射成功后返回該地址。
  • 參數(shù)length:代表將文件中多大的部分映射到內(nèi)存。
  • 參數(shù)prot:映射區(qū)域的保護(hù)方式。可以為以下幾種方式的組合:
    PROT_NONE 無權(quán)限,基本沒有用
    PROT_READ 讀權(quán)限
    PROT_WRITE 寫權(quán)限
    PROT_EXEC執(zhí)行權(quán)限
  • 參數(shù)flags: 描述了映射的類型。
    MAP_FIXED 開啟這個選項,則 addr 參數(shù)指定的地址是作為必須而不是建議。如果由于空間不足等問題無法映射則調(diào)用失敗。不建議使用。
    MAP_PRIVATE 表明這個映射不是共享的。文件使用 copy on write 機(jī)制映射,任何內(nèi)存中的改動并不反映到文件之中。也不反映到其他映射了這個文件的進(jìn)程之中。如果只需要讀取某個文件而不改變文件內(nèi)容,可以使用這種模式。
    MAP_SHARED 和其他進(jìn)程共享這個文件。往內(nèi)存中寫入相當(dāng)于往文件中寫入。會影響映射了這個文件的其他進(jìn)程。與 MAP_PRIVATE沖突。
  • 參數(shù)fd: 文件描述符。進(jìn)行 map 之后,文件的引用計數(shù)會增加。因此,我們可以在 map 結(jié)束后關(guān)閉 fd,進(jìn)程仍然可以訪問它。當(dāng)我們 unmap 或者結(jié)束進(jìn)程,引用計數(shù)會減少。
  • 參數(shù)offset: 文件偏移,從文件起始算起。

應(yīng)用

Android中也有不少地方用到,比如匿名共享內(nèi)存,Binder機(jī)制
這里記錄下log4a中如何使用
java端:

public void init(String bufferPath, int capacity, String logPath) {
        try {
            ptr = initNative(bufferPath, capacity, logPath);
        }catch (Exception e) {
            Log.e(TAG, Log4a.getStackTraceString(e));
        }
}
public void write(String log) {
        if (ptr != 0) {
            try {
                writeNative(ptr, log);
            }catch (Exception e) {
                Log.e(TAG, Log4a.getStackTraceString(e));
            }
        }
}
public void flushAsync() {
        if (ptr != 0) {
            try {
                flushAsyncNative(ptr);
            }catch (Exception e) {
                Log.e(TAG, Log4a.getStackTraceString(e));
            }
        }
}
public void release() {
        if (ptr != 0) {
            try {
                releaseNative(ptr);
            }catch (Exception e) {
                Log.e(TAG, Log4a.getStackTraceString(e));
            }
            ptr = 0;
        }
}
initNative

初始化方法 initNative 接受3個參數(shù),分別是緩存文件的路徑,緩存文件的大小,日志的路徑

static jlong initNative(JNIEnv *env, jclass type, jstring buffer_path_,
           jint capacity, jstring log_path_) {
    const char *buffer_path = env->GetStringUTFChars(buffer_path_, 0);
    const char *log_path = env->GetStringUTFChars(log_path_, 0);
    const size_t buffer_size = static_cast(capacity);
    // 打開緩存文件
    int buffer_fd = open(buffer_path, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    // 打開日志文件
    int log_fd = open(log_path, O_RDWR|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    // buffer 的第一個字節(jié)會用于存儲日志路徑名稱長度,后面緊跟日志路徑,之后才是日志信息
    if (strlen(log_path) > CHAR_MAX / 2) {
        jclass je = env->FindClass("java/lang/IllegalArgumentException");
        std::ostringstream oss;
        oss << "The length of log path must be less than " << CHAR_MAX / 2;
        env -> ThrowNew(je, oss.str().c_str());
        return 0;
    }
    // 初始化異步文件刷新
    if (fileFlush == nullptr) {
        fileFlush = new AsyncFileFlush(log_fd);
    }
    char *buffer_ptr = openMMap(buffer_fd, buffer_size);
    bool map_buffer = true;
    //如果打開 mmap 失敗,則降級使用內(nèi)存緩存
    if(buffer_ptr == nullptr) {
        buffer_ptr = new char[capacity];
        map_buffer = false;
    }
    env->ReleaseStringUTFChars(buffer_path_, buffer_path);
    env->ReleaseStringUTFChars(log_path_, log_path);
    LogBuffer* logBuffer = new LogBuffer(buffer_ptr, buffer_size);
    //將buffer內(nèi)的數(shù)據(jù)清0, 并寫入日志文件路徑
    logBuffer->initData(log_path);
    logBuffer->map_buffer = map_buffer;
    return reinterpret_cast(logBuffer);
}
static char* openMMap(int buffer_fd, size_t buffer_size) {
    char* map_ptr = nullptr;
    if (buffer_fd != -1) {
        // 寫臟數(shù)據(jù)
        writeDirtyLogToFile(buffer_fd);
        // 根據(jù) buffer size 調(diào)整 buffer 文件大小
        ftruncate(buffer_fd, static_cast(buffer_size));
        lseek(buffer_fd, 0, SEEK_SET);
        map_ptr = (char *) mmap(0, buffer_size, PROT_WRITE | PROT_READ, MAP_SHARED, buffer_fd, 0);
        if (map_ptr == MAP_FAILED) {
            map_ptr = nullptr;
        }
    }
    return map_ptr;
}
  1. 回寫上次因斷電(泛指,包括強(qiáng)殺進(jìn)程)來不及寫到日志文件中的臟數(shù)據(jù)
  2. 根據(jù) buffer size, 使用 ftruncate 調(diào)整 buffer 文件大小
  3. 使用 mmap 創(chuàng)建文件內(nèi)存映射
writeNative
static void writeNative(JNIEnv *env, jobject instance, jlong ptr,
            jstring log_) {
    const char *log = env->GetStringUTFChars(log_, 0);
    LogBuffer* logBuffer = reinterpret_cast(ptr);
    size_t log_size = strlen(log);
    // 緩存寫不下時異步刷新
    if (log_size >= logBuffer->emptySize()) {
        logBuffer->async_flush(fileFlush);
    }
    logBuffer->append(log);
    env->ReleaseStringUTFChars(log_, log);
}

寫文件是 LogBuffer 和 AsyncFileFlush的async_flush方法 協(xié)作完成的

Android本身Api是否有提供mmap功能呢:

MappedByteBuffer

使用sample

static void writeDemo() {
    File dir = new File(logFileDir);
    if (!dir.exists()) {
        boolean mk = dir.mkdirs();
        Log.d(defTag, "make dir " + mk);
    }
    File eFile = new File(logFileDir + File.separator + fileName);
    byte[] strBytes = logContent.getBytes();
    try {
        RandomAccessFile randomAccessFile = new RandomAccessFile(eFile, "rw");
        MappedByteBuffer mappedByteBuffer;
        final int inputLen = strBytes.length;
        if (!eFile.exists()) {
            boolean nf = eFile.createNewFile();
            Log.d(defTag, "new log file " + nf);
            mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE);
        } else {
            mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, inputLen);
        }
        if (mappedByteBuffer.remaining() < inputLen) {
            mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE + inputLen);
        }
        mappedByteBuffer.put(strBytes);
        gCurrentLogPos += inputLen;
    } catch (Exception e) {
        Log.e(defTag, "WriteRunnable run: ", e);
        if (!eFile.exists()) {
            boolean nf = eFile.createNewFile();
            Log.d(defTag, "new log file " + nf);
        }
        FileOutputStream os = new FileOutputStream(eFile, true);
        os.write(logContent.getBytes());
        os.flush();
        os.close();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容