为什么说一个线程组中的所有线程使用使用和该线程组的领头线程相同对PID

第一章 理解 Linux 线程:从 “轻量级进程” 说起

1.1 线程与进程的本质区别
  • 进程:传统意义上的 “程序执行实例”,拥有独立的地址空间、文件描述符、信号处理上下文等资源(内核视角:task_struct结构体)。
  • 线程:进程内的 “执行流”,共享进程的地址空间、文件描述符等资源,但拥有独立的栈、寄存器上下文、信号掩码等(内核视角:仍用task_struct表示,但通过标志位标记为线程)。

Linux 实现线程的核心思想是:“线程是共享资源的进程”。每个线程在 Kernel 看来都是一个task_struct,但通过clone()系统调用的标志位(如CLONE_VM共享地址空间、CLONE_FILES共享文件描述符)来实现资源共享。

1.2 线程组的诞生:从clone()系统调用说起

当调用pthread_create()创建线程时,底层会调用clone()系统调用:

int clone(int (*fn)(void*), void *stack, int flags, void *arg, ...);

关键标志位:

  • CLONE_THREAD:告诉内核,新创建的task_struct属于当前线程组(设置tgid字段为领头线程的 PID)。
  • CLONE_VM:共享虚拟地址空间(线程间可见同一块内存)。

此时,内核会做两件事:

  1. 为新线程创建独立的task_struct(包含独立的栈、寄存器等)。
  2. 将新线程的tgid(线程组 ID)设置为领头线程的pid(即task_struct->tgid = leader->pid)。
第二章 PID 与 TGID:内核如何区分进程和线程组
2.1 task_struct中的两个关键字段
  • pid:当前线程的唯一 ID(TID,线程 ID),对于领头线程,pid == tgid
  • tgid:线程组 ID(Thread Group ID),整个线程组共享的 ID,等于领头线程的pid

内核通过这两个字段区分:

  • tgid == pid时,该线程是线程组的领头线程(主线程)。
  • 其他线程的tgid等于领头线程的pidpid是自己的唯一 ID。

举个例子(假设领头线程 PID=1000):

线程角色pidtgid
领头线程10001000
线程 110011000
线程 210021000
2.2 用户空间看到的 PID:getpid()返回的是 TGID

当你在代码中调用getpid()时,无论当前线程是不是领头线程,返回的都是tgid(线程组 ID),即整个线程组共用的 PID。而获取当前线程的 TID 需要调用syscall(SYS_gettid)或 POSIX 提供的pthread_self()(但pthread_self()返回的是用户空间的线程 ID,内核 TID 需通过系统调用获取)。

这解释了为什么所有线程调用getpid()都会返回同一个值 —— 因为它们属于同一个线程组,共享 tgid。

第三章 线程组的 PID 机制设计哲学
3.1 兼容性:保持 POSIX 语义与传统进程模型的统一
  • POSIX 标准要求:线程属于进程,杀死进程(通过 PID)应终止所有线程。
  • 传统 Linux 工具(如pskill)基于 PID 操作,若每个线程有独立 PID,会破坏现有工具链的逻辑。

因此,内核设计为:线程组对外呈现为一个单一的 PID(tgid),而内部每个线程有独立的 TID(pid)。这样:

  • 用户用ps -eLf看到的PID列是 tgid(线程组 ID),LWP列是 TID(内核线程 ID)。
  • kill -9 <PID>会杀死整个线程组,符合 POSIX 对 “进程终止” 的定义。
3.2 资源管理:以线程组为单位统计

内核在统计资源时(如/proc/<PID>/下的文件),以 tgid 为单位汇总所有线程的数据:

  • /proc/1000/status中的Tgid: 1000表示线程组 ID。
  • VmSizeCPU时间等指标是整个线程组的总和,而非单个线程。

这种设计让传统进程的资源视图(如内存、打开文件数)无需修改即可适用于多线程程序,简化了系统实现。

第四章 深入内核源码:线程组的创建与 PID 设置
4.1 clone()系统调用的核心逻辑(基于 Linux 6.2 内核)
  1. 分配新的task_struct:通过copy_process()函数创建子任务。
  2. 设置 PID 和 TGID
    if (clone_flags & CLONE_THREAD) {
        // 属于同一线程组,tgid设为父线程的tgid
        child->tgid = current->tgid;
        // 父线程是领头线程时,子线程的pid是新分配的
        child->pid = alloc_pid(pid_namespace);
    } else {
        // 新进程组,tgid等于自己的pid
        child->tgid = child->pid = alloc_pid(pid_namespace);
    }
    
  3. 共享资源:根据clone_flags(如CLONE_VMCLONE_FILES)复制或共享地址空间、文件描述符等。

关键点:CLONE_THREAD标志决定了是否属于同一线程组,此时 tgid 继承自父线程的 tgid,而 pid 是新分配的 TID。

4.2 领头线程的特殊性

当创建一个全新的进程(非线程,比如通过fork()),clone_flags不含CLONE_THREAD,此时:

  • 新进程的tgid = pid(自己成为领头线程)。
  • 若后续在该进程中通过pthread_create()创建线程(带CLONE_THREAD),新线程的tgid等于领头线程的pidpid是新 TID。

这就是为什么 “单线程进程” 的pid == tgid,而多线程进程中只有领头线程的pid == tgid,其他线程pid != tgid

第五章 用户空间视角:如何观察线程组的 PID
5.1 使用ps命令查看线程组
# 显示所有线程,LWP列是TID,PID列是TGID(线程组ID)
ps -eLf | grep my_program

F S UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
0 S 1000     12345 12344 12345  0    4 14:00 pts/0    0:00 my_program  # 领头线程,LWP=PID=12345(tgid=12345)
0 S 1000     12345 12344 12346  0    4 14:00 pts/0    0:00 my_program  # 线程1,LWP=12346(TID),PID=12345(tgid)
0 S 1000     12345 12344 12347  0    4 14:00 pts/0    0:00 my_program  # 线程2,LWP=12347(TID),PID=12345(tgid)

  • NLWP列:线程组中的线程总数(包括领头线程)。
  • LWP(Light Weight Process):内核视角的线程 ID,等于task_struct->pid
5.2 通过/proc文件系统查看

每个线程在/proc中有两个目录:

  • tgid命名的目录:代表整个线程组(如/proc/12345/)。
  • pid(TID)命名的目录:代表单个线程(如/proc/12346//proc/12347/)。

查看单个线程的 tgid:

cat /proc/12346/status | grep Tgid
Tgid: 12345  # 线程组ID等于领头线程的PID
第六章 特殊场景:PID 命名空间与线程组
6.1 PID 命名空间(Namespace)的影响

在支持 PID 命名空间的容器(如 Docker)中,情况会更复杂:

  • 容器内看到的 PID 是 “命名空间内的局部 PID”,而宿主机看到的是 “全局 PID”。
  • 但线程组的 tgid 在命名空间内仍然保持统一:同一线程组在容器内的 tgid 相同,不同命名空间中的 PID 可能不同。

例如:

  • 宿主机中线程组的全局 PID=10000,容器内的局部 PID=500。
  • 容器内所有线程的getpid()返回 500(tgid),而syscall(SYS_gettid)返回局部 TID(如 501、502)。
6.2 线程组与信号处理

当向线程组发送信号(如kill -15 <PID>):

  • 内核会将信号发送给线程组内的所有线程,但每个线程可以独立处理信号(通过信号掩码、处理函数)。
  • 例外:SIGKILLSIGSTOP会无条件终止 / 停止所有线程,无法被阻塞或忽略。

这体现了 “线程组作为一个整体” 的设计:外部操作(如杀死、暂停)针对整个组,而内部信号处理可以精细化到单个线程。

第七章 常见误区与最佳实践
7.1 误区:“线程是轻量级进程,所以每个线程有独立 PID”

错误!Linux 中的线程本质是共享资源的进程(task_struct),但通过tgid将它们归为同一组,对外呈现单一 PID。独立 PID 的是传统进程,而非线程。

7.2 最佳实践:区分 TID 和 TGID
  • 当需要操作整个线程组(如终止、查看资源),使用 TGID(getpid()返回值)。
  • 当需要操作单个线程(如获取线程局部存储、设置信号掩码),使用 TID(通过syscall(SYS_gettid)获取内核 TID,或pthread_self()获取用户空间 ID)。
7.3 调试工具:gdb中的线程查看
(gdb) info threads  # 显示所有线程,ID列是用户空间线程ID(pthread_self()返回值)
  1 Thread 0x7ffff7ffd700 (LWP 12345)  main ()  # LWP是内核TID,等于task_struct->pid
  2 Thread 0x7ffff77fc700 (LWP 12346)  thread_func ()
(gdb) thread 2  # 切换到线程2
(gdb) call syscall(SYS_gettid)  # 打印内核TID(12346)
(gdb) call getpid()  # 打印TGID(12345,与线程1相同)

第八章 总结:PID 机制如何支撑线程组的存在

Linux 通过pidtgid的双字段设计,巧妙地将 “多线程” 融入传统的 “单进程” 模型:

  1. 对外统一:所有线程组内的线程共享 TGID(PID),保持了pskill等工具的兼容性,资源统计以组为单位。
  2. 对内独立:每个线程有独立的 TID(pid),支持内核调度、信号处理等需要区分单个执行流的场景。
  3. 实现高效:通过clone()的标志位控制资源共享,复用task_struct结构体,避免为线程设计全新的数据结构。

这种设计体现了 Linux 内核的 “实用主义哲学”—— 在兼容传统模型的同时,为新特性(多线程)提供支持,成为 UNIX 系系统中线程实现的经典范例。

形象比喻:把线程组想象成 “户口本家庭”

你可以把 Linux 中的线程组想象成一个 “户口本家庭”,而每个线程就是家庭里的成员:

  1. 领头线程(主线程):相当于家庭中的 “户主”,他去派出所(内核)申请了一个唯一的 “家庭编号”—— 这就是整个家庭的PID(进程 ID)
  2. 其他线程:相当于家庭中的其他成员(配偶、孩子),他们不会单独申请新的家庭编号,而是和户主共用同一个户口本上的家庭编号(PID)。
  3. 线程自己的 ID:每个家庭成员虽然共用家庭编号,但每个人还有自己的 “身份证号”—— 这就是线程独有的TID(线程 ID),由内核通过pthread_self()等函数提供。

举个生活例子:
假设你创建了一个进程(相当于成立一个家庭),进程里的主线程(户主)拿到 PID=1000。这时你通过pthread_create创建了 3 个新线程(生了 3 个孩子),这 3 个线程不会有独立的 PID,而是和主线程共用 PID=1000。但每个线程有自己的 TID(比如 1001、1002、1003),就像每个孩子有自己的身份证号,但户口本上的家庭编号都是 1000。

这样设计的好处是:内核把整个线程组视为一个 “进程单元”,比如 kill 命令用 PID 杀死的是整个线程组,资源统计(如内存、CPU 时间)也是以 PID 为单位汇总所有线程的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值