平常我都是使用adb shell logcat 抓日志,开发的时候直接使用Android Studio查看Android的日志信息,没有仔细思考过Android的日志子系统究竟是怎么工作的。最近有个小功能需要在应用进程端抓取实时的日志信息,于是写了如下的抓取日志的demo代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class MainActivity : Activity(), CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
private var logcatJob: Job? = null private var logJob: Job? = null fun start(view: android.view.View) { logcatJob?.cancel() logJob?.cancel() val tv = findViewById<TextView>(R.id.tv) tv.text = null logcatJob = launch (CoroutineName("log read")){ flow { while (currentCoroutineContext().isActive) { Runtime.getRuntime().exec("logcat -c") Runtime.getRuntime().exec("logcat").inputStream?.use { val bytes = ByteArray(1024) while (currentCoroutineContext().isActive) { val len = it.read(bytes) if (len != -1) { emit(String(bytes, 0, len)) } else { break } } } } }.flowOn(Dispatchers.IO).collect { tv.append(it) } }
logJob = launch(Dispatchers.IO + CoroutineName("log write")) { var i = 0 while (true) { delay(2000) Log.d("Cxx", "${i++}") } } }
}
|
这段的确能抓到日志,但是我发现了一个以前一直没注意到的现象,这段代码在普通的应用进程只能抓到自己进程的日志,不能抓到其他进程的日志,而把这段代码放到标记system uid属性的进程,就能抓到全局的日志信息了。顿时感到好奇Android究竟是采用的什么样的机制来向不同的进程返回不同的日志信息的?
创建子进程的方式
抓取日志时用到了 Runtime.getRuntime().exec() 方法,它会直接启动一个子进程去执行输入的命令,根据我现在的知识,Android应用进程有三种方式可以启动子线程:
使用 Runtime.getRuntime().exec() 执行命令,它启动了一个新的进程去执行命令,exec创建的子进程的父进程pid是创建者的进程的pid,但不同于fork,exec创建的子进程不会拷贝父进程的内存空间,也没有Java运行时环境。
使用 fork 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| extern "C" JNIEXPORT void JNICALL Java_com_test_fork_App_fork( JNIEnv *env, jobject ) { pid_t pid = fork(); LOGD("native pid: %d", pid); if (!pid) { LOGD("child process"); int x = 0; while (1) { LOGD("child process print:%d", x++); } } LOGD("parent pid: %d", pid); }
|
创建出的子进程会拷贝父进程的内存空间,自然它会有Java运行时环境,但该进程没有走过Android进程的启动逻辑,导致没有注册过Binder IPC,会在使用到Binder通信的地方报错。
组件声明 android:process属性 ,这是Android中创建子进程主要方法,创建的子进程实际上是一个新的Android进程,所以完全具备Java、Android的运行环境,它的uid和应用主进程一样,但父进程却是Zygote进程。
logcat的基本原理
应用进程启动logcat子进程后,会找到logcat命令对应的入口方法执行:
1 2 3 4 5 6 7 8 9 10 11
| system/logging/logcat/logcat.cpp
-> main() ->logcat.Run() while true -> android_logger_list_read() -> LogdRead() -> logdOpen() -> recv(&log_msg) end while
|
logcat是通过socket通信的方式获取日志信息的,logdOpen方法会打开监听 dev/socket/logdr 端口,然后开始接收socket返回的数据,收到日志数据后再对它进行格式处理,最后写入到文件流中。
logd进程的主要逻辑
logcat是socket的客户端,那一定存在一个与之通信的服务端,写日志的服务端是在logd进程中创建的,logd进程是Android中专门处理日志的进程,由init进程直接启动:
1 2 3 4 5 6
| system/logging/logd/logd.rc service logd /system/bin/logd socket logd stream 0666 logd logd socket logdr seqpacket 0666 logd logd socket logdw dgram+passcred 0222 logd logd ......
|
init进程在解析logd.rc文件时会启动logd进程,并创建三个socket:
- dev/socket/logd 用于接收客户端发送的指令
- dev/socket/logdw 用于客户端写入日志信息
- dev/socket/logdr用于返回给客户端日志信息。
主要关注与dev/socket/logdr相关的代码:
1 2 3 4 5 6 7 8 9 10 11 12
| system/logging/logd/main.cpp
-> main() -> new LogReader(log_buffer, &reader_list); -> reader->startListener() -> pthread_create(&mThread, nullptr, SocketListener::threadStart) -> SocketListener::threadStart -> runListener() while true -> accept4() -> onDataAvailable(c)
|
logd进程启动LogReader线程,用于监听dev/socket/logdr,一旦有客户端请求到来,调用onDataAvailable方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| bool LogReader::onDataAvailable(SocketClient* cli) { static bool name_set; if (!name_set) { prctl(PR_SET_NAME, "logd.reader"); name_set = true; }
char buffer[255]; int len = read(cli->getSocket(), buffer, sizeof(buffer) - 1); if (len <= 0) { DoSocketDelete(cli); return false; } buffer[len] = '\0';
...... unsigned int logMask = -1; ......
pid_t pid = 0; static const char _pid[] = " pid="; cp = strstr(buffer, _pid); if (cp) { pid = atol(cp + sizeof(_pid) - 1); }
bool nonBlock = false; if (!fastcmp<strncmp>(buffer, "dumpAndClose", 12)) { 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); }
std::unique_ptr<LogWriter> socket_log_writer(new SocketLogWriter(this, cli, privileged)); ...... auto flush_to_state = log_buffer_->CreateFlushToState(sequence, logMask); log_buffer_->FlushTo(socket_log_writer.get(), *flush_to_state, 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); cli->incRef(); reader_list_->reader_threads().emplace_front(std::move(entry));
struct timeval t = { LOGD_SNDTIMEO, 0 }; setsockopt(cli->getSocket(), SOL_SOCKET, SO_SNDTIMEO, (const char*)&t, sizeof(t));
return true; }
|
该方法主要是监听socket的请求,一旦有请求,读取发送的数据,并解析出对应的属性,例如对方的pid,logMusk,权限等,之后log_buffer_->FlushTo 向socket对端写入此时 log_buffer 搜集的日志,除此之外新建了一个 LogReaderThread 线程用于监听 log_buffer ,一旦有新日志到来调用 reader_thread->TriggerReader 通知该线程去读日志,最后也通过 log_buffer_->FlushTo 写入socket对端。
log_buffer_->FlushTo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| system/logging/logd/SerializedLogBuffer.cpp
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(); 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; } ...... if (!reinterpret_cast<SerializedLogEntry*>(entry_copy)->Flush(writer, log_id)) { logd_lock.lock(); return false; }
logd_lock.lock(); } return true; }
|
最后终于在这里找到logd进程向不同进程发送不同日志的判断依据:向客户端socket写入日志的时候,会判断下socket对端的uid是否是特权uid:
1 2 3
| static bool UserIsPrivileged(int id) { return id == AID_ROOT || id == AID_SYSTEM || id == AID_LOG; }
|
以及判断是不是本日志信息对应的uid,如果两者都不是,那就会跳过这段日志。
注意,这里是判断的进程的uid,而不是pid,刚才讲过应用通过组件声明 android:process属性时,该进程的uid与应用主进程的uid一致,那么我们在Android子进程中是可以读取到父进程的日志信息的,反之同理。