使用 mmap 函數(shù)實現(xiàn)高效日志寫入

一、前言

在性能敏感的場景中,傳統(tǒng)的文件讀寫操作可能成為瓶頸。本文將通過 mmap 的高效內存映射特性,介紹如何構建一個高性能的日志系統(tǒng),并詳細拆解實現(xiàn)過程。


二. 為什么選擇 mmap

問題背景

在日志寫入場景中,頻繁的 I/O 調用可能帶來以下問題:

  • 性能瓶頸:傳統(tǒng)文件寫入需要在用戶態(tài)和內核態(tài)頻繁切換,效率較低。
  • 資源浪費:頻繁打開、關閉文件或調整文件大小可能導致系統(tǒng)資源耗盡。

mmap 的優(yōu)勢

  • 高效性:將文件映射到虛擬內存后,可以像操作普通內存一樣訪問文件內容,避免頻繁 I/O 調用。
  • 雙向同步:內存和文件之間的內容修改會自動同步,省去了額外的寫回操作。
  • 固定大小:適用于需要固定大小日志文件的場景,控制存儲空間使用。

適用場景包括:

  • 高性能日志系統(tǒng)。
  • 共享內存實現(xiàn)多進程間通信。
  • 高速文件讀寫操作。

三. mmap 的核心概念

mmap 是 Linux 提供的系統(tǒng)調用,功能是將文件或設備映射到進程的虛擬地址空間中,核心作用包括:

  • 虛擬地址到文件的映射:進程可以直接通過內存地址訪問文件內容。
  • 支持雙向數(shù)據(jù)同步:修改虛擬地址空間中的數(shù)據(jù)會反映到文件中,反之亦然。

函數(shù)原型

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
參數(shù) 含義
addr 指定映射的起始地址,通常傳 nullptr 讓系統(tǒng)選擇。
length 映射的內存大小,必須為頁大小的整數(shù)倍。
prot 映射內存的訪問權限,如 PROT_READ 、 PROT_WRITE
flags 映射類型,如 MAP_SHARED:映射的內存區(qū)域與文件共享,修改映射區(qū)域的內容會影響文件。
fd 文件描述符,指定映射到內存的文件。
offset 是映射開始位置在文件中的偏移。指定從文件開頭偏移的字節(jié)數(shù),表示映射區(qū)域在文件中的起始位置。該偏移必須是頁面大小的整數(shù)倍,因為內存映射操作通常按頁面(通常是4KB)對齊。

返回值為映射內存的起始地址,失敗時返回 MAP_FAILED


四. 日志系統(tǒng)的設計與實現(xiàn)邏輯

設計目標

  • 固定大小日志文件:日志文件大小固定為 1MB,避免無限增長占用存儲空間。
  • 高效日志寫入:利用 mmap 減少 I/O 調用,提升性能。
  • 簡單易用:提供寫入和讀取日志內容的基礎功能。

實現(xiàn)步驟

以下是實現(xiàn)的核心步驟:


image.png

Step 1:打開文件

int fd = open("logfile.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
  • 功能:打開或創(chuàng)建一個名為 logfile.txt 的日志文件。
  • 參數(shù)解析
    • O_RDWR:以讀寫模式打開文件。
    • O_CREAT:如果文件不存在,則創(chuàng)建。
    • O_TRUNC:如果文件已存在,清空文件內容。
  • 權限0666 表示所有用戶可讀寫。

Step 2:調整文件大小

if (ftruncate(fd, LOG_SIZE) == -1) {
    perror("Error truncating file");
    close(fd);
    return -1;
}
  • 功能:將文件大小調整為 LOG_SIZE(1MB)。
  • 作用:為 mmap 分配固定大小的映射范圍。
  • 注意:文件大小不足時會填充空字節(jié),超出時會截斷。

Step 3:映射文件到內存

char* mapped = (char*)mmap(nullptr, LOG_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
    perror("Error mapping file");
    close(fd);
    return -1;
}
  • 功能:將文件映射到虛擬內存地址。
  • 關鍵參數(shù)
    • PROT_READ | PROT_WRITE:支持讀寫權限。
    • MAP_SHARED:允許進程間共享映射。
    • fd0:指定映射的文件和偏移。
    • 返回值:成功返回映射地址,失敗返回 MAP_FAILED

Step 4:寫入日志內容

void write_log(char* mapped, const char* log_msg, size_t offset) {
    if (offset < LOG_SIZE) {
        strcpy(mapped + offset, log_msg);
    } else {
        std::cerr << "Offset exceeds log size!" << std::endl;
    }
}
  • 功能:將日志內容寫入映射的內存區(qū)域。
  • 參數(shù)
    • mapped是指向已映射到內存的文件內容的指針。
    • log_msg是要寫入日志的字符串。
    • offset是要寫入日志的起始位置。
  • 邏輯
    這個strcpy是字符串復制函數(shù),用于將log_msg(源字符串)復制到mapped + offset(目標位置)。
    由于內存映射是通過mmap將文件內容加載到內存中,mapped是指向文件內容的指針。通過mapped + offset來指定在內存中的哪個位置開始寫入數(shù)據(jù)。
    需要確保offset不超過映射的大小(LOG_SIZE),否則可能會導致越界寫入,造成不可預知的錯誤。

步驟 5:讀取日志內容

void read_log(char* mapped) {
    std::cout << "Log content: " << std::endl;
    std::cout << mapped << std::endl;
}
  • 功能:從映射內存中讀取日志內容。
  • 邏輯:直接訪問映射的內存,讀取日志內容并打印。

步驟 6:取消映射與資源釋放

if (munmap(mapped, LOG_SIZE) == -1) {
    perror("Error unmapping file");
}
close(fd);
  • 功能:釋放映射的內存區(qū)域和文件資源。
    • munmap 取消映射,進程在映射空間對共享內容的改變并不直接寫回到磁盤文件中。
    • close 關閉文件描述符,釋放系統(tǒng)資源。

4. 完整代碼示例

以下是完整實現(xiàn):

#include <jni.h>
#include <string>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <android/log.h>

// Android日志宏
#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

const size_t LOG_SIZE = 1024 * 1024; // 1MB

// 獲取當前日志文件的偏移量
size_t get_current_offset(const char* mapped) {
    size_t offset = 0;
    while (offset < LOG_SIZE && mapped[offset] != '\0') {
        ++offset;
    }
    return offset;
}

void write_log(char* mapped, const char* log_msg, size_t offset) {
    size_t log_len = strlen(log_msg);
    if (offset + log_len < LOG_SIZE) {
        strcpy(mapped + offset, log_msg);
    } else {
        LOGI("Not enough space to append log!");
    }
}

void read_log(char* mapped) {
    LOGI("Log content: %s", mapped);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_mmapsamples_MainActivity_writeLog(JNIEnv *env, jobject thiz, jstring msg) {
    // 獲取應用的私有文件路徑
    const char* filename = "/data/data/com.xgimi.mmapsamples/files/logfile.txt";

    // 打開日志文件
    int fd = open(filename, O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("Error opening file");
        return;
    }

    // 擴展文件大小
    if (ftruncate(fd, LOG_SIZE) == -1) {
        perror("Error truncating file");
        close(fd);
        return;
    }

    // 映射文件到內存
    char* mapped = (char*)mmap(nullptr, LOG_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("Error mapping file");
        close(fd);
        return;
    }

    // 獲取偏移量并寫入日志
    const char* log_msg = env->GetStringUTFChars(msg, nullptr);
    size_t offset = get_current_offset(mapped);
    write_log(mapped, log_msg, offset);
    env->ReleaseStringUTFChars(msg, log_msg);

     // 解除映射
    if (munmap(mapped, LOG_SIZE) == -1) {
        perror("Error unmapping file");
    }

    // 關閉文件
    close(fd);

五. 總結與擴展

  • 總結:通過 mmap,可以高效地實現(xiàn)文件讀寫操作,特別適用于固定大小的日志存儲場景。
  • 擴展
    • mmap 的應用場景不限于日志,還可用于共享內存、多線程通信等。
    • 可結合環(huán)形緩沖區(qū)設計,實現(xiàn)循環(huán)日志存儲,進一步優(yōu)化日志系統(tǒng)。

六.參考

內存映射的一些理論概述,可參考如下文章:
https://blog.csdn.net/luo_boke/article/details/109311432?utm_source=chatgpt.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容