体系编程课上遇到的一个问题:Linux下,假如一个 pthread_create 创立的线程没有被 pthread_join 收回,是否会和僵尸进程相同,发生“僵尸线程”,并一直占用一个 pid/tid?

猜测

僵尸进程

关于进程与子进程来说,假如子进程退出了,可是父进程不对子进程进行 reap (即运用 wait/waitpid 对子进程进行收回),则子进程的 PCB(内核中的 task_struct)依然会保留,用于记载回来状况直到父进程获取,而且状况将被设置成 ZOMBIE,即发生“僵尸线程”。

#include <unistd.h>
int main()
{
    if(fork() == 0) {
        return 0; // child exits immediately
    }
    while(1); // parent loops
}

运转后能够看到子进程 607727 的状况为 Zombie,而且在最终有 <defunct> 标志。

USER     PID %CPU %MEM  VSZ  RSS TTY   STAT START  TIME COMMAND
miigon    607726 97.5  0.0   2640   988 pts/0    R+   21:55   0:28 ./child
miigon    607727  0.0  0.0      0     0 pts/0    Z+   21:55   0:00 [child] <defunct>

僵尸线程

#include <stdlib.h>
#include <pthread.h>
void *child_thread(void *args) {
    // child thread exits immediately.
    return (void*)666;
}
int main() {
    pthread_t t1;
    pthread_create(&t1, NULL, child_thread, NULL);
    while(1);
    // parent and child never join.
    // pthread_join(t1, NULL);
}

子线程发动后立刻回来,而父线程无限循环,而且不 join 子线程,不查看其回来状况。

Linux 内核中(至少在调度上)并不区别线程和进程,都视为 task,故合理猜测:或许这儿的 pthread_create 和 pthread_join 也能够类比 fork 和 wait,假如一个线程被创立后,不进行 pthread_join,那在子线程履行结束后,或许子线程也会进入 Zombie 状况,直至被父线程收回?(猜测)

验证

对上述猜测进行验证,编译运转上述线程代码:

$ gcc pt.c -o pt -lpthread -g
$ ./pt

假如咱们的猜测正确,当查看 pt 的一切线程的时分,理论上应该能够看到一个主线程,还有一个 defunct 状况的子线程 task。

运用 ps 查看 pt 的一切线程:

$ ps -T -C pt
    PID    SPID TTY          TIME CMD
 610281  610281 pts/1    00:00:09 pt

发现只有一个主线程(PID == SPID),没有观察到 defunct 状况的子线程,子线程退出后尽管主线程没有 pthread_join 读取其回来值,可是子线程 pid/tid 依然被收回了,并没有进入僵尸状况。

验证一下假如把子线程函数换成死循环,运转后能够观察到子线程存在,阐明测验方法没有问题,扫除子线程没有创立成功或许观测方法有误的或许性:

void *child_thread(void *args) {
    while(1);
}
$ ps -T -C pt
    PID    SPID TTY          TIME CMD
 610762  610762 pts/1    00:00:05 pt
 610762  610763 pts/1    00:00:05 pt

阐明咱们的猜测是不精确的,并没有观察到子线程退出后变为僵尸状况。

探求

因为已知在 Linux 上,创立线程和创立进程实际上走的是同一套机制,本质上都是 fork/clone,仅仅调用者指定的资源共享程度不同,所以差异出现的诱因只能是位于 fork/clone 的调用者,即位于 pthread 的代码中。

pthread 在 Linux 上一般是由 libc 完结的,最常见的 libc 是 glibc(另一个 Linux 上常用的 libc 的比如是 musl,更轻量,不打开)。glibc 的 pthread 完结叫做 NPTL(替换掉之前的远古完结叫 LinuxThreads,也不打开),能够在 codebrowser.dev/glibc/glibc… 很方便地在线浏览相关代码。

本文环境 ubuntuserver 22.04.1 + linux5.15.0 + glibc2.35;一切源代码文件以这些版本为准。
假如发现 glibc/NPTL 部分代码的锁进很乱,那是因为本来的代码便是这么锁进的,不是文章格式化错误。

线程等候 pthread_join()

首先查看 pthread_join 的源码,因为根据咱们的猜测,假如是会发生“僵尸线程”的话,pthread_join 要收回这个“僵尸线程”,必然要调用 wait/waitpid 系的体系调用。

codebrowser.dev/glibc/glibc…
codebrowser.dev/glibc/glibc…

// pthread_join.c:21
int
___pthread_join (pthread_t threadid, void **thread_return)
{
  return __pthread_clockjoin_ex (threadid, thread_return, 0 /* Ignored */,
				 NULL, true);
}

核心部分:

// pthread_join_common.c:35
int
__pthread_clockjoin_ex (pthread_t threadid, void **thread_return,
                        clockid_t clockid,
                        const struct __timespec64 *abstime, bool block)
{
  struct pthread *pd = (struct pthread *) threadid;
  // ......
  if (block) // true
    {
	  // 等候线程履行完结
      pthread_cleanup_push (cleanup, &pd->joinid);
      pid_t tid;
      while ((tid = atomic_load_acquire (&pd->tid)) != 0) // 获取锁
        {
	  int ret = __futex_abstimed_wait_cancelable64 ( // 经过等候一个 futex 来等候线程履行完结,仅仅 futex syscall 的封装,内部并没有调用 wait/waitpid
	    (unsigned int *) &pd->tid, tid, clockid, abstime, LLL_SHARED);
	  if (ret == ETIMEDOUT || ret == EOVERFLOW)
	    {
	      result = ret;
	      break;
	    }
	}
      pthread_cleanup_pop (0);
    }
  void *pd_result = pd->result; // 获取线程的回来值,阐明线程现已履行完结
  if (__glibc_likely (result == 0)) // 等候成功
    {
      /* We mark the thread as terminated and as joined.  */
      pd->tid = -1;
      /* Store the return value if the caller is interested.  */
      if (thread_return != NULL)
	*thread_return = pd_result;
      /* Free the TCB.  */
      __nptl_free_tcb (pd); // 释放 TCB,即 pthread 结构体
    }
  // ......
}

经过 JOIN 的这部分关键代码,能够推测出这几个重要信息:

  • glibc 上 pthread_join 等候子线程完结,并不是经过传统的 wait/waitpid 完结的,而是由 pthread 自己再保护了一个 futex (在这儿作为「线程履行结束」的条件变量 condition variable),经过等候这个 futex 完结。
  • 经过 __nptl_free_tcb(pd) 能够知道,所谓的 “TCB” 这个概念实际上便是 pthread 结构体自身(pthread_t 是指向其的指针),而且是存储在用户态的,由 glibc/nptl 办理,而不是在内核态办理。
  • pthread_join 只担任释放用户态 pthread 结构体(pd),而和释放线程在内核中占用的资源没有关系。

综合「pthread_join 不担任收回(reap)内核态线程」以及「观察到子线程在履行完结后,在主线程什么都没有做的状况下自己消失了」这两个信息,进一步猜测子线程是退出后被内核主动 reap 掉了。

可是按照正常进程来说,除非是父进程设置了 signal(SIGCHLD, SIG_IGN);,否则操作体系是不会主动 reap 掉子进程的,假定内核不区别进程和线程,对线程而言应该也是这个行为(需求等候父进程 reap,否则就处于 ZOMBIE 状况)才对。

由此猜测有或许是两种或许性中的一种:

  1. 内核或许对线程 task 有一定的特别照顾/特别处理,使得线程的 task 会在退出时主动 reap,而进程则等候父进程收回。
  2. 也有一种或许性是 pthread 自己在子线程履行结束做了特别处理,让操作体系 reap 掉自己(真的或许做到吗?)

后面的内容和探求都是环绕测验检验这两个猜测打开的。

线程创立 pthread_create()

因为已知线程 task 不是由 pthread_join 收回的,必然是内核或许 pthread 在什么其他当地进行了收回,故追寻整个线程 pthread 从创立开端的生命周期:

codebrowser.dev/glibc/glibc…

// pthread_create.c:619
int
__pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
		      void *(*start_routine) (void *), void *arg)
{
  void *stackaddr = NULL;
  size_t stacksize = 0;
  // ......
  // 分配 TCB (pthread 结构体)和线程栈空间
  struct pthread *pd = NULL;
  int err = allocate_stack (iattr, &pd, &stackaddr, &stacksize);
  int retval = 0;
  // ......
  // 初始化 TCB
  pd->start_routine = start_routine; // 子线程进口
  pd->arg = arg; // 子线程进口参数
  pd->c11 = c11;
  // ......
  /* Setup tcbhead.  */
  tls_setup_tcbhead (pd);
  /* Pass the descriptor to the caller.  */
  *newthread = (pthread_t) pd;
  // ......
  // .......some more signal related stuff
  /* Start the thread.  */
  if (__glibc_unlikely (report_thread_creation (pd))) // 假如需求陈述 TD_CREATE 工作
    {
      // ......在咱们的比如中不会走到这儿,疏忽
      // event 是 debug 时用的,例如用来告诉 gdb 线程现已创立
    }
  else
    // !!创立内核态线程 task!!
    retval = create_thread (pd, iattr, &stopped_start, stackaddr,
			    stacksize, &thread_ran);
  // ......
  if (__glibc_unlikely (retval != 0))
    {
      // ......错误处理,疏忽
    }
  else
    {
	  // 放开 pd 上的锁,让子线程自在运转
      if (stopped_start) lll_unlock (pd->lock, LLL_PRIVATE);
      // ......
    }
 out:
  if (destroy_default_attr)
    __pthread_attr_destroy (&default_attr.external);
  return retval;
}

pthread_create 做的工作并不复杂:

  1. 为子线程分配栈空间和 TCB(pd)
  2. 预备/配置好 TCB 各项参数,包括子线程进口和进口参数
  3. 调用 create_thread() 发动子线程 task

线程 task 创立 create_thread()

这个函数在 pthread_create() 中担任为子线程创立实际的内核态 task,经过调用 clone 完结(__clone_internal() 是 clone/clone2/clone3 的简略封装)。

// pthread_create.c:231
static int create_thread (struct pthread *pd, const struct pthread_attr *attr,
			  bool *stopped_start, void *stackaddr,
			  size_t stacksize, bool *thread_ran)
{
  // ......
  /* We rely heavily on various flags the CLONE function understands:
     CLONE_VM, CLONE_FS, CLONE_FILES
	These flags select semantics with shared address space and
	file descriptors according to what POSIX requires.
     ...... 篇幅原因缩略,查看原始文件或 `man clone` 查询每个 flag 效果
     The termination signal is chosen to be zero which means no signal
     is sent.  */
  const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
			   | CLONE_SIGHAND | CLONE_THREAD // !!留意到这个 CLONE_THREAD flag
			   | CLONE_SETTLS | CLONE_PARENT_SETTID
			   | CLONE_CHILD_CLEARTID
			   | 0);
  TLS_DEFINE_INIT_TP (tp, pd);
  struct clone_args args =
    {
      .flags = clone_flags, // clone flags
      .pidfd = (uintptr_t) &pd->tid,
      .parent_tid = (uintptr_t) &pd->tid, // CLONE_PARENT_SETTID
      .child_tid = (uintptr_t) &pd->tid,
      .stack = (uintptr_t) stackaddr,
      .stack_size = stacksize,
      .tls = (uintptr_t) tp,              // CLONE_SETTLS
    };
  // clone,子线程从 start_thread() 开端履行
  int ret = __clone_internal (&args, &start_thread, pd);
  if (__glibc_unlikely (ret == -1))
    return errno;
  // ......
  return 0;
}

这儿留意到,调用 clone 的时分,传递给内核的 flags 中含有 CLONE_THREAD 这个 flag。

这个 flag 意味着用户态显式地告知了内核,克隆出来的 task 应该被作为一个线程看待。先不论这个 flag 的详细影响是什么,传递这个 flag 这件工作自身足以阐明,内核实际上对一般进程 task 和线程 task 还是有专门的区别的,并不是除了资源共享程度不同以外其他都彻底一模相同。

咱们前面针对子线程的 task 会被主动 reap 掉这件事,做出了两种猜测:或许是内核特别处理了线程 task,也或许是 pthread 自己在子线程结束收回了子线程。

CLONE_THREAD 这个 flag 的存在加大了第一种猜测正确的或许性,不过并不彻底扫除第二种猜测,要扫除第二种猜测,需求看子线程的用户例程履行结束后,在 pthread 中都做了什么,有没有收回掉子线程 task。

子线程 task 履行进口 start_thread()

create_thread() 创立的子线程的履行进口固定为 start_thread(),这个函数再从 pd->start_routinepd->args 获得用户函数的地址和参数,并跳转到用户函数开端履行。

/* Local function to start thread and handle cleanup.  */
static int _Noreturn
start_thread (void *arg)
{
  struct pthread *pd = arg;
  // ......
  // ......thread local storage and stuff
  // ......unwinders and stuff, for cancellation and/or exception handling
  // ......
  if (__glibc_likely (! not_first_call))
    {
      /* Store the new cleanup handler info.  */
      THREAD_SETMEM (pd, cleanup_jmp_buf, &unwind_buf);
      __libc_signal_restore_set (&pd->sigmask);
      LIBC_PROBE (pthread_start, 3, (pthread_t) pd, pd->start_routine, pd->arg);
      /* Run the code the user provided.  */
      void *ret;
      if (pd->c11)
	{
	  // ......c11 规范下多了一个类型转化问题,咱们不运用 c11 规范,不走到这儿,略
	}
      else
	ret = pd->start_routine (pd->arg); // 运用用户供给的参数,调用用户函数,得到回来值 ret
      THREAD_SETMEM (pd, result, ret); // 用户函数回来值 ret 存入到 pd->result
    }
  // ======= 到这儿,用户函数现已履行结束,子线程使命完结,进入结束阶段 ======
  // thread local storage、thread local data 析构
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    __call_tls_dtors ();
  /* Run the destructor for the thread-local data.  */
  __nptl_deallocate_tsd ();
  /* Clean up any state libc stored in thread-local variables.  */
  __libc_thread_freeres ();
  /* Report the death of the thread if this is wanted.  */
  if (__glibc_unlikely (pd->report_events))
    {
    // ......在需求时,陈述 TD_DEATH 工作,略
    // event 是 debug 时用的,例如用来告诉 gdb 线程现已退出
    }
  // ......
  if (__glibc_unlikely (atomic_decrement_and_test (&__nptl_nthreads)))
    /* This was the last thread.  */
    exit (0); // 假如这个线程是最终一个线程,退出整个线程组(thread group)
  // ......signal and mutex and stuff
  // ......假如栈空间是 pthread 分配的(而不是用户创立线程时供给的),则收回栈空间
  // ......
  // 假如线程被 pthread_detach 过,则趁便收回 TCB(留意 TCB 不是 task_struct,是用户态 pthread 结构体)
  // 默许行为是不在这儿收回 TCB,咱们的比如中也是默许状况,即不会在这儿履行 `__nptl_free_tcb(pd)`,而是需求等到 pthread_join() 时才会收回 TCB
  // 这是因为 pd 结构体中含有用户态回来值 pd->result 或许会被父线程需求;pthread_detach 则意味着父线程不关心子线程的履行成果。
  if (IS_DETACHED (pd))
    /* Free the TCB.  */
    __nptl_free_tcb (pd);
out:
  // 子线程彻底结束,最终一步:调用 sys_exit 体系调用,结束【子线程】(不是整个进程)
  while (1)
    INTERNAL_SYSCALL_CALL (exit, 0);
  /* NOTREACHED */
}

留意到最终一步 sys_exit 和常见的 exit()/_exit() 不同,前者是体系调用,后者是由 libc 供给的用户态包装方法。

两者的效果效果也不相同:

  • sys_exit 是退出当前【调度单元】,即 task,在这儿是指当前【线程】
  • exit()/_exit() 实际上包装的是 sys_exit_group 体系调用,代表退出整个【线程组】,即整个进程的一切线程。

命名的紊乱是前史原因,因为一开端 Linux 只支撑多进程,最初 exit()/_exit() 也的确封装的是 sys_exit。

而后来加入多线程后,Linux 在内核态内引入了一个新概念:thread group。本来的进程变成了 task,task_struct->pid 变成了线程 id(gettid() 回来 task_struct->pid),而现在常说的进程 id,则是新的task_struct->tgid(thread group id,getpid() 回来 task_struct->tgid )。同时,libc 的 exit()/_exit() 也被改为调用新的 sys_exit_group,即结束整个线程组。本来的 sys_exit 的效果不变,依然是结束一个调度单元 task,仅仅「调度单元」的概念改变了而已。

这个比较 hack 的方法,使得 linux 以较小的改动,完结了对线程的支撑,害处便是导致用户态的 “pid” 的概念和内核态中的 “pid” 的意义不一致,不留意的话简单混淆。

能够看到,子线程的进口函数 start_thread(),在履行完用户函数后,销毁了栈和 thread local storage,然后履行了 sys_exit 结束子线程 task。

这个进程和多进程模型中一个子进程调用 exit() 退出线程是相似的,并不保证一定清理掉 task_struct,而是理论上有或许使 task 进入 ZOMBIE 状况。而且这儿能够看到,pthread 也没有让子进程做(诸如自己 wait 自己?)之类的魔法来显式清理掉子进程的 task。

这阐明咱们之前的猜测2是不正确的,子线程的内核 task_struct 并不是 pthread 在子线程退出后进行特别处理 reap 收回的。只能是 sys_exit 体系调用中,退出子线程 task 的时分,内核自己决议要直接 reap 掉这个 task。

实际上猜测2自身也不或许,一个 task 不或许自己收回自己的资源,因为只有现已结束的 task 才干被收回,可是现已结束的 task 就无法履行任何代码了,也就没法收回自己。

退出子线程 sys_exit 体系调用

前面经过扫除,将咱们子线程的 task 被 reap 的精确方位定位到了 sys_exit 中了。咱们从用户态的 glibc 以及 pthread,持续进入到内核态代码的范围中。

前一末节结束说到,咱们发现子线程的 task 在用户态是正常 sys_exit 退出的,可是 sys_exit 后 pid 以及 task_struct 被立刻收回掉,而不是像一般进程相同进入僵尸状况,这儿看到内核 do_exit() 方法(sys_exit 体系调用的内核态处理函数):

elixir.bootlin.com/linux/v5.15…

// kernel/exit.c:727
// 不用仔细看这个函数的每一步,这儿全放出来仅仅为了表现步骤有多么多而已
void __noreturn do_exit(long code)
{
	struct task_struct *tsk = current;
	int group_dead;
	// ......
	profile_task_exit(tsk);
	kcov_task_exit(tsk);
	ptrace_event(PTRACE_EVENT_EXIT, code);
	validate_creds_for_do_exit(tsk);
	// ......
	io_uring_files_cancel();
	exit_signals(tsk);  /* sets PF_EXITING */
	/* sync mm's RSS info before statistics gathering */
	if (tsk->mm)
		sync_mm_rss(tsk->mm);
	acct_update_integrals(tsk);
	group_dead = atomic_dec_and_test(&tsk->signal->live);
	if (group_dead) {
		/*
		 * If the last thread of global init has exited, panic
		 * immediately to get a useable coredump.
		 */
		if (unlikely(is_global_init(tsk)))
			panic("Attempted to kill init! exitcode=0x%08x\n",
				tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
		hrtimer_cancel(&tsk->signal->real_timer);
		exit_itimers(tsk->signal);
#endif
		if (tsk->mm)
			setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
	}
	acct_collect(code, group_dead);
	if (group_dead)
		tty_audit_exit();
	audit_free(tsk);
	tsk->exit_code = code;
	taskstats_exit(tsk, group_dead);
	exit_mm();
	if (group_dead)
		acct_process();
	trace_sched_process_exit(tsk);
	exit_sem(tsk);
	exit_shm(tsk);
	exit_files(tsk);
	exit_fs(tsk);
	if (group_dead)
		disassociate_ctty(1);
	exit_task_namespaces(tsk);
	exit_task_work(tsk);
	exit_thread(tsk);
	perf_event_exit_task(tsk);
	sched_autogroup_exit_task(tsk);
	cgroup_exit(tsk);
	flush_ptrace_hw_breakpoint(tsk);
	exit_tasks_rcu_start();
	exit_notify(tsk, group_dead);
	proc_exit_connector(tsk);
	mpol_put_task_policy(tsk);
#ifdef CONFIG_FUTEX
	if (unlikely(current->pi_state_cache))
		kfree(current->pi_state_cache);
#endif
	/*
	 * Make sure we are holding no locks:
	 */
	debug_check_no_locks_held();
	if (tsk->io_context)
		exit_io_context(tsk);
	if (tsk->splice_pipe)
		free_pipe_info(tsk->splice_pipe);
	if (tsk->task_frag.page)
		put_page(tsk->task_frag.page);
	validate_creds_for_do_exit(tsk);
	check_stack_usage();
	preempt_disable();
	if (tsk->nr_dirtied)
		__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
	exit_rcu();
	exit_tasks_rcu_finish();
	lockdep_free_task(tsk);
	do_task_dead();
}
EXPORT_SYMBOL_GPL(do_exit);

能够看到一个 task 退出时需求清理/释放的资源种类非常之多,主流程do_exit()里的子流程函数调用就有好几十个了。

咱们想要从中找到这个逻辑:exit 的时分,内核根据什么决议是否直接 reap 掉 task。

利用已知常识帮助快速定位到想要找的逻辑的代码:已知关于一般进程来说,假如父进程设置了疏忽 SIGCHLD 信号(signal(SIGCHLD, SIG_IGN)),则子进程 exit 的时分会【reap 掉 task】,否则子进程会【进入 ZOMBIE 状况】。这实际上正是咱们要找的「exit 决议是否直接 reap 掉 task」的决议计划进程的一部分。猜测关于线程 task 是否主动 reap 的决议计划逻辑也是在相同的方位或附近。

故以【进入 ZOMBIE 状况】为头绪反查,直接搜索 = EXIT_ZOMBIE 测验找一切将 task 状况设置为 ZOMBIE 的当地,快速定位到 exit_notify() 中:

/*
 * Send signals to all our closest relatives so that they know
 * to properly mourn us..
 */
static void exit_notify(struct task_struct *tsk, int group_dead)
{
	bool autoreap; // 是否主动 reap 掉 task
	// ......
	tsk->exit_state = EXIT_ZOMBIE; // 默许将 task 置入 EXIT_ZOMBIE 状况
	if (unlikely(tsk->ptrace)) { // 假如启用了 ptrace(一般用于 debug)
		int sig = // ......
		autoreap = do_notify_parent(tsk, sig);
	} else if (thread_group_leader(tsk)) { // 假如是线程组组长,即主线程
	    // 而且整个线程组(进程)中一切线程都现已退出
	    // 则发送 SIGCHLD 给 parent,假如父进程 SIG_IGN 掉了 SIGCHLD,则主动 reap
		autoreap = thread_group_empty(tsk) &&
			do_notify_parent(tsk, tsk->exit_signal);
	} else { // 其他任何状况,即:不是线程组组长(例如子线程)
		autoreap = true; // 则均主动 reap
	}
	if (autoreap) { // 主动 reap 的进程直接进入 EXIT_DEAD 状况
		tsk->exit_state = EXIT_DEAD;
		list_add(&tsk->ptrace_entry, &dead);
	}
	// ......
}

这儿证实了猜测1:当一个 task 不是一个线程组的组长的时分,内核会在 exit 的时分直接 reap 掉子线程的 task。所以在子线程履行结束可是未 join 之间,ps 会看不到子线程,因为子线程 task 现已被内核收回了。

也便是说,只有一个线程组(也便是进程)的主线程能够进入僵尸状况,一切的子线程都不或许会有僵尸状况。子线程的 task 在用户程序代码履行结束后,就立刻退出并被收回了。

而子线程的履行成果(回来值等)则保存在用户态 pthread 结构体中,供后续或许的 JOIN 运用。

定论

关于 Linux 平台上的 pthread 线程,在子线程比父线程先退出且没被 JOIN 的状况下,不会发生和传统意义上的僵尸进程相似的“僵尸线程”(即 ps 不会看到有 defunct 的线程 task,子线程 task 会在 exit 时被内核直接收回掉,不等父进程 JOIN)。

可是并不意味着没有被 pthread_join 的线程彻底不会占用资源。

没被收回的线程尽管不会占用内核的 task 资源,可是会在用户态留下 pthread 结构体(TCB)以及线程的栈(因为 pthread 结构体和线程栈是一同分配一同释放的),假如未被 JOIN 的线程累积过多,依然或许会导致用户态资源耗尽而导致该进程无法创立新的线程。

与僵尸进程不同的是,“僵尸线程”堆积的影响只约束在一个进程之内,理论上不会导致体系上其他进程创立失利(因为不占用 task_struct 和 pid/tid)。

留意到该定论只适用于 Linux,因为 Linux 完结线程的方法为内核轻改动,大多数线程相关的功用完结都在用户态中完结(glibc)。不扫除其他 POSIX 体系或许内核级原生支撑 pthread 线程。

pthread_detach() 过的线程,则 pthread 会在线程履行完结后主动释放 pthread 结构体以及栈,所以对不关心履行成果的线程,应当运用 pthread_detach() 进行脱离。若不进行脱离,则必须确保一切线程在合适的时间都能被 pthread_join() 收回。

Reference:
manpages.debian.org/bullseye/ma…
codebrowser.dev/glibc/glibc…
elixir.bootlin.com/linux/v5.15…