Android 与 Linux 内核的这段互动patch历史演进之五
摘要: Logger是Android早期为解决内核日志机制不足而设计的专用日志系统,采用结构化环形缓冲区存储带元数据的日志条目,通过字符设备接口实现高效读写。其核心采用生产者-消费者模式,通过wait_queue和mutex同步。但随着Linux主线追踪技术(如ftrace、tracepoints、perf)的发展,Logger因功能单一、性能不足、内核冗余等缺陷被逐步替代。最终这个典型案例展示了
Logger 的故事是一个典型的“被主线技术完全替代”的案例。它最初是 Android 为解决特定问题而引入的简单方案,但随着主线 Linux 日志和追踪技术的飞速发展,Logger 的功能被更强大、更通用的机制所覆盖,最终被从内核中移除。
第五部分:Logger - Android 的日志系统
1. Logger 的设计目标与核心思想
在 Android 的早期,现有的内核日志机制 printk 和用户空间日志机制(如 syslog)存在一些不足:
-
printk的局限性:-
环形缓冲区大小固定,日志容易被冲掉。
-
所有日志混在一起,缺乏分类和过滤。
-
日志级别控制不够精细。
-
性能开销较大,尤其是在频繁打印时。
-
-
用户空间日志的需求:
-
Android 应用和系统组件(如
ActivityManager,WindowManager)需要产生大量结构化的日志。 -
需要一种高效、低延迟的日志机制,用于调试和问题分析。
-
日志需要分不同的“标签”(tag)进行组织。
-
Logger的设计目标:
-
高效性: 为用户空间提供一种非常快速的内核级日志记录机制。
-
多缓冲区: 为不同类型的日志提供独立的环形缓冲区(如
main,events,radio)。 -
结构化: 每条日志条目都带有精确的时间戳、进程ID、线程ID和用户自定义的标签。
-
简单性: 提供一个简单的字符设备接口(如
/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. 软件设计模式分析
-
生产者-消费者模式:
-
生产者: 调用
write的 Android 组件和应用程序。 -
消费者:
logcat工具和其他读取/dev/log/*的进程。 -
缓冲区:
logger_log->buffer环形缓冲区。 -
同步: 使用
wait_queue_head_t和mutex进行同步。当缓冲区为空时,消费者休眠;当生产者写入新数据时,唤醒消费者。
-
-
装饰器模式: Logger 可以被看作是对原始日志消息的装饰。它在原始消息(
buf)的基础上,添加了 PID、TID、时间戳等元数据,形成了一个结构化的logger_entry。 -
迭代器模式:
logger_reader结构体和一个日志缓冲区相关联,并维护着一个读偏移量r_off。这就像一个迭代器,顺序地遍历环形缓冲区中的日志条目。
4. Logger 的消亡与主线替代方案
尽管 Logger 简单有效,但它存在固有的缺陷,最终导致其被抛弃:
Logger 的缺陷:
-
内核冗余: 其核心功能(环形缓冲区、等待队列)与内核已有的机制大量重叠,违反了“不要重复造轮子”的原则。
-
性能并非最优: 每次写入都需要两次拷贝(用户空间->内核头部,用户空间->内核有效载荷),并且持有互斥锁的时间较长。
-
功能单一: 只是一个简单的日志存储和转发机制,缺乏强大的过滤、分类、动态启用/禁用等高级功能。
-
调试能力弱: 当系统崩溃时,Logger 缓冲区的内容会丢失,因为它存在于易失性内存中。
主线的替代方案:ftrace、perf、tracepoints 等
主线 Linux 内核发展出了一套无比强大的跟踪(Tracing)子系统,完全覆盖并超越了 Logger 的功能。
-
ftrace:-
功能: 内核函数跟踪器。可以跟踪几乎所有内核函数的调用情况,包括调用时间、耗时、参数等。
-
对比 Logger:
ftrace使用 per-CPU 缓冲区,锁竞争更少,性能极高。它可以通过trace_printk输出格式化消息,功能远超 Logger 的简单文本记录。 -
使用:
echo 1 > /sys/kernel/debug/tracing/tracing_on,然后查看/sys/kernel/debug/tracing/trace。
-
-
tracepoints:-
功能: 在内核关键路径上放置的静态钩子。当跟踪开启时,会触发相关的探测函数,记录数据。
-
对比 Logger: Android 的
events日志缓冲区本质上是一种简单的tracepoint。主线的tracepoint更规范、更高效,并且与perf、ftrace等工具无缝集成。
-
-
perf:-
功能: 一个强大的性能分析工具,可以采样 CPU 性能计数器、硬件事件、软件事件(包括
tracepoints)。 -
对比 Logger:
perf可以记录带有时间戳和调用栈的事件,分析能力是 Logger 无法比拟的。
-
-
pstore/ramoops:-
功能: 在系统崩溃(如 Oops 或 Panic)时,将内核日志、控制台输出等保存到一块在重启后依然能保留内容的存储区(如特定的 RAM 区域或闪存)。
-
对比 Logger: 解决了 Logger 崩溃日志丢失的问题。
-
结局: 随着 Android 内核与主线内核的不断融合,以及这些主线追踪技术的成熟和稳定,Logger 驱动最终在 Linux 内核 3.4 版本左右被完全移除。Android 的用户空间日志也迁移到了纯用户空间的实现(如 logd 守护进程),它使用 socket 进行通信,不再依赖内核驱动。
本段总结:详细分析了 Logger 驱动的设计目标、核心数据结构、写入和读取的完整路径。看到了其作为一个简单、高效、结构化的日志方案的优点。但更重要的是,分析了其固有的缺陷,以及它如何被主线 Linux 更强大、更通用、性能更好的追踪和调试子系统(如 ftrace, tracepoints, perf)所完全取代。这是一个“专用方案被通用方案淘汰”的完美案例。
更多推荐

所有评论(0)