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

Android日志收集:

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

保證性能:

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

保證完整:

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

mmap概念

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


image.png

mmap 內(nèi)存映射文件之后综芥,可以直接通過操作內(nèi)存來讀寫文件,性能上接近直接讀寫內(nèi)存猎拨。針對(duì)一次寫文件征峦,節(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)自動(dò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 開啟這個(gè)選項(xiàng)卢厂,則 addr 參數(shù)指定的地址是作為必須而不是建議乾蓬。如果由于空間不足等問題無法映射則調(diào)用失敗慎恒。不建議使用。
    MAP_PRIVATE 表明這個(gè)映射不是共享的融柬。文件使用 copy on write 機(jī)制映射,任何內(nèi)存中的改動(dòng)并不反映到文件之中粒氧。也不反映到其他映射了這個(gè)文件的進(jìn)程之中。如果只需要讀取某個(gè)文件而不改變文件內(nèi)容靠欢,可以使用這種模式。
    MAP_SHARED 和其他進(jìn)程共享這個(gè)文件门怪。往內(nèi)存中寫入相當(dāng)于往文件中寫入。會(huì)影響映射了這個(gè)文件的其他進(jìn)程掷空。與 MAP_PRIVATE沖突肋殴。
  • 參數(shù)fd: 文件描述符。進(jìn)行 map 之后护锤,文件的引用計(jì)數(shù)會(huì)增加。因此烙懦,我們可以在 map 結(jié)束后關(guān)閉 fd,進(jìn)程仍然可以訪問它氯析。當(dāng)我們 unmap 或者結(jié)束進(jìn)程,引用計(jì)數(shù)會(huì)減少掩缓。
  • 參數(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個(gè)參數(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 的第一個(gè)字節(jié)會(huì)用于存儲(chǔ)日志路徑名稱長度叉讥,后面緊跟日志路徑,之后才是日志信息
    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 失敗图仓,則降級(jí)使用內(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);
    // 緩存寫不下時(shí)異步刷新
    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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末幅骄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拆座,更是在濱河造成了極大的恐慌主巍,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件孕索,死亡現(xiàn)場離奇詭異,居然都是意外死亡搞旭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門选脊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脸甘,“玉大人,你說我怎么就攤上這事丹诀《鄣模” “怎么了铆遭?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長枚荣。 經(jīng)常有香客問我,道長橄妆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任害碾,我火速辦了婚禮,結(jié)果婚禮上慌随,老公的妹妹穿的比我還像新娘芬沉。我一直安慰自己阁猜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布剃袍。 她就那樣靜靜地躺著,像睡著了一般笛园。 火紅的嫁衣襯著肌膚如雪侍芝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天州叠,我揣著相機(jī)與錄音,去河邊找鬼凶赁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虱肄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咏窿,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼集嵌!你這毒婦竟也來了萝挤?” 一聲冷哼從身側(cè)響起根欧,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凤粗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫌拣,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年亭罪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片应役。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡情组,死狀恐怖箩祥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袍祖,我是刑警寧澤底瓣,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站捐凭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏茁肠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一垦梆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧托猩,春花似錦印蓖、人聲如沸京腥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摆尝。三九已至因悲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勺爱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國打工琐鲁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人围段。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像奈泪,于是被迫代替她去往敵國和親适贸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涝桅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348