关于android:Android中的进程名和线程名

11次阅读

共计 18801 个字符,预计需要花费 48 分钟才能阅读完成。

本文剖析基于 Android S(12)

今人起名颇为考究,不单有名,还有字。文人雅士有时还会给本人取个“别号”。所谓“名为正体,字以表德,号以寓怀”,三者独特展示一个人的品格和谋求。在 Android 的世界里,过程和线程的名称也多种多样,有的中央用的是“名”,用的中央用的是“字”,并不对立。所以本文的目标就是深究实质,让观众老爷们晓得,拿掉这些代号后的主体到底是谁。先来浅问几个问题:

  1. Android 利用的主线程在 trace 文件中的名称是什么?在 tombstone 文件中的名称又是什么?
  2. Java 层创立的新线程默认名称是什么?Native 层创立的新线程默认名称又是什么?
  3. Trace 文件最顶部的 ”Cmd line” 本意为何?为什么它会和利用的包名统一?
  4. 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 -Aps -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。可是看完下面的输入,会发现有几个奇怪的点。

  1. 为什么有的过程名是可执行文件的名称,有的却是利用的包名?
  2. 为什么有的过程名会用方括号括住?

上面咱们翻译翻译,什么 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\04\0“)

  1. 将所有的 ’\0’ 用空格代替,解决后的字符串能够显示在 CMDLINE 列。(解决后变为:”/system/bin/top -d 4″)
  2. 将之前代替的第一个 ’\0’ 前的字符串作为 argv[0],并将 argv[0]字符串最初一个 ’/’ 前的信息去除,只保留根本的文件名(英文又称 basename),解决后的字符串能够显示在 NAME 列。(解决后变为:”top”)
  3. 将 argv[0]中的门路信息去除,然而保留后续的参数信息,解决后的字符串能够显示在 ARGS 列。(解决后变为:”top -d 4″)

因而咱们能够晓得,cmdline 文件节点读出的信息最终被用在了三个中央,有点一鱼三吃的感觉。

最终显示在 ps -A 中的过程名,是 argv[0]字符串的 basename,通常是可执行文件的名称。而 Android 利用之所以显示为包名,是因为过程启动过程中改写了 argv[0]的值,这个放到前面再说。

那为什么有些过程名用方括号括起来了呢?

答案是这些过程没有 cmdline(内核过程和一些非凡的用户过程)。当 cmdline 文件节点读不到任何信息时,ps 会将该过程的 task_struct.comm 值取出,并用方括号括住来代替显示。对这些过程而言,CMDLINENAMEARGS 列显示的都是同一个字符串。所以看到这样的过程名时,咱们大概率能够揣测这是一个内核过程。

2.2 Top 显示的过程名

Top 显示的过程名为 ARGS(具体含意见 #2.1)。严格来说,它不能叫做”过程名“,而应该叫”参数列表“。这里咱们以 top 过程示例(第二行),能够看到ARGS 为 ”top -d 4″,它蕴含了后续的参数信息。

2.3 Ps -T - p 显示的线程名

# 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 利用那一节再论述。

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函数会做三件事:

  1. 批改 zygote 主线程的名称为 ”zygote64″,也即批改 task_struct.comm 字段。
  2. 批改位于栈底的 command line,将原有数据全副清空,更改为 ”zygote64″。这时候拜访 /proc/[zygote's pid]/cmdline 文件节点,获取到的只有 ”zygote64″。这样不管咱们应用 cmdline 残缺的字符串,还是 argv[0]指向的字符串,抑或是 argv[0]剥离门路后的 basename,都将失去 ”zygote64″。因而,咱们有理由说,此时的过程名曾经被改为了 ”zygote64″。
  3. __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 线程。

  1. 第一层含意:task_struct.comm字段,也即 pthread 的线程名,该名称寄存于内核地址空间。
  2. 第二层含意:每个 ART 线程都会对应一个 art::Thread 对象,其外部有一个字段:tlsPtr_.name。该名称寄存于用户地址空间。

回到 SetThreadName 函数,它会别离批改两层含意的线程名。首先将 tlsPtr_.name 字段改为 ”main”,接着通过 ::art::SetThreadNametask_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对传入的名称有些非凡解决,解决规定如下。

  1. 如果传入的名称含有 ’@’ 符号,或者 含 ’.’ 符号,则在截断时保留前半部分。
  2. 否则字符串在截断时保留后半局部。

理解规定并不重要,了解规定背地的思考才重要。这里说下我对于这个规定的了解: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)。

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.commtlsPtr_.name

4.4 Trace 文件和 Tombstone 文件中的过程名和线程名

对大多数开发者而言,他们接触到过程名和线程名的中央次要是 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", &current_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.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,它的线程名只存在于tlsPtr_.name

这样一来,利用的主线程名将显示为截断的包名,之所以截断,是因为 task_struct.comm 有 16 位长度限度。而且因为包名含有 ’.’ 符号,采纳前截断保留后半局部。另外,其余线程的名称也可能被截断,而这种状况在 trace 文件中不会产生。譬如同样是 ”ReferenceQueueDaemon” 线程,trace 文件中的名称显示残缺,而 tombstone 文件中的名称则被截断。

结语

本文采纳由下到上、层层递进的视角剖析了过程名 / 线程名的不同了解。细节颇多,看起来容易凌乱,因而这里做下总结。

  1. Ps 视角下的过程名为 command line 的第一个参数,即 argv[0],不过来除了 ’/’ 前的门路信息;Trace 文件和 tombstone 文件里的过程名都是残缺的 command line,利用过程在启动时将 command line 改写为了包名,长度超过 77 个字符的局部将会被截断。
  2. Pthread 视角下的线程名为task_struct.comm,有 16 位长度限度;ART thread 视角下的线程名为tlsPtr_.name,没有长度限度。
  3. Pthread\_create 或 std::thread 创立的线程,默认的线程名和创建者统一;Java 层 Thread 创立的线程,默认的线程名为 ”Thread-“+” 序号 ”。

这篇文章看起来颇有些“茴香豆的茴有几种写法”的感觉,但我并不是闲着没事做憋出的这篇文章。前段时间开发一个个性,须要依据过程名区别设置,在写代码的时候我就在想:过程名到底是什么?底层看到的过程名和下层看到的过程名是否统一?想完这个问题后,我发现我并不懂。不懂就要去钻研,因而才有了这篇文章。

正文完
 0