本文剖析基于Android S(12)
今人起名颇为考究,不单有名,还有字。文人雅士有时还会给本人取个“别号”。所谓“名为正体,字以表德,号以寓怀”,三者独特展示一个人的品格和谋求。在Android的世界里,过程和线程的名称也多种多样,有的中央用的是“名”,用的中央用的是“字”,并不对立。所以本文的目标就是深究实质,让观众老爷们晓得,拿掉这些代号后的主体到底是谁。先来浅问几个问题:
- Android利用的主线程在trace文件中的名称是什么?在tombstone文件中的名称又是什么?
- Java层创立的新线程默认名称是什么?Native层创立的新线程默认名称又是什么?
- Trace文件最顶部的"Cmd line"本意为何?为什么它会和利用的包名统一?
- Ps -A(或ps -e)显示的后果里,CMD那一列显示的是什么?为何有的名称用"[]"括起来了?
钻研事物时,咱们要用历史变迁的角度来思考。因而,想要了解Android,必先了解Linux。
1. Linux 视角下的过程名和线程名
Linux kernel中有一个重要的概念:task\_struct。我将它了解为调度实体,也即参加调度的根本单元。从执行角度来看,一个线程就是一个调度实体。从内存角度来看,多个线程组成过程的概念,它们之间共享用户空间的内存(当然,线程间共享的不仅仅是内存)。
当咱们须要启动一个新程序时,首先会通过fork或clone失去一个新的运行实体。之后在新的运行实体中通过exec来启动程序。Exec有很多变种,咱们以常见的excel为例。
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
1.1 Command Line
该函数的第一个参数是可执行文件的路径名,前面的参数则独特形成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数据并不存在于内核地址空间。
1.2 Command Name
每一个调度实体都有本人的名字,也即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_strcut.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最初始的含意为可执行文件的名称,不过随着零碎的倒退,它的含意早已超出当初的设定。
2. Ps视角下的过程名和线程名
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.
2.1 Ps -A显示的过程名
ps -A
和ps -e
执行的是同一个动作,都是显示所有过程。
-A All-e Synonym for -A
上面是ps -A
的一个示例输入。
# ps -AUSER PID PPID VSZ RSS WCHAN ADDR S NAMEroot 1 0 13001184 14608 do_epoll_+ 0 S initroot 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 logdlmkd 279 1 13060480 7372 do_epoll_+ 0 S lmkdsystem 1383 1 13504456 60264 do_epoll_+ 0 S surfaceflinger...u0_a150 5280 1105 16943368 103628 do_freeze+ 0 S com.android.mmsu0_a190 5334 1105 16966004 134128 do_freeze+ 0 S com.android.permissioncontrolleru0_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
")
- 将所有的'\0'用空格代替,解决后的字符串能够显示在
CMDLINE
列。(解决后变为:"/system/bin/top -d 4") - 将之前代替的第一个'\0'前的字符串作为argv[0],并将argv[0]字符串最初一个'/'前的信息去除,只保留根本的文件名(英文又称basename),解决后的字符串能够显示在
NAME
列。(解决后变为:"top") - 将argv[0]中的门路信息去除,然而保留后续的参数信息,解决后的字符串能够显示在
ARGS
列。(解决后变为:"top -d 4")
因而咱们能够晓得,cmdline文件节点读出的信息最终被用在了三个中央,有点一鱼三吃的感觉。
最终显示在ps -A
中的过程名,是argv[0]字符串的basename,通常是可执行文件的名称。而Android利用之所以显示为包名,是因为过程启动过程中改写了argv[0]的值,这个放到前面再说。
那为什么有些过程名用方括号括起来了呢?
答案是这些过程没有cmdline(内核过程和一些非凡的用户过程)。当cmdline文件节点读不到任何信息时,ps会将该过程的task_struct.comm
值取出,并用方括号括住来代替显示。对这些过程而言,CMDLINE
、NAME
和ARGS
列显示的都是同一个字符串。所以看到这样的过程名时,咱们大概率能够揣测这是一个内核过程。
2.2 Top显示的过程名
Top显示的过程名为ARGS
(具体含意见#2.1)。严格来说,它不能叫做”过程名“,而应该叫”参数列表“。这里咱们以top过程示例(第二行),能够看到ARGS
为"top -d 4",它蕴含了后续的参数信息。
2.3 Ps -T -p显示的线程名
# ps -T -p 5280USER PID TID PPID VSZ RSS WCHAN ADDR S CMDu0_a150 5280 5280 1105 16943368 103628 do_freeze+ 0 S com.android.mmsu0_a150 5280 5281 1105 16943368 103628 do_freeze+ 0 S Runtime workeru0_a150 5280 5282 1105 16943368 103628 do_freeze+ 0 S Runtime workeru0_a150 5280 5283 1105 16943368 103628 do_freeze+ 0 S Runtime workeru0_a150 5280 5284 1105 16943368 103628 do_freeze+ 0 S Runtime workeru0_a150 5280 5285 1105 16943368 103628 do_freeze+ 0 S Signal Catcheru0_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 Connecu0_a150 5280 5288 1105 16943368 103628 do_freeze+ 0 S Jit thread poolu0_a150 5280 5289 1105 16943368 103628 do_freeze+ 0 S HeapTaskDaemonu0_a150 5280 5290 1105 16943368 103628 do_freeze+ 0 S ReferenceQueueDu0_a150 5280 5291 1105 16943368 103628 do_freeze+ 0 S FinalizerDaemonu0_a150 5280 5292 1105 16943368 103628 do_freeze+ 0 S FinalizerWatchdu0_a150 5280 5293 1105 16943368 103628 do_freeze+ 0 S Binder:5280_1u0_a150 5280 5294 1105 16943368 103628 do_freeze+ 0 S Binder:5280_2u0_a150 5280 5295 1105 16943368 103628 do_freeze+ 0 S Binder:5280_3u0_a150 5280 5303 1105 16943368 103628 do_freeze+ 0 S k worker threadu0_a150 5280 5307 1105 16943368 103628 do_freeze+ 0 S Binder:5280_4u0_a150 5280 5310 1105 16943368 103628 do_freeze+ 0 S queued-work-loou0_a150 5280 5312 1105 16943368 103628 do_freeze+ 0 S ent.InfoHandleru0_a150 5280 5313 1105 16943368 103628 do_freeze+ 0 S nt.EventHandleru0_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利用那一节再论述。
3. Pthread视角下的线程名
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;
4. Android视角下的过程名和线程名
4.1 Zygote的过程名和线程名
以下探讨均为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
函数会做三件事:
- 批改zygote主线程的名称为"zygote64",也即批改
task_struct.comm
字段。 - 批改位于栈底的command line,将原有数据全副清空,更改为"zygote64"。这时候拜访
/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的线程名,该名称寄存于内核地址空间。 - 第二层含意:每个ART线程都会对应一个art::Thread对象,其外部有一个字段:
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 ReferenceQueueDu0_a150 5280 5291 1105 16943368 103628 do_freeze+ 0 S FinalizerDaemonu0_a150 5280 5292 1105 16943368 103628 do_freeze+ 0 S FinalizerWatchdu0_a150 5280 5310 1105 16943368 103628 do_freeze+ 0 S queued-work-loou0_a150 5280 5312 1105 16943368 103628 do_freeze+ 0 S ent.InfoHandler
持续回到zygote过程。当虚拟机启动结束后,zygote的主线程名更改为"main",不论是pthread的视角(task_struct.comm
),还是ART线程的视角(tlsPtr_.name
)。
4.2 Android利用的过程名和线程名
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"。
4.3 Java中新线程的名称
咱们在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
。
4.4 Trace文件和Tombstone文件中的过程名和线程名
对大多数开发者而言,他们接触到过程名和线程名的中央次要是trace文件和tombstone文件。
[Trace文件示例]
----- pid 9000 at 2022-03-17 05:00:52.489353500+0000 -----Cmd line: com.hangl.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowoDALVIK 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() << "\n";#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.helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhellowopid: 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,它的线程名只存在于tlsPtr_.name
。
这样一来,利用的主线程名将显示为截断的包名,之所以截断,是因为task_struct.comm
有16位长度限度。而且因为包名含有'.'符号,采纳前截断保留后半局部。另外,其余线程的名称也可能被截断,而这种状况在trace文件中不会产生。譬如同样是"ReferenceQueueDaemon"线程,trace文件中的名称显示残缺 ,而tombstone文件中的名称则被截断。
结语
本文采纳由下到上、层层递进的视角剖析了过程名/线程名的不同了解。细节颇多,看起来容易凌乱,因而这里做下总结。
- Ps视角下的过程名为command line的第一个参数,即argv[0],不过来除了'/'前的门路信息;Trace文件和tombstone文件里的过程名都是残缺的command line,利用过程在启动时将command line改写为了包名,长度超过77个字符的局部将会被截断。
- Pthread视角下的线程名为
task_struct.comm
,有16位长度限度;ART thread视角下的线程名为tlsPtr_.name
,没有长度限度。 - Pthread\_create或std::thread创立的线程,默认的线程名和创建者统一;Java层Thread创立的线程,默认的线程名为"Thread-"+"序号"。
这篇文章看起来颇有些“茴香豆的茴有几种写法”的感觉,但我并不是闲着没事做憋出的这篇文章。前段时间开发一个个性,须要依据过程名区别设置,在写代码的时候我就在想:过程名到底是什么?底层看到的过程名和下层看到的过程名是否统一?想完这个问题后,我发现我并不懂。不懂就要去钻研,因而才有了这篇文章。