一、前言
在性能敏感的場景中,傳統(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
:允許進程間共享映射。 -
fd
和0
:指定映射的文件和偏移。 - 返回值:成功返回映射地址,失敗返回
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