最近一直在看关于mmap写入文件方面的文章,为了巩固学习成果,外加网络上使用mmap替代sharepreference的开源库很多,但是并没有使用mmap实现日志打印的开源库,遂实现了一份Android系统下使用mmap打印日志的工具log_kid。
源码分享:
https://gitee.com/gggl/log_kid
MMAP原理篇:
网络上关于MMAP原理的讲解有很多,其实主要记住三点即可:
-
传统IO进行写入的时候需要有两次的用户空间到内核空间的拷贝。
-
MMAP读写文件只有一次内存拷贝,就是在缺页中断的时候,从磁盘中直接读取到用户空间。
-
使用MMAP写文件,可以想象成对内存操作。
大家可以参考:
https://bbs.huaweicloud.com/blogs/291892
这部分网络上很多。
log_kid实现原理:
log_kid的实现参考了mmkv的实现方式。
1、log_kid写入日志文件,文件的前4个字节是文件的内容大小。由于mmap每次需要映射页的整数倍的内存(系统规定,因为mmap的读写是靠的缺页中断实现:
https://juejin.cn/post/6956031662916534279
),所以文件末尾会有一些0的填充数据,通过在文件头写入4个字节来控制文件的具体写入位置。
2、通过write_log(tag , message)进行日志写入。
每条日志的前4个字节为tag的大小,跟着tag内容,4个字节的message大小,message内容。
文件结构:
源码流程:
MemoryFile对应了硬盘中的日志文件。
-
初始化文件
-
open进行文件的创建
-
fstat获取文件大小,如果文件大小不是页的整数倍,扩容至页的整数倍
if (fileSize < pageSize || fileSize % pageSize != 0)
-
truncate函数对文件进行扩容,但是truncate扩容的文件属于稀疏文件,当系统出现内存或者硬盘空间不足的时候可能出现崩溃的问题,所以扩容后用0填充扩容的内容。
-
mmap映射文件,mmap具体参数含义网络上的讲解也比较多。
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)
-
获取文件大小,读取ptr的前4个字节,如果是新文件则为一个页的大小。
memcpy(&actualSize , ptr , 4) ;
void MemoryFile::reloadFromFile() {
int lastSlash = logFilePath.find_last_of('/') + 1 ;
std::string path = logFilePath.substr(0 , lastSlash) ;
mkPath(path) ;
fd = open(logFilePath.c_str() , O_RDWR | O_CREAT | O_CLOEXEC, S_IRWXU) ;
if (fd < 0) {
LOGD("faild to open %s" , logFilePath.c_str()) ;
} else {
struct stat st = {} ;
if (fstat(fd , &st) != -1) {
fileSize = (int)st.st_size ;
}
bool newFile = fileSize == 0 ;
if (fileSize < pageSize || fileSize % pageSize != 0) {
int roundSize = ((fileSize / pageSize) + 1 ) * pageSize ;
if (truncate(roundSize)) {
fileSize = roundSize ;
}
}
smmap() ;
if (newFile) {
actualSize = 0 ;
memcpy(ptr , &actualSize , 4) ;
} else {
memcpy(&actualSize , ptr , 4) ;
}
actualSize = actualSize + 4 ;
}
}
-
日志写入:
计算本次写入的数据总大小:logSize,如果总大小加上actualSize大于文件大小,则文件进行扩容,grow,每次扩容为上次文件的一倍大小。
writeInt和writeStr为真正写入数据的方法。
bool MemoryFile::writeLog(const char *tag, const char *msg) {
int logSize = 0 ;
int tagSize = strlen(tag) ;
int msgSize = strlen(msg) ;
int intSize = 4 ;
logSize = tagSize + msgSize + intSize * 2 ;
if (actualSize + logSize > fileSize) {
grow() ;
}
writeInt(tagSize) ;
writeStr(tag , tagSize) ;
writeInt(msgSize) ;
writeStr(msg , msgSize) ;
memcpy(ptr , &actualSize , 4) ;
return true ;
}
void MemoryFile::writeInt(int &value) {
memcpy(ptr + actualSize , &value , 4) ;
actualSize += 4 ;
}
void MemoryFile::writeStr(const char *value , int &size) {
memcpy(ptr + actualSize , value , size) ;
actualSize += size ;
}
总结:
整个项目实现简单轻便,易于理解,易于上手,可能还达不到商业级组件的高度,还需慢慢打磨,也希望有兴趣的同学可以一起来维护这套项目。
实测表现:
使用mmap写入日志速度大概快于传统IO的4-5倍以上,目前项目并没有进一步优化,优化后的速度应该还可以进一步提升。
源码分享:
https://gitee.com/gggl/log_kid