博客 / 詳情

返回

[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

對, 在調度器的眼中, 線程就是 共享地址空間, 共享打開文件表, 共享文件系統, 共享信號 的進程. 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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.