一. 前言
在前文中,我們分析了內核啓動的整個過程以及系統調用的過程,從本文開始我們會介紹Linux系統各個重要的組成部分。這一切就從進程和線程開始,在 Linux 裏面,無論是進程,還是線程,到了內核裏面,我們統一都叫任務(Task),由一個統一的結構 task_struct
進行管理。這個結構非常複雜,本文將細細分析task_struct
結構。主要分析順序會按照該架構體中的成員變量和函數的作用進行分類,主要包括:
- 任務ID
- 親緣關係
- 任務狀態
- 任務權限
- 運行統計
- 進程調度
- 信號處理
- 內存管理
- 文件與文件系統
- 內核棧
二. 詳細介紹
2.1 任務ID
任務ID是任務的唯一標識,在tast_struct
中,主要涉及以下幾個ID
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;
之所以有pid(process id)
,tgid(thread group ID)
以及group_leader
,是因爲線程和進程在內核中是統一管理,視爲相同的任務(task)。
任何一個進程,如果只有主線程,那 pid
和tgid
相同,group_leader
指向自己。但是,如果一個進程創建了其他線程,那就會有所變化了。線程有自己的pid
,tgid
就是進程的主線程的 pid
,group_leader
指向的進程的主線程。因此根據pid
和tgid
是否相等我們可以判斷該任務是進程還是線程。
2.2 親緣關係
除了0號進程以外,其他進程都是有父進程的。全部進程其實就是一顆進程樹,相關成員變量如下所示
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
parent
指向其父進程。當它終止時,必須向它的父進程發送信號。children
指向子進程鏈表的頭部。鏈表中的所有元素都是它的子進程。sibling
用於把當前進程插入到兄弟鏈表中。
通常情況下,real_parent 和 parent 是一樣的,但是也會有另外的情況存在。例如,bash 創建一個進程,那進程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 來 debug 一個進程,這個時候 GDB 是 parent,bash 是這個進程的 real_parent。
2.3 任務狀態
任務狀態部分主要涉及以下變量
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;
其中狀態state
通過設置比特位的方式來賦值,具體值在include/linux/sched.h
中定義
/* Used in tsk->state: */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* Used in tsk->exit_state: */
#define EXIT_DEAD 16
#define EXIT_ZOMBIE 32
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_NOLOAD 1024
#define TASK_NEW 2048
#define TASK_STATE_MAX 4096
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
TASK_RUNNING
並不是說進程正在運行,而是表示進程在時刻準備運行的狀態。當處於這個狀態的進程獲得時間片的時候,就是在運行中;如果沒有獲得時間片,就說明它被其他進程搶佔了,在等待再次分配時間片。在運行中的進程,一旦要進行一些 I/O 操作,需要等待 I/O 完畢,這個時候會釋放 CPU,進入睡眠狀態。
在 Linux 中,有兩種睡眠狀態。
- 一種是
TASK_INTERRUPTIBLE
,可中斷的睡眠狀態。這是一種淺睡眠的狀態,也就是說,雖然在睡眠,等待 I/O 完成,但是這個時候一個信號來的時候,進程還是要被喚醒。只不過喚醒後,不是繼續剛纔的操作,而是進行信號處理。當然程序員可以根據自己的意願,來寫信號處理函數,例如收到某些信號,就放棄等待這個 I/O 操作完成,直接退出;或者收到某些信息,繼續等待。 - 另一種睡眠是
TASK_UNINTERRUPTIBLE
,不可中斷的睡眠狀態。這是一種深度睡眠狀態,不可被信號喚醒,只能死等 I/O 操作完成。一旦 I/O 操作因爲特殊原因不能完成,這個時候,誰也叫不醒這個進程了。你可能會說,我 kill 它呢?別忘了,kill 本身也是一個信號,既然這個狀態不可被信號喚醒,kill 信號也被忽略了。除非重啓電腦,沒有其他辦法。因此,這其實是一個比較危險的事情,除非程序員極其有把握,不然還是不要設置成TASK_UNINTERRUPTIBLE
。 - 於是,我們就有了一種新的進程睡眠狀態,
TASK_KILLABLE
,可以終止的新睡眠狀態。進程處於這種狀態中,它的運行原理類似TASK_UNINTERRUPTIBLE
,只不過可以響應致命信號。由於TASK_WAKEKILL
用於在接收到致命信號時喚醒進程,因此TASK_KILLABLE
即在TASK_UNINTERUPTIBLE
的基礎上增加一個TASK_WAKEKILL
標記位即可。
TASK_STOPPED
是在進程接收到 SIGSTOP
、SIGTTIN
、SIGTSTP
或者 SIGTTOU
信號之後進入該狀態。
TASK_TRACED
表示進程被 debugger 等進程監視,進程執行被調試程序所停止。當一個進程被另外的進程所監視,每一個信號都會讓進程進入該狀態。
一旦一個進程要結束,先進入的是 EXIT_ZOMBIE
狀態,但是這個時候它的父進程還沒有使用wait()
等系統調用來獲知它的終止信息,此時進程就成了殭屍進程。EXIT_DEAD
是進程的最終狀態。EXIT_ZOMBIE
和 EXIT_DEAD
也可以用於 exit_state
。
上面的進程狀態和進程的運行、調度有關係,還有其他的一些狀態,我們稱爲標誌。放在 flags
字段中,這些字段都被定義成爲宏,以 PF 開頭。
#define PF_EXITING 0x00000004
#define PF_VCPU 0x00000010
#define PF_FORKNOEXEC 0x00000040
PF_EXITING
表示正在退出。當有這個 flag 的時候,在函數 find_alive_thread()
中,找活着的線程,遇到有這個 flag 的,就直接跳過。
PF_VCPU
表示進程運行在虛擬 CPU 上。在函數 account_system_time
中,統計進程的系統運行時間,如果有這個 flag,就調用 account_guest_time
,按照客戶機的時間進行統計。
PF_FORKNOEXEC
表示 fork
完了,還沒有 exec
。在 _do_fork ()
函數裏面調用 copy_process()
,這個時候把 flag 設置爲 PF_FORKNOEXEC()
。當 exec
中調用了 load_elf_binary()
的時候,又把這個 flag 去掉。
2.4 任務權限
任務權限主要包括以下兩個變量,real_cred
是指可以操作本任務的對象,而red
是指本任務可以操作的對象。
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
cred定義如下所示
struct cred {
......
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
......
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
......
} __randomize_layout;
從這裏的定義可以看出,大部分是關於用戶和用戶所屬的用戶組信息。
uid
和gid
,註釋是 real user/group id。一般情況下,誰啓動的進程,就是誰的 ID。但是權限審覈的時候,往往不比較這兩個,也就是說不大起作用。euid
和egid
,註釋是 effective user/group id。一看這個名字,就知道這個是起“作用”的。當這個進程要操作消息隊列、共享內存、信號量等對象的時候,其實就是在比較這個用戶和組是否有權限。fsuid
和fsgid
,也就是 filesystem user/group id。這個是對文件操作會審覈的權限。
在Linux中,我們可以通過chmod u+s program
命令更改更改euid
和fsuid
來獲取權限。
除了以用戶和用戶組控制權限,Linux 還有另一個機制就是 capabilities
。
原來控制進程的權限,要麼是高權限的 root 用戶,要麼是一般權限的普通用戶,這時候的問題是,root 用戶權限太大,而普通用戶權限太小。有時候一個普通用戶想做一點高權限的事情,必須給他整個 root 的權限。這個太不安全了。於是,我們引入新的機制 capabilities,用位圖表示權限,在capability.h
可以找到定義的權限。我這裏列舉幾個。
#define CAP_CHOWN 0
#define CAP_KILL 5
#define CAP_NET_BIND_SERVICE 10
#define CAP_NET_RAW 13
#define CAP_SYS_MODULE 16
#define CAP_SYS_RAWIO 17
#define CAP_SYS_BOOT 22
#define CAP_SYS_TIME 25
#define CAP_AUDIT_READ 37
#define CAP_LAST_CAP CAP_AUDIT_READ
對於普通用戶運行的進程,當有這個權限的時候,就能做這些操作;沒有的時候,就不能做,這樣粒度要小很多。
2.5 運行統計
運行統計從宏觀來說也是一種狀態變量,但是和任務狀態不同,其存儲的主要是運行時間相關的成員變量,具體如下所示
u64 utime;//用戶態消耗的CPU時間
u64 stime;//內核態消耗的CPU時間
unsigned long nvcsw;//自願(voluntary)上下文切換計數
unsigned long nivcsw;//非自願(involuntary)上下文切換計數
u64 start_time;//進程啓動時間,不包含睡眠時間
u64 real_start_time;//進程啓動時間,包含睡眠時間
2.6 進程調度
進程調度部分較爲複雜,會單獨拆分講解,這裏先簡單羅列成員變量。
//是否在運行隊列上
int on_rq;
//優先級
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
//調度器類
const struct sched_class *sched_class;
//調度實體
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
//調度策略
unsigned int policy;
//可以使用哪些CPU
int nr_cpus_allowed;
cpumask_t cpus_allowed;
struct sched_info sched_info;
2.7 信號處理
信號處理相關的數據結構如下所示
/* Signal handlers: */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked;
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
這裏將信號分爲三類
- 阻塞暫不處理的信號(blocked)
- 等待處理的信號(pending)
- 正在通過信號處理函數處理的信號(sighand)
信號處理函數默認使用用戶態的函數棧,當然也可以開闢新的棧專門用於信號處理,這就是 sas_ss_xxx
這三個變量的作用。
2.8 內存管理
內存管理部分成員變量如下所示
struct mm_struct *mm;
struct mm_struct *active_mm;
由於內存部分較爲複雜,會放在後面單獨介紹,這裏了先不做詳細說明。
2.9 文件與文件系統
文件系統部分也會在後面詳細說明,這裏先簡單列舉成員變量
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
2.10 內核棧
內核棧相關的成員變量如下所示。爲了介紹清楚其作用,我們需要從爲什麼需要內核棧開始逐步討論。
struct thread_info thread_info;
void *stack;
當進程產生系統調用時,會利用中斷陷入內核態。而內核態中也存在着各種函數的調用,因此我們需要有內核態函數棧。Linux 給每個 task 都分配了內核棧。在 32 位系統上 arch/x86/include/asm/page_32_types.h
,是這樣定義的:一個 PAGE_SIZE
是 4K,左移一位就是乘以 2,也就是 8K。
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
內核棧在 64 位系統上 arch/x86/include/asm/page_64_types.h
,是這樣定義的:在 PAGE_SIZE 的基礎上左移兩位,也即 16K,並且要求起始地址必須是 8192 的整數倍。
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
內核棧的結構如下所示,首先是預留的8個字節,然後是存儲寄存器,最後存儲thread_info
結構體。
這個結構是對 task_struct
結構的補充。因爲 task_struct
結構龐大但是通用,不同的體系結構就需要保存不同的東西,所以往往與體系結構有關的,都放在 thread_info
裏面。在內核代碼裏面採用一個 union
將thread_info
和stack
放在一起,在 include/linux/sched.h
中定義用以表示內核棧。由代碼可見,這裏根據架構不同可能採用舊版的task_struct
直接放在內核棧,而新版的均採用thread_info
,以節約空間。
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
另一個結構 pt_regs
,定義如下。其中,32 位和 64 位的定義不一樣。
#ifdef __i386__
struct pt_regs {
unsigned long bx;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long bp;
unsigned long ax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};
#else
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
#endif
內核棧和task_struct
是可以互相查找的,而這裏就需要用到task_struct
中的兩個內核棧相關成員變量了。
2.10.1 通過task_struct查找內核棧
如果有一個 task_struct
的 stack
指針在手,即可通過下面的函數找到這個線程內核棧:
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
從 task_struct
如何得到相應的 pt_regs
呢?我們可以通過下面的函數,先從 task_struct
找到內核棧的開始位置。然後這個位置加上 THREAD_SIZE
就到了最後的位置,然後轉換爲 struct pt_regs
,再減一,就相當於減少了一個 pt_regs
的位置,就到了這個結構的首地址。
/*
* TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.
* This is necessary to guarantee that the entire "struct pt_regs"
* is accessible even if the CPU haven't stored the SS/ESP registers
* on the stack (interrupt gate does not save these registers
* when switching to the same priv ring).
* Therefore beware: accessing the ss/esp fields of the
* "struct pt_regs" is possible, but they may contain the
* completely wrong values.
*/
#define task_pt_regs(task) \
({ \
unsigned long __ptr = (unsigned long)task_stack_page(task); \
__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \
((struct pt_regs *)__ptr) - 1; \
})
這裏面有一個TOP_OF_KERNEL_STACK_PADDING
,這個的定義如下:
#ifdef CONFIG_X86_32
# ifdef CONFIG_VM86
# define TOP_OF_KERNEL_STACK_PADDING 16
# else
# define TOP_OF_KERNEL_STACK_PADDING 8
# endif
#else
# define TOP_OF_KERNEL_STACK_PADDING 0
#endif
也就是說,32 位機器上是 8,其他是 0。這是爲什麼呢?因爲壓棧 pt_regs
有兩種情況。我們知道,CPU 用 ring 來區分權限,從而 Linux 可以區分內核態和用戶態。因此,第一種情況,我們拿涉及從用戶態到內核態的變化的系統調用來說。因爲涉及權限的改變,會壓棧保存 SS、ESP 寄存器的,這兩個寄存器共佔用 8 個 byte。另一種情況是,不涉及權限的變化,就不會壓棧這 8 個 byte。這樣就會使得兩種情況不兼容。如果沒有壓棧還訪問,就會報錯,所以還不如預留在這裏,保證安全。在 64 位上,修改了這個問題,變成了定長的。
2.10.2 通過內核棧找task_struct
首先來看看thread_info
的定義吧。下面所示爲早期版本的thread_info
和新版本thread_info
的源碼
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
struct thread_info {
unsigned long flags; /* low level flags */
unsigned long status; /* thread synchronous flags */
};
老版中採取current_thread_info()->task
來獲取task_struct
。thread_info
的位置就是內核棧的最高位置,減去 THREAD_SIZE,就到了 thread_info
的起始地址。
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);
}
而新版本則採用了另一種current_thread_info
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif
那 current
又是什麼呢?在 arch/x86/include/asm/current.h
中定義了。
struct task_struct;
DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(current_task);
}
#define current get_current
新的機制裏面,每個 CPU 運行的 task_struct
不通過thread_info
獲取了,而是直接放在 Per CPU 變量裏面了。多核情況下,CPU 是同時運行的,但是它們共同使用其他的硬件資源的時候,我們需要解決多個 CPU 之間的同步問題。Per CPU 變量是內核中一種重要的同步機制。顧名思義,Per CPU 變量就是爲每個 CPU 構造一個變量的副本,這樣多個 CPU 各自操作自己的副本,互不干涉。比如,當前進程的變量 current_task 就被聲明爲 Per CPU 變量。要使用 Per CPU 變量,首先要聲明這個變量,在 arch/x86/include/asm/current.h
中有:
DECLARE_PER_CPU(struct task_struct *, current_task);
然後是定義這個變量,在 arch/x86/kernel/cpu/common.c
中有:
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;
也就是說,系統剛剛初始化的時候,current_task
都指向init_task
。當某個 CPU 上的進程進行切換的時候,current_task
被修改爲將要切換到的目標進程。例如,進程切換函數__switch_to
就會改變 current_task
。
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
......
this_cpu_write(current_task, next_p);
......
return prev_p;
}
當要獲取當前的運行中的 task_struct
的時候,就需要調用 this_cpu_read_stable
進行讀取。
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
通過這種方式,即可輕鬆的獲得task_struct
的地址。
三. 總結
本文大體介紹了task_struct
的整體結構,對於很多涉及到複雜模塊的部分並未展開講解,在後文中會一一敘述。
相關源碼
[1] task_struct
參考文獻
[1] Linux-insides
[2] 深入理解Linux內核
[3] Linux內核設計的藝術
[4] 極客時間 趣談Linux操作系統