Android log_kid 极速日志打印(MMAP)

  • Post author:
  • Post category:其他


最近一直在看关于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对应了硬盘中的日志文件。


  1. 初始化文件

  • 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 ;
    }
}

  1. 日志写入:

计算本次写入的数据总大小: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



版权声明:本文为mldxs原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。