Logger 的故事是一个典型的“被主线技术完全替代”的案例。它最初是 Android 为解决特定问题而引入的简单方案,但随着主线 Linux 日志和追踪技术的飞速发展,Logger 的功能被更强大、更通用的机制所覆盖,最终被从内核中移除。


第五部分:Logger - Android 的日志系统

1. Logger 的设计目标与核心思想

在 Android 的早期,现有的内核日志机制 printk 和用户空间日志机制(如 syslog)存在一些不足:

  • printk 的局限性

    • 环形缓冲区大小固定,日志容易被冲掉。

    • 所有日志混在一起,缺乏分类和过滤。

    • 日志级别控制不够精细。

    • 性能开销较大,尤其是在频繁打印时。

  • 用户空间日志的需求

    • Android 应用和系统组件(如 ActivityManager, WindowManager)需要产生大量结构化的日志。

    • 需要一种高效、低延迟的日志机制,用于调试和问题分析。

    • 日志需要分不同的“标签”(tag)进行组织。

Logger的设计目标

  1. 高效性: 为用户空间提供一种非常快速的内核级日志记录机制。

  2. 多缓冲区: 为不同类型的日志提供独立的环形缓冲区(如 main, events, radio)。

  3. 结构化: 每条日志条目都带有精确的时间戳、进程ID、线程ID和用户自定义的标签。

  4. 简单性: 提供一个简单的字符设备接口(如 /dev/log/main),供用户空间进程写入。

核心思想: 在内核中为每一类日志维护一个独立的、结构化的环形缓冲区,用户空间进程通过 write 系统调用向这些设备写入日志。

2. Logger 核心代码(以其被移除前的版本为例)

代码文件drivers/staging/android/logger.c (它长期处于 staging 目录,这本身就意味着代码“有待改进”)

第一部分:核心数据结构

/*
 * 描述一个日志缓冲区(例如 main, events, radio)。
 */
struct logger_log {
    unsigned char *buffer;    // 指向环形缓冲区内存的指针
    struct miscdevice misc;   // 杂项设备,用于自动创建 /dev/log/<name>
    wait_queue_head_t wq;     // 等待队列,供读取者等待新日志
    struct list_head readers; // 链接所有正在读取此日志的 reader
    struct mutex mutex;       // 保护整个 logger_log 结构的互斥锁
    size_t w_off;             // **当前写位置偏移量** - 关键!
    size_t head;              // 环形缓冲区的头部(最老的有效数据)
    size_t size;              // 环形缓冲区的总大小
};
​
/*
 * 描述一条日志条目在缓冲区中的布局。
 * 这是Logger“结构化”特性的体现。
 */
struct logger_entry {
    uint16_t len;      // 有效载荷(payload)的长度
    uint16_t __pad;    // 填充,用于对齐
    int32_t pid;       // 写入日志的进程ID
    int32_t tid;       // 写入日志的线程ID
    int32_t sec;       // 时间戳(秒部分)
    int32_t nsec;      // 时间戳(纳秒部分)
    char msg[0];       // 柔性数组,存储实际的日志消息(以空字符结尾的字符串)
};
​
/*
 * 描述一个正在读取日志的客户端。
 */
struct logger_reader {
    struct logger_log *log;   // 正在读取的日志缓冲区
    struct list_head list;    // 用于挂入 log->readers 链表
    size_t r_off;             // **当前读位置偏移量** - 关键!
    bool r_all;               // 是否读取所有日志(忽略日志级别)
    int r_ver;                // 读取器版本
};

第二部分:日志写入 - logger_write

这是最核心的函数,展示了Logger如何将一条日志写入环形缓冲区。

/*
 * 当用户空间进程向 /dev/log/main 等设备写入时,会调用此函数。
 * @filp: 文件指针
 * @buf: 用户空间缓冲区,包含要写入的日志消息
 * @count: 要写入的字节数
 * @ppos: 文件位置(对于Logger来说,这个参数被忽略,因为它是顺序写入的)
 */
static ssize_t logger_write(struct file *filp, const char __user *buf,
                size_t count, loff_t *ppos)
{
    struct logger_log *log = filp->private_data;
    struct logger_entry header;
    struct timespec now;
    ssize_t ret = 0;
​
    // 获取当前时间戳。这提供了每条日志的精确时间。
    now = current_kernel_time();
​
    /* 
     * 填充日志条目的头部信息。
     * 这就是Logger结构化的核心:每条日志都带有丰富的元数据。
     */
    header.pid = current->tgid; // 在Linux中,线程组ID就是进程ID
    header.tid = current->pid;  // 而 pid 字段实际上是线程ID
    header.sec = now.tv_sec;
    header.nsec = now.tv_nsec;
    header.len = min_t(size_t, count, LOGGER_ENTRY_MAX_PAYLOAD);
    header.__pad = 0;
​
    // 计算一条完整日志记录的总大小:头部大小 + 有效载荷大小
    size_t len = sizeof(header) + header.len;
​
    mutex_lock(&log->mutex);
​
    /*
     * **关键步骤:写入环形缓冲区**
     * 这里逻辑是:如果缓冲区末尾剩余空间不足以放下整条日志,则:
     *   a) 将剩余空间标记为废弃(通过写入一个长度为0的logger_entry)。
     *   b) 将写指针绕回到缓冲区开头。
     */
    if (log->w_off + len > log->size) {
        // 情况A:日志需要被环绕(wrap)
        struct logger_entry *fill;
​
        // 在缓冲区末尾创建一个“填充”条目,其len=0,表示这是无效数据。
        fill = (struct logger_entry *)(log->buffer + log->w_off);
        fill->len = 0;
        fill->pid = 0; // PID为0的填充条目
        fill->tid = 0;
        fill->sec = 0;
        fill->nsec = 0;
        // 写指针回到缓冲区开头
        log->w_off = 0;
    }
​
    /* 
     * 现在,log->w_off 指向一个可以写入完整日志记录的位置。
     * 将日志条目头部拷贝到缓冲区。
     */
    memcpy(log->buffer + log->w_off, &header, sizeof(header));
    log->w_off += sizeof(header);
​
    // 将用户空间的日志消息(有效载荷)拷贝到缓冲区。
    if (copy_from_user(log->buffer + log->w_off, buf, header.len)) {
        // 如果拷贝失败,需要回滚写指针。这是一个重要的错误处理。
        log->w_off -= sizeof(header);
        ret = -EFAULT;
        goto out;
    }
    log->w_off += header.len;
​
    // 更新环形缓冲区的头部(head)。如果新写入的数据覆盖了旧数据,head需要前进。
    ret = header.len; // 返回成功写入的字节数(只是有效载荷的长度)
​
    // ... 更新 log->head 的逻辑(略)...
​
    /* 
     * **通知等待的读取器**:有新的日志可读。
     * 这会唤醒所有在 log->wq 上等待的进程。
     */
    wake_up_interruptible(&log->wq);
​
out:
    mutex_unlock(&log->mutex);
    return ret;
}

第三部分:日志读取 - logger_read

这是 logcat 等工具读取日志的原理。

/*
 * 当用户空间进程从 /dev/log/main 等设备读取时,会调用此函数。
 */
static ssize_t logger_read(struct file *filp, char __user *buf,
               size_t count, loff_t *ppos)
{
    struct logger_reader *reader = filp->private_data;
    struct logger_log *log = reader->log;
    ssize_t ret = 0;
    DEFINE_WAIT(wait);
​
start:
    mutex_lock(&log->mutex);
​
    // 准备等待:将当前进程加入到 log->wq 等待队列
    prepare_to_wait(&log->wq, &wait, TASK_INTERRUPTIBLE);
​
    // 循环,直到有数据可读或被信号中断
    while (ret == 0) {
        struct logger_entry *entry;
        size_t len;
​
        // 检查当前读位置是否追上了写位置(没有新数据)
        if (log->w_off == reader->r_off) {
            if (filp->f_flags & O_NONBLOCK) {
                ret = -EAGAIN;
                break;
            }
            // 如果没有数据且不是非阻塞模式,则休眠
            mutex_unlock(&log->mutex);
            schedule(); // 在这里进程进入睡眠,直到被 logger_write 中的 wake_up 唤醒
            finish_wait(&log->wq, &wait);
            if (signal_pending(current))
                return -EINTR;
            goto start; // 被唤醒后,重新获取锁并检查数据
        }
​
        // **有数据可读**:获取当前读位置的 logger_entry
        entry = (struct logger_entry *)(log->buffer + reader->r_off);
​
        // 检查这条日志条目是否有效(len > 0)。因为缓冲区环绕处有len=0的填充条目。
        if (entry->len == 0) {
            // 遇到填充条目,读指针跳到缓冲区开头
            reader->r_off = 0;
            continue;
        }
​
        // 计算要拷贝到用户空间的数据长度:条目头 + 有效载荷
        len = sizeof(struct logger_entry) + entry->len;
        if (count < len) {
            // 用户提供的缓冲区太小,无法容纳一条完整的日志
            ret = -EINVAL;
            break;
        }
​
        // 将完整的日志记录(头部+消息)拷贝到用户空间
        if (copy_to_user(buf, entry, len)) {
            ret = -EFAULT;
            break;
        }
​
        // 前进读指针。这里要处理环绕。
        reader->r_off = logger_offset(reader->r_off + len, log->size);
​
        ret = len; // 返回成功读取的字节数
        break;
    }
​
    finish_wait(&log->wq, &wait);
    mutex_unlock(&log->mutex);
    return ret;
}
3. 软件设计模式分析
  1. 生产者-消费者模式

    • 生产者: 调用 write 的 Android 组件和应用程序。

    • 消费者logcat 工具和其他读取 /dev/log/* 的进程。

    • 缓冲区logger_log->buffer 环形缓冲区。

    • 同步: 使用 wait_queue_head_tmutex 进行同步。当缓冲区为空时,消费者休眠;当生产者写入新数据时,唤醒消费者。

  2. 装饰器模式: Logger 可以被看作是对原始日志消息的装饰。它在原始消息(buf)的基础上,添加了 PID、TID、时间戳等元数据,形成了一个结构化的 logger_entry

  3. 迭代器模式logger_reader 结构体和一个日志缓冲区相关联,并维护着一个读偏移量 r_off。这就像一个迭代器,顺序地遍历环形缓冲区中的日志条目。

4. Logger 的消亡与主线替代方案

尽管 Logger 简单有效,但它存在固有的缺陷,最终导致其被抛弃:

Logger 的缺陷

  1. 内核冗余: 其核心功能(环形缓冲区、等待队列)与内核已有的机制大量重叠,违反了“不要重复造轮子”的原则。

  2. 性能并非最优: 每次写入都需要两次拷贝(用户空间->内核头部,用户空间->内核有效载荷),并且持有互斥锁的时间较长。

  3. 功能单一: 只是一个简单的日志存储和转发机制,缺乏强大的过滤、分类、动态启用/禁用等高级功能。

  4. 调试能力弱: 当系统崩溃时,Logger 缓冲区的内容会丢失,因为它存在于易失性内存中。

主线的替代方案:ftraceperftracepoints

主线 Linux 内核发展出了一套无比强大的跟踪(Tracing)子系统,完全覆盖并超越了 Logger 的功能。

  1. ftrace

    • 功能: 内核函数跟踪器。可以跟踪几乎所有内核函数的调用情况,包括调用时间、耗时、参数等。

    • 对比 Loggerftrace 使用 per-CPU 缓冲区,锁竞争更少,性能极高。它可以通过 trace_printk 输出格式化消息,功能远超 Logger 的简单文本记录。

    • 使用echo 1 > /sys/kernel/debug/tracing/tracing_on,然后查看 /sys/kernel/debug/tracing/trace

  2. tracepoints

    • 功能: 在内核关键路径上放置的静态钩子。当跟踪开启时,会触发相关的探测函数,记录数据。

    • 对比 Logger: Android 的 events 日志缓冲区本质上是一种简单的 tracepoint。主线的 tracepoint 更规范、更高效,并且与 perfftrace 等工具无缝集成。

  3. perf

    • 功能: 一个强大的性能分析工具,可以采样 CPU 性能计数器、硬件事件、软件事件(包括 tracepoints)。

    • 对比 Loggerperf 可以记录带有时间戳和调用栈的事件,分析能力是 Logger 无法比拟的。

  4. pstore/ramoops

    • 功能: 在系统崩溃(如 Oops 或 Panic)时,将内核日志、控制台输出等保存到一块在重启后依然能保留内容的存储区(如特定的 RAM 区域或闪存)。

    • 对比 Logger: 解决了 Logger 崩溃日志丢失的问题。

结局: 随着 Android 内核与主线内核的不断融合,以及这些主线追踪技术的成熟和稳定,Logger 驱动最终在 Linux 内核 3.4 版本左右被完全移除。Android 的用户空间日志也迁移到了纯用户空间的实现(如 logd 守护进程),它使用 socket 进行通信,不再依赖内核驱动。


本段总结:详细分析了 Logger 驱动的设计目标、核心数据结构、写入和读取的完整路径。看到了其作为一个简单、高效、结构化的日志方案的优点。但更重要的是,分析了其固有的缺陷,以及它如何被主线 Linux 更强大、更通用、性能更好的追踪和调试子系统(如 ftrace, tracepoints, perf)所完全取代。这是一个“专用方案被通用方案淘汰”的完美案例。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐