本文分析基于 Android S(12)
在 Android 的世界里,进程和线程的名称也多种多样,有的地方用的是'名',用的地方用的是'字',并不统一。所以本文的目的就是深究本质,让观众知道,拿掉这些代号后的主体到底是谁。先来浅问几个问题:
本文深入分析了 Android 系统中进程名与线程名的底层实现机制。从 Linux 内核的 task_struct 和 command line 出发,探讨了 ps、top 等工具显示名称的来源。重点阐述了 Zygote 启动、ART 虚拟机及 Java 层线程命名对 task_struct.comm 和 tlsPtr_.name 的影响。最后对比了 Trace 文件与 Tombstone 文件中进程线程名的差异及其截断规则,揭示了不同视角下名称不一致的根本原因。

本文分析基于 Android S(12)
在 Android 的世界里,进程和线程的名称也多种多样,有的地方用的是'名',用的地方用的是'字',并不统一。所以本文的目的就是深究本质,让观众知道,拿掉这些代号后的主体到底是谁。先来浅问几个问题:
研究事物时,我们要用历史变迁的角度来思考。因此,想要理解 Android,必先理解 Linux。
Linux kernel 中有一个重要的概念:task_struct。我将它理解为调度实体,也即参与调度的基本单元。从执行角度来看,一个线程就是一个调度实体。从内存角度来看,多个线程组成进程的概念,它们之间共享用户空间的内存(当然,线程间共享的不仅仅是内存)。
当我们需要启动一个新程序时,首先会通过 fork 或 clone 得到一个新的运行实体。之后在新的运行实体中通过 exec 来启动程序。Exec 有很多变种,咱们以常见的 ls 为例。
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
该函数的第一个参数是可执行文件的路径名,后面的参数则共同构成 command line。内核在处理这些 command line 参数时,会将它们顺次连接地存放在栈底,每个参数中间通过'\0'进行分隔。这些参数在程序启动后会传递给 main 方法,也即我们经常看到的 argv[]。按照约定俗成的规定(非强制),第一个参数(argv[0] 指向的字符串) 是文件名,当然如果你喜欢,也可以传入任何其他字符串。
比如下面的输入,最终产生的 command line 就是"banana\0-l\0",\0 对应的 ASCII 码为 0x00。
execl("/bin/ls", "banana", "-l", NULL);
由于 command line 中各个字符串通过'\0'进行分隔,因此如果简单地通过 printf 进行输出,我们将只会看到 argv[0] 指向的字符串。如果想要完整地获取 command line 中的所有内容,通常需要一些特殊处理。如下是 Android 提供的两种获取方式,get_command_line 将获取其中所有的字符串,而 get_process_name 只会获取 argv[0] 指向的字符串,对纯 native 进程而言,它通常是可执行文件的名称,也可以理解为进程名。不过对于 Android 应用而言,它却有别的含义。此处按下不表,后文再述。
[/system/core/debuggerd/util.cpp]
std::vector<std::string> get_command_line(pid_t pid) {
std::vector<std::string> result;
std::string cmdline;
android::base::ReadFileToString(android::base::StringPrintf("/proc/%d/cmdline", pid), &cmdline);
auto it = cmdline.cbegin();
while (it != cmdline.cend()) {
// string::iterator is a wrapped type, not a raw char*.
auto terminator = std::find(it, cmdline.cend(), '\0');
result.emplace_back(it, terminator);
it = std::find_if(terminator, cmdline.cend(), [](char c) { return c != '\0'; });
}
if (result.empty()) {
result.emplace_back("<unknown>");
}
return result;
}
std::string get_process_name(pid_t pid) {
std::string result = "<unknown>";
android::base::ReadFileToString(android::base::StringPrintf("/proc/%d/cmdline", pid), &result);
// We only want the name, not the whole command line, so truncate at the first NUL.
return result.c_str();
}
上文提到,command line 会顺次连接地存放在栈底,也即用户空间。当我们在 adb shell 中执行 cat /proc/[pid]/cmdline 指令时,本质上是访问一个特殊的文件节点 (该文件节点只可读)。这个访问动作最后会触发内核空间的一个函数,如下。
[/kernel/common/fs/proc/base.c]
REG("cmdline", S_IRUGO, proc_pid_cmdline_ops),
[/kernel//common/fs/proc/base.c]
static const struct file_operations proc_pid_cmdline_ops = {
.read = proc_pid_cmdline_read,
.llseek = generic_file_llseek,
};
proc_pid_cmdline_read 函数会通过 access_remote_vm 来访问 [pid] 进程的地址空间,进而获得它存放在用户空间的 command line 数据,并将其拷贝到输出的 buffer 中。因此,command line 数据并不存在于内核地址空间。
每一个调度实体都有自己的名字,也即 task_struct 中的"comm"字段。Comm 本意为 command name,并非上述的 command line,这里要注意区分。
[/kernel/common/include/linux/sched.h]
/* Task command name length: */
#define TASK_COMM_LEN 16
...
struct task_struct {
...
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
...
}
comm 字符串存储的到底是什么?只有源码最清楚。当我们调用 exec 执行可执行文件时,它在 kernel 层会调用 load_elf_binary,其中便会设置 task_struct.comm 字段。
[/kernel/common/fs/exec.c]
__set_task_comm(me, kbasename(bprm->filename), true);
[/kernel/common/include/linux/string.h]
/**
* kbasename - return the last part of a pathname.
*
* @path: path to extract the filename from.
*/
static inline const char* kbasename(const char* path) {
const char* tail = strrchr(path, '/');
return tail ? tail + 1 : path;
}
Exec 传入的第一个参数虽说是文件名,但它是带有路径的文件名,譬如 /system/bin/surfaceflinger,而存入 comm 字段的名称则是剥离路径的文件名,也即 surfaceflinger。另外需要注意的是,comm 长度为 16,任何过长的文件名都会被截断。因此,comm 最初始的含义为可执行文件的名称,不过随着系统的发展,它的含义早已超出当初的设定。
Ps 最初是 Linux shell 中的一个指令,用于展示进程相关的一些信息。不过 Android 采用的是 toybox 里的实现,和原生 ps 在使用方法上有些许差异。源码位于 /external/toybox/toys/posix/ps.c。
Toybox combines the most common Linux command line utilities together into a single BSD-licensed executable that's simple, small, fast, reasonably standards-compliant, and powerful enough to turn Android into a development environment. See the links on the left for details.
ps -A 和 ps -e 执行的是同一个动作,都是显示所有进程。
-A All
-e Synonym for -A
下面是 ps -A 的一个示例输出。
# ps -A
USER PID PPID VSZ RSS WCHAN ADDR S NAME
root 1 0 13001184 14608 do_epoll_+ 0 S init
root 2 0 0 0 kthreadd 0 S [kthreadd]
root 3 2 0 0 rescuer_t+ 0 I [rcu_gp]
...
logd 278 1 13036024 7516 __do_sys_+ 0 S logd
lmkd 279 1 13060480 7372 do_epoll_+ 0 S lmkd
system 1383 1 13504456 60264 do_epoll_+ 0 S surfaceflinger
...
u0_a150 5280 1105 16943368 103628 do_freeze+ 0 S com.android.mms
u0_a190 5334 1105 16966004 134128 do_freeze+ 0 S com.android.permissioncontroller
u0_a37 5352 1105 16778080 100784 do_freeze+ 0 S com.android.providers.calendar
注意最后一列:NAME,它的含义如下所示:Process name。可是看完上面的输出,会发现有几个奇怪的点。
下面我们翻译翻译,什么 TM 的是 TM 的进程名。
[/external/toybox/toys/posix/ps.c]
// String fields (-1 is procpid->str, rest are str+offset[1-slot])
{"TTY", "Controlling terminal", -8, -2},
{"WCHAN", "Wait location in kernel", -6, -3},
{"LABEL", "Security label", -30, -4},
{"COMM", "EXE filename (/proc/PID/exe)", -27, -5},
{"NAME", "Process name (PID's argv[0])", -27, -7},
{"COMMAND", "EXE path (/proc/PID/exe)", -27, -5},
{"CMDLINE", "Command line (argv[])", -27, -6},
{"ARGS", "CMDLINE minus initial path", -27, -6},
{"CMD", "Thread name (/proc/TID/stat:2)", -15, -1},
Process name 按照上述的注释,可以理解为 argv[0] 指向的字符串。这个数据从 /proc/[pid]/cmdline 文件节点读出,但需要经过一些特殊的处理。
[/external/toybox/toys/posix/ps.c]
struct {
char *name; // Path under /proc/$PID directory
long long bits; // Only fetch extra data if an -o field is displaying it
} fetch[] = {
// sources for procpid->offset[] data
{"fd/", _PS_TTY}, {"wchan", _PS_WCHAN}, {"attr/current", _PS_LABEL},
{"exe", _PS_COMMAND|_PS_COMM}, {"cmdline", _PS_CMDLINE|_PS_ARGS|_PS_NAME},
{"", _PS_NAME}
};
从 cmdline 文件节点读出来的原始信息包含所有参数,它们中间由'\0'进行分隔。Ps 进程拿到这些数据后将会进行如下处理:
(假设我们拿到的原始信息是:"/system/bin/top\0-d\0-4\0")
CMDLINE 列。(处理后变为:"/system/bin/top -d 4")NAME 列。(处理后变为:"top")ARGS 列。(处理后变为:"top -d 4")因此我们可以知道,cmdline 文件节点读出的信息最终被用在了三个地方,有点一鱼三吃的感觉。
最终显示在 ps -A 中的进程名,是 argv[0] 字符串的 basename,通常是可执行文件的名称。而 Android 应用之所以显示为包名,是因为进程启动过程中改写了 argv[0] 的值,这个放到后面再说。
那为什么有些进程名用方括号括起来了呢?
答案是这些进程没有 cmdline(内核进程和一些特殊的用户进程)。当 cmdline 文件节点读不到任何信息时,ps 会将该进程的 task_struct.comm 值取出,并用方括号括住来替代显示。对这些进程而言,CMDLINE、NAME 和 ARGS 列显示的都是同一个字符串。所以看到这样的进程名时,我们大概率可以推测这是一个内核进程。
Top 显示的进程名为 ARGS(具体含义见 #2.1)。严格来说,它不能叫做'进程名',而应该叫'参数列表'。这里我们以 top 进程示例(第二行),可以看到 ARGS 为"top -d 4",它包含了后续的参数信息。
# ps -T -p 5280
USER PID TID PPID VSZ RSS WCHAN ADDR S CMD
u0_a150 5280 5280 1105 16943368 103628 do_freeze+ 0 S com.android.mms
u0_a150 5280 5281 1105 16943368 103628 do_freeze+ 0 S Runtime worker
u0_a150 5280 5282 1105 16943368 103628 do_freeze+ 0 S Runtime worker
u0_a150 5280 5283 1105 16943368 103628 do_freeze+ 0 S Runtime worker
u0_a150 5280 5284 1105 16943368 103628 do_freeze+ 0 S Runtime worker
u0_a150 5280 5285 1105 16943368 103628 do_freeze+ 0 S Signal Catcher
u0_a150 5280 5286 1105 16943368 103628 do_freeze+ 0 S perfetto_hprof_
u0_a150 5280 5287 1105 16943368 103628 do_freeze+ 0 S ADB-JDWP Connec
u0_a150 5280 5288 1105 16943368 103628 do_freeze+ 0 S Jit thread pool
u0_a150 5280 5289 1105 16943368 103628 do_freeze+ 0 S HeapTaskDaemon
u0_a150 5280 5290 1105 16943368 103628 do_freeze+ 0 S ReferenceQueueD
u0_a150 5280 5291 1105 16943368 103628 do_freeze+ 0 S FinalizerDaemon
u0_a150 5280 5292 1105 16943368 103628 do_freeze+ 0 S FinalizerWatchd
u0_a150 5280 5293 1105 16943368 103628 do_freeze+ 0 S Binder:5280_1
u0_a150 5280 5294 1105 16943368 103628 do_freeze+ 0 S Binder:5280_2
u0_a150 5280 5295 1105 16943368 103628 do_freeze+ 0 S Binder:5280_3
u0_a150 5280 5303 1105 16943368 103628 do_freeze+ 0 S k worker thread
u0_a150 5280 5307 1105 16943368 103628 do_freeze+ 0 S Binder:5280_4
u0_a150 5280 5310 1105 16943368 103628 do_freeze+ 0 S queued-work-loo
u0_a150 5280 5312 1105 16943368 103628 do_freeze+ 0 S ent.InfoHandler
u0_a150 5280 5313 1105 16943368 103628 do_freeze+ 0 S nt.EventHandler
u0_a150 5280 6312 1105 16943368 103628 do_freeze+ 0 S android.bg
Ps -T -p 将会显示特定进程下的所有线程。这里的显示名为 CMD,该信息通过访问 /proc/TID/stat:2 节点信息获取,它本质上就是 task_struct.comm 字段,有 16 位长度的限制。
(2) comm %s
The filename of the executable, in parentheses.
Strings longer than TASK_COMM_LEN (16) characters
(including the terminating null byte) are silently
truncated. This is visible whether or not the
executable is swapped out.
如果仔细观察上述的 CMD 信息,你会发现一个奇怪的现象:有些截断是保留名称的后半段(譬如"nt.EventHandler"),而有些截断是保留名称的前半段(譬如"ReferenceQueueD")。这个具体的原因我们保留到 Android 应用那一节再阐述。
Native 层的线程创建一般采用 pthread,不论是 libcxx 里的 std::thread 还是 Java 层的 Thread,其底层都是 pthread。所以想要准确地理解应用中的线程名,pthread 这一关必须得过。
对于 pthread 而言,它的线程名就是 task_struct.comm 字段。
当我们通过 pthread_create() 创建线程时,有一点值得注意:该函数的内部并没有设置线程名,因此 clone 动作会将调用线程的 comm 字段复制给新的线程。也就是说,新线程默认的线程名和调用线程一致。这也是为什么我们会在 surfaceflinger 进程内看到多个同名线程的原因。
改变线程名称可以采用 pthread_setname_np 函数,其最终会修改位于内核空间的 task_struct.comm 字段。这里依然有个地方需要注意,即传入的名称长度不能超过 16,否则设置无效。
[/bionic/libc/bionic/pthread_setname_np.cpp]
int pthread_setname_np(pthread_t t, const char* thread_name) {
ErrnoRestorer errno_restorer;
size_t thread_name_len = strlen(thread_name);
if (thread_name_len >= MAX_TASK_COMM_LEN) return ERANGE;
以下讨论均为 64 位 zygote 进程
Init 进程根据 init.zygote64.rc 文件来启动 64 位的 zygote 进程,其本质也是 fork 完之后执行 exec 调用,传入的参数如下,总长度为 78(包含结尾的'\0')。【现有机器上很多根据 init.zygote64_32.rc 文件来启动 64 位 zygote 进程,这是参数总长度为 99,包含结尾的'\0'。】
/system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
[/frameworks/base/cmds/app_process/app_main.cpp]
#if defined(__LP64__)
static const char ABI_LIST_PROPERTY[] = "ro.product.cpu.abilist64";
static const char ZYGOTE_NICE_NAME[] = "zygote64";
#else
...
int main(int argc, char* const argv[]) {
...
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
...
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
...
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string(), true /* setProcName */);
}
Exec 执行之后,zygote 的进程名和主线程名均会被设置为 app_process64,也即可执行文件的名称。不过 main 函数内部会对它们进行修改,通过 setArgv0 函数。
[/frameworks/base/core/jni/AndroidRuntime.cpp]
void AndroidRuntime::setArgv0(const char* argv0, bool setProcName) {
// Set the kernel's task name, for as much of the name as we can fit.
// The kernel's TASK_COMM_LEN minus one for the terminating NUL == 15.
if (setProcName) {
int len = strlen(argv0);
if (len < 15) {
pthread_setname_np(pthread_self(), argv0);
} else {
pthread_setname_np(pthread_self(), argv0 + len - 15);
}
}
// Directly change the memory pointed to by argv[0].
memset(mArgBlockStart, 0, mArgBlockLength);
strlcpy(mArgBlockStart, argv0, mArgBlockLength);
// Let bionic know that we just did that, because __progname points
// into argv[0] (https://issuetracker.google.com/152893281).
setprogname(mArgBlockStart);
}
SetArgv0 函数会做三件事:
task_struct.comm 字段。/proc/[zygote's pid]/cmdline 文件节点,获取到的只有"zygote64"。这样不论我们使用 cmdline 完整的字符串,还是 argv[0] 指向的字符串,抑或是 argv[0] 剥离路径后的 basename,都将得到"zygote64"。因此,我们有理由说,此时的进程名已经被改为了"zygote64"。__progname 指向 command line 的开头,该字段主要在 libc 中使用。执行完 setArgv0 后,zygote 进程的进程名和主线程名都更改为了"zygote64"。可是,事情到这里就结束了么?并不会!
随后 zygote 还会启动虚拟机,在虚拟机启动的尾声执行如下函数。
[/art/runtime/thread.cc]
void Thread::FinishStartup() {
Runtime* runtime = Runtime::Current();
CHECK(runtime->IsStarted());
// Finish attaching the main thread.
ScopedObjectAccess soa(Thread::Current());
soa.Self()->CreatePeer("main", false, runtime->GetMainThreadGroup());
CreatePeer 内部会调用 SetThreadName 再次修改线程的名称。
[/art/runtime/thread.cc]
void Thread::SetThreadName(const char* name) {
tlsPtr_.name->assign(name);
::art::SetThreadName(name);
Dbg::DdmSendThreadNotification(this, CHUNK_TYPE("THNM"));
}
这里线程名将拥有两层含义,因为启动虚拟机之后的主线程将不仅仅是一个 pthread 线程,还是一个 ART 线程。
task_struct.comm 字段,也即 pthread 的线程名,该名称存放于内核地址空间。tlsPtr_.name。该名称存放于用户地址空间。回到 SetThreadName 函数,它会分别修改两层含义的线程名。首先将 tlsPtr_.name 字段改为"main",接着通过 ::art::SetThreadName 将 task_struct.comm 字段更改为"main"。
[/art/libartbase/base/utils.cc]
void SetThreadName(const char* thread_name) {
bool hasAt = false;
bool hasDot = false;
const char* s = thread_name;
while (*s) {
if (*s == '.') {
hasDot = true;
} else if (*s == '@') {
hasAt = true;
}
s++;
}
int len = s - thread_name;
if (len < 15 || hasAt || !hasDot) {
s = thread_name;
} else {
s = thread_name + len - 15;
}
#if defined(__linux__) || defined(_WIN32)
// pthread_setname_np fails rather than truncating long strings.
char buf[16]; // MAX_TASK_COMM_LEN=16 is hard-coded in the kernel.
strncpy(buf, s, sizeof(buf)-1);
buf[sizeof(buf)-1] = '\0';
errno = pthread_setname_np(pthread_self(), buf);
if (errno != 0) {
PLOG(WARNING) << "Unable to set the name of current thread to '" << buf << "'";
}
#else // __APPLE__
pthread_setname_np(thread_name);
#endif
}
::art::SetThreadName 对传入的名称有些特殊处理,处理规则如下。
了解规则并不重要,理解规则背后的思考才重要。这里说下我对于这个规则的理解:16 字符的长度限制是内核空间为了控制 task_struct 结构体大小不得不做的牺牲,对于长度超过 16 的名称,Google 设计的目标是保留其中最有信息量的部分。通过'.'符号来分割的名称,前半部分的信息含量一般较低。我们以包名举例,前半部分一般为"com"、"org"之类的名称,而最后才是每个包名最独特的地方,也是信息量最大的部分。而'@'符号后面跟的一般是版本信息,它对于我们了解线程的身份并不重要,因为系统中很少有多个版本同时存在。
回到之前 ps -T -p 显示过的线程名,5290 线程保留了前半部分,5312 线程保留了后半部分。配合刚刚介绍的规则,我想你可以更加深入地理解。
u0_a150 5280 5290 1105 16943368 103628 do_freeze+ 0 S ReferenceQueueD
u0_a150 5280 5291 1105 16943368 103628 do_freeze+ 0 S FinalizerDaemon
u0_a150 5280 5292 1105 16943368 103628 do_freeze+ 0 S FinalizerWatchd
u0_a150 5280 5310 1105 16943368 103628 do_freeze+ 0 S queued-work-loo
u0_a150 5280 5312 1105 16943368 103628 do_freeze+ 0 S ent.InfoHandler
继续回到 zygote 进程。当虚拟机启动完毕后,zygote 的主线程名更改为"main",不论是 pthread 的视角 (task_struct.comm),还是 ART 线程的视角 (tlsPtr_.name)。
Android 应用进程由 zygote fork 而出,而且这个 fork 动作发生在 zygote 的主线程。当 fork 完毕后,应用进程(目前只有一个线程)主线程的 task_struct.comm 和 zygote 主线程一致,且它的 tlsPtr_.name 也和 zygote 主线程一致,均为"main"。
应用进程主线程会接着调用 SpecializeCommon 函数,其中会再次修改线程名。nice_name 也即应用在 manifest 中声明的进程名,默认情况下它和包名是一致的,除非我们设置了"android:process"。
[/frameworks/base/core/jni/com_android_internal_os_Zygote.cpp]
// Make it easier to debug audit logs by setting the main thread's name to the
// nice name rather than "app_process".
if (nice_name.has_value()) {
SetThreadName(nice_name.value());
} else if (is_system_server) {
SetThreadName("system_server");
}
不过需要注意的是,这个 SetThreadName 只会修改 task_struct.comm,而不会修改 tlsPtr_.name。因此如果我们将这个线程看作 pthread,那么它的名称就是包名;可是如果我们将它看作 ART thread,那么它的名称就是"main"。
[/frameworks/base/core/jni/com_android_internal_os_Zygote.cpp]
void SetThreadName(const std::string& thread_name) {
bool hasAt = false;
bool hasDot = false;
for (const char str_el : thread_name) {
if (str_el == '.') {
hasDot = true;
} else if (str_el == '@') {
hasAt = true;
}
}
const char* name_start_ptr = thread_name.c_str();
if (thread_name.length() >= MAX_NAME_LENGTH && !hasAt && hasDot) {
name_start_ptr += thread_name.length() - MAX_NAME_LENGTH;
}
// pthread_setname_np fails rather than truncating long strings.
char buf[16]; // MAX_TASK_COMM_LEN=16 is hard-coded into bionic
strlcpy(buf, name_start_ptr, sizeof(buf) - 1);
errno = pthread_setname_np(pthread_self(), buf);
if (errno != 0) {
ALOGW("Unable to set the name of current thread to '%s': %s", buf, strerror(errno));
}
// Update base::logging default tag.
android::base::SetDefaultTag(buf);
}
待 SpecializeCommon 执行完毕后,主线程会调用 setArgv0 来修改进程名,将 command line 由"zygote64"改为应用包名。
至此,应用进程的 command line 和主线程的 task_struct.comm 均设置为包名,而主线程的 tlsPtr_.name 依旧为"main"。
我们在 Java 中创建的线程,它本质上是 ART 线程,而 Java 层的 Thread 对象更像是个傀儡,其核心的运作和数据都在 Native 层的 art::Thread 对象中。当我们在 Java 层 new 一个 Thread 对象时,与之对应的 art::Thread 并没有创建。只有当我们调用 Thread.start() 时,art::Thread 才会创建。
art::Thread 创建并启动成功后,新线程会将自己的名称改为创建 Thread 时传入的名称。如果我们在创建时并未指定名称,则系统会按照"Thread"+"序号"的方式自动命名,这一点和 pthread 不同。
[/libcore/ojluni/src/main/java/java/lang/Thread.java]
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
不过即便线程启动完毕,我们也可以在后续过程中通过 Thread.setName 来修改线程名。
与主线程不同,这些线程在修改名称时会同时修改 task_struct.comm 和 tlsPtr_.name。
对大多数开发者而言,他们接触到进程名和线程名的地方主要是 trace 文件和 tombstone 文件。
[Trace 文件示例]
----- pid 9000 at 2022-03-17 05:00:52.489353500+0000 -----
Cmd line: com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowo
DALVIK THREADS (16):
"Signal Catcher" daemon prio=10 tid=5 Runnable
...
"main" prio=5 tid=1 Native
...
"ReferenceQueueDaemon" daemon prio=5 tid=12 Waiting
"Cmd line"后的字符串是通过访问 /proc/self/cmdline 文件节点获取到的。只不过将原始字符串去除了尾部多余的'\0',且将分隔的'\0'替换为了空格。根据前文可知,待 SpecializeCommon 执行完毕后,应用主线程会调用 setArgv0 来修改进程名,将 command line 由"zygote64"改为应用包名。
[/art/runtime/signal_catcher.cc]
static void DumpCmdLine(std::ostream& os) {
#if defined(__linux__)
// Show the original command line, and the current command line too if it's changed.
// On Android, /proc/self/cmdline will have been rewritten to something like "system_server".
// Note: The string "Cmd line:" is chosen to match the format used by debuggerd.
std::string current_cmd_line;
if (android::base::ReadFileToString("/proc/self/cmdline", ¤t_cmd_line)) {
current_cmd_line.resize(current_cmd_line.find_last_not_of('\0') + 1); // trim trailing '\0's
std::replace(current_cmd_line.begin(), current_cmd_line.end(), '\0', ' ');
os << "Cmd line: " << current_cmd_line << "\n";
const char* stashed_cmd_line = GetCmdLine();
if (stashed_cmd_line != nullptr && current_cmd_line != stashed_cmd_line
&& strcmp(stashed_cmd_line, "<unset>") != 0) {
os << "Original command line: " << stashed_cmd_line << "\n";
}
}
#else
os << "Cmd line: " << GetCmdLine();
#endif
}
继续延申下,其实这里显示的"Cmd line"也是有长度限制的。它的最大长度为 init.zygote64.rc 启动 zygote 时传入的参数长度,现阶段为 78(包括结尾'\0')。不知道你们注意到上述的示例 trace 文件没有,我声明的包名是超过最大长度的,"Cmd line"只保留了前 77 个字符,加上结尾的'\0'正好 78。对比如下。(如果你的机器上用的是 init.zygote64_32.rc,那么将会保留 98 个字符。)
Package Name: com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworld
----- pid 5129 at 2022-03-18 03:23:41 -----
Cmd line: com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowo
接着是 trace 文件中显示的线程名。根据源码可知,这里显示的是 tlsPtr_.name,而并没有用 task_struct.comm。应用主线程的 tlsPtr_.name 为"main",task_struct.comm 为包名,因此这里主线程名为"main"。其他线程则不会存在这种分歧。
[/art/runtime/thread.cc]
if (thread != nullptr) {
os << '"' << *thread->tlsPtr_.name << '"';
[Tombstone 文件示例]
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Cmdline: com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowo
pid: 9000, tid: 9000, name: worldhelloworld >>> com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowo <<<
...
pid: 9000, tid: 9010, name: ReferenceQueueD >>> com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowo <<<
Tombstone 中的 Cmdline 和 Trace 一致,均是用空格替换用于分隔的'\0'。
[/system/core/debuggerd/util.cpp]
std::vector<std::string> get_command_line(pid_t pid) {
std::vector<std::string> result;
std::string cmdline;
android::base::ReadFileToString(android::base::StringPrintf("/proc/%d/cmdline", pid), &cmdline);
auto it = cmdline.cbegin();
while (it != cmdline.cend()) {
// string::iterator is a wrapped type, not a raw char*.
auto terminator = std::find(it, cmdline.cend(), '\0');
result.emplace_back(it, terminator);
it = std::find_if(terminator, cmdline.cend(), [](char c) { return c != '\0'; });
}
if (result.empty()) {
result.emplace_back("<unknown>");
}
return result;
}
[/system/core/debuggerd/libdebuggerd/tombstone_proto_to_text.cpp]
CB(should_log, "Cmdline: %s", android::base::Join(tombstone.command_line(), " ").c_str());
不过线程名的显示和 Trace 不同,这里采用的是 task_struct.comm,而非 tlsPtr_.name。其实这个很好理解,因为 tombstone 是针对所有用户进程的机制,它只能将线程看作 pthread,而无法将它看作 ART thread。作为 pthread,它的线程名只存在于 task_struct.comm。
这样一来,应用的主线程名将显示为截断的包名,之所以截断,是因为 task_struct.comm 有 16 位长度限制。而且由于包名含有'.'符号,采用前截断保留后半部分。另外,其他线程的名称也可能被截断,而这种情况在 trace 文件中不会发生。譬如同样是"ReferenceQueueDaemon"线程,trace 文件中的名称显示完整,而 tombstone 文件中的名称则被截断。
本文采用由下到上、层层递进的视角分析了进程名/线程名的不同理解。细节颇多,看起来容易混乱,因此这里做下总结。
task_struct.comm,有 16 位长度限制;ART thread 视角下的线程名为 tlsPtr_.name,没有长度限制。这篇文章看起来颇有些'茴香豆的茴有几种写法'的感觉,但我并不是闲着没事做憋出的这篇文章。前段时间开发一个特性,需要根据进程名区别设置,在写代码的时候我就在想:进程名到底是什么?底层看到的进程名和上层看到的进程名是否一致?想完这个问题后,我发现我并不懂。不懂就要去研究,因此才有了这篇文章。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online