0%

应用进程抓取日志

平常我都是使用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应用进程有三种方式可以启动子线程:

  1. 使用 Runtime.getRuntime().exec() 执行命令,它启动了一个新的进程去执行命令,exec创建的子进程的父进程pid是创建者的进程的pid,但不同于forkexec创建的子进程不会拷贝父进程的内存空间,也没有Java运行时环境。

  2. 使用 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 /* this */) {
    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通信的地方报错。

  3. 组件声明 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进程的主要逻辑

logcatsocket的客户端,那一定存在一个与之通信的服务端,写日志的服务端是在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

  1. dev/socket/logd 用于接收客户端发送的指令
  2. dev/socket/logdw 用于客户端写入日志信息
  3. 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)// c: SocketClient

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
// Note returning false will release the SocketClient instance.
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';

......
//客户端socket标志位
unsigned int logMask = -1;
......

//读取客户端进程的pid
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)) {
// 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);
}

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);
......

//创建监听log_buffer的线程
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);
// release client and entry reference counts once done
cli->incRef();
reader_list_->reader_threads().emplace_front(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;
}

该方法主要是监听socket的请求,一旦有请求,读取发送的数据,并解析出对应的属性,例如对方的pidlogMusk,权限等,之后log_buffer_->FlushTosocket对端写入此时 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());

//是否是特权uid,是否是本日志对应的uid
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;//root,system,log
}

以及判断是不是本日志信息对应的uid,如果两者都不是,那就会跳过这段日志。
注意,这里是判断的进程的uid,而不是pid,刚才讲过应用通过组件声明 android:process属性时,该进程的uid与应用主进程的uid一致,那么我们在Android子进程中是可以读取到父进程的日志信息的,反之同理。