[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 对象. 而每个 task_struct 都有很多个 list_head 类型的对象, 然后这些 list_head 分别为某个链表/树的一个节点.

内核获取到这个 list_head 对象后, 可以直接使用 container_of 宏来获取包含它的结构体, 也可以通过 list::next 成员来获取下一个节点的 list_head, 从而获取下面一个 task_struct. 也就是说, 我们可以这么干:

//ts是某个任务对象
struct task_struct* tsk = container_of(ts->sibling->next, task_struct, sibling);
//这个的作用就是获取和 ts 同级别的任务链表的下一个任务. 例如如果 ts 是上面图的 pid=3 的任务, 那么 tsk 就是上面图的 pid=4 的任务.
//注意, 有些时候container_of是有坑的, 这个就涉及到这个宏的原理层面的东西了, 所以后续的文章里面讲

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

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

3.线程

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

thread_clone

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

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

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

	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 是不是进程(更准确的说, 线程组的组长):

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

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

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

	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 中, 有几行很有趣的代码:

	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 进程树模型:

pgtree

本期文章写到这, 感谢大家的观看哦~萌新初涉 Linux 内核, 有错误也请多多指正~

版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
作者: Sudo-su-Bash (Alien-Bash)
发布时间: 2026-02-18
原文链接: https://www.cnblogs.com/SudosuBash/p/19623122

posted @ 2026-02-18 14:28  SudosuBash  阅读(149)  评论(0)    收藏  举报