找回密码
 立即注册
首页 业界区 安全 [LKD/Linux 内核] Linux 中的 进程, 线程

[LKD/Linux 内核] Linux 中的 进程, 线程

余思洁 2026-2-20 07:40:00
Linux 3.2 进程, 线程

前言

注意: 本文章默认你学过操作系统的进程部分,了解进程的概念.
我们都知道, 在 Linux 中, 我们使用 LWP 来描述线程, 即不区分线程/进程, 统一用 task_struct 描述它.
但是在 Linux 中, 线程, 进程, 进程组实际上还是有点区别的.
这篇文章来聊聊进程, 线程.
1.Linux 的 task_struct

1.1 task_struct 结构体介绍

如 LKD 中所讲的, Linux 采用 task_struct 来描述进程.

当然, 这只是简写. 实际上, Linux 的 task_struct 要比文章中写的复杂的多. 每个字段的用途, 到时候穿插在其他文章讲吧, 一次性摆在这会容易绕晕.
1.2 CPU核心如何获取当前进程的的 task_struct?

答案藏在之前所讲的 current_thread_info 里面.
  1. struct thread_info {    struct task_struct        *task;    //...}
复制代码
之前文章讲过, 在 x86_64 中, thread_info 存在于当前 CPU 核心的内核栈中, 而 current_thread_info 就是获取这个变量.
那我们只需要执行以下代码, 就可以获取当前正在执行的 task_struct 了:
  1. struct task_struct* tsk = current_thread_info()->task;
复制代码
2.Linux 中的进程链表

首先我们要研究的就是 task_struct 中的如下几个字段.
  1. struct task_struct {    struct list_head sibling; //兄弟节点    struct list_head children; //孩子节点    struct list_head real_parent; //创建时候的父进程    struct list_head parent; //更准确的说, 这个是负责接收收尸信号 SIGCHLD 的进程    struct signal_struct signal; //后面要考};
复制代码
每个字段的作用, 都在上面注释的很清楚了.
在 Linux 中, 进程以的方式组织起来。
不过,这棵树的组织方式有点特殊, 是长这样的...

图中连着的节点, 就是list_head.
这个 list_head 是一个链表节点, 是嵌在 task_struct 中的.
内核获取到这个 list_head 对象后, 可以直接使用 container_of 宏来获取包含它的结构体, 也就是说, 你我们可以这么干:
  1. struct task_struct* tsk = container_of(sibling);
复制代码
此时, sibling 链表节点就相当于一个身份牌, 我们只需要通过遍历链表, 来知道这个身份牌, 然后使用 container_of 宏就可以通过这个身份牌获取到整个进程信息.
关于这方面的解释, 可以见我关于数据结构的blog, 我先挖个坑吧(好吧, 日常挖坑)
3.线程

在 LKD 中是这么描述线程的:

对, 在调度器的眼中, 线程就是 共享地址空间, 共享打开文件表, 共享文件系统, 共享信号 的进程. task_struct是参与调度的最小单位.
但是, 这只是在调度器的眼中, 既然是线程, 那肯定会做一些特殊处理, 并且肯定会以某种奇怪的方式存在于进程中.
为了探究这些, 让我们看一下 fork 的其中几行源代码(准确的说, 是 copy_process):
  1.         if (clone_flags & CLONE_THREAD) {//线程组                current->signal->nr_threads++; //持有信号的总线程数量+1                atomic_inc(&current->signal->live); //存活线程+1                atomic_inc(&current->signal->sigcnt); //此结构体的引用计数+1                p->group_leader = current->group_leader; //设置线程组的组长                list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);                //thread_group加入线程组链表,方便内核foreach同一线程组内的所有线程        }
复制代码
相信你们第一看到这个代码的时候, 估计整个人都懵了吧, 反正我是懵了:
current->signal是什么鬼??? thread_group又是什么鬼???
答案就在---
4. 线程组和进程

对, 没想到吧? Linux 中, 进程实际上就是一组线程, 换句话说, 一个线程组.
而线程组那肯定就得有一个组长(group_leader), 而这个组长, 就代表整个组, 也就是进程. 在 Linux 中, 有一个函数, 可以判断一个 task_struct 是不是进程(更准确的说, 线程组的组长):
  1. static inline bool thread_group_leader(struct task_struct *p);
复制代码
4.1 这个线程组的组员存在哪?

那其他不是组长的进程, 存在哪呢?
误区: 上面的 sibling, parent 等成员都描述的是进程. 线程是不能存在这些链表中的.
对于线程, task_struct有一个专门的链表来存放这些线程:
  1. struct task_struct {    struct list_head thread_group; //线程组};
复制代码
所有进程的子线程, 都会存放在这. 而子线程添加线程组的代码, 位于上面所描述的这行:
  1.     INIT_LIST_HEAD(&p->thread_group);    //...    if (clone_flags & CLONE_THREAD) //线程组                list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);                //thread_group加入线程组链表,方便内核foreach同一线程组内的所有线程
复制代码
4.2 signal_struct->live又是什么鬼?

然后, 还有一点, 就是必须有一堆结构体, 来管理线程组, 其中有一个结构体是长这样的
  1. struct signal_struct {    atomic_t        sigcnt; //对结构体的引用计数?    atomic_t                live; //还有多少个活着的进程?    //...}
复制代码
关于引用计数 sigcnt, 这个涉及到另一个主题--资源引用计数.
这个结构体说来话长, 原来是管信号的. 后面开发者发现这个结构体的一些其他的东西可以用来管进程本身, 于是顺带就拿来管理进程了, 例如这个 live 变量, 就是线程组中剩下的线程的数量.
这个 live 变量在初始化的时候, 在 copy_signal 函数里:
  1. static int copy_signal(unsigned long clone_flags, struct task_struct *tsk){        struct signal_struct *sig;        if (clone_flags & CLONE_THREAD) //状态是线程                return 0; //不予初始化        sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL); //分配内存块        tsk->signal = sig; //设置信号        if (!sig)                return -ENOMEM;        sig->nr_threads = 1; //初始化为1        atomic_set(&sig->live, 1); //存活进程为1        atomic_set(&sig->sigcnt, 1); //进程信号为1    //...}
复制代码
如图, 这玩意在 tsk 为进程的时候, 压根不给机会初始化, 所以也不会进行 live 和 sigcnt 的设置.
而在 copy_process 中, 才会对线程的 live 和 sigcnt 做自增:
  1.         if (clone_flags & CLONE_THREAD) {//线程                current->signal->nr_threads++; //持有信号的总线程数量+1                atomic_inc(&current->signal->live); //存活线程+1                atomic_inc(&current->signal->sigcnt); //此结构体的引        //...    }
复制代码
于是, 就有了上面的代码.
那你可能会问了:
5. 父子进程是如何关联的? pid, tgid到底是什么?

还是在 copy_process 中, 有几行很有趣的代码:
  1.         p->pid = pid_nr(pid);        p->tgid = p->pid;        if (clone_flags & CLONE_THREAD) //线程                p->tgid = current->tgid; //线程的tgid是父亲进程    //...        if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {                p->real_parent = current->real_parent;        } else {                p->real_parent = current;        }
复制代码
首先来看上面的代码, 它貌似给每个进程都分配了 pid, 但是这个其实并不是我们用户态下看到的pid. 在线程的语境下, 这个其实是线程的 tid(线程ID) .
而我们在用户态下看到的 pid, 实际上是 线程组ID(tgid). 所以, 你会看到当 CLONE_THREAD 的时候, 它 tgid 是当前进程的 pid(仔细想想, 就清楚了, 这样能保证 子线程创建子线程的场景也能获得进程的pid).
下面的代码处理了一个什么情况呢? 当p是线程的时候, 父进程的语义就变了, 变成线程组长的父进程, 此时此刻, 所有子线程(纵然是子线程嵌套子线程)和主线程是兄弟关系.
The End

由此, 我们可以得出这样的一个 Linux 进程树模型:

本期文章写到这, 感谢大家的观看哦~萌新初涉 Linux 内核, 有错误也请多多指正~
版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
作者: Sudo-su-Bash (Alien-Bash)
发布时间: 2026-02-18
原文链接: https://www.cnblogs.com/SudosuBash/p/19623122

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册