当前位置: 首页 > news >正文

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

Linux 3.2 进程, 线程

前言

注意: 本文章默认你学过操作系统的进程部分,了解进程的概念.

我们都知道, 在 Linux 中, 我们使用 LWP 来描述线程, 即不区分线程/进程, 统一用 task_struct 描述它.

但是在 Linux 中, 线程, 进程, 进程组实际上还是有点区别的.

这篇文章来聊聊进程, 线程.

1.Linux 的 task_struct

1.1 task_struct 结构体介绍

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

tss

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

1.2 CPU核心如何获取当前进程的的 task_struct?

答案藏在之前所讲的 current_thread_info 里面.

struct thread_info {struct task_struct	*task;//...
}

之前文章讲过, 在 x86_64 中, thread_info 存在于当前 CPU 核心的内核栈中, 而 current_thread_info 就是获取这个变量.

那我们只需要执行以下代码, 就可以获取当前正在执行的 task_struct 了:

struct task_struct* tsk = current_thread_info()->task;

2.Linux 中的进程链表

首先我们要研究的就是 task_struct 中的如下几个字段.

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 中, 管理进程用的是

不过,这棵树的组织方式有点特殊, 是长这样的...

proc1

图中连着的节点, 就是list_head.

这个 list_head 是一个链表节点, 是嵌在 task_struct 中的.

内核获取到这个 list_head 对象后, 可以直接使用 container_of 宏来获取包含它的结构体, 也就是说, 你我们可以这么干:

struct task_struct* tsk = container_of(sibling);

此时, sibling 链表节点就相当于一个身份牌, 我们只需要通过遍历链表, 来知道这个身份牌, 然后使用 container_of 宏就可以通过这个身份牌获取到整个进程信息.

关于这方面的解释, 可以见我关于数据结构的blog, 我先挖个坑吧(好吧, 日常挖坑)

3.线程

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

thread_clone
clone](thread_clone.png)

对, 在调度器的眼中, 线程就是 共享地址空间, 共享打开文件表, 共享文件系统, 共享信号 的进程. task_struct是参与调度的最小单位.

但是, 这只是在调度器的眼中, 既然是线程, 那肯定会做一些特殊处理, 并且肯定会以某种奇怪的方式存在于进程中.

为了探究这些, 让我们看一下 fork 的其中几行源代码(准确的说, 是 copy_process):

	if (clone_flags & CLONE_THREAD) {//线程组current->signal->nr_threads++; //持有信号的总线程数量+1atomic_inc(&current->signal->live); //存活线程+1atomic_inc(&current->signal->sigcnt); //此结构体的引用计数+1p->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 是不是进程(更准确的说, 线程组的组长):

static inline bool thread_group_leader(struct task_struct *p);

4.1 这个线程组的组员存在哪?

那其他不是组长的进程, 存在哪呢?

误区: 上面的 sibling, parent 等成员都描述的是进程. 线程是不能存在这些链表中的.

对于线程, task_struct有一个专门的链表来存放这些线程:

struct task_struct {struct list_head thread_group; //线程组
};

所有进程的子线程, 都会存放在这. 而子线程添加线程组的代码, 位于上面所描述的这行:

    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又是什么鬼?

然后, 还有一点, 就是必须有一堆结构体, 来管理线程组, 其中有一个结构体是长这样的

struct signal_struct {atomic_t        sigcnt; //对结构体的引用计数?atomic_t		live; //还有多少个活着的进程?//...
}

关于引用计数 sigcnt, 这个涉及到另一个主题--资源引用计数.

这个结构体说来话长, 原来是管信号的. 后面开发者发现这个结构体的一些其他的东西可以用来管进程本身, 于是顺带就拿来管理进程了, 例如这个 live 变量, 就是线程组中剩下的线程的数量.

这个 live 变量在初始化的时候, 在 copy_signal 函数里:

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; //初始化为1atomic_set(&sig->live, 1); //存活进程为1atomic_set(&sig->sigcnt, 1); //进程信号为1//...
}

如图, 这玩意在 tsk 为进程的时候, 压根不给机会初始化, 所以也不会进行 livesigcnt 的设置.

而在 copy_process 中, 才会对线程的 livesigcnt 做自增:

	if (clone_flags & CLONE_THREAD) {//线程current->signal->nr_threads++; //持有信号的总线程数量+1atomic_inc(&current->signal->live); //存活线程+1atomic_inc(&current->signal->sigcnt); //此结构体的引//...}

于是, 就有了上面的代码.

那你可能会问了:

5. 父子进程是如何关联的? pid, tgid到底是什么?

还是在 copy_process 中, 有几行很有趣的代码:

	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 内核, 有错误也请多多指正~

http://www.jsqmd.com/news/391985/

相关文章:

  • 用过才敢说 一键生成论文工具 千笔·专业学术智能体 VS WPS AI 专科生必备
  • 从此告别拖延!最受喜爱的AI论文工具 —— 千笔AI
  • 强烈安利! 专科生专属降AI率网站 千笔·专业降AI率智能体 VS 灵感ai
  • 一文讲透|9个AI论文软件测评:本科生毕业论文写作全攻略
  • github镜像代理网站2026
  • 摆脱论文困扰!10个AI论文网站测评:专科生毕业论文+开题报告写作神器推荐
  • 深入解析:生产级 Amazon MSK (Express 模式) 架构构建与选型实战白皮书
  • 英语_阅读_Three Good Things of the Day_待读
  • 题解:洛谷 P1955 [NOI2015] 程序自动分析
  • 题解:洛谷 P1892 [BalticOI 2003] 团伙
  • 计算机视觉opencv之金字塔直方图 - 详解
  • 题解:洛谷 P1229 遍历问题
  • 百联OK卡回收攻略:快速实现交易,解决常见问题 - 团团收购物卡回收
  • 西门子 博图V15洁净室温湿度串级控制结构化编程 使用串级PID的方式控制空调的回风,送风的温...
  • 英语_作文练习_Curiosity Leads Me_待读
  • 题解:洛谷 P5266 【深基17.例6】学籍管理
  • 题解:洛谷 P1918 保龄球
  • 2026评价好的接线防爆箱供应商怎么选?秘籍大揭秘,住宅配电柜/高压配电柜/金属封闭高压柜,防爆箱厂家怎么选择 - 品牌推荐师
  • 2026金相镶嵌机供应商推荐,性能稳定更可靠,单点加力金相磨抛机/试验机/电动洛氏硬度计,金相镶嵌机企业找哪家 - 品牌推荐师
  • 永辉超市卡怎么回收?实用技巧让你不再浪费! - 团团收购物卡回收
  • COMSOL仿真研究:单个金纳米颗粒光热效应的复现与波动光学、固体传热机理的探索
  • YOLOv12 改进 | Backbone改进 2
  • 生产环境【大模型学习】提示词工程(Prompt Engineering)技术深度报告最佳实践与性能优化
  • 学习笔记:连续子数组和问题的优化思路与工程实现思考
  • 学习笔记:二进制数组中0和1数量相等的最长连续子数组——从常规解法到性能优化
  • 量子网络:从理论到工程化探索
  • 分期乐购物额度回收平台推荐:省钱、省力的优选方法 - 团团收购物卡回收
  • PNG 转 JPG 在线工具推荐:免费、批量、无需注册的实用网站整理
  • 深入解析:基于机器学习的农产品价格数据分析与预测系统
  • 定稿前必看!10个降AIGC工具:继续教育降AI率全测评