美文网首页
Android mmap学习笔记

Android mmap学习笔记

作者: R7_Perfect | 来源:发表于2020-09-10 10:58 被阅读0次

Android日志收集:

日志的收集一直有个痛点,就是性能与日志完整性无法兼得。

保证性能:

要实现高性能的日志收集,势必要使用大量内存,先将日志写入内存中,然后在合适的时机将内存里的日志写入到文件系统中(flush), 如果在 flush 之前用户强杀了进程,那么内存里的内容会因此而丢失 。

保证完整:

日志实时写入文件可以保证日志的完整性,但是写文件是 IO 操作,涉及到用户态与内核态的切换,而且这种开销是开启线程都无法避免的,也就是说即使开启一个新线程实时写入也是相对耗时的。

mmap概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
特点:实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。


image.png

mmap 内存映射文件之后,可以直接通过操作内存来读写文件,性能上接近直接读写内存。针对一次写文件,节省了用户态到内核态切换的开销,也减少了数据拷贝的次数。

mmap代码

地址:android/platform/bionic/libc/bionic/mmap.cpp:

mmap原型函数:

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
  • 参数start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
  • 参数length:代表将文件中多大的部分映射到内存。
  • 参数prot:映射区域的保护方式。可以为以下几种方式的组合:
    PROT_NONE 无权限,基本没有用
    PROT_READ 读权限
    PROT_WRITE 写权限
    PROT_EXEC执行权限
  • 参数flags: 描述了映射的类型。
    MAP_FIXED 开启这个选项,则 addr 参数指定的地址是作为必须而不是建议。如果由于空间不足等问题无法映射则调用失败。不建议使用。
    MAP_PRIVATE 表明这个映射不是共享的。文件使用 copy on write 机制映射,任何内存中的改动并不反映到文件之中。也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式。
    MAP_SHARED 和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程。与 MAP_PRIVATE冲突。
  • 参数fd: 文件描述符。进行 map 之后,文件的引用计数会增加。因此,我们可以在 map 结束后关闭 fd,进程仍然可以访问它。当我们 unmap 或者结束进程,引用计数会减少。
  • 参数offset: 文件偏移,从文件起始算起。

应用

Android中也有不少地方用到,比如匿名共享内存Binder机制
这里记录下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个参数,分别是缓存文件的路径,缓存文件的大小,日志的路径

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 的第一个字节会用于存储日志路径名称长度,后面紧跟日志路径,之后才是日志信息
    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 失败,则降级使用内存缓存
    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内的数据清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) {
        // 写脏数据
        writeDirtyLogToFile(buffer_fd);
        // 根据 buffer size 调整 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. 回写上次因断电(泛指,包括强杀进程)来不及写到日志文件中的脏数据
  2. 根据 buffer size, 使用 ftruncate 调整 buffer 文件大小
  3. 使用 mmap 创建文件内存映射
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);
    // 缓存写不下时异步刷新
    if (log_size >= logBuffer->emptySize()) {
        logBuffer->async_flush(fileFlush);
    }
    logBuffer->append(log);
    env->ReleaseStringUTFChars(log_, log);
}

写文件是 LogBuffer 和 AsyncFileFlush的async_flush方法 协作完成的

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();
    }
}

相关文章

  • Android mmap学习笔记

    Android日志收集: 日志的收集一直有个痛点,就是性能与日志完整性无法兼得。 保证性能: 要实现高性能的日志收...

  • android 进程间通讯之mmap(转载补充)

    一、序 说到内存映射函数mmap大家可能觉得陌生,其实Android中的Binder机制就是mmap来实现的。不仅...

  • Android线程池学习笔记(一)

    Android线程学习笔记学习了线程源码,Android Future学习笔记学习了Future体系,接下来我们就...

  • Android学习--binder机制(二)MMAP

    这篇文章讲的很详细很好。Android-内存映射mmap_mcryeasy的博客-CSDN博客[https://b...

  • 学习mmap

    最近在工作中遇到一个mmap使用相关的问题,造成了一定的困惑,于是花了些时间补了下 mmap的功课,在这里分享给大...

  • Binder驱动之内存映射`binder_mmap`

    一 内存映射函数的实现 binder_mmap(kernel/drivers/android/binder.c) ...

  • android动画你看这篇就够了

    1、基础知识Android动画学习笔记-Android Animation

  • 指纹识别-Android

    指纹识别-Android @(Android进阶资料)[Android, 学习, 读书笔记, Markdown]指...

  • mmap

    简单的目录 mmap基础概念 mmap内存映射原理 mmap和常规文件操作的区别 mmap优点总结 mmap相关函...

  • MMAP总结笔记

    内核空间与用户空间 1.现代计算机都有两种以上的运行模式(普通模式、特权模式),linux系统只有两层:高优先级模...

网友评论

      本文标题:Android mmap学习笔记

      本文链接:https://www.haomeiwen.com/subject/mkguektx.html