1. 概览
经过上一章的分析,现在也是时候讨论下logd的初始化了,虽然 logd 在代码量上来说并不大,但是还是分模块进行分析比较合适。所以这里就不贴整体代码了,这部分代码也被包含在AOSP t 的代码中,有兴趣的读者可以自己下载查看。
2. 屏蔽 SIGPIPE 信号
下面先看看这部分代码长什么样
main
signal(SIGPIPE, SIG_IGN);
对于 signal 可以查看 man 手册,
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
DESCRIPTION
The behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead. See Portability below.
signal() sets the disposition of the signal signum to handler, which is either SIG_IGN, SIG_DFL, or the address of a programmer-defined function (a
"signal handler").
If the signal signum is delivered to the process, then one of the following happens:
* If the disposition is set to SIG_IGN, then the signal is ignored.
* If the disposition is set to SIG_DFL, then the default action associated with the signal (see signal(7)) occurs.
* If the disposition is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below),
and then handler is called with argument signum. If invocation of the handler caused the signal to be blocked, then the signal is unblocked upon
return from the handler.
The signals SIGKILL and SIGSTOP cannot be caught or ignored.
RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error. In the event of an error, errno is set to indicate the cause.
signal 的作用就是为调用进程注册一个对于信号量的处理函数,例如在logd中这句代码的含义则是,logd 在接收到信号 SIGPIPE 时调用函数 SIG_IGN 进行处理。而 SIG_IGN 则是什么也不做,即忽略了该信号。对于 SIGPIPE 信号,系统默认的操作则是让该进程异常退出。
那么系统是在什么场景下会发送 SIGPIPE 到进程中?描述如下
当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。
因此 logd 作为一个socket的服务端,以上场景一样会发生在它身上,例如一个 logcat 何时退出是不会告知 logd 的。所以在 logd 中是需要忽略掉信号 SIGPIPE 的。
3. log 时间戳时区设置
log中的时间戳非常的重要,时间戳代表着何时这部分log被打印,这一般也意味着此时程序想告诉开发人员它想表达的事项。因此 logd 为了统一,直接设置时区为UTC了。
main
setenv("TZ", "UTC", 1);
下面是 Google 对其的解释
logd is written under the assumption that the timezone is UTC. If TZ is not set, persist.sys.timezone is looked up in some time utility
libc functions, including mktime. It confuses the logd time handling, so here explicitly set TZ to UTC, which overrides the property.
4. logd 内部 log 的输出位置设置
Andorid中在 logd 之后启动的组件可以将 log 传递给 logd 去处理,但是对于 logd 来说就没这么好运了。但是对于 logd 也是需要开发调试的,因此它的 log 也是很重要的。下面则是用于设置 logd 内部log的输出
android::base::InitLogging(
argv, [](android::base::LogId log_id, android::base::LogSeverity severity,
const char* tag, const char* file, unsigned int line, const char* message) {
if (tag && strcmp(tag, "logd") != 0) {//code 1
auto prefixed_message = android::base::StringPrintf("%s: %s", tag, message);
android::base::KernelLogger(log_id, severity, "logd", file, line,
prefixed_message.c_str());
} else {//code 2
android::base::KernelLogger(log_id, severity, "logd", file, line, message);
}
});
logd 的输出又分两种,如果 log 的 tag 为 logd 那么就使用 code 2 进行打印,否则使用 code 1 的格式。 code 1 的方式多了 log 来源的 tag,但还是由 logd 打印所以 log 过滤时还是用 logd 标签。最终这部分 log 会被打印到 kernel log 中去,即可以使用 dmesg 来获取。
5. logd buffer
buffer 顾名思义,用来存放需要 logd 处理的 log 信息。在 logd 中,我们是可以通过设置属性值 logd.buffer_type 来决定使用不同的buffer的。
//system/logging/logd/README.property
logd.buffer_type string (empty) Set the log buffer type. Current choices are 'simple',
'chatty', or 'serialized'. Defaults to 'chatty' if empty.
从上面可知,目前支持 3 中buffer类型simple、chatty 和 serialized。但是属性 logd.buffer_type 为空的情况在 AndroidT 中可不是 chatty 而是 serialized
//file:system\logging\logd\main.cpp
main
std::string buffer_type = GetProperty("logd.buffer_type", "serialized");
因此我们也就以 serialized 类型的 log buffer 作为例子进行讲解。
main
LogBuffer* log_buffer = nullptr;
if (buffer_type == "chatty") {
...
} else if (buffer_type == "serialized") {
log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);
} else if (buffer_type == "simple") {
...
} else {
LOG(FATAL) << "buffer_type must be one of 'chatty', 'serialized', or 'simple'";
}
5.1 SerializedLogBuffer 类图
类的结构对了解类的作用及工作原理还是至关重要的,下面给出 SerializedLogBuffer 对应的类图。
5.2 SerializedLogBuffer 初始化解析
LogBuffer 仅仅是个接口类,并没有对应的构造方法。因此只要看看 SerializedLogBuffer 的构造方法就可以剖析初始化了。
log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);
reader_list_(reader_list), tags_(tags), stats_(stats)
Init();
log_id_for_each(i) {//code 1
if (!SetSize(i, GetBufferSizeFromProperties(i))) {
SetSize(i, kLogBufferMinSize);
}
}
//code 2
for (const auto& reader_thread : reader_list_->running_reader_threads()) {
reader_thread->TriggerReader();
}
代码很简单,说下设计意图吧。 reader_list 里面会维护所有读者信息,在buffer来了数据后就会通知 reader_list 里面所有的读者来读取 log 数据,至于各个读者施展什么手段 buffer 并不关心,使用 reader_list_ 隔离掉了。
在logd初始化阶段,显然是没有读者的,所以 SerializedLogBuffer 的构造方法也就剩两个事
1)记录传进来的 reader_list、log_tag等变量。
2)设置各个类型buffer的大小,如system/main/radio等,log_id_for_each 定义如下
//system\logging\logd\LogStatistics.h
#define log_id_for_each(i) \
for (log_id_t i = LOG_ID_MIN; (i) < LOG_ID_MAX; (i) = (log_id_t)((i) + 1))
从这里就可以看出 i 就是LOG_ID,现在来看看 SetSize
//system\logging\logd\SerializedLogBuffer.cpp
SetSize(i, GetBufferSizeFromProperties(i))
//system\logging\logd\LogSize.cpp
size = GetBufferSizeFromProperties(i)//code 1
max_size_[id] = size;//code 2
// 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.
MaybePrune(id);//code 3
buffer 大小的设置大致分成如下几个步骤
1) 在Android T 中已经不支持灵活的通过属性来配置对应各个buffer的大小了,描述如下
We've been seeing timeouts from logcat in bugreports for years, but the
rate has gone way up lately. The suspicion is that this is because we
have a lot of dogfooders who still have custom (large) log sizes but
the new compressed logging is cramming way more in. The bugreports I've seen
have had 1,000,000+ lines, so taking 10s to collect that much logging seems
plausible. Of course, it's also possible that logcat is timing out because
the log is being *spammed* as it's being read. But temporarily disabling
custom log sizes like this should help us confirm (or deny) whether the
problem really is this simple.
只根据设备是不是低内存设备来决定 各个buffer的大小
//Xsystem\logging\logd\LogSize.h
static constexpr size_t kDefaultLogBufferSize = 256 * 1024;
static constexpr size_t kLogBufferMinSize = 64 * 1024;
//system\logging\logd\LogSize.cpp
if (android::base::GetBoolProperty("ro.config.low_ram", false)) {
return kLogBufferMinSize;
}
return kDefaultLogBufferSize;
如果是低内存设备那么就返回 kLogBufferMinSize 的值也就是 64KB,否则返回 kDefaultLogBufferSize 为 256KB。
2) 在 SerializedLogBuffer 内部使用变量 max_size_ 记录下各个log buffer的能力大小。
6. kernel log 处理器 – klogd
6.1 klogd 功能选择
从 logcat 工具也是能看出,它是支持 kernel log 的显示的。klogd也就是 logd 实现 kernel 显示的关键
erd8535:/ # logcat -h
Usage: logcat [options] [filterspecs]
General options:
-b, --buffer=<buffer> Request alternate ring buffer(s):
main system radio events crash default all
Additionally, 'kernel' for userdebug and eng builds, and
'security' for Device Owner installations.
Additionally 这个副词也说明了 kernel log 在 logd 中的地位有些不一样,有点像孤儿、送的、没人要的。实际上 kernel log 的打印确实和 logd 中 main/system 这些 log 不一样。logd 是通过读取节点 /proc/kmsg 来获取 kernel log 的。这里值得注意的是 /proc/kmsg 是不支持多读者的,即一个读者读走内容后,下一个读者就获取不到 log 信息了。所以例如在使用 logcat -b kernel 后,不要再类似手动 cat 这个节点了。下面就来看看实现吧。
main
//code 1
static const char dev_kmsg[] = "/dev/kmsg";
int fdDmesg = android_get_control_file(dev_kmsg);
//code 2
bool klogd = GetBoolPropertyEngSvelteDefault("ro.logd.kernel");
if (klogd) {//code 3
SetProperty("ro.logd.kernel", "true");
static const char proc_kmsg[] = "/proc/kmsg";
fdPmesg = android_get_control_file(proc_kmsg);
}
code 1 用于获取 kmsg 节点的 fd,它在init启动lod得时候就被以仅写的权限打开了
//system\logging\logd\logd.rc
service logd /system/bin/logd
file /dev/kmsg w
用于确认是否开启 klogd,即 logd 是否支持 kernel log 的处理。下面来看看 code 2 的代码实现
//system\logging\logd\main.cpp
bool klogd = GetBoolPropertyEngSvelteDefault("ro.logd.kernel");
bool default_value =
GetBoolProperty("ro.debuggable", false) && !GetBoolProperty("ro.config.low_ram", false);
return GetBoolProperty(name, default_value);
ro.debuggable 只有在 eng 和 userdebug 两个版本中为 1,下面是 Android 团队的官方解释,供参考。android build-variants
同样的 ro.config.low_ram 在低内存设备上为false。
综上,只要满足任意一个条件,logd 对 kernel log 的功能默认就是关闭的,毕竟 kernel log 一般包含系统关键信息。
a) 低内存设备
b) user 版本的Android 系统
当然用户可以明确通过属性 ro.logd.kernel 来开启 klog 的功能。
6.2 klog – LogKlog 的初始化
最终 klogd 如果被设置为 true,那么 logd 就支持处理 kernel log 的特性。LogKlog 类就是被用于 logd 处理kernel log的。下面先来看看它的类图
可见它的父类就是 SocketListener,对于它的讲解见 《AndroidT(13) Log 系统 – SocketListener 帮助类详解(六)》 章节。下面先来看看它的初始化
//system\logging\logd\main.cpp
main
...
kl = new LogKlog(log_buffer/*buf*/, fdDmesg/*fdWrite*/, fdPmesg/*fdRead*/, al != nullptr/*auditd*/, &log_statisticsstats/**/);
SocketListener(fdRead, false),//code 1
logbuf(buf),
...
//code 2
static const char klogd_message[] = "%s%s%" PRIu64 "\n";
char buffer[strlen(priority_message) + strlen(klogdStr) +
strlen(klogd_message) + 20];
snprintf(buffer, sizeof(buffer), klogd_message, priority_message, klogdStr,
signature.nsec());
write(fdWrite, buffer, strlen(buffer));
值得注意的是,LogKlog 中使用 SocketListener 来监听文件句柄(fdPmesg 为 /proc/kmsg ),而非 socket的连接i请求,因为/proc/kmsg 并不是 socket 句柄,如果有客户端想获取kernel log的话,那也是通过 logr 的。所以此处 SocketListener 传入的 listen 为 false。
code 2 用于设置当前进程所读取到的kernel log的格式,下面是对应的格式
//system\logging\logd\LogKlog.cpp
static const char klogdStr[] = "logd.klogd: ";
"priority_message""klogdStr""signature(CLOCK_MONOTONIC)"
的拆解过程如下,最终就是去掉"<44>"后缀’\0’的内容
//bionic\libc\include\syslog.h
#define LOG_SYSLOG (5<<3)
#define LOG_INFO 6
#define KMSG_PRIORITY(LOG_INFO) \
'<',
'0' + ((5<<3) | 6) / 10,
'0' + (10 1000b | 1100b) / 10,
'0' + 44 / 10,
'4',
'0' + ((5<<3) | (6)) % 10,
'4',
'>'
static const char priority_message[] = { KMSG_PRIORITY(LOG_INFO), '\0' };
{
'<',
'4',
'4',
'>'
}
6.3 LogKlog 的监听启动
从 LogKlog 的类图可知它的父类为 SocketListener ,所以启动监听则是通过它的 startListener 接口实现,启动成功后新线程会被创建用于监听所给句柄是否有数据可读,并且调用子类也就是 LogKlog 中的 onDataAvailable 方法进行处理。
//system\logging\logd\main.cpp
bool LogKlog::onDataAvailable(SocketClient* cli){
prctl(PR_SET_NAME, "logd.klogd");
for (;;) {
read(cli->getSocket(), buffer + len, sizeof(buffer) - 1 - len);
}
}
对于 LogKlog 是如何处理kmsg的log即它重写的 onDataAvailable 实现细节放到后面章节讲解,本章只关注 logd 的初始化。
7. logd 监听听注册器 – logdr
logdr 这个 socket 节点用于客户端获取 logd 中所管理的 log,例如典型的 logcat 工具就是通过和该 socket 节点进行通信从而获取 log的。
7.1 LogReader 的初始化
logd 中则是使用类 LogReader 来实现上面提到的部分功能的。实际上 LogReader 并不参与数据的传输,它只负责处理客户端连接 logdr 的请求。下面先看看它的类图
从类图可知它的结构很简单,就一个父类 SocketListener,下面就看看它的构造
main
LogReader* reader = new LogReader(log_buffer, &reader_list);
//code 1
SocketListener(getLogSocket(), true)
//code 2
log_buffer_(logbuf)
//code 3
reader_list_(reader_list)
code 1,又见老朋友 SocketListener 了,不过这里的 mListen LogKlog 中的不一样,前者是支持作为服务端功能的,即可以处理客户端发过来的连接请求。这里的客户端也就是对 logd 所管理的 log 感兴趣的,例如前面提到过的 logcat。对于 SocketListener 的初始化,这里就不再赘述了,请参考相关章节的解析。
7.2 LogReader 的启动
从 LogReader 的类图可知它的父类为 SocketListener ,所以启动监听则是通过它的 startListener 接口实现,对于客户端的连接请求处理在 SocketListener 章节中也有详细的说明。对于 mListen 为true的情况,SocketListener 的子类只要在 onDataAvailable 中处理已经连接上的客户端的数据即可。
bool LogReader::onDataAvailable(SocketClient* cli) {
//code 1
prctl(PR_SET_NAME, "logd.reader");
//code 2
int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1);
...
//code 3
auto entry = std::make_unique<LogReaderThread>(log_buffer_, reader_list_,
std::move(socket_log_writer), nonBlock, tail,
logMask, pid, start, sequence, deadline);
//code 4
reader_list_->AddPendingThread(std::move(entry));
}
code 1,之前提到过 onDataAvailable 是跑在新建立的线程的,所以此处设置下线程名,方便区分。
code 2,既然是客户端来了数据,那么总得先把数据读出来。
code 3,在 LogReader 使用类 LogReaderThread 来管理每一个读者,实际上其内部也会创建一个线程进行log数据的传输。下面先给出它的类图供欣赏。
code 4,此处将被 LogReaderThread 封装的新的 log reader 加到 reader list中去,以便统一管理。
至于细节,会放到对应章节中去,此处就不赘述了。
8. log 接收器 – logdw
logd 进程相当于一个 log 收集器,因此需要被显示的 log 最终也会被发送到 logd 进程来。logdw socket节点就是用来接受来自系统各个进程的log的。在 logd 中使用 LogListener 来实现这块功能。它就简单很多了,因为只要处理 socket 本身的数据即可。
8.1 logdw 的初始化 – LogListener
下面来看看它的初始化
LogListener::LogListener(LogBuffer* buf)
socket_(GetLogSocket())
logbuf_(buf)
打开并记录下 logdw socket,然后记录下buf共后面来 log 数据后的存储及处理,这个 buffer 实际上就是 SerializedLogBuffer 的一个实例。
8.2 logdw 的监听启动
虽然没有使用 startListener ,但是启动的接口名却保持了一致
//system\logging\logd\main.cpp
main
LogListener* swl = new LogListener(log_buffer);
swl->StartListener()
auto thread = std::thread(&LogListener::ThreadFunction, this);
thread.detach();
return true;
}
可见此处又会启动一个新的线程来处理,它的处理方法为 LogListener::ThreadFunction , 入参为 LogListener 的当前实例。最终 ThreadFunction 会被调用
//system\logging\logd\LogListener.cpp
ThreadFunction
prctl(PR_SET_NAME, "logd.writer");
while (true) {
HandleData();
}
9. logd 的控制器 – logd
logd 节点用于接收对 logd 本身的控制请求,例如获取/设置 logd 中维护 buffer 的大小。在 logd 中使用类 CommandListener 来实现该功能,下面是对应的类图。
9.1 CommandListener 类图
它的顶层基类还是老朋友 SocketListener,邻近父类为 FrameworkListener,所以在看初始化的时候不能拉下这两个基类对应的构造方法了。
9.2 CommandListener 的初始化
//system\logging\logd\main.cpp
main
CommandListener* cl = new CommandListener(log_buffer/*buf*/, &log_tags/*tags*/, &prune_list/*prune*/, &log_statistics/*stats*/);
//code 1
FrameworkListener(getLogSocket()/*int sock*/)//system\logging\logd\CommandListener.cpp
//code 1-1
SocketListener(sock, true)
//code 1-2
init(nullptr, false);
//code 2
registerCmd(new ClearCmd(this));
...
registerCmd(new ExitCmd(this));
code 1 部分为 CommandListener 父类的构造, code 1-1 用于监听 logd 这个socket 句柄,并且它是支持作为server的,即会处理客户端的连接请求的,这也意味这 FrameworkListener 要处理的内容是客户端连接上后通过新建立的 socket 句柄发送而来的。code 1-2 仅仅是变量初始化,没什么好说的。
code 2 以及后面的都是一样的含义,用于注册命令,最终 CommandListener 会根据客户端要求执行的命令来调用对应的命令类。下面就看看 code 2 中的例子。
9.2.1 ClearCmd 类的由来
//system\logging\logd\CommandListener.h
#define LogCmd(name, command_string) \
class name##Cmd : public FrameworkCommand { \
public: \
explicit name##Cmd(CommandListener* parent) \
: FrameworkCommand(#command_string), parent_(parent) {} \
virtual ~name##Cmd() {} \
int runCommand(SocketClient* c, int argc, char** argv); \
\
private: \
LogBuffer* buf() const { return parent_->buf_; } \
LogTags* tags() const { return parent_->tags_; } \
PruneList* prune() const { return parent_->prune_; } \
LogStatistics* stats() const { return parent_->stats_; } \
CommandListener* parent_; \
}
LogCmd(Clear, clear);
还是相当的简单的,通过宏 LogCmd 就可以创建出一个类,下面是预编译后加了换行符的类定义
LogCmd(Clear/*name*/, clear/*command_string*/);
#define LogCmd(name, command_string)
class ClearCmd : public FrameworkCommand {
public:
explicit ClearCmd(CommandListener* parent)
: FrameworkCommand(clear), parent_(parent) {}
virtual ~ClearCmd() {}
int runCommand(SocketClient* c, int argc, char** argv);
private:
LogBuffer* buf() const { return parent_->buf_; }
LogTags* tags() const { return parent_->tags_; }
PruneList* prune() const { return parent_->prune_; }
LogStatistics* stats() const { return parent_->stats_; }
CommandListener* parent_;
};
ClearCmd 类的实现,其实只要实现 runCommand 就可以了
//system\logging\logd\CommandListener.cpp
int CommandListener::ClearCmd::runCommand(SocketClient* cli, int argc, char** argv) {
...
return LogIdCommand(cli, argc, argv, [&](log_id_t id) {
cli->sendMsg(buf()->Clear(id, uid) ? "success" : "busy");
});
}
9.2.2 LogIdCommand
其中 LogIdCommand 是一个模板方法,只要传入的处理方法不一致,那就是发生了函数重载,所以是可以支持多命令复用的
//system\logging\logd\CommandListener.cpp
template <typename F>
static int LogIdCommand(SocketClient* cli, int argc, char** argv, F&& function) {
setname();
function(static_cast<log_id_t>(log_id));
return 0;
}
可见最终还是调用传入的函数来处理对应的命令。
9.3 CommandListener 的启动
CommandListener 也是调用 startListener 启动的,它继承自它的顶层基类 SocketListener
main
CommandListener* cl = new CommandListener(log_buffer, &log_tags, &prune_list, &log_statistics);
cl->startListener()
// Notify that others can now interact with logd
SetProperty("logd.ready", "true");
10.总结
至此整个 logd 至此也准备完成了,其中的各种 unix domain socket 节点都可以使用了,例如客户端写 log 内容到 logdw,客户端同 logdr 注册log监听。