Android 日志系统源码解析:从 Log.i() 到 logcat 输出,一条日志的完整旅程
本文基于
system/logging仓库源码(Android 14 / U 时期,含 SerializedLogBuffer 与 LogReaderThread 架构),从应用打日志讲到 logcat 屏幕输出,把 liblog → logdw → logd → logdr → logcat 整条链路逐层拆开。所有代码均摘自本仓库真实源码,标注了文件:行号,可对照阅读。适合读者:做过 Android Framework/App 开发,用过
adb logcat,想搞清楚"这条日志到底是怎么到我眼前的"的同学。
1. 全景图:一条日志的一生
先给结论。你在屏幕上看到的一行日志:
less
08-15 10:23:45.123 1234 5678 I ActivityManager: Start proc for activity
背后的完整链路是这样的:
scss
┌─────────────────────────────────────────────────────────────────────────┐
│ 写入侧(任何进程) │
│ │
│ Java: Log.i() ──JNI──┐ │
│ Native: ALOGI() ──────┼─► liblog.so │
│ │ __android_log_write() │
│ │ └► 组包 [prio|tag\0|msg\0] │
│ │ └► 加 android_log_header_t{id,tid,realtime}│
│ │ └► writev() ──────────────┐ │
│ │ PmsgWrite() ─► /dev/pmsg0 │(重启前日志) │
└────────────────────────────────────────────────────────┼────────────────┘
▼
/dev/socket/logdw (AF_UNIX DGRAM)
│
┌────────────────────────────────────────────────────────▼────────────────┐
│ logd 进程(uid=logd) │
│ │
│ LogListener 线程 "logd.writer" ──recvmsg + SCM_CREDENTIALS 拿可信uid │
│ └► SerializedLogBuffer::Log() ──全局 sequence++,写入 SerializedLogChunk│
│ └► MaybePrune() ──超限则按 chunk 淘汰最老的 │
│ └► LogReaderList::NotifyNewLog() ──唤醒所有 watching 的读者 │
│ │
│ 其他日志来源:LogKlog(/proc/kmsg) LogAudit(NETLINK_AUDIT) TrustyLog │
│ │
│ LogReader 线程 "logd.reader" ──接受 logcat 连接,解析命令 │
│ └► 每个连接 spawn 一个 LogReaderThread "logd.reader.per" │
│ └► FlushTo() 按 sequence 归并多缓冲区 ──► 写回 socket │
└────────────────────────────────────────────────────────┼────────────────┘
▼
/dev/socket/logdr (AF_UNIX SEQPACKET)
│
┌────────────────────────────────────────────────────────▼────────────────┐
│ logcat 进程 │
│ android_logger_list_read() ─► recv() 拿 logger_entry+payload │
│ └► ProcessBuffer() 解析 prio/tag/msg │
│ └► 过滤(filterspec/regex/pid/uid) ─► 格式化(threadtime) ─► stdout │
└─────────────────────────────────────────────────────────────────────────┘
三个核心角色,三个 socket(定义在 logd/logd.rc):
| 组件 | 职责 | 关键文件 |
|---|---|---|
| liblog | 客户端库,所有进程打日志的入口,负责组包发给 logd | liblog/logger_write.cpp、logd_writer.cpp |
| logd | 日志守护进程,接收、存储、淘汰、分发日志 | logd/main.cpp、LogListener.cpp、SerializedLogBuffer.cpp、LogReader.cpp |
| logcat | 读取工具,连接 logd 拉取日志并格式化输出 | logcat/logcat.cpp、liblog/logprint.cpp |
| socket | 类型 | 权限 | 用途 |
|---|---|---|---|
logdw |
dgram+passcred,0222(任何人可写) |
写 | 客户端 → logd 写日志 |
logdr |
seqpacket,0666 |
读 | logcat → logd 读日志 |
logd |
stream,0666 |
控制 | logcat -g/-G/-c/-S 等管理命令 |
sql
/* logd/logd.rc:1-12 */
service logd /system/bin/logd
socket logd stream 0666 logd logd
socket logdr seqpacket 0666 logd logd
socket logdw dgram+passcred 0222 logd logd
file /proc/kmsg r
file /dev/kmsg w
user logd
group logd system package_info readproc
capabilities SYSLOG AUDIT_CONTROL
priority 10
task_profiles ServiceCapacityLow
onrestart setprop logd.ready false
注意 logdw 是 0222(只写)+ passcred :任何进程都能写日志,但写的时候内核会自动附带发送方凭证(uid/gid/pid),logd 以此做权限判断------身份由内核背书,客户端伪造不了。这是整个日志安全模型的基石,后文会反复看到。
还有一个容易被忽略的点:日志并不只来自应用。logd 自己还会从 /proc/kmsg 收内核日志(LogKlog)、从 NETLINK_AUDIT 收 SELinux 拒绝日志(LogAudit)、从 /dev/trusty-log0 收 Trusty TEE 日志。它们汇入同一个 LogBuffer,所以在 logcat 里你能用 -b kernel 看到内核 printk。
下面按"由浅入深"的顺序展开:先讲应用怎么打日志(第 2 节),再讲日志怎么发出去(第 3 节),然后是 logd 怎么收、怎么存(第 4、5 节),接着是 logcat 怎么读、怎么显示(第 6、7、8 节),最后是控制通道、周边来源和设计复盘。
2. 入口层:应用是怎么打日志的
2.1 八个缓冲区,六个优先级
日志先要回答两个问题:写到哪个缓冲区 、什么严重级别。
arduino
/* liblog/include/android/log.h:138-162 */
typedef enum log_id {
LOG_ID_MIN = 0,
/** The main log buffer. This is the only log buffer available to apps. */
LOG_ID_MAIN = 0,
/** The radio log buffer. */
LOG_ID_RADIO = 1,
/** The event log buffer. */
LOG_ID_EVENTS = 2,
/** The system log buffer. */
LOG_ID_SYSTEM = 3,
/** The crash log buffer. */
LOG_ID_CRASH = 4,
/** The statistics log buffer. */
LOG_ID_STATS = 5,
/** The security log buffer. */
LOG_ID_SECURITY = 6,
/** The kernel log buffer. */
LOG_ID_KERNEL = 7,
LOG_ID_MAX,
} log_id_t;
| 缓冲区 | 内容 | 谁能写 | logcat 选项 |
|---|---|---|---|
| main | 应用日志(Log.v/d/i/w/e) | 所有 app | 默认 |
| system | 系统进程日志(Slog) | 所有进程 | 默认 |
| radio | 电话/基带相关 | 所有进程 | -b radio |
| events | 二进制结构化事件(EventLog) | 所有 app | -b events |
| crash | 崩溃日志(含 abort 消息) | 所有 app | 默认 |
| stats | logd 内部统计 | logd 自己 | -b stats |
| security | 安全审计事件(Device Owner 场景) | 仅 system/root/log 组 | -b security |
| kernel | 内核 printk(经 LogKlog 转发) | logd 收 | 默认(userdebug/eng) |
优先级从低到高:VERBOSE(2) < DEBUG(3) < INFO(4) < WARN(5) < ERROR(6) < FATAL(7),另有 SILENT(8) 表示彻底关闭。logcat 里那列单字母 V/D/I/W/E/F 就是它们的缩写。
2.2 Java 层和 Native 层殊途同归
Java 层的 android.util.Log / android.util.Slog / android.util.EventLog(framework/base,不在本仓库)最终都通过 JNI 调到 liblog 的 C 接口:
Log.i()/Log.e()→__android_log_buf_write(LOG_ID_MAIN, ...)Slog.i()→__android_log_buf_write(LOG_ID_SYSTEM, ...)EventLog.writeEvent()→__android_log_bwrite()(二进制 events 缓冲区)
Native 层(C/C++)用的 ALOGI、ALOGE 宏,以及 Rust 的 android_logger,同样收敛到这几个函数。也就是说,无论哪种语言、哪个进程,打日志的终点都是 liblog 的这一组函数。
最常用的 __android_log_write() 本身只有一行------它就是 main 缓冲区的语法糖:
arduino
/* liblog/logger_write.cpp:360-362 */
int __android_log_write(int prio, const char* tag, const char* msg) {
return __android_log_buf_write(LOG_ID_MAIN, prio, tag, msg);
}
2.3 第一道过滤:__android_log_is_loggable
日志在离开进程之前 就可能被丢弃。__android_log_buf_write() 的第一步是查系统属性做优先级过滤:
arduino
/* liblog/logger_write.cpp:386-397 */
int __android_log_buf_write(int bufID, int prio, const char* tag, const char* msg) {
ErrnoRestorer errno_restorer;
if (!__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE)) {
return -EPERM;
}
__android_log_message log_message = {
sizeof(__android_log_message), bufID, prio, tag, nullptr, 0, msg};
__android_log_write_log_message(&log_message);
return 1;
}
__android_log_is_loggable()(liblog/properties.cpp:244)按下面的优先级查询属性,先到先得:
c
log.tag.<TAG> # 只对这个 tag 生效
persist.log.tag.<TAG>
log.tag # 全局默认
persist.log.tag
比如 setprop log.tag.ActivityManager WARN 后,AMS 里所有 V/D/I 级别的日志在客户端本地就被丢弃了,根本不会发到 logd ------这是零成本的降噪手段,比在 logcat 端过滤省得多。查询结果带缓存(__android_log_level() 里有两层 cache,通过 property serial 检测变更),不会每条日志都触发系统调用。
2.4 可替换的 logger 后端
__android_log_write_log_message() 里有个不太显眼但很关键的设计------logger 函数是可以替换的:
scss
/* liblog/logger_write.cpp:364-384 */
void __android_log_write_log_message(__android_log_message* log_message) {
ErrnoRestorer errno_restorer;
if (log_message->buffer_id != LOG_ID_DEFAULT && log_message->buffer_id != LOG_ID_MAIN &&
log_message->buffer_id != LOG_ID_SYSTEM && log_message->buffer_id != LOG_ID_RADIO &&
log_message->buffer_id != LOG_ID_CRASH) {
return;
}
if (log_message->tag == nullptr) {
log_message->tag = GetDefaultTag().c_str();
}
#if __BIONIC__
if (log_message->priority == ANDROID_LOG_FATAL) {
android_set_abort_message(log_message->message);
}
#endif
get_logger_function()(log_message);
}
三个细节:
- buffer 白名单 :通用写接口只放行 5 个文本缓冲区,events/stats/security 走专用的
__android_log_bwrite()系列(第 2.2 节)。 - FATAL 特殊处理 :
ANDROID_LOG_FATAL级别的消息会同时写进abort message(存在 tombstoned 那边,crash 时能看到"Fatal message")。 get_logger_function()(logger_write.cpp:184):默认在 Android 上返回__android_log_logd_logger(发给 logd),但可以通过__android_log_set_logger()替换------Flutter、游戏引擎、单元测试常用它把日志重定向到自己的管道;另外如果设置了ro.log.file_logger.path属性,日志会直接写文件而不走 logd。
接下来顺着默认路径 __android_log_logd_logger() 往下走:
ini
/* liblog/logger_write.cpp:345-358 */
void __android_log_logd_logger(const struct __android_log_message* log_message) {
int buffer_id = log_message->buffer_id == LOG_ID_DEFAULT ? LOG_ID_MAIN : log_message->buffer_id;
struct iovec vec[3];
vec[0].iov_base =
const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(&log_message->priority));
vec[0].iov_len = 1;
vec[1].iov_base = const_cast<void*>(static_cast<const void*>(log_message->tag));
vec[1].iov_len = strlen(log_message->tag) + 1;
vec[2].iov_base = const_cast<void*>(static_cast<const void*>(log_message->message));
vec[2].iov_len = strlen(log_message->message) + 1;
write_to_log(static_cast<log_id_t>(buffer_id), vec, 3);
}
payload 的内存布局就此定型 :[1 字节优先级][tag 字符串+\0][消息字符串+\0],三段 iovec,一次 writev() 发出去,避免多次拷贝。
3. liblog 发送层:logd_writer 的三个关键设计
write_to_log()(logger_write.cpp:226)做两件事:给 SECURITY 缓冲区做权限预检(必须是 system/root/log 身份,且设备处于 Device Owner 管理状态),然后同时写两个目的地:
arduino
/* liblog/logger_write.cpp:226-259(节选) */
static int write_to_log(log_id_t log_id, struct iovec* vec, size_t nr) {
int ret;
struct timespec ts;
if (log_id == LOG_ID_KERNEL) {
return -EINVAL;
}
clock_gettime(CLOCK_REALTIME, &ts); // 时间戳在客户端取!
/* ...SECURITY/EVENTS 的额外校验,略... */
ret = LogdWrite(log_id, &ts, vec, nr); // 主路径:发给 logd
PmsgWrite(log_id, &ts, vec, nr); // 副路径:写 /dev/pmsg0
return ret;
}
PmsgWrite 把同样的内容写进 /dev/pmsg0(pstore/ramoops),机器异常重启后这些日志还能被捞回来,logcat -L 读的就是它。日志会尽力双写,但 pmsg 失败不影响主路径。
主路径 LogdWrite() 是本节主角:
ini
/* liblog/logd_writer.cpp:119-205(节选) */
int LogdWrite(log_id_t logId, struct timespec* ts, struct iovec* vec, size_t nr) {
ssize_t ret;
static const unsigned headerLength = 1;
struct iovec newVec[nr + headerLength];
android_log_header_t header;
size_t i, payloadSize;
static atomic_int dropped;
LogdSocket& logd_socket =
logId == LOG_ID_SECURITY ? LogdSocket::BlockingSocket() : LogdSocket::NonBlockingSocket();
if (logd_socket.sock() < 0) {
return -EBADF;
}
/* logd, after initialization and priv drop */
if (getuid() == AID_LOGD) {
// ignore log messages we send to ourself (logd).
return 0;
}
header.tid = gettid();
header.realtime.tv_sec = ts->tv_sec;
header.realtime.tv_nsec = ts->tv_nsec;
newVec[0].iov_base = (unsigned char*)&header;
newVec[0].iov_len = sizeof(header);
/* ...如果有历史丢弃计数,先发一条 liblog 统计事件,见 3.3 节... */
header.id = logId;
for (payloadSize = 0, i = headerLength; i < nr + headerLength; i++) {
newVec[i].iov_base = vec[i - headerLength].iov_base;
payloadSize += newVec[i].iov_len = vec[i - headerLength].iov_len;
if (payloadSize > LOGGER_ENTRY_MAX_PAYLOAD) { // 4068 字节截断
newVec[i].iov_len -= payloadSize - LOGGER_ENTRY_MAX_PAYLOAD;
if (newVec[i].iov_len) {
++i;
}
break;
}
}
// EAGAIN occurs if logd is overloaded, other errors indicate that something
// went wrong with the connection, so we reset it and try again.
ret = TEMP_FAILURE_RETRY(writev(logd_socket.sock(), newVec, i));
if (ret < 0 && errno != EAGAIN) {
logd_socket.Reconnect();
ret = TEMP_FAILURE_RETRY(writev(logd_socket.sock(), newVec, i));
}
if (ret < 0) {
ret = -errno;
}
if (ret > (ssize_t)sizeof(header)) {
ret -= sizeof(header);
} else if (ret < 0) {
atomic_fetch_add_explicit(&dropped, 1, memory_order_relaxed);
}
return ret;
}
3.1 发送协议:一个 header + 原始 payload
发给 logd 的完整报文由 android_log_header_t 打头(include/private/android_logger.h:49,packed,共 17 字节),后面直接拼第 2.4 节的三段 payload:
arduino
/* liblog/include/private/android_logger.h:48-53 */
/* Header Structure to logd, and second header for pmsg */
typedef struct __attribute__((packed)) {
uint8_t id; // log_id_t:写到哪个缓冲区
uint16_t tid; // 线程号(客户端自己取 gettid())
log_time realtime; // tv_sec + tv_nsec 各 4 字节
} android_log_header_t;
官方协议文档 liblog/README.protocol.md 给出了完整布局:
c
struct {
android_log_header_t header; // id | tid | realtime
union {
struct {
char prio; // 1 字节优先级
char tag[...]; // \0 结尾
char message[...]; // \0 结尾
} string; // ← main/system/radio/crash 用这种
struct {
android_event_header_t event_header; // int32 tag 号
android_event_*_t payload[...]; // 类型化的二进制事件
} binary; // ← events/stats/security 用这种
};
};
注意两点:uid 和 pid 不在报文里 ------它们由内核通过 SCM_CREDENTIALS 附带(见 4.2 节),客户端没机会伪造;时间戳是客户端打的,所以同一时刻不同进程的日志时间可能有微小偏差,logd 内部另有全局递增的 sequence number 用来定序(见 5.3 节)。
payload 上限 LOGGER_ENTRY_MAX_PAYLOAD = 4068(include/log/log.h:71),即单条日志消息体最大约 4KB,超长部分在客户端就被截断。
3.2 socket 管理:惰性建连、非阻塞、可重连
LogdSocket(logd_writer.cpp:41)体现了打日志路径的三个原则:懒初始化、非阻塞、自愈。
csharp
/* liblog/logd_writer.cpp:41-112(节选) */
class LogdSocket {
public:
static LogdSocket& BlockingSocket() { ... } // 仅 SECURITY 缓冲区用
static LogdSocket& NonBlockingSocket() { ... } // 其余全部用这个
void Reconnect() { LogdConnect(sock_); }
void Close() { // Zygote fork 后清理 fd 用
if (sock_ != kUninitialized) {
close(sock_);
}
sock_ = kUninitialized;
}
int sock() {
GetSocket();
return sock_;
}
private:
// Note that it is safe to call connect() multiple times on DGRAM Unix domain
// sockets, so this function is used to reconnect to logd without requiring
// a new socket.
static void LogdConnect(int sock) {
sockaddr_un un = {};
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/dev/socket/logdw");
TEMP_FAILURE_RETRY(connect(sock, reinterpret_cast<sockaddr*>(&un), sizeof(sockaddr_un)));
}
void GetSocket() { // 进程第一次打日志时才创建 socket,CAS 保证只建一次
if (sock_ != kUninitialized) {
return;
}
int flags = SOCK_DGRAM | SOCK_CLOEXEC;
if (!blocking_) {
flags |= SOCK_NONBLOCK; // ← 关键:非阻塞
}
int new_socket = TEMP_FAILURE_RETRY(socket(PF_UNIX, flags, 0));
if (new_socket < 0) {
return;
}
LogdConnect(new_socket);
int uninitialized_value = kUninitialized;
if (!sock_.compare_exchange_strong(uninitialized_value, new_socket)) {
close(new_socket);
return;
}
}
static const int kUninitialized = -1;
atomic_int sock_ = kUninitialized;
bool blocking_;
};
设计意图逐条说:
- SOCK_DGRAM(数据报) :一条日志一个数据报,天然有边界,logd 端
recvmsg()一次拿一条,不需要应用层分包协议。 - 非阻塞 :logd 一旦卡死或 buffer 满,
writev()立即返回EAGAIN,打日志的进程绝不因为日志而卡住。只有 SECURITY 缓冲区例外------审计日志宁可等也不能丢,所以它用阻塞 socket。 - DGRAM socket 上重复 connect 是合法的:连接断了(比如 logd 重启)不用重建 fd,直接再 connect 一次就行,代码注释里专门强调了这点。
- Zygote 特化 :zygote 预连接的 fd 会通过
Close()清掉(__android_log_close()),避免 fork 出的 app 共享同一个 socket。
3.3 丢日志的"发票"机制
如果 writev 失败(比如 logd 忙不过来),日志就丢了------但 liblog 不会无声无息地丢。它有一个进程内的 dropped 原子计数器,每次发送失败 +1;等下一次成功写日志时,先补发一条特殊的 events 日志,把累计丢了多少条报上去:
ini
/* liblog/logd_writer.cpp:151-168 */
int32_t snapshot = atomic_exchange_explicit(&dropped, 0, memory_order_relaxed);
if (snapshot && __android_log_is_loggable_len(ANDROID_LOG_INFO, "liblog", strlen("liblog"),
ANDROID_LOG_VERBOSE)) {
android_log_event_int_t buffer;
header.id = LOG_ID_EVENTS;
buffer.header.tag = LIBLOG_LOG_TAG;
buffer.payload.type = EVENT_TYPE_INT;
buffer.payload.data = snapshot; // 丢了 N 条
newVec[headerLength].iov_base = &buffer;
newVec[headerLength].iov_len = sizeof(buffer);
ret = TEMP_FAILURE_RETRY(writev(logd_socket.sock(), newVec, 2));
if (ret != (ssize_t)(sizeof(header) + sizeof(buffer))) {
atomic_fetch_add_explicit(&dropped, snapshot, memory_order_relaxed); // 补发也失败,还回去
}
}
补发的是一条 events 日志,tag 号 1006(liblog/event.logtags:37 定义为 liblog (dropped|1),由 Android.bp 里 -DLIBLOG_LOG_TAG=1006 编进代码)。logcat 里会渲染成形如 I liblog: [3] 的行------数字就是累计丢掉的条数。重压场景下日志"凭空消失",先找它。
顺带澄清一个流传很广的旧知识:老版本(Android 11 及以前)logd 里的 chatty 机制------把连续重复的消息折叠成
chatty: uid=xxx expire N lines------在 Android 12 已被整体移除,由压缩取而代之(见 5.4 节)。所以现在的高版本设备上不会再看到 "expire N lines" 这种行。
4. logd 服务端(上):启动与接收
4.1 main():五个常驻角色
logd 由 init 拉起,main() 本身很短------它是个"装配车间",把各个组件 new 出来、起好线程,然后就 pause() 睡大觉:
scss
/* logd/main.cpp:192-319(节选,中文注释为笔者所加) */
int main(int argc, char* argv[]) {
signal(SIGPIPE, SIG_IGN); // 读者断开不能把 logd 干掉
setenv("TZ", "UTC", 1); // logd 内部一律按 UTC 处理时间
/* --reinit 分支:logd-reinit service 用它热加载配置,略 */
// logd 自己的日志走 KernelLogger 直接写 /dev/kmsg,避免自我依赖
android::base::InitLogging(argv, [](/*...*/) { /* ...KernelLogger... */ });
/* ...打开 /dev/kmsg、/proc/kmsg,DropPrivs() 降权(只留 SYSLOG/AUDIT_CONTROL),略... */
// A cache of event log tags
LogTags log_tags;
// Pruning configuration.
PruneList prune_list;
std::string buffer_type = GetProperty("logd.buffer_type", "serialized");
LogStatistics log_statistics(GetBoolPropertyEngSvelteDefault("logd.statistics"),
buffer_type == "serialized");
// Serves the purpose of managing the last logs times read on a socket connection,
// and as a reader lock on a range of log entries.
LogReaderList reader_list;
// LogBuffer is the object which is responsible for holding all log entries.
LogBuffer* log_buffer = nullptr;
if (buffer_type == "serialized") {
log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);
} else if (buffer_type == "simple") {
log_buffer = new SimpleLogBuffer(&reader_list, &log_tags, &log_statistics);
} else {
LOG(FATAL) << "buffer_type must be one of 'serialized' or 'simple'";
}
// LogReader listens on /dev/socket/logdr. When a client
// connects, log entries in the LogBuffer are written to the client.
LogReader* reader = new LogReader(log_buffer, &reader_list);
if (reader->startListener()) return EXIT_FAILURE; // 线程 "logd.reader"
// LogListener listens on /dev/socket/logdw for client
// initiated log messages. New log entries are added to LogBuffer
// and LogReader is notified to send updates to connected clients.
LogListener* swl = new LogListener(log_buffer);
if (!swl->StartListener()) return EXIT_FAILURE; // 线程 "logd.writer"
// Command listener listens on /dev/socket/logd for incoming logd
// administrative commands.
CommandListener* cl = new CommandListener(log_buffer, &log_tags, &prune_list, &log_statistics);
if (cl->startListener()) return EXIT_FAILURE; // 线程 "logd.control"
// Notify that others can now interact with logd
SetProperty("logd.ready", "true"); // ← logcat 启动前会等这个属性
// LogAudit listens on NETLINK_AUDIT socket for selinux
// initiated log messages. ...
LogAudit* al = nullptr;
if (auditd) {
int dmesg_fd = GetBoolProperty("ro.logd.auditd.dmesg", true) ? fdDmesg : -1;
al = new LogAudit(log_buffer, dmesg_fd, &log_statistics);
}
LogKlog* kl = nullptr;
if (klogd) {
kl = new LogKlog(log_buffer, fdDmesg, fdPmesg, al != nullptr, &log_statistics);
}
readDmesg(al, kl); // 把开机以来积压的 dmesg 先灌一遍
if (kl && kl->startListener()) delete kl; // 线程 "logd.klogd"
if (al && al->startListener()) delete al; // 线程 "logd.auditd"
TrustyLog::create(log_buffer); // 有 trusty-log 设备才启用
TEMP_FAILURE_RETRY(pause());
return EXIT_SUCCESS;
}
跑起来之后,logd 里常驻这些线程(adb shell ps -T | grep logd 可验证):
| 线程名 | 类 | 职责 |
|---|---|---|
logd.writer |
LogListener | 收 /dev/socket/logdw 的日志,写入 LogBuffer |
logd.reader |
LogReader | 接受 /dev/socket/logdr 连接,为每个连接建 reader 线程 |
logd.reader.per |
LogReaderThread | 每个 logcat 连接一个,把日志推给该连接 |
logd.control |
CommandListener | 处理 logd socket 上的管理命令 |
logd.klogd |
LogKlog | 读 /proc/kmsg,内核日志 → kernel 缓冲区 |
logd.auditd |
LogAudit | 读 NETLINK_AUDIT,SELinux 拒绝 → main 缓冲区 |
两个值得注意的细节:
- logd 自己的日志直接写 /dev/kmsg (
KernelLogger),不写进自己的缓冲区------避免"收自己的日志"造成递归/死锁。客户端 liblog 也有对应防御:LogdWrite()里getuid() == AID_LOGD直接返回。 logd.ready属性 :所有组件就绪后才置 true,logcat 启动时会WaitForProperty("logd.ready", "true", 1s)(logcat.cpp:1061),避免开机竞态下读到空日志。
DropPrivs()(main.cpp:76)也值得一提:logd 以 logd 用户运行,主动丢掉几乎所有 capabilities,只保留 CAP_SYSLOG(读内核日志)和 CAP_AUDIT_CONTROL(收 audit 事件),并且设为后台调度策略(SP_BACKGROUND)------日志服务不该抢业务进程的 CPU。
4.2 LogListener:把内核凭证变成日志的身份
LogListener 是写路径的服务端。整个类只做一件事:死循环 recvmsg(),解析、校验、入队:
ini
/* logd/LogListener.cpp:54-123(节选) */
void LogListener::HandleData() {
// + 1 to ensure null terminator if MAX_PAYLOAD buffer is received
__attribute__((uninitialized)) char
buffer[sizeof(android_log_header_t) + LOGGER_ENTRY_MAX_PAYLOAD + 1];
struct iovec iov = {buffer, sizeof(buffer) - 1};
alignas(4) char control[CMSG_SPACE(sizeof(struct ucred))];
struct msghdr hdr = {
nullptr, 0, &iov, 1, control, sizeof(control), 0,
};
ssize_t n = recvmsg(socket_, &hdr, 0);
if (n <= (ssize_t)(sizeof(android_log_header_t))) {
return;
}
buffer[n] = 0;
struct ucred* cred = nullptr;
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&hdr);
while (cmsg != nullptr) {
if (cmsg->cmsg_level == SOL_SOCKET &&
cmsg->cmsg_type == SCM_CREDENTIALS) {
cred = (struct ucred*)CMSG_DATA(cmsg); // ← 内核附带的 {pid,uid,gid}
break;
}
cmsg = CMSG_NXTHDR(&hdr, cmsg);
}
if (cred == nullptr) {
return;
}
if (cred->uid == AID_LOGD) {
// ignore log messages we send to ourself.
return;
}
android_log_header_t* header =
reinterpret_cast<android_log_header_t*>(buffer);
log_id_t logId = static_cast<log_id_t>(header->id);
if (/* logId < LOG_ID_MIN || */ logId >= LOG_ID_MAX ||
logId == LOG_ID_KERNEL) {
return;
}
if (logId == LOG_ID_SECURITY) {
if (!__android_log_security()) {
return;
}
if (!clientCanWriteSecurityLog(cred->uid, cred->gid, cred->pid)) {
return;
}
}
char* msg = ((char*)buffer) + sizeof(android_log_header_t);
n -= sizeof(android_log_header_t);
logbuf_->Log(logId, header->realtime, cred->uid, cred->pid, header->tid, msg,
((size_t)n <= UINT16_MAX) ? (uint16_t)n : UINT16_MAX);
}
关键就是那条 control message:socket 创建时开了 SO_PASSCRED(LogListener::GetLogSocket(),LogListener.cpp:125,init 传来的 socket 由 rc 里的 dgram+passcred 声明),内核会在每个数据报上附加 struct ucred {pid, uid, gid}。这个身份是内核担保的,与报文内容无关,所以 logd 可以放心地把它记录为日志的属主,后面读取侧的按 UID 隔离都建立在它之上。
SECURITY 缓冲区在这里做了第二道闸:不仅要设备处于组织管理状态(__android_log_security() 检查 persist.logd.security=true 且 ro.organization_owned=1,见 properties.cpp:325),还要确认发送方有 AID_SECURITY_LOG_WRITER 或 AID_LOG 组身份(LogPermissions.cpp:129)。
5. logd 服务端(下):LogBuffer 的存储、淘汰与压缩
5.1 两种 LogBuffer 实现
LogBuffer 是个纯虚基类(logd/LogBuffer.h:58),定义了存储后端的最小契约:
arduino
/* logd/LogBuffer.h:58-79 */
class LogBuffer {
public:
virtual ~LogBuffer() {}
virtual void Init() = 0;
virtual int Log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid, pid_t tid,
const char* msg, uint16_t len) = 0;
virtual std::unique_ptr<FlushToState> CreateFlushToState(uint64_t start, LogMask log_mask)
REQUIRES(logd_lock) = 0;
virtual bool FlushTo(
LogWriter* writer, FlushToState& state,
const std::function<FilterResult(log_id_t log_id, pid_t pid, uint64_t sequence,
log_time realtime)>& filter) REQUIRES(logd_lock) = 0;
virtual bool Clear(log_id_t id, uid_t uid) = 0;
virtual size_t GetSize(log_id_t id) = 0;
virtual bool SetSize(log_id_t id, size_t size) = 0;
virtual uint64_t sequence() const = 0;
};
有两个实现,由 logd.buffer_type 属性选择:
SerializedLogBuffer(默认):日志紧密序列化在连续内存块(chunk)里,读取零拷贝、内存紧凑,chunk 写满后整体 zstd 压缩。Android 12 引入,是现在的默认。SimpleLogBuffer:老结构,每条日志一个堆分配的LogBufferElement对象挂在链表上。保留它是为了低内存设备(logd.buffer_type=simple)和兼容性。
本仓库的车厂定制版在
SerializedLogBuffer::Log()入口加了个adayo.logdsystem.close属性开关(置 1 时拒绝所有写入,返回 -EACCES),可用于复现"日志系统关闭"的测试场景。AOSP 原版没有这段,阅读时注意区分。
5.2 一条日志入库的完整流程
scss
/* logd/SerializedLogBuffer.cpp:140-168 */
int SerializedLogBuffer::Log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid, pid_t tid,
const char* msg, uint16_t len) {
if (log_id >= LOG_ID_MAX || len == 0) {
return -EINVAL;
}
if (len > LOGGER_ENTRY_MAX_PAYLOAD) {
len = LOGGER_ENTRY_MAX_PAYLOAD;
}
if (!ShouldLog(log_id, msg, len)) {
stats_->AddTotal(log_id, len); // 被服务端过滤掉的也记入统计
return -EACCES;
}
auto sequence = sequence_.fetch_add(1, std::memory_order_relaxed); // ← 全局序号
auto lock = std::lock_guard{logd_lock};
auto entry = LogToLogBuffer(logs_[log_id], max_size_[log_id], sequence, realtime, uid, pid,
tid, msg, len);
stats_->Add(entry->ToLogStatisticsElement(log_id));
MaybePrune(log_id);
reader_list_->NotifyNewLog(1 << log_id); // 唤醒正在看这个缓冲区的读者
return len;
}
五步:服务端过滤 → 取全局序号 → 持锁写入 → 可能淘汰 → 通知读者。
ShouldLog() 会再做一次 __android_log_is_loggable 检查------和客户端用同一套逻辑(liblog 的 properties.cpp 被静态链进 logd)。这保证了修改 log.tag.* 属性对已运行进程也生效:客户端属性缓存未刷新没关系,logd 这道闸兜底。
全局 sequence number 是整个系统最精妙的设计之一。它是一个跨所有缓冲区单调递增的原子计数器。每条日志不管进哪个 buffer 都先领一个号,于是:
- 多个缓冲区(main/system/events...)的日志可以按 sequence 归并排序成一条全局时间线------这正是 logcat 多 buffer 混合输出时依然有序的原因;
- 读者的读取进度就是一个
uint64_t序号,断点续读、-t N回溯都建立在它上面。
5.3 存储结构:Chunk + 紧凑序列化条目
数据结构分三层。最外层每个 log_id 一个 chunk 链表;每个 chunk 是一块连续内存(默认大小 = 缓冲区上限/4,即默认 256K/4 = 64KB);日志条目用 placement new 紧凑地排布在 chunk 里:
scss
/* logd/SerializedLogBuffer.cpp:34-49 */
static SerializedLogEntry* LogToLogBuffer(std::list<SerializedLogChunk>& log_buffer,
size_t max_size, uint64_t sequence, log_time realtime,
uid_t uid, pid_t pid, pid_t tid, const char* msg,
uint16_t len) {
if (log_buffer.empty()) {
log_buffer.push_back(SerializedLogChunk(max_size / SerializedLogBuffer::kChunkSizeDivisor));
}
auto total_len = sizeof(SerializedLogEntry) + len;
if (!log_buffer.back().CanLog(total_len)) { // 当前 chunk 放不下了
log_buffer.back().FinishWriting(); // 封盘 + 压缩
log_buffer.push_back(SerializedLogChunk(max_size / SerializedLogBuffer::kChunkSizeDivisor));
}
return log_buffer.back().Log(sequence, realtime, uid, pid, tid, msg, len);
}
arduino
/* logd/SerializedLogChunk.cpp:84-92 */
SerializedLogEntry* SerializedLogChunk::Log(uint64_t sequence, log_time realtime, uid_t uid,
pid_t pid, pid_t tid, const char* msg, uint16_t len) {
auto new_log_address = contents_.data() + write_offset_;
auto* entry = new (new_log_address) SerializedLogEntry(uid, pid, tid, sequence, realtime, len);
memcpy(entry->msg(), msg, len);
write_offset_ += entry->total_len();
highest_sequence_number_ = sequence;
return entry;
}
条目本身是个 packed 结构,消息体贴在结构体尾部(柔性数组的现代写法):
arduino
/* logd/SerializedLogEntry.h:34-98(节选) */
// These structs are packed into a single chunk of memory for each log type within a
// SerializedLogChunk object. Their message is contained immediately at the end of
// the struct. The address of the next log in the buffer is *this + sizeof(SerializedLogEntry)
// + msg_len_. If that value would overflow the chunk of memory associated with the
// SerializedLogChunk object, then a new SerializedLogChunk must be allocated to contain
// the next SerializedLogEntry.
class __attribute__((packed)) SerializedLogEntry {
public:
SerializedLogEntry(uid_t uid, pid_t pid, pid_t tid, uint64_t sequence, log_time realtime,
uint16_t len)
: uid_(uid),
pid_(pid),
tid_(tid),
sequence_(sequence),
realtime_(realtime),
msg_len_(len) {}
// ... accessor 略 ...
char* msg() { return reinterpret_cast<char*>(this) + sizeof(*this); }
const char* msg() const { return reinterpret_cast<const char*>(this) + sizeof(*this); }
uint16_t total_len() const { return sizeof(*this) + msg_len_; }
private:
const uint32_t uid_; // 4B ── 条目头共 30 字节(packed)
const uint32_t pid_; // 4B
const uint32_t tid_; // 4B
const uint64_t sequence_; // 8B
const log_time realtime_; // 8B
const uint16_t msg_len_; // 2B
};
相比 SimpleLogBuffer 每条日志一个堆对象(含 std::string、shared_ptr 等,一条日志的元数据开销可能上百字节),SerializedLogEntry 的元数据开销固定 30 字节,且遍历就是指针加法(LogEntryIterator,见 SerializedLogChunk.h:38)------这是它被选为默认实现的直接原因。
5.4 写满即压缩:zstd 上场
chunk 写满封盘时,整块压缩,原始内容在无读者引用时释放:
arduino
/* logd/SerializedLogChunk.cpp:28-58 */
void SerializedLogChunk::FinishWriting() {
writer_active_ = false;
CHECK_EQ(compressed_log_.size(), 0U);
CompressionEngine::GetInstance().Compress(contents_, write_offset_, compressed_log_);
LOG(VERBOSE) << "Compressed Log, buffer max size: " << contents_.size()
<< " size used: " << write_offset_
<< " compressed size: " << compressed_log_.size();
if (reader_ref_count_ == 0) {
contents_.Resize(0); // 没人读,直接把解压内存还回去
}
}
// TODO: Develop a better reference counting strategy to guard against the case where the writer is
// much faster than the reader, and we needlessly compess / decompress the logs.
void SerializedLogChunk::IncReaderRefCount() {
if (++reader_ref_count_ != 1 || writer_active_) {
return;
}
contents_.Resize(write_offset_);
CompressionEngine::GetInstance().Decompress(compressed_log_, contents_); // 有人要读,解压回来
}
void SerializedLogChunk::DecReaderRefCount() {
CHECK_NE(reader_ref_count_, 0U);
if (--reader_ref_count_ != 0) {
return;
}
if (!writer_active_) {
contents_.Resize(0);
}
}
压缩引擎目前是 zstd(CompressionEngine.cpp:25 里 new ZstdCompressionEngine(),代码里同时保留了 zlib 实现可切换)。文本日志压缩比相当可观,实际效果是同样 256KB 的缓冲区上限能装下数倍的历史日志。
这段压缩代码的来历值得一说------它是来"还愿"的。Android 12 之前的 logd 有个 chatty 机制:连续重复的消息折叠成一条 chatty: expire N lines,占用超过 12.5% 缓冲区的 UID 会被针对性删除。听起来聪明,实际效果很差,仓库里的 logd/README.compression.md 记录了官方复盘:
- 开发者困惑:日志"失踪"和被去重,排查问题时分不清是没打还是被打掉了;
- CPU 开销高、内存失控(chatty 不把元数据算进缓冲区,1MB 配置实际吃掉远超 1MB);
- 换来的日志容量提升微乎其微。
于是 Android 12 用"压缩"替换了"折叠":同样的实测负载(Pixel 4 五天日志回放)下,3.5 倍的消息容量、CPU 和内存各降一半,而且日志不再有洞。工程上这是一次教科书式的取舍------不聪明的启发式(猜什么是垃圾)输给了不挑食的通用技术(把所有东西压小)。
配套的资源账本也要看懂:淘汰时按"占内存"算,压缩后的 chunk 只按压缩后大小计------
csharp
/* logd/SerializedLogChunk.h:77-79 */
// If this buffer has been compressed, we only consider its compressed size when accounting
// for memory consumption for pruning. This is since the uncompressed log is only by used by
// readers, and thus not a representation of how much these logs cost to keep in memory.
size_t PruneSize() const {
return sizeof(*this) + (compressed_log_.size() ?: contents_.size());
}
而对外查询"可读大小"(logcat -g 里的 readable)时报的是解压后的字节数,所以你会看到 ring buffer is 256 KiB (xxx KiB consumed, 2.5 MiB readable) 这种"可读比分配还大"的现象,别惊讶,那正是压缩的功劳。
5.5 淘汰策略:整块丢、从最老丢
缓冲区大小是有硬约束的,超了就要淘汰。SerializedLogBuffer 的策略简单粗暴但高效------按 chunk 整体淘汰,从最老的开始:
arduino
/* logd/SerializedLogBuffer.cpp:170-236(节选) */
void SerializedLogBuffer::MaybePrune(log_id_t log_id) {
size_t total_size = GetSizeUsed(log_id);
size_t after_size = total_size;
if (total_size > max_size_[log_id]) {
Prune(log_id, total_size - max_size_[log_id]);
after_size = GetSizeUsed(log_id);
}
stats_->set_overhead(log_id, after_size);
}
void SerializedLogBuffer::Prune(log_id_t log_id, size_t bytes_to_free) {
auto& log_buffer = logs_[log_id];
auto it = log_buffer.begin();
while (it != log_buffer.end()) {
for (const auto& reader_thread : reader_list_->running_reader_threads()) {
if (!reader_thread->IsWatching(log_id)) {
continue;
}
/* ...wrapped reader 提前唤醒,略... */
// Some readers may be still reading from this log chunk, log a warning that they are
// about to lose logs.
if (reader_thread->start() <= it->highest_sequence_number()) {
LOG(WARNING) << "Skipping entries from slow reader, " << reader_thread->name()
<< ", from LogBuffer::Prune()";
}
}
// Increment ahead of time since we're going to erase this iterator from the list.
auto it_to_prune = it++;
// Readers may have a reference to the chunk to track their last read log_position.
// Notify them to delete the reference.
it_to_prune->NotifyReadersOfPrune(log_id);
size_t buffer_size = it_to_prune->PruneSize();
RemoveChunkFromStats(log_id, *it_to_prune);
log_buffer.erase(it_to_prune);
if (buffer_size >= bytes_to_free) {
return;
}
bytes_to_free -= buffer_size;
}
}
两个细节:
-
慢读者警告 :如果某个读者还没读到即将被删的数据(
reader_thread->start() <= chunk 的最大序号),logd 会打Skipping entries from slow reader警告------它明确告诉你这个客户端要丢日志了,但仍然会删(存储压力优先于读者)。logcat 端对应用户看到的就是那句著名的错误:Unexpected EOF! ... either the device shut down, logd crashed, or this instance of logcat was unable to read log messages as quickly as they were being produced. -
删除 chunk 前必须
NotifyReadersOfPrune(),因为读者(FlushToState)可能持有指向该 chunk 的迭代器,要先解引用再删,否则就是悬垂指针。这是整个模块里引用计数最密集的场景。
另外还有一个更精细的 PruneList(logcat -P 设置),支持按 UID/PID 优先淘汰、白名单保留、"~!" 自动压制最吵的 UID------但注意:PruneList 的精细策略只在 SimpleLogBuffer 里生效,SerializedLogBuffer 目前走的是最老优先的整块淘汰(这也是它删除 PruneList 交互、简化锁模型换来的性能)。
5.6 缓冲区多大?谁说了算
arduino
/* logd/LogSize.h:23-25 */
static constexpr size_t kDefaultLogBufferSize = 256 * 1024;
static constexpr size_t kLogBufferMinSize = 64 * 1024;
static constexpr size_t kLogBufferMaxSize = 256 * 1024 * 1024;
logd 启动时 Init() 对每个 buffer 调 GetBufferSizeFromProperties()(LogSize.cpp:48),按下面的顺序找配置:
arduino
persist.logd.size.<buffer名> # 例:persist.logd.size.main=1M
ro.logd.size.<buffer名>
persist.logd.size # 所有 buffer 统一大小
ro.logd.size
→ 都没配:ro.config.low_ram=true 则 64K,否则默认 256K
运行期调整用 logcat -G 1M(走 CommandListener 的 setLogSize 命令,不落属性,重启失效)。合法性校验 [64K, 256M](IsValidBufferSize)。
本仓库这块有个车机相关的补丁:
ro.hardware.type == "automotive"且ro.debuggable时才读上述属性(AOSP 上游因 b/196856709 曾直接禁掉 automotive 的自定义尺寸)。做车机日志容量规划时要清楚你们的产品吃的是哪套逻辑。
6. 读取链路(上):liblog 读 API 与 logdr 协议
写路径讲完了,现在换到读取视角。读取 API 也在 liblog 里(logger_read.cpp),logcat 就是它的第一个用户:
arduino
/* liblog/logger_read.cpp:93-132 */
int android_logger_list_read(struct logger_list* logger_list, struct log_msg* log_msg) {
if (logger_list == nullptr || logger_list->log_mask == 0) {
return -EINVAL;
}
int ret = 0;
#ifdef __ANDROID__
if (logger_list->mode & ANDROID_LOG_PSTORE) {
ret = PmsgRead(logger_list, log_msg); // logcat -L:读 pstore
} else {
ret = LogdRead(logger_list, log_msg); // 常规:读 logd
}
#endif
/* ...报文合法性校验,略... */
log_msg->buf[log_msg->entry.len + log_msg->entry.hdr_size] = '\0';
return ret;
}
logger_list 结构(liblog/logger.h:28)就是读取会话的全部状态:socket fd、模式(阻塞/非阻塞/pstore)、tail 条数、起始时间、pid 过滤、关注的 buffer 掩码。注意一个有趣的实现:android_logger_open() 返回的 logger* 指针其实是个编码过的整数(低 3 位存 buffer id,高位存 logd/pmsg 标志),并没有真的"打开"什么:
arduino
/* liblog/logger_read.cpp:64-74 */
struct logger* android_logger_open(struct logger_list* logger_list, log_id_t logId) {
if (!logger_list || (logId >= LOG_ID_MAX)) {
return nullptr;
}
logger_list->log_mask |= 1 << logId;
uintptr_t logger = logId;
logger |= (logger_list->mode & ANDROID_LOG_PSTORE) ? LOGGER_PMSG : LOGGER_LOGD;
return reinterpret_cast<struct logger*>(logger);
}
真正的连接发生在第一次 read 时(logdOpen()),连接后第一件事是把读取参数序列化成一条文本命令发给 logd:
ini
/* liblog/logd_reader.cpp:275-357(节选) */
static int logdOpen(struct logger_list* logger_list) {
char buffer[256], *cp, c;
int ret, remaining, sock;
sock = atomic_load(&logger_list->fd);
if (sock > 0) {
return sock;
}
sock = socket_local_client("logdr", SOCK_SEQPACKET, false); // ← 连 logdr
/* ...错误处理略... */
strcpy(buffer, (logger_list->mode & ANDROID_LOG_NONBLOCK) ? "dumpAndClose" : "stream");
cp = buffer + strlen(buffer);
strcpy(cp, " lids");
cp += 5;
c = '=';
remaining = sizeof(buffer) - (cp - buffer);
for (size_t log_id = 0; log_id < LOG_ID_MAX; ++log_id) {
if ((1 << log_id) & logger_list->log_mask) {
ret = snprintf(cp, remaining, "%c%zu", c, log_id); // lids=0,3,4
/* ...略... */
}
}
if (logger_list->tail) {
ret = snprintf(cp, remaining, " tail=%u", logger_list->tail); // logcat -t/-T N
/* ...略... */
}
if (logger_list->start.tv_sec || logger_list->start.tv_nsec) {
if (logger_list->mode & ANDROID_LOG_WRAP) {
ret = snprintf(cp, remaining, " timeout=%u", ANDROID_LOG_WRAP_DEFAULT_TIMEOUT);
/* ...略... */
}
ret = snprintf(cp, remaining, " start=%" PRIu32 ".%09" PRIu32,
logger_list->start.tv_sec, logger_list->start.tv_nsec); // logcat -T 时间
/* ...略... */
}
if (logger_list->pid) {
ret = snprintf(cp, remaining, " pid=%u", logger_list->pid); // logcat --pid=
/* ...略... */
}
ret = TEMP_FAILURE_RETRY(write(sock, buffer, cp - buffer));
/* ...存 fd、返回... */
}
也就是说,当你敲 logcat -t 100 -s WindowManager 时,liblog 实际发给 logd 的第一条消息长这样:
ini
dumpAndClose lids=0,3,4,7 tail=100
两种模式由第一个词决定:
stream(默认):一条连接一直读,新日志源源不断推过来------logcat常驻模式;dumpAndClose(-d、-t):把现有内容倒完就断开。
随后每次 LogdRead() 就是普通的 recv():
arduino
/* liblog/logd_reader.cpp:360-376 */
/* Read from the selected logs */
int LogdRead(struct logger_list* logger_list, struct log_msg* log_msg) {
int ret = logdOpen(logger_list);
if (ret < 0) {
return ret;
}
/* NOTE: SOCK_SEQPACKET guarantees we read exactly one full entry */
ret = TEMP_FAILURE_RETRY(recv(ret, log_msg, LOGGER_ENTRY_MAX_LEN, 0));
if ((logger_list->mode & ANDROID_LOG_NONBLOCK) && ret == 0) {
return -EAGAIN; // dump 完了
}
if (ret == -1) {
return -errno;
}
return ret;
}
用 SOCK_SEQPACKET(有序、可靠、保边界)而非 DGRAM:读取是长连接会话,需要知道对端断开(返回 0)以区分"暂时没有"和"连接没了"。
7. 读取链路(下):logd 侧的读者线程模型
7.1 LogReader:一个连接,一个线程,一条命令
LogReader::onDataAvailable()(logd/LogReader.cpp:107)处理每个新连接。它解析第 6 节那条文本命令,做完权限检查后,为这个连接创建专属的 LogReaderThread:
scss
/* logd/LogReader.cpp:107-280(节选) */
bool LogReader::onDataAvailable(SocketClient* cli) {
/* ...读一行命令、断开检查,略... */
unsigned long tail = 0;
static const char _tail[] = " tail=";
char* cp = strstr(buffer, _tail);
if (cp) {
tail = atol(cp + sizeof(_tail) - 1);
}
log_time start(log_time::EPOCH);
static const char _start[] = " start=";
cp = strstr(buffer, _start);
if (cp) {
// Parse errors will result in current time
start.strptime(cp + sizeof(_start) - 1, "%s.%q");
}
/* ...timeout=、lids=、pid= 同款解析,略... */
bool nonBlock = false;
if (!fastcmp<strncmp>(buffer, "dumpAndClose", 12)) {
// Allow writer to get some cycles, and wait for pending notifications
sched_yield();
logd_lock.lock();
logd_lock.unlock();
sched_yield();
nonBlock = true;
}
bool privileged = clientHasLogCredentials(cli);
bool can_read_security = CanReadSecurityLogs(cli);
if (!can_read_security) {
logMask &= ~(1 << LOG_ID_SECURITY); // 没有 system 身份,security buffer 直接屏蔽
}
std::unique_ptr<LogWriter> socket_log_writer(new SocketLogWriter(this, cli, privileged));
uint64_t sequence = 1;
// Convert realtime to sequence number
if (start != log_time::EPOCH) { // -T "时间" → 先扫一遍找对应 sequence
/* ...log_find_start 回调扫描,略... */
}
auto lock = std::lock_guard{logd_lock};
auto entry = std::make_unique<LogReaderThread>(log_buffer_, reader_list_,
std::move(socket_log_writer), nonBlock, tail,
logMask, pid, start, sequence, deadline);
bool only_read_event_logs = (logMask ^ (1 << LOG_ID_EVENTS)) == 0;
// The logd access will be granted when any of the following three
// conditions is satisfied:
// 1. the client (native processes) is exempted from user consent check.
// 2. the client doesn't have log credentials and so can only access its own log data anyway
// 3. the client (for EventLog API) only wants to access the event log.
if (clientIsExemptedFromUserConsent(cli) || !clientHasLogCredentials(cli) ||
only_read_event_logs) {
reader_list_->AddAndRunThread(std::move(entry));
} else {
// 有 log 权限的 app(如持有 READ_LOGS)→ 走 binder 问 system_server 的
// LogcatManagerService 是否有用户授权(Android 14 新增的 consent 机制)
if (GetLogdBinderServiceStatus(reader_list_)) {
reader_list_->AddPendingThread(std::move(entry));
} else {
entry->Revoke(); // binder 服务不在,撤销特权,只能看自己的日志
reader_list_->AddAndRunThread(std::move(entry));
}
}
// Set acceptable upper limit to wait for slow reader processing b/27242723
struct timeval t = { LOGD_SNDTIMEO, 0 };
setsockopt(cli->getSocket(), SOL_SOCKET, SO_SNDTIMEO, (const char*)&t,
sizeof(t));
return true;
}
这段浓缩了读取侧的整个权限模型 ,值得展开成一张表(LogPermissions.cpp):
| 检查 | 逻辑 | 失败后果 |
|---|---|---|
clientHasLogCredentials() |
uid/gid ∈ {root, system, log},或补充组含 AID_LOG(用 SO_PEERGROUPS 查,老内核回退读 /proc/pid/status) |
非特权读者------只能看自己 UID 的日志 |
CanReadSecurityLogs() |
uid/gid == system | security buffer 从 logMask 里被摘掉 |
clientIsExemptedFromUserConsent() |
uid < AID_APP_START(native 进程) |
app 读者需要 LogcatManagerService 的用户授权(Android 14+) |
"非特权读者只能看自己的日志" enforcement 在 SerializedLogBuffer::FlushTo() 里(见 7.3 节 !writer->privileged() && entry->uid() != writer->uid() 那行)。adb 之所以能看所有日志,是因为 adbd 以 system 组身份运行,补足了 AID_LOG 凭证。
SO_SNDTIMEO 这行也很有意思:给这个连接的 socket 发送设置了超时上限------如果某个 logcat 卡住不收数据,logd 最多等这么久,然后 Flush 失败、线程退出,一个慢客户端最多拖死自己,拖不死 logd。
7.2 LogReaderThread:两遍扫描实现 tail
每个连接一个线程,线程函数是读取侧的心脏:
scss
/* logd/LogReaderThread.cpp:53-110 */
void LogReaderThread::ThreadFunction() {
prctl(PR_SET_NAME, "logd.reader.per");
auto lock = std::unique_lock{logd_lock};
auto lock_assertion = android::base::ScopedLockAssertion{logd_lock};
while (!release_) {
if (deadline_.time_since_epoch().count() != 0) {
if (thread_triggered_condition_.wait_until(lock, deadline_) ==
std::cv_status::timeout) {
deadline_ = {};
}
if (release_) {
break;
}
}
if (tail_) { // ← 有 tail 需求:先数一遍总数
auto first_pass_state = log_buffer_->CreateFlushToState(flush_to_state_->start(),
flush_to_state_->log_mask());
log_buffer_->FlushTo(writer_.get(), *first_pass_state,
[this](log_id_t log_id, pid_t pid, uint64_t sequence,
log_time realtime) REQUIRES(logd_lock) {
return FilterFirstPass(log_id, pid, sequence, realtime);
});
}
bool flush_success = log_buffer_->FlushTo(
writer_.get(), *flush_to_state_,
[this](log_id_t log_id, pid_t pid, uint64_t sequence,
log_time realtime) REQUIRES(
logd_lock) { return FilterSecondPass(log_id, pid, sequence, realtime); });
/* ...start_time 清零注释,略... */
if (!flush_success) {
break;
}
if (non_block_ || release_) {
break; // dumpAndClose 模式:倒完就收工
}
CleanSkip();
if (deadline_.time_since_epoch().count() == 0) {
thread_triggered_condition_.wait(lock); // ← 没有新日志,睡眠等唤醒
}
}
writer_->Release();
reader_list_->RemoveRunningThread(this);
}
主循环的节奏是:冲刷(FlushTo)→ 没新日志就睡在条件变量上 → 被唤醒再来一遍 。唤醒方就是写路径末尾的 LogReaderList::NotifyNewLog():
rust
/* logd/LogReaderList.cpp:44-54 */
// When we are notified a new log entry is available, inform
// listening sockets who are watching this entry's log id.
void LogReaderList::NotifyNewLog(LogMask log_mask) const {
for (const auto& entry : running_reader_threads_) {
if (!entry->IsWatchingMultiple(log_mask)) {
continue; // 只叫醒在等这个 buffer 的人
}
if (entry->deadline().time_since_epoch().count() != 0) {
continue; // --wrap 模式的读者有自己的 deadline 节奏
}
entry->TriggerReader();
}
}
tail=N(只要最近 N 条)的实现是"两遍扫描":第一遍不写任何东西,只数符合条件的条数(FilterFirstPass 只 ++count_ 恒返回 kSkip);第二遍真正发送,跳过前 count_ - tail_ 条:
arduino
/* logd/LogReaderThread.cpp:112-165(节选) */
// A first pass to count the number of elements
FilterResult LogReaderThread::FilterFirstPass(log_id_t, pid_t pid, uint64_t, log_time realtime) {
if ((!pid_ || pid_ == pid) && (start_time_ == log_time::EPOCH || start_time_ <= realtime)) {
++count_;
}
return FilterResult::kSkip;
}
// A second pass to send the selected elements
FilterResult LogReaderThread::FilterSecondPass(log_id_t log_id, pid_t pid, uint64_t,
log_time realtime) {
if (skip_ahead_[log_id]) {
skip_ahead_[log_id]--;
return FilterResult::kSkip;
}
/* ...non_block + tail 截断、pid 过滤、start_time 过滤、release 检查,略... */
if (!tail_) {
goto ok;
}
++index_;
if (count_ > tail_ && index_ <= (count_ - tail_)) {
return FilterResult::kSkip; // 前面的全跳过
}
if (!non_block_) {
tail_ = 0; // 阻塞模式下,倒完 tail 之后转为正常流式
}
ok:
if (!skip_ahead_[log_id]) {
return FilterResult::kWrite;
}
return FilterResult::kSkip;
}
这也解释了一个日常现象:logcat -t 100 几乎瞬时返回,但 logcat -T 100(不退出、继续流式)在输出 100 条历史后会无缝衔接到实时日志------第二遍结束时把 tail_ 清零,后面就走全量路径了。
7.3 FlushTo:跨缓冲区归并 + 释放锁发送
SerializedLogBuffer::FlushTo() 负责把数据真正送到客户端,两处设计值得细品:
scss
/* logd/SerializedLogBuffer.cpp:257-305(节选) */
bool SerializedLogBuffer::FlushTo(
LogWriter* writer, FlushToState& abstract_state,
const std::function<FilterResult(log_id_t log_id, pid_t pid, uint64_t sequence,
log_time realtime)>& filter) {
auto& state = reinterpret_cast<SerializedFlushToState&>(abstract_state);
while (state.HasUnreadLogs()) {
LogWithId top = state.PopNextUnreadLog(); // 取全局 sequence 最小的那条
auto* entry = top.entry;
auto log_id = top.log_id;
if (entry->sequence() < state.start()) {
continue;
}
state.set_start(entry->sequence());
if (!writer->privileged() && entry->uid() != writer->uid()) {
continue; // ← 按 UID 隔离就在这一行
}
if (filter) {
auto ret = filter(log_id, entry->pid(), entry->sequence(), entry->realtime());
if (ret == FilterResult::kSkip) {
continue;
}
if (ret == FilterResult::kStop) {
break;
}
}
// We copy the log entry such that we can flush it without the lock. We never block pruning
// waiting for this Flush() to complete.
constexpr size_t kMaxEntrySize = sizeof(*entry) + LOGGER_ENTRY_MAX_PAYLOAD + 1;
unsigned char entry_copy[kMaxEntrySize] __attribute__((uninitialized));
CHECK_LT(entry->msg_len(), LOGGER_ENTRY_MAX_PAYLOAD + 1);
memcpy(entry_copy, entry, sizeof(*entry) + entry->msg_len());
logd_lock.unlock(); // ← 发送时不持全局锁!
if (!reinterpret_cast<SerializedLogEntry*>(entry_copy)->Flush(writer, log_id)) {
logd_lock.lock();
return false;
}
logd_lock.lock();
}
state.set_start(state.start() + 1);
return true;
}
-
跨缓冲区归并 :
PopNextUnreadLog()(SerializedFlushToState.cpp:125)在所有被关注的缓冲区里挑 sequence 最小的下一条------这就是第 5.2 节全局序号存在的意义,多路归并保证 logcat 混合输出依然按写入顺序排列:ini/* logd/SerializedFlushToState.cpp:125-146(节选) */ LogWithId SerializedFlushToState::PopNextUnreadLog() { uint64_t min_sequence = std::numeric_limits<uint64_t>::max(); log_id_t log_id; const SerializedLogEntry* entry = nullptr; log_id_for_each(i) { if (!log_positions_[i] || logs_needed_from_next_position_[i]) { continue; } if (log_positions_[i]->log_entry()->sequence() < min_sequence) { log_id = i; entry = log_positions_[i]->log_entry(); min_sequence = entry->sequence(); } } /* ...推进该 buffer 的读游标,略... */ } -
发送时释放 logd_lock :把 entry 拷到栈上,解锁后再写 socket(socket 可能因为客户端卡住而阻塞------虽然有 SO_SNDTIMEO 兜底)。全局锁绝不等待客户端,这是 logd 在重读写负载下依然稳定的关键不变量。
最终 SerializedLogEntry::Flush() 把条目翻译成 logger_entry 协议头,交 LogWriter 写出:
ini
/* logd/SerializedLogEntry.h:65-78 */
bool Flush(LogWriter* writer, log_id_t log_id) const {
struct logger_entry entry = {};
entry.hdr_size = sizeof(struct logger_entry);
entry.lid = log_id;
entry.pid = pid();
entry.tid = tid();
entry.uid = uid();
entry.sec = realtime().tv_sec;
entry.nsec = realtime().tv_nsec;
entry.len = msg_len();
return writer->Write(entry, msg());
}
ini
/* logd/LogReader.cpp:74-101(节选) */
class SocketLogWriter : public LogWriter {
public:
bool Write(const logger_entry& entry, const char* msg) override {
struct iovec iovec[2];
iovec[0].iov_base = const_cast<logger_entry*>(&entry);
iovec[0].iov_len = entry.hdr_size;
iovec[1].iov_base = const_cast<char*>(msg);
iovec[1].iov_len = entry.len;
return client_->sendDatav(iovec, 1 + (entry.len != 0)) == 0;
}
/* ...Release/Shutdown/name 略... */
};
于是 logcat 收到的每个 SEQPACKET 报文 = logger_entry 头 + 原始 payload。logger_entry(liblog/include/log/log_read.h:39):
arduino
/* liblog/include/log/log_read.h:39-48 */
struct logger_entry {
uint16_t len; /* length of the payload */
uint16_t hdr_size; /* sizeof(struct logger_entry) */
int32_t pid; /* generating process's pid */
uint32_t tid; /* generating process's tid */
uint32_t sec; /* seconds since Epoch */
uint32_t nsec; /* nanoseconds */
uint32_t lid; /* log id of the payload, bottom 4 bits currently */
uint32_t uid; /* generating process's uid */
};
对比发送协议你会发现:payload 原封不动地回来了,但身份和时间信息被 logd 重新封装 ------pid/uid 来自内核凭证,tid/时间来自客户端报文头,全部收进 28 字节的 logger_entry。4068 + 28 ≈ 4096,这正是 LOGGER_ENTRY_MAX_PAYLOAD 取 4068 这个奇怪数字的原因。
8. logcat 工具:解析、过滤、格式化
8.1 主循环
logcat 的 Run() 前半段全是参数解析(600 多行 getopt),真正干活的循环只有 40 来行:
scss
/* logcat/logcat.cpp:1058-1105(节选) */
bool blocking = !(mode & ANDROID_LOG_NONBLOCK);
SetupOutputAndSchedulingPolicy(blocking); // 写文件时降为后台调度,别抢前台 CPU
if (!WaitForProperty("logd.ready", "true", std::chrono::seconds(1))) {
error(EXIT_FAILURE, 0, "Failed to wait for logd.ready to become true. logd not running?");
}
while (!max_count_ || print_count_ < max_count_) {
struct log_msg log_msg;
int ret = android_logger_list_read(logger_list.get(), &log_msg);
if (!ret) {
error(EXIT_FAILURE, 0, R"init(Unexpected EOF!
This means that either the device shut down, logd crashed, or this instance of logcat was unable
to read log messages as quickly as they were being produced.
If you have enabled significant logging, look into using the -G option to increase log buffer
sizes.)init");
}
if (ret < 0) {
if (ret == -EAGAIN) break; // 非阻塞模式倒完了
/* ...其余错误处理,略... */
}
if (log_msg.id() > LOG_ID_MAX) {
error(EXIT_FAILURE, 0, "Unexpected log id (%d) over LOG_ID_MAX (%d).", log_msg.id(),
LOG_ID_MAX);
}
if (!uids.empty() && uids.count(log_msg.entry.uid) == 0) {
continue; // --uid= 过滤(在 logcat 侧做,不走 socket 协议)
}
if (print_binary_) {
WriteFully(&log_msg, log_msg.len()); // -B:原样输出二进制
} else {
ProcessBuffer(&log_msg); // 常规:解析+格式化
}
if (blocking && output_file_ == stdout) fflush(stdout);
}
注意 Unexpected EOF 这个错误就是 5.5 节"慢读者"的客户端镜像------logd 把你甩了(连接断开返回 0)。代码里给的处方(-G 调大缓冲区)就是针对 logcat 消费不过来的场景。
8.2 ProcessBuffer:二进制 → 结构化
ProcessBuffer() 把 logger_entry + payload 解析成人话,处理过滤与输出:
scss
/* logcat/logcat.cpp:189-227 */
void Logcat::ProcessBuffer(struct log_msg* buf) {
AndroidLogEntry entry;
char binaryMsgBuf[1024] __attribute__((__uninitialized__));
bool is_binary =
buf->id() == LOG_ID_EVENTS || buf->id() == LOG_ID_STATS || buf->id() == LOG_ID_SECURITY;
int err;
if (is_binary) {
if (!event_tag_map_ && !has_opened_event_tag_map_) {
event_tag_map_.reset(android_openEventTagMap(nullptr)); // /system/etc/event-log-tags
has_opened_event_tag_map_ = true;
}
// This causes entry to point to binaryMsgBuf!
err = android_log_processBinaryLogBuffer(&buf->entry, &entry, event_tag_map_.get(),
binaryMsgBuf, sizeof(binaryMsgBuf));
} else {
err = android_log_processLogBuffer(&buf->entry, &entry); // 文本日志解析
}
if (err < 0 && !debug_) return;
if (android_log_shouldPrintLine(logformat_.get(), std::string(entry.tag, entry.tagLen).c_str(),
entry.priority)) {
bool match = !regex_ ||
std::regex_search(entry.message, entry.message + entry.messageLen, *regex_);
print_count_ += match;
if (match || print_it_anyway_) {
PrintDividers(buf->id(), print_dividers_);
out_byte_count_ += android_log_printLogLine(logformat_.get(), output_file_, &entry);
}
}
if (log_rotate_size_kb_ > 0 && (out_byte_count_ / 1024) >= log_rotate_size_kb_) {
RotateLogs();
}
}
文本缓冲区的解析函数 android_log_processLogBuffer()(在 logprint.cpp,逻辑直白):payload 第一字节是优先级,接下来到 \0 是 tag,剩下的是消息,对应填进 AndroidLogEntry{tv_sec, tv_nsec, pid, uid, tid, priority, tag, message}。二进制缓冲区(events)则查 event-log-tags 数据库把数字 tag 号翻译成名字和参数格式。
过滤发生在 android_log_shouldPrintLine():命令行 FILTERSPEC(如 logcat WindowManager:V *:S)被编译成 tag→最低优先级表,这里逐条比对。注意过滤是在 logcat 收到数据之后做的 ------数据已经从 logd 传到 logcat 了,所以过滤器再严也省不了传输,只省显示(要省传输得用 log.tag.* 属性,见 2.3 节)。
8.3 threadtime:默认格式的拼装
最后一步,android_log_formatLogLine()(liblog/logprint.cpp:1414)把 entry 拼成最终那行字。默认的 threadtime 格式核心就一句:
ini
/* liblog/logprint.cpp:1553-1563 */
case FORMAT_THREADTIME:
ret = strchr(uid, ':');
if (ret) {
*ret = ' ';
}
len = snprintf(prefixBuf + prefixLen, sizeof(prefixBuf) - prefixLen,
"%s %s%5d %5d %c %-8.*s: ", timeBuf, uid, entry->pid, entry->tid, priChar,
(int)entry->tagLen, entry->tag);
strcpy(suffixBuf + suffixLen, "\n");
++suffixLen;
break;
其中 timeBuf 由前面几步生成(logprint.cpp:1447-1479):localtime_r + strftime("%m-%d %H:%M:%S") + .%03ld 毫秒。组合起来:
less
08-15 10:23:45.123 1234 5678 I ActivityManager: Start proc for activity
└──timeBuf───┘ └pid┘ └tid┘ │ └──tag───┘└── message ──┘
└ priChar(filterPriToChar)
同文件里一张表管所有格式:brief / process / tag / raw / thread / threadtime / time / long,加上修饰动词 color / epoch / monotonic / printable / uid / usec / UTC / year / zone-------v 的全部语义就在这个 switch 里。
另外 PrintDividers()(logcat.cpp:229)负责 --------- beginning of system 这些分隔行:每个 buffer 第一条输出时打印 "beginning of",切换 buffer 时打印 "switch to"(需要 -D 强制每次切换都打)。
9. 控制通道与运维:logcat -g/-G/-c/-S/-P 的背后
第 6 节提到第三个 socket logd(stream),它跑的是 FrameworkListener 协议------一行一条文本命令 ,CommandListener(logd/CommandListener.cpp:44)注册了 10 条命令:
scss
/* logd/CommandListener.cpp:44-58 */
CommandListener::CommandListener(LogBuffer* buf, LogTags* tags, PruneList* prune,
LogStatistics* stats)
: FrameworkListener(getLogSocket()), buf_(buf), tags_(tags), prune_(prune), stats_(stats) {
registerCmd(new ClearCmd(this)); // "clear <log_id>" ← logcat -c
registerCmd(new GetBufSizeCmd(this)); // "getLogSize <id>" ← logcat -g
registerCmd(new SetBufSizeCmd(this)); // "setLogSize <id> N" ← logcat -G
registerCmd(new GetBufSizeReadableCmd(this)); // "getLogSizeReadable <id>"
registerCmd(new GetBufSizeUsedCmd(this)); // "getLogSizeUsed <id>"
registerCmd(new GetStatisticsCmd(this)); // "getStatistics ..." ← logcat -S
registerCmd(new SetPruneListCmd(this)); // "setPruneList ..." ← logcat -P
registerCmd(new GetPruneListCmd(this)); // "getPruneList"
registerCmd(new GetEventTagCmd(this)); // "getEventTag ..."
registerCmd(new ReinitCmd(this)); // "reinit"
registerCmd(new ExitCmd(this));
}
liblog 侧的封装在 logd_reader.cpp:android_logger_clear() 拼出 "clear 0" 发过去,读回 "success"。以 clear 为例:
scss
/* logd/CommandListener.cpp:87-96 */
int CommandListener::ClearCmd::runCommand(SocketClient* cli, int argc, char** argv) {
uid_t uid = cli->getUid();
if (clientHasLogCredentials(cli)) {
uid = AID_ROOT; // 有凭证 → 可以清所有人的;没凭证 → 只能清自己的(按 UID 清)
}
return LogIdCommand(cli, argc, argv, [&](log_id_t id) {
cli->sendMsg(buf()->Clear(id, uid) ? "success" : "busy");
});
}
SerializedLogBuffer::Clear()(SerializedLogBuffer.cpp:307):uid==0 时 Prune(id, ULONG_MAX) 全清;否则 ClearLogsByUid() 只清该 UID 的日志------普通 app 调 Log.clear()(暴露给 LogManager 的接口)时清的就是自己的部分。
统计 (logcat -S)是排查"谁在刷屏"的利器。LogStatistics 在每次 Log()/Prune() 时同步维护按 uid/pid/tag 维度的字节数(stats_->Add(entry->ToLogStatisticsElement()),见 5.2 节),输出形如:
css
logcat -S 输出示例(节选):
UID PACKAGE V/D/I/W/E/F TotalSize Entries
u0a85 (com.example) 0/120/3k/2/1/0 180000 3123
...
统计默认只在 eng/debuggable 且非 low_ram 设备开启(GetBoolPropertyEngSvelteDefault("logd.statistics"),main.cpp:247),因为维护统计本身也有开销。
prune 规则 (logcat -P '~10 xxx ~!')格式:UID、UID/PID、/PID,~ 前缀表示优先淘汰,~! 表示自动压制当前最吵的 UID------再次提醒,这套路由只在 SimpleLogBuffer 生效。
10. 日志的其他三个来源:LogKlog、LogAudit、TrustyLog
回到 main(),除了 socket 收日志,logd 还挂着三个"采集器",它们都直接调 log_buffer_->Log() 入库,复用同一套存储与分发:
10.1 LogKlog:内核日志进 logcat
ro.logd.kernel(默认 debuggable 且非 low_ram 时为 true)开启后,LogKlog 线程阻塞读 /proc/kmsg,逐行解析 printk 的 <6>[时间戳] 消息 格式,去掉 syslog 优先级前缀,写进 LOG_ID_KERNEL 缓冲区。麻烦在于内核时间戳是 monotonic 的,而用户态日志全是 realtime,所以 LogKlog 启动时算好两者的偏移并动态校准:
css
/* logd/LogKlog.cpp:200-202 */
log_time LogKlog::correction = (log_time(CLOCK_REALTIME) < log_time(CLOCK_MONOTONIC))
? log_time(log_time::EPOCH)
: (log_time(CLOCK_REALTIME) - log_time(CLOCK_MONOTONIC));
readDmesg()(main.cpp:126)还会在启动时把 dmesg 里积压的存量日志灌一遍,所以刚开机就能 logcat -b kernel 看到完整内核启动日志。
10.2 LogAudit:SELinux 拒绝日志
SELinux 的 avc denial 从 NETLINK_AUDIT socket 进来(内核 audit 子系统),LogAudit 线程解析后以 avc: tag 写进 main 缓冲区(并镜像一份到 dmesg):
ini
08-15 10:23:45.123 1234 5678 E avc: denied { read } for name="x" scontext=u:r:untrusted_app:s0 tcontext=u:object_r:foo:s0 tclass=file
logd.rc 里的 logd-auditctl 服务还会执行 auditctl -r 5------限速 5 条/秒,防止 SELinux 风暴把日志系统自己冲垮。
10.3 TrustyLog 与 pmsg
- TrustyLog :如果设备有 Trusty TEE(
/dev/trusty-log0存在),把 TEE 侧日志加trusty前缀后入库------安全世界的日志也能在 logcat 里看到。 - pmsg (前文 3 节已提及):
PmsgWrite双写/dev/pmsg0(ramoops/pstore),机器崩溃重启后logcat -L从 pstore 里把重启前的日志捞出来。这是"死前遗言"的唯一通道,代价是每条日志一次额外写入(pstore 有容量上限,循环覆盖)。
11. 设计复盘:九个"为什么"
读完源码,把散落各处的设计决策串起来,你会发现每一处都在回答同一个母题:日志系统必须永远活着、永远不挡业务的路,同时在权限上滴水不漏。
- 为什么用 unix socket 而不是 binder? binder 依赖 servicemanager 和 binder 驱动的完整初始化,而日志需要在最早期(init 第一阶段之后)就能用;unix socket 是零依赖的。另外 binder 调用有死锁风险(打日志的进程可能持有 logd 需要的锁),socket 单向写入没有这个问题。
- 为什么客户端 socket 非阻塞? 打日志在业务关键路径上。logd 无论多忙(GC、磁盘 IO、CPU 饥饿),业务进程的
Log.i()都不能被拖住------EAGAIN 就丢弃,靠 dropped 计数补报。 - 为什么 uid/pid 走 SCM_CREDENTIALS 而不放在报文里? 报文内容客户端可控,凭证由内核打上。若放报文里,任何 app 都能伪装成 system 打日志,整套按 UID 的读隔离就全废了。
- 为什么时间戳客户端打、序号 logd 打? 时间戳反映业务真实发生时刻(且省一次服务端系统调用);但客户端时钟可能回拨/漂移,所以全局排序交给 logd 的原子 sequence。读取侧按 sequence 归并,时间戳只用于显示和
-T定位(LogReader会先扫一遍把时间翻译成 sequence)。 - 为什么每个 logcat 连接一个线程? 读取是长阻塞会话(客户端可能卡死),epoll 单线程模型里一个坏客户端会阻塞所有读者;thread-per-connection + 发送时释放全局锁 + SO_SNDTIMEO,把"慢客户端"的爆炸半径限制在它自己的线程里。
- 为什么全局一把
logd_lock? logd 的共享状态(chunk 链表、读者游标、统计)高度耦合,粗锁换来的是:所有临界区都是纯内存操作(微秒级),从不持锁做 IO(发送前拷贝+解锁,见 7.3 节)。在日志这种"高频小操作"的负载下,粗锁+短临界区反而优于细锁的复杂度。 - 为什么要压缩? 8 个 buffer × 256KB 默认共约 2MB 常驻内存,还不算 statistics。文本日志 zstd 压缩比通常 3~10 倍,同样的内存预算下历史日志容量翻数倍------代价只是读取时的解压(且有 reader 引用计数,只在需要时解压)。
- 为什么淘汰按整块? 精细化按条淘汰(旧 LogBufferElement 时代的 PruneList 策略)需要遍历条目、逐条调整统计与读者游标,锁持有时间随淘汰量线性增长;chunk 整块淘汰是 O(chunks) 级操作,配合压缩后内存利用率已经够高,简单方案赢了。
- 为什么"能看全部日志"是特权? 日志里全是敏感信息(URL、token、用户行为)。Android 14 之前持有 READ_LOGS 的 app 就能拿到一切,之后加了 LogcatManagerService 的用户授权环节(7.1 节的 pending thread 机制),adb/native 进程天然豁免。这是日志系统从"工程师工具"走向"隐私合规资产"的分水岭。
12. 实战速查
r
# 容量
adb shell logcat -g # 看各 buffer 分配/已用/可读(可读>分配 = 压缩生效)
adb shell logcat -G 1M # 临时调大(重启失效)
adb shell setprop persist.logd.size.main 4M # 持久化(需确认你的设备是否读取该属性)
# 降噪:进程内过滤,零传输开销(改完立即生效,logd 侧还有同款兜底)
adb shell setprop log.tag.MyTag WARN # 只保留 MyTag 的 W 及以上
adb shell setprop persist.log.tag I # 全局默认 INFO
# 排查
adb shell logcat -S # 谁在刷屏(uid/pid/tag 维度统计)
adb shell logcat -b all -d -v uid # 带上 UID 看
adb logcat -T "08-15 10:23:45.000" # 从某时刻开始
adb shell ps -T -p $(pidof logd) # 验证六个线程是否健在
adb shell getprop logd.ready # logd 是否就绪
# 崩溃后取证
adb shell logcat -L -b all -d # 读 pstore 里上次重启前的日志
adb shell dmesg | grep logd # logd 自己的日志走 kmsg,不进自己的 buffer
常见报错对照表:
| 现象 | 根因 | 出处 |
|---|---|---|
Unexpected EOF! ... unable to read ... quickly |
logcat 消费太慢,被 logd 按"慢读者"断连 | logcat.cpp:1069 / SerializedLogBuffer.cpp:216 |
I liblog: [3](events) |
客户端发送失败 N 次,liblog 丢弃计数补报 | logd_writer.cpp:151 |
Skipping entries from slow reader |
logd 侧同一件事的服务端视角警告 | SerializedLogBuffer.cpp:216 |
| 日志里看不到别的 app | 非特权读者按 UID 隔离(正常行为) | SerializedLogBuffer.cpp:273 |
--------- beginning of kernel 什么都没有 |
user 版 ro.logd.kernel=false,LogKlog 未启用 |
main.cpp:224 |
附录 A:本仓库文件地图
| 路径 | 内容 | 本文章节 |
|---|---|---|
liblog/logger_write.cpp |
客户端写入口、logger 函数可替换机制 | 2、3 |
liblog/logd_writer.cpp |
logdw socket 管理、报文组包、丢弃计数 | 3 |
liblog/pmsg_writer.cpp |
/dev/pmsg0 双写(重启前日志) | 3、10 |
liblog/properties.cpp |
log.tag.* 属性过滤、security 开关 | 2、4 |
liblog/logger_read.cpp / logd_reader.cpp |
读 API、logdr/logd 命令封装 | 6、9 |
liblog/logprint.cpp |
解析与 threadtime 等 8 种格式化 | 8 |
liblog/README.protocol.md |
官方 wire protocol 文档 | 3.1 |
logd/main.cpp |
装配车间:5 个常驻角色 | 4.1 |
logd/LogListener.cpp |
logdw 收包、SCM_CREDENTIALS | 4.2 |
logd/SerializedLogBuffer.cpp |
写入/淘汰/FlushTo/尺寸 | 5 |
logd/SerializedLogChunk.cpp |
chunk 内存管理、zstd 压缩 | 5.3、5.4 |
logd/SerializedLogEntry.h |
30 字节紧凑条目、Flush 出口 | 5.3、7.3 |
logd/SerializedFlushToState.cpp |
读取游标、按 sequence 归并 | 7.3 |
logd/LogReader.cpp / LogReaderList.cpp |
连接接入、权限、consent | 7.1 |
logd/LogReaderThread.cpp |
读者线程、两遍扫描 tail | 7.2 |
logd/CommandListener.cpp |
控制命令 10 条 | 9 |
logd/LogPermissions.cpp |
凭证检查全集 | 4.2、7.1 |
logd/LogKlog.cpp / LogAudit.cpp / TrustyLog.cpp |
内核/SELinux/TEE 采集器 | 10 |
logd/LogSize.cpp |
尺寸属性解析 | 5.6 |
logcat/logcat.cpp |
工具主体 | 8 |
附录 B:版本与定制说明
-
本仓库为 Android 14(U)时期的
system/logging,判据:SerializedLogBuffer(Android 12 引入并成为默认)+LogReaderList/LogReaderThread拆分 +ILogcatManagerService用户授权机制(Android 14 引入)。 -
含少量厂商(车机)定制,阅读 AOSP 对照时注意两处:
SerializedLogBuffer::Log()入口的wkk.logdsystem.close写入总开关;LogSize.cpp中 automotive + debuggable 才读取persist.logd.size.*属性。cint SerializedLogBuffer::Log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid, pid_t tid, const char* msg, uint16_t len) { std::string close_type = GetProperty("wkk.logdsystem.close", "0"); //通过属性来控制整个日志开关 if (close_type == "1") { return -EACCES; } if (log_id >= LOG_ID_MAX || len == 0) { return -EINVAL; } if (len > LOGGER_ENTRY_MAX_PAYLOAD) { len = LOGGER_ENTRY_MAX_PAYLOAD; } if (!ShouldLog(log_id, msg, len)) { stats_->AddTotal(log_id, len); return -EACCES; } -
Java 层(
android.util.Log等)在 framework/base,不在本仓库;文中 Java→JNI 链路为通用常识性描述,其余代码均出自本仓库。
全文完。如果只想记一件事:日志系统的一切设计都围绕两个不变量------打日志永不阻塞业务,身份永远由内核背书。