深入了解Linux內(nèi)核:task_struct結(jié)構(gòu)詳解
在 Linux 系統(tǒng)那如浩瀚宇宙般復(fù)雜又精妙的內(nèi)核世界里,隱藏著無數(shù)掌控全局的 “關(guān)鍵密碼”,而今天要帶大家認識的 task_struct 結(jié)構(gòu),無疑是其中最為耀眼的一顆明星。當你打開電腦,啟動 Linux 系統(tǒng),瞬間仿佛開啟了一場盛大的狂歡派對,無數(shù)的進程在幕后馬不停蹄地忙碌著,有的負責(zé)渲染精美的圖形界面,有的保障網(wǎng)絡(luò)連接順暢無阻,還有的默默守護著系統(tǒng)的安全防線。而這每一個進程,它們的 “身世檔案”、“成長軌跡” 以及 “一舉一動”,統(tǒng)統(tǒng)都被記錄在一個神奇的結(jié)構(gòu)體 ——task_struct 之中。
它就像是一位超級幕后管家,知曉進程何時誕生,由哪個用戶啟動,占用了多少寶貴的系統(tǒng)資源,又該在何時退場謝幕。無論是深入探究系統(tǒng)性能瓶頸,精準調(diào)試詭異的程序錯誤,還是試圖理解 Linux 內(nèi)核如何有條不紊地調(diào)度千軍萬馬般的進程,掌握 task_struct 結(jié)構(gòu),都如同握住了一把開啟內(nèi)核智慧寶庫的萬能鑰匙。此刻,就請緊跟我的腳步,一起深入剖析這個 Linux 內(nèi)核中至關(guān)重要的 task_struct 結(jié)構(gòu),探尋進程背后那些不為人知的精彩故事吧!
一、引言
在前文中,我們分析了內(nèi)核啟動的整個過程以及系統(tǒng)調(diào)用的過程,從本文開始我們會介紹Linux系統(tǒng)各個重要的組成部分。這一切就從進程和線程開始,在 Linux 里面,無論是進程,還是線程,到了內(nèi)核里面,我們統(tǒng)一都叫任務(wù)(Task),由一個統(tǒng)一的結(jié)構(gòu) task_struct 進行管理。
這個結(jié)構(gòu)非常復(fù)雜,本文將細細分析task_struct結(jié)構(gòu)。主要分析順序會按照該架構(gòu)體中的成員變量和函數(shù)的作用進行分類,主要包括:
- 任務(wù)ID
- 親緣關(guān)系
- 任務(wù)狀態(tài)
- 任務(wù)權(quán)限
- 運行統(tǒng)計
- 進程調(diào)度
- 信號處理
- 內(nèi)存管理
- 文件與文件系統(tǒng)
- 內(nèi)核棧
二、Task_struct結(jié)構(gòu)
2.1 任務(wù)ID
任務(wù)ID是任務(wù)的唯一標識,在tast_struct中,主要涉及以下幾個ID
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;
之所以有pid(process id),tgid(thread group ID)以及group_leader,是因為線程和進程在內(nèi)核中是統(tǒng)一管理,視為相同的任務(wù)(task)。
任何一個進程,如果只有主線程,那 pid 和tgid相同,group_leader 指向自己。但是,如果一個進程創(chuàng)建了其他線程,那就會有所變化了。線程有自己的pid,tgid 就是進程的主線程的 pid,group_leader 指向的進程的主線程。因此根據(jù)pid和tgid是否相等我們可以判斷該任務(wù)是進程還是線程。
2.2 親緣關(guān)系
除了0號進程以外,其他進程都是有父進程的。全部進程其實就是一顆進程樹,相關(guān)成員變量如下所示:
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 指向其父進程。當它終止時,必須向它的父進程發(fā)送信號。
- children 指向子進程鏈表的頭部。鏈表中的所有元素都是它的子進程。
- sibling 用于把當前進程插入到兄弟鏈表中。
通常情況下,real_parent 和 parent 是一樣的,但是也會有另外的情況存在。例如,bash 創(chuàng)建一個進程,那進程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 來 debug 一個進程,這個時候 GDB 是 parent,bash 是這個進程的 real_parent。
2.3 任務(wù)狀態(tài)
任務(wù)狀態(tài)部分主要涉及以下變量
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;
其中狀態(tài)state通過設(shè)置比特位的方式來賦值,具體值在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并不是說進程正在運行,而是表示進程在時刻準備運行的狀態(tài)。當處于這個狀態(tài)的進程獲得時間片的時候,就是在運行中;如果沒有獲得時間片,就說明它被其他進程搶占了,在等待再次分配時間片。在運行中的進程,一旦要進行一些 I/O 操作,需要等待 I/O 完畢,這個時候會釋放 CPU,進入睡眠狀態(tài)。
在Linux中有兩種睡眠狀態(tài):
- 一種是 TASK_INTERRUPTIBLE,可中斷的睡眠狀態(tài)。這是一種淺睡眠的狀態(tài),也就是說,雖然在睡眠,等待 I/O 完成,但是這個時候一個信號來的時候,進程還是要被喚醒。只不過喚醒后,不是繼續(xù)剛才的操作,而是進行信號處理。當然程序員可以根據(jù)自己的意愿,來寫信號處理函數(shù),例如收到某些信號,就放棄等待這個 I/O 操作完成,直接退出;或者收到某些信息,繼續(xù)等待。
- 另一種睡眠是 TASK_UNINTERRUPTIBLE,不可中斷的睡眠狀態(tài)。這是一種深度睡眠狀態(tài),不可被信號喚醒,只能死等 I/O 操作完成。一旦 I/O 操作因為特殊原因不能完成,這個時候,誰也叫不醒這個進程了。你可能會說,我 kill 它呢?別忘了,kill 本身也是一個信號,既然這個狀態(tài)不可被信號喚醒,kill 信號也被忽略了。除非重啟電腦,沒有其他辦法。因此,這其實是一個比較危險的事情,除非程序員極其有把握,不然還是不要設(shè)置成 TASK_UNINTERRUPTIBLE。
- 于是,我們就有了一種新的進程睡眠狀態(tài),TASK_KILLABLE,可以終止的新睡眠狀態(tài)。進程處于這種狀態(tài)中,它的運行原理類似 TASK_UNINTERRUPTIBLE,只不過可以響應(yīng)致命信號。由于TASK_WAKEKILL 用于在接收到致命信號時喚醒進程,因此TASK_KILLABLE即在TASK_UNINTERUPTIBLE的基礎(chǔ)上增加一個TASK_WAKEKILL標記位即可。
TASK_STOPPED是在進程接收到 SIGSTOP、SIGTTIN、SIGTSTP或者 SIGTTOU 信號之后進入該狀態(tài)。
TASK_TRACED 表示進程被 debugger 等進程監(jiān)視,進程執(zhí)行被調(diào)試程序所停止。當一個進程被另外的進程所監(jiān)視,每一個信號都會讓進程進入該狀態(tài)。
一旦一個進程要結(jié)束,先進入的是 EXIT_ZOMBIE 狀態(tài),但是這個時候它的父進程還沒有使用wait() 等系統(tǒng)調(diào)用來獲知它的終止信息,此時進程就成了僵尸進程。EXIT_DEAD 是進程的最終狀態(tài)。EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。
上面的進程狀態(tài)和進程的運行、調(diào)度有關(guān)系,還有其他的一些狀態(tài),我們稱為標志。放在 flags字段中,這些字段都被定義成為宏,以 PF 開頭。
#define PF_EXITING 0x00000004
#define PF_VCPU 0x00000010
#define PF_FORKNOEXEC 0x00000040
PF_EXITING 表示正在退出。當有這個 flag 的時候,在函數(shù) find_alive_thread() 中,找活著的線程,遇到有這個 flag 的,就直接跳過。
PF_VCPU 表示進程運行在虛擬 CPU 上。在函數(shù) account_system_time中,統(tǒng)計進程的系統(tǒng)運行時間,如果有這個 flag,就調(diào)用 account_guest_time,按照客戶機的時間進行統(tǒng)計。
PF_FORKNOEXEC 表示 fork 完了,還沒有 exec。在 _do_fork ()函數(shù)里面調(diào)用 copy_process(),這個時候把 flag 設(shè)置為 PF_FORKNOEXEC()。當 exec 中調(diào)用了 load_elf_binary() 的時候,又把這個 flag 去掉。
圖片
2.4 任務(wù)權(quán)限
任務(wù)權(quán)限主要包括以下兩個變量,real_cred是指可以操作本任務(wù)的對象,而red是指本任務(wù)可以操作的對象。
/* 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;
從這里的定義可以看出,大部分是關(guān)于用戶和用戶所屬的用戶組信息。
- uid和 gid,注釋是 real user/group id。一般情況下,誰啟動的進程,就是誰的 ID。但是權(quán)限審核的時候,往往不比較這兩個,也就是說不大起作用。
- euid 和 egid,注釋是 effective user/group id。一看這個名字,就知道這個是起“作用”的。當這個進程要操作消息隊列、共享內(nèi)存、信號量等對象的時候,其實就是在比較這個用戶和組是否有權(quán)限。
- fsuid 和fsgid,也就是 filesystem user/group id。這個是對文件操作會審核的權(quán)限。
在Linux中,我們可以通過chmod u+s program命令更改更改euid和fsuid來獲取權(quán)限。
除了以用戶和用戶組控制權(quán)限,Linux 還有另一個機制就是 capabilities。
原來控制進程的權(quán)限,要么是高權(quán)限的 root 用戶,要么是一般權(quán)限的普通用戶,這時候的問題是,root 用戶權(quán)限太大,而普通用戶權(quán)限太小。有時候一個普通用戶想做一點高權(quán)限的事情,必須給他整個 root 的權(quán)限。這個太不安全了。于是,我們引入新的機制 capabilities,用位圖表示權(quán)限,在capability.h可以找到定義的權(quán)限。我這里列舉幾個。
#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
對于普通用戶運行的進程,當有這個權(quán)限的時候,就能做這些操作;沒有的時候,就不能做,這樣粒度要小很多。
2.5 運行統(tǒng)計
運行統(tǒng)計從宏觀來說也是一種狀態(tài)變量,但是和任務(wù)狀態(tài)不同,其存儲的主要是運行時間相關(guān)的成員變量,具體如下所示
u64 utime;//用戶態(tài)消耗的CPU時間
u64 stime;//內(nèi)核態(tài)消耗的CPU時間
unsigned long nvcsw;//自愿(voluntary)上下文切換計數(shù)
unsigned long nivcsw;//非自愿(involuntary)上下文切換計數(shù)
u64 start_time;//進程啟動時間,不包含睡眠時間
u64 real_start_time;//進程啟動時間,包含睡眠時間
2.6 進程調(diào)度
進程調(diào)度部分較為復(fù)雜,會單獨拆分講解,這里先簡單羅列成員變量。
//是否在運行隊列上
int on_rq;
//優(yōu)先級
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
//調(diào)度器類
const struct sched_class *sched_class;
//調(diào)度實體
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
//調(diào)度策略
unsigned int policy;
//可以使用哪些CPU
int nr_cpus_allowed;
cpumask_t cpus_allowed;
struct sched_info sched_info;
2.7 信號處理
信號處理相關(guān)的數(shù)據(jù)結(jié)構(gòu)如下所示
/* 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)
- 正在通過信號處理函數(shù)處理的信號(sighand)
信號處理函數(shù)默認使用用戶態(tài)的函數(shù)棧,當然也可以開辟新的棧專門用于信號處理,這就是 sas_ss_xxx 這三個變量的作用。
2.8 內(nèi)存管理
內(nèi)存管理部分成員變量如下所示
struct mm_struct *mm;
struct mm_struct *active_mm;
由于內(nèi)存部分較為復(fù)雜,會放在后面單獨介紹,這里了先不做詳細說明。
2.9 文件與文件系統(tǒng)
文件系統(tǒng)部分也會在后面詳細說明,這里先簡單列舉成員變量
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
2.10 內(nèi)核棧
內(nèi)核棧相關(guān)的成員變量如下所示。為了介紹清楚其作用,我們需要從為什么需要內(nèi)核棧開始逐步討論。
struct thread_info thread_info;
void *stack;
當進程產(chǎn)生系統(tǒng)調(diào)用時,會利用中斷陷入內(nèi)核態(tài)。而內(nèi)核態(tài)中也存在著各種函數(shù)的調(diào)用,因此我們需要有內(nèi)核態(tài)函數(shù)棧。Linux 給每個 task 都分配了內(nèi)核棧。在 32 位系統(tǒng)上 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)
內(nèi)核棧在 64 位系統(tǒng)上 arch/x86/include/asm/page_64_types.h,是這樣定義的:在 PAGE_SIZE 的基礎(chǔ)上左移兩位,也即 16K,并且要求起始地址必須是 8192 的整數(shù)倍。
#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)
內(nèi)核棧的結(jié)構(gòu)如下所示,首先是預(yù)留的8個字節(jié),然后是存儲寄存器,最后存儲thread_info結(jié)構(gòu)體。
這個結(jié)構(gòu)是對 task_struct 結(jié)構(gòu)的補充。因為 task_struct 結(jié)構(gòu)龐大但是通用,不同的體系結(jié)構(gòu)就需要保存不同的東西,所以往往與體系結(jié)構(gòu)有關(guān)的,都放在 thread_info 里面。在內(nèi)核代碼里面采用一個 union將thread_info和stack 放在一起,在 include/linux/sched.h 中定義用以表示內(nèi)核棧。由代碼可見,這里根據(jù)架構(gòu)不同可能采用舊版的task_struct直接放在內(nèi)核棧,而新版的均采用thread_info,以節(jié)約空間。
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)];
};
另一個結(jié)構(gòu) 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
內(nèi)核棧和task_struct是可以互相查找的,而這里就需要用到task_struct中的兩個內(nèi)核棧相關(guān)成員變量了。
⑴通過task_struct查找內(nèi)核棧
如果有一個 task_struct 的 stack 指針在手,即可通過下面的函數(shù)找到這個線程內(nèi)核棧:
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
從 task_struct 如何得到相應(yīng)的 pt_regs 呢?我們可以通過下面的函數(shù),先從 task_struct找到內(nèi)核棧的開始位置。然后這個位置加上 THREAD_SIZE 就到了最后的位置,然后轉(zhuǎn)換為 struct pt_regs,再減一,就相當于減少了一個 pt_regs 的位置,就到了這個結(jié)構(gòu)的首地址。
/*
* 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 來區(qū)分權(quán)限,從而 Linux 可以區(qū)分內(nèi)核態(tài)和用戶態(tài)。因此,第一種情況,我們拿涉及從用戶態(tài)到內(nèi)核態(tài)的變化的系統(tǒng)調(diào)用來說。因為涉及權(quán)限的改變,會壓棧保存 SS、ESP 寄存器的,這兩個寄存器共占用 8 個 byte。另一種情況是,不涉及權(quán)限的變化,就不會壓棧這 8 個 byte。這樣就會使得兩種情況不兼容。如果沒有壓棧還訪問,就會報錯,所以還不如預(yù)留在這里,保證安全。在 64 位上,修改了這個問題,變成了定長的。
⑵通過內(nèi)核棧找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 的位置就是內(nèi)核棧的最高位置,減去 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 變量是內(nèi)核中一種重要的同步機制。顧名思義,Per CPU 變量就是為每個 CPU 構(gòu)造一個變量的副本,這樣多個 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;
也就是說,系統(tǒng)剛剛初始化的時候,current_task 都指向init_task。當某個 CPU 上的進程進行切換的時候,current_task 被修改為將要切換到的目標進程。例如,進程切換函數(shù)__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 的時候,就需要調(diào)用 this_cpu_read_stable 進行讀取。
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
通過這種方式,即可輕松地獲得task_struct的地址。
二、task_struct:進程的“靈魂檔案”
2.1 定義與地位
在 Linux 內(nèi)核的代碼世界里,task_struct被精心定義為一個結(jié)構(gòu)體,其內(nèi)部成員眾多,宛如一個龐大而有序的信息倉庫 。在<linux/sched.h>頭文件中,我們能一窺它的定義全貌:
struct task_struct {
volatile long state; /* 進程狀態(tài) */
void *stack; /* 指向內(nèi)核棧的指針 */
pid_t pid; /* 進程ID */
pid_t tgid; /* 線程組ID */
struct task_struct *real_parent; /* 指向真正的父進程 */
struct task_struct *parent; /* 指向接收SIGCHLD信號的父進程 */
struct list_head children; /* 子進程鏈表 */
struct list_head sibling; /* 兄弟進程鏈表 */
struct mm_struct *mm; /* 指向內(nèi)存描述符 */
struct mm_struct *active_mm; /* 指向活躍的內(nèi)存描述符 */
// 此處省略大量其他成員
};
task_struct就如同進程的 “靈魂檔案”,從進程誕生的那一刻起,它便如影隨形,詳細記錄著進程的各種屬性和狀態(tài)信息。無論是進程的唯一標識 —— 進程 ID(PID),還是進程所處的運行狀態(tài)(如運行、就緒、睡眠等),亦或是進程與其他進程之間的親屬關(guān)系(父進程、子進程、兄弟進程等),以及進程所占用的內(nèi)存資源、打開的文件描述符等關(guān)鍵信息,都被一一存儲在這個結(jié)構(gòu)體中??梢院敛豢鋸埖卣f,task_struct是進程在 Linux 內(nèi)核中的 “代言人”,內(nèi)核正是通過對task_struct結(jié)構(gòu)體的管理和操作,實現(xiàn)了對進程的創(chuàng)建、調(diào)度、終止等一系列生命周期的有效管控 。
2.2 內(nèi)存布局奧秘
在 Linux 內(nèi)核中,task_struct的內(nèi)存分配與內(nèi)核棧有著緊密的聯(lián)系 。通常情況下,內(nèi)核會為每個進程分配一個大小固定的內(nèi)存區(qū)域,這個區(qū)域同時包含了task_struct結(jié)構(gòu)體和進程的內(nèi)核棧。以常見的 x86 架構(gòu)為例,內(nèi)核會一次性分配兩個連續(xù)的物理頁面(每個頁面大小通常為 4KB,共 8KB)來存儲這兩部分內(nèi)容 。其中,task_struct結(jié)構(gòu)體大約占用底部的 1KB 空間,而剩余的 7KB 空間則用于存放進程的內(nèi)核棧 。
這種內(nèi)存布局方式并非隨意為之,而是有著深刻的設(shè)計考量 。一方面,將task_struct與內(nèi)核棧放在一起,能夠減少內(nèi)存碎片的產(chǎn)生,提高內(nèi)存的使用效率 。當進程創(chuàng)建時,一次性分配連續(xù)的內(nèi)存空間,避免了多次分配內(nèi)存可能導(dǎo)致的內(nèi)存碎片化問題 。另一方面,這種布局方式也方便了內(nèi)核在進行進程上下文切換時對相關(guān)信息的快速訪問 。在進程上下文切換過程中,內(nèi)核需要保存和恢復(fù)進程的各種狀態(tài)信息,包括 CPU 寄存器的值、堆棧指針等 。將task_struct與內(nèi)核棧相鄰放置,使得內(nèi)核能夠通過簡單的指針運算,快速找到并操作這些關(guān)鍵信息,從而大大提高了進程上下文切換的效率 。
例如,當進程從用戶態(tài)切換到內(nèi)核態(tài)時,CPU 需要將當前的堆棧指針切換到內(nèi)核棧的地址 。由于task_struct與內(nèi)核棧在內(nèi)存中是連續(xù)的,內(nèi)核可以根據(jù)task_struct中保存的棧指針信息,迅速定位到內(nèi)核棧的起始地址,完成堆棧指針的切換 。同樣,在進程返回用戶態(tài)時,內(nèi)核也能夠輕松地恢復(fù)用戶棧的地址和相關(guān)狀態(tài)信息 。這種緊密的內(nèi)存布局關(guān)系,就像是一場精心編排的舞蹈,task_struct與內(nèi)核棧相互配合,共同保障了進程在 Linux 內(nèi)核中的高效運行 。
2.3 task_struct 與系統(tǒng)調(diào)用
(1)以 fork 系統(tǒng)調(diào)用為例看 task_struct 的復(fù)制與初始化
在 Linux 中,fork 系統(tǒng)調(diào)用是創(chuàng)建新進程的重要方式。當執(zhí)行 fork 調(diào)用時,內(nèi)核會為新創(chuàng)建的子進程分配一個新的 task_struct 結(jié)構(gòu)體,這是整個創(chuàng)建子進程流程中極為關(guān)鍵的起始步驟。
起初,內(nèi)核會盡可能多地將父進程 task_struct 里的內(nèi)容復(fù)制到子進程的 task_struct 中。不過,這里要注意并非所有內(nèi)容都會馬上被原樣復(fù)制,像內(nèi)存頁相關(guān)的部分,就會采用寫時拷貝(COW)機制來優(yōu)化性能,避免不必要的資源浪費以及保證數(shù)據(jù)在后續(xù)使用時的獨立性和安全性。
例如,在一個文本編輯進程執(zhí)行 fork 操作創(chuàng)建子進程時,父進程 task_struct 中記錄的該文本編輯程序代碼段、數(shù)據(jù)段等相關(guān)內(nèi)存區(qū)域信息會先嘗試復(fù)制給子進程,而那些具體的內(nèi)存頁面數(shù)據(jù)可能暫不實際復(fù)制,只是設(shè)置好寫時拷貝相關(guān)的機制,等到子進程要對相應(yīng)內(nèi)存數(shù)據(jù)進行修改操作時,才會真正去復(fù)制一份屬于子進程自己的數(shù)據(jù)副本,以保證父子進程后續(xù)數(shù)據(jù)操作的互不干擾。
完成基本的復(fù)制后,內(nèi)核還需要進行一系列必要的修改來確保子進程能夠獨立于父進程運行。其中會設(shè)置子進程的 PID(進程 ID),使其擁有一個新的唯一的進程 ID,而父進程的 PID 保持不變;同時,子進程的 PPID(父進程 ID)會被設(shè)置為調(diào)用 fork 的進程的 PID。除此之外,像進程組、會話等關(guān)系也會相應(yīng)地更新或初始化,信號處理器、文件描述符表等同樣要進行適當?shù)恼{(diào)整,以符合子進程后續(xù)獨立運行的需求。
(2)對進程管理的影響與意義
task_struct 在 fork 系統(tǒng)調(diào)用創(chuàng)建子進程時的這種復(fù)制與初始化機制,對進程管理有著多方面重要的影響和意義。
從資源分配角度來看,通過寫時拷貝機制,在子進程創(chuàng)建初期可以避免大量不必要的內(nèi)存數(shù)據(jù)復(fù)制開銷,多個子進程可以在初期共享父進程的內(nèi)存資源,只有當真正需要修改數(shù)據(jù)時才各自分配獨立的內(nèi)存空間,這樣能更高效地利用系統(tǒng)內(nèi)存資源,尤其是在創(chuàng)建多個相似子進程的場景下,能顯著節(jié)省內(nèi)存開銷,使得系統(tǒng)整體資源分配更加合理且靈活。
在進程調(diào)度方面,新創(chuàng)建并初始化好的子進程會被加入到內(nèi)核的調(diào)度隊列中(通常是就緒隊列),等待 CPU 的調(diào)度執(zhí)行。每個子進程擁有獨立的 task_struct 結(jié)構(gòu)體,意味著調(diào)度器可以依據(jù)各個進程(包括父子進程以及不同的子進程之間) task_struct 里記錄的不同狀態(tài)、優(yōu)先級等調(diào)度相關(guān)信息,來公平且合理地分配 CPU 時間片,確保系統(tǒng)中各個進程都能有序地獲得執(zhí)行機會,保障系統(tǒng)的并發(fā)處理能力和整體運行效率。
從進程獨立性和安全性來講,盡管子進程是以父進程為模板進行 task_struct 的復(fù)制和初始化,但經(jīng)過修改關(guān)鍵標識信息以及后續(xù)內(nèi)存數(shù)據(jù)寫時拷貝等操作后,子進程能夠獨立運行,不會因為自身的操作(比如修改內(nèi)存數(shù)據(jù)、接收信號等)而影響到父進程或者其他子進程的正常運行,保障了每個進程在系統(tǒng)中的獨立性,同時也避免了因進程間不合理的相互干擾而可能引發(fā)的安全問題,增強了整個系統(tǒng)進程管理的穩(wěn)定性和安全性。
三、結(jié)構(gòu)成員深度剖析
3.1 進程狀態(tài)標識
在task_struct結(jié)構(gòu)體中,state和exit_state成員肩負著標識進程狀態(tài)的重要使命 。state成員通過一系列預(yù)定義的常量值,細致地描述了進程當前的運行狀態(tài) 。其中,TASK_RUNNING狀態(tài)猶如賽道上蓄勢待發(fā)的選手,表示進程要么正在 CPU 上全力奔跑(執(zhí)行),要么已經(jīng)站在起跑線上,時刻準備著獲取 CPU 的青睞(就緒) 。當我們在系統(tǒng)中運行一個簡單的計算程序時,在程序執(zhí)行的過程中,該進程就處于TASK_RUNNING狀態(tài) 。如果系統(tǒng)中同時存在多個處于TASK_RUNNING狀態(tài)的進程,它們就會像一群渴望上場比賽的選手,等待著調(diào)度器按照一定的規(guī)則安排它們輪流在 CPU 這個賽道上奔跑 。
TASK_INTERRUPTIBLE狀態(tài)則像是一位暫時休息的選手,進程處于可中斷的睡眠狀態(tài),它正在耐心等待某個特定事件的發(fā)生,比如等待讀取文件的數(shù)據(jù)、等待網(wǎng)絡(luò)請求的響應(yīng)等 。當一個進程發(fā)起文件讀取操作時,由于磁盤 I/O 速度相對較慢,在數(shù)據(jù)讀取完成之前,進程會進入TASK_INTERRUPTIBLE狀態(tài),暫時讓出 CPU 資源,進入睡眠狀態(tài) 。此時,如果有信號傳來,就如同有人呼喊這位休息的選手,它會被喚醒,從睡眠狀態(tài)中蘇醒過來,加入到可運行狀態(tài)的隊伍中,等待再次獲得 CPU 資源,繼續(xù)執(zhí)行后續(xù)的操作 。
與TASK_INTERRUPTIBLE狀態(tài)類似,TASK_UNINTERRUPTIBLE狀態(tài)下的進程也處于睡眠狀態(tài),但它是深度睡眠,如同一位陷入沉睡的選手,不會被信號輕易喚醒,只有當它所等待的特定事件完成時,才會被喚醒 。在某些情況下,進程可能會等待特定的硬件資源,比如等待磁盤設(shè)備完成初始化,此時進程會進入TASK_UNINTERRUPTIBLE狀態(tài),以確保在硬件資源準備好之前,不會被其他因素干擾 。
__TASK_STOPPED狀態(tài)表示進程被暫時喊停,處于停止執(zhí)行的狀態(tài),就像比賽中的選手因為某些特殊原因被裁判要求暫停比賽 。通常,當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU等信號時,就會進入這種狀態(tài) 。例如,當我們在調(diào)試程序時,使用調(diào)試工具向進程發(fā)送SIGSTOP信號,進程就會停止執(zhí)行,方便我們進行調(diào)試操作 。
__TASK_TRACED狀態(tài)則表示進程正在被像調(diào)試器這樣的 “裁判助手” 密切監(jiān)視著,進程的一舉一動都在監(jiān)控之下 。在調(diào)試程序時,調(diào)試器會將進程設(shè)置為__TASK_TRACED狀態(tài),以便實時獲取進程的運行信息,幫助開發(fā)者找出程序中的問題 。
而exit_state成員主要用于記錄進程在終止階段的相關(guān)狀態(tài) 。EXIT_ZOMBIE狀態(tài)意味著進程已經(jīng)完成了它的使命,執(zhí)行被終止,但它的 “后事” 還未處理完畢,父進程還沒有使用wait()等系統(tǒng)調(diào)用來獲取它的終止信息 。就像一位選手完成了比賽,但還沒有和教練(父進程)進行最后的交接 。EXIT_DEAD狀態(tài)則表示進程已經(jīng)徹底結(jié)束,所有的資源都已被釋放,如同選手已經(jīng)離開賽場,一切都已塵埃落定 。
進程狀態(tài)的轉(zhuǎn)換就像一場精心編排的舞蹈,隨著系統(tǒng)中各種事件的發(fā)生而有序進行 。當一個處于TASK_INTERRUPTIBLE狀態(tài)的進程等待的事件完成時,它會像被喚醒的選手一樣,從睡眠狀態(tài)轉(zhuǎn)換為TASK_RUNNING狀態(tài),重新獲得執(zhí)行的機會 。當進程接收到終止信號時,它會從當前狀態(tài)轉(zhuǎn)換為EXIT_ZOMBIE狀態(tài),等待父進程的處理 。父進程調(diào)用wait()系統(tǒng)調(diào)用后,進程才會最終進入EXIT_DEAD狀態(tài),完成它的整個生命周期 。
3.2 身份標識
pid和tgid作為task_struct結(jié)構(gòu)體中的身份標識成員,在進程和線程的識別與管理中扮演著至關(guān)重要的角色 。pid,即進程 ID,是系統(tǒng)為每個進程分配的獨一無二的 “身份證號碼”,它就像班級里每個學(xué)生的學(xué)號,用于唯一標識一個進程 。在 Linux 系統(tǒng)中,pid是一個整型數(shù)值,從 1 開始依次遞增,每個新創(chuàng)建的進程都會被賦予一個比之前進程pid更大的唯一值 。當我們在系統(tǒng)中運行多個程序時,通過pid可以準確地區(qū)分不同的進程,操作系統(tǒng)也能夠根據(jù)pid對進程進行各種操作,如發(fā)送信號、終止進程等 。例如,使用kill命令時,就需要指定進程的pid來向該進程發(fā)送終止信號 。
tgid,即線程組 ID,與線程組的概念緊密相連 。在 Linux 系統(tǒng)中,線程被視為輕量級進程,它們共享同一進程的資源,如內(nèi)存空間、文件描述符等 。線程組是由一個或多個線程組成的集合,這些線程共同協(xié)作完成特定的任務(wù) 。tgid用于標識線程組,同一線程組中的所有線程都擁有相同的tgid,它就像一個團隊的標志,將同一組的線程緊密聯(lián)系在一起 。對于只有主線程的進程來說,pid和tgid的值是相等的,因為此時進程就是一個單一的線程組,主線程既是進程的代表,也是線程組的唯一成員 。但當一個進程創(chuàng)建了多個線程時,情況就有所不同了 。每個線程都有自己獨立的pid,就像團隊中的每個成員都有自己的個性標識,但它們都共享同一個tgid,以表明它們屬于同一個線程組 。
這種進程和線程的標識機制,為操作系統(tǒng)的高效管理提供了有力支持 。在多線程編程中,通過pid和tgid,操作系統(tǒng)能夠清晰地識別每個線程的身份,合理地分配 CPU 資源,確保線程組內(nèi)的線程能夠協(xié)同工作,同時也能對不同線程組的進程進行有效的調(diào)度和管理 。例如,在一個多線程的網(wǎng)絡(luò)服務(wù)器程序中,主線程負責(zé)監(jiān)聽網(wǎng)絡(luò)連接,而多個工作線程負責(zé)處理接收到的請求 。通過pid和tgid,操作系統(tǒng)可以準確地調(diào)度這些線程,保證服務(wù)器能夠高效地處理大量并發(fā)請求 。
3.3 優(yōu)先級與調(diào)度
在task_struct結(jié)構(gòu)體中,prio、static_prio、normal_prio和rt_priority等成員在進程調(diào)度的舞臺上扮演著關(guān)鍵角色,它們共同決定了進程在 CPU 資源競爭中的優(yōu)先級順序 。
static_prio是進程的靜態(tài)優(yōu)先級,它就像一個學(xué)生的基礎(chǔ)成績,在進程創(chuàng)建時就被確定下來,并且在進程的生命周期中相對穩(wěn)定 。靜態(tài)優(yōu)先級的值越小,代表進程的優(yōu)先級越高 。對于實時進程,其靜態(tài)優(yōu)先級范圍通常是 0 - 99,而普通進程的靜態(tài)優(yōu)先級范圍是 100 - 139 。例如,一個用于實時視頻播放的進程,為了保證視頻播放的流暢性,可能會被賦予較低的靜態(tài)優(yōu)先級,以確保它能夠優(yōu)先獲得 CPU 資源 。靜態(tài)優(yōu)先級可以通過nice()或者setpriority等系統(tǒng)調(diào)用在用戶空間進行修改,新創(chuàng)建的進程會繼承父進程的靜態(tài)優(yōu)先級 。
rt_priority表示進程的實時優(yōu)先級,主要用于實時進程 。實時進程對時間的要求非常嚴格,需要在規(guī)定的時間內(nèi)完成任務(wù) 。實時優(yōu)先級的范圍是 1 - 99,值越大表示優(yōu)先級越高 。在一個工業(yè)控制系統(tǒng)中,用于控制生產(chǎn)設(shè)備的實時進程,其rt_priority可能會被設(shè)置得較高,以確保能夠及時響應(yīng)設(shè)備的各種信號,保證生產(chǎn)的正常進行 。普通進程的rt_priority通常為 0 。
normal_prio是歸一化優(yōu)先級,它是根據(jù)靜態(tài)優(yōu)先級、實時優(yōu)先級和調(diào)度策略綜合計算得出的 。調(diào)度器在進行調(diào)度決策時,會參考歸一化優(yōu)先級來確定進程的執(zhí)行順序 。對于普通進程,其歸一化優(yōu)先級通常就是靜態(tài)優(yōu)先級;而對于實時進程,歸一化優(yōu)先級則與實時優(yōu)先級相關(guān) 。
prio是進程的動態(tài)優(yōu)先級,它是調(diào)度器實際用于調(diào)度的優(yōu)先級 。動態(tài)優(yōu)先級在運行時可以根據(jù)進程的運行情況進行調(diào)整 。例如,當一個進程長時間占用 CPU 資源時,調(diào)度器可能會降低它的動態(tài)優(yōu)先級,以便讓其他進程也有機會獲得 CPU 資源 。相反,當一個進程處于等待狀態(tài),等待某個資源的時間較長時,調(diào)度器可能會適當提高它的動態(tài)優(yōu)先級,以提高系統(tǒng)的整體性能 。
不同的調(diào)度策略對這些優(yōu)先級成員有著不同的運用方式 。在完全公平調(diào)度(CFS)策略下,調(diào)度器會根據(jù)進程的虛擬運行時間(vruntime)和歸一化優(yōu)先級來分配 CPU 時間,確保每個進程都能公平地獲得 CPU 資源 。而在實時調(diào)度策略中,如SCHED_FIFO(先進先出調(diào)度)和SCHED_RR(時間片輪轉(zhuǎn)調(diào)度),實時優(yōu)先級rt_priority起著關(guān)鍵作用,高優(yōu)先級的實時進程會優(yōu)先獲得 CPU 資源,并且在沒有更高優(yōu)先級實時進程的情況下,會一直占用 CPU,直到完成任務(wù)或者主動讓出 CPU 。
3.4 內(nèi)存管理指針
在task_struct結(jié)構(gòu)體中,mm和active_mm成員猶如進程內(nèi)存管理世界的 “導(dǎo)航儀”,在進程用戶空間內(nèi)存管理的復(fù)雜旅程中發(fā)揮著關(guān)鍵作用 。
mm指針指向一個mm_struct結(jié)構(gòu)體,這個結(jié)構(gòu)體就像是進程內(nèi)存世界的 “大管家”,詳細記錄了進程用戶空間的內(nèi)存布局和管理信息 。它包含了進程的代碼段、數(shù)據(jù)段、堆、棧等內(nèi)存區(qū)域的映射信息,以及頁表、虛擬內(nèi)存區(qū)域(VMA)列表等重要數(shù)據(jù) 。當一個進程運行時,它所需要的代碼和數(shù)據(jù)都存儲在這些內(nèi)存區(qū)域中 。例如,一個 C 語言程序在運行時,程序的可執(zhí)行代碼會存儲在代碼段,全局變量和靜態(tài)變量會存儲在數(shù)據(jù)段,動態(tài)分配的內(nèi)存會在堆中進行管理,而函數(shù)調(diào)用時的局部變量和參數(shù)則會存儲在棧中 。mm_struct結(jié)構(gòu)體通過對這些內(nèi)存區(qū)域的有效管理,確保進程能夠正確地訪問和使用內(nèi)存資源 。
active_mm主要用于處理內(nèi)核線程的內(nèi)存管理問題 。對于普通用戶進程來說,active_mm通常指向與該進程關(guān)聯(lián)的mm_struct,就像一個專屬的內(nèi)存管理助手,時刻為進程提供內(nèi)存管理服務(wù) 。然而,內(nèi)核線程的情況有所不同 。內(nèi)核線程只在內(nèi)核空間中運行,不需要訪問用戶空間內(nèi)存,因此它們通常沒有自己獨立的mm_struct 。在這種情況下,active_mm會指向最后一個運行在該 CPU 上的用戶進程的mm_struct 。這就好比內(nèi)核線程在內(nèi)存管理方面沒有自己的 “家”,但它可以借用最后一個在該 CPU 上運行的用戶進程的 “家” 來進行一些必要的內(nèi)存操作 。例如,當內(nèi)核線程執(zhí)行某些需要訪問內(nèi)存的操作時,它可以通過active_mm找到合適的內(nèi)存上下文,從而確保內(nèi)存操作的正確執(zhí)行 。
這種內(nèi)存管理機制,既保證了普通進程能夠高效地管理和使用自己的用戶空間內(nèi)存,又巧妙地解決了內(nèi)核線程在內(nèi)存管理方面的特殊需求,使得整個系統(tǒng)的內(nèi)存管理更加靈活和高效 。在系統(tǒng)中同時運行多個進程和內(nèi)核線程的情況下,通過mm和active_mm的協(xié)同工作,能夠有條不紊地進行內(nèi)存分配、回收和訪問控制,為系統(tǒng)的穩(wěn)定運行提供了堅實的保障 。
3.5 親屬關(guān)系指針
在task_struct結(jié)構(gòu)體中,real_parent、parent、children和sibling等成員如同一張無形的關(guān)系網(wǎng),清晰地描繪了進程間的親屬關(guān)系,在進程管理和信號傳遞的舞臺上發(fā)揮著不可或缺的作用 。
real_parent指針指向進程真正的父進程,就像孩子指向自己的親生父母 。在正常情況下,當一個進程被創(chuàng)建時,它的real_parent會指向創(chuàng)建它的父進程 。例如,當我們在終端中通過命令行啟動一個新的進程時,這個新進程的real_parent就是終端進程 。然而,在某些特殊情況下,如使用調(diào)試工具(如 GDB)調(diào)試進程時,情況會有所不同 。假設(shè)在 bash 中使用 GDB 來調(diào)試一個進程,此時進程的parent是 GDB,因為 GDB 負責(zé)監(jiān)控和控制進程的執(zhí)行;而real_parent仍然是 bash,因為 bash 是最初創(chuàng)建該進程的父進程 。這種區(qū)分在進程管理和信號傳遞中非常重要,它確保了進程能夠正確地繼承父進程的資源和屬性,并且在需要時能夠向正確的父進程發(fā)送信號 。
parent指針同樣指向父進程,但它主要用于接收SIGCHLD信號和wait4()系統(tǒng)調(diào)用的報告 。當一個進程終止時,它會向自己的parent發(fā)送SIGCHLD信號,通知父進程自己的狀態(tài)發(fā)生了變化 。父進程可以通過wait4()系統(tǒng)調(diào)用來獲取子進程的終止信息,如子進程的退出狀態(tài)、資源使用情況等 。這就像孩子完成任務(wù)后向家長匯報情況,家長可以根據(jù)這些信息進行相應(yīng)的處理 。
children是一個鏈表頭,它將所有屬于該進程的子進程串聯(lián)在一起,形成了一個家族樹 。鏈表中的每個元素都是該進程的子進程,通過children鏈表,父進程可以方便地管理和訪問自己的子進程 。例如,父進程可以遍歷children鏈表,對每個子進程進行資源分配、狀態(tài)查詢等操作 。
sibling指針則用于將當前進程插入到兄弟進程鏈表中,它就像連接兄弟姐妹之間的紐帶 。擁有同一父進程的所有進程互為兄弟進程,它們通過sibling指針相互關(guān)聯(lián) 。通過這個鏈表,進程可以快速找到自己的兄弟進程,實現(xiàn)進程間的協(xié)作和通信 。例如,在一個多進程的應(yīng)用程序中,兄弟進程之間可能需要共享某些資源或者傳遞數(shù)據(jù),通過sibling鏈表,它們可以方便地找到彼此并進行交互 。
在進程管理和信號傳遞中,這些親屬關(guān)系指針起著至關(guān)重要的作用 。當一個進程接收到信號時,它會根據(jù)自己的親屬關(guān)系將信號傳遞給合適的進程 。例如,當父進程接收到SIGCHLD信號時,它可以通過children鏈表找到對應(yīng)的子進程,并進行相應(yīng)的處理 。這種基于親屬關(guān)系的信號傳遞機制,確保了信號能夠準確地到達目標進程,提高了系統(tǒng)的響應(yīng)速度和穩(wěn)定性 。
3.6 時間與統(tǒng)計信息
在task_struct結(jié)構(gòu)體中,utime、stime、start_time等時間相關(guān)成員,以及nvcsw、nivcsw等統(tǒng)計信息成員,就像一個個精準的記錄員,詳細地記錄著進程的時間開銷和運行統(tǒng)計信息,為系統(tǒng)的性能分析和進程管理提供了重要的數(shù)據(jù)支持 。
utime表示進程在用戶態(tài)下消耗的時間,就像運動員在比賽中實際奔跑的時間 。它記錄了進程執(zhí)行用戶代碼所花費的時間,這個時間不包括進程在系統(tǒng)調(diào)用和內(nèi)核態(tài)下的時間 。例如,一個計算密集型的進程在進行復(fù)雜的數(shù)學(xué)運算時,utime會隨著運算的進行而不斷增加 。通過統(tǒng)計utime,我們可以了解進程在用戶態(tài)下的執(zhí)行效率,判斷進程是否存在性能瓶頸 。
stime則記錄了進程在內(nèi)核態(tài)下消耗的時間,如同運動員在比賽中準備和調(diào)整的時間 。當進程進行系統(tǒng)調(diào)用,如讀取文件、分配內(nèi)存等操作時,會進入內(nèi)核態(tài),stime會統(tǒng)計這部分時間 。系統(tǒng)調(diào)用通常涉及到內(nèi)核資源的訪問和管理,stime的統(tǒng)計可以幫助我們了解進程對內(nèi)核資源的使用情況,評估系統(tǒng)調(diào)用的開銷 。
start_time記錄了進程的啟動時間,就像比賽的開始時間 。它是一個時間戳,表示進程從創(chuàng)建到開始執(zhí)行的時間點 。通過start_time,我們可以計算進程的運行時長,了解進程在系統(tǒng)中的存活時間 。在系統(tǒng)性能分析中,運行時長是一個重要的指標,它可以幫助我們判斷進程是否長時間占用系統(tǒng)資源,是否需要進行優(yōu)化 。
nvcsw和nivcsw屬于統(tǒng)計信息成員,分別表示自愿上下文切換次數(shù)和非自愿上下文切換次數(shù) 。自愿上下文切換是指進程主動放棄 CPU,例如進程在等待 I/O 操作完成時,會主動讓出 CPU,此時nvcsw會增加 。這就像運動員在比賽中主動休息,調(diào)整狀態(tài) 。非自愿上下文切換則是指進程被調(diào)度器強制剝奪 CPU,例如當有更高優(yōu)先級的進程需要運行時,當前進程會被切換出去,nivcsw會增加 。這就像運動員在比賽中被裁判要求暫停,讓其他選手上場 。通過統(tǒng)計這兩個值,我們可以了解進程在 CPU 競爭中的表現(xiàn),評估調(diào)度器的性能 。如果一個進程的nivcsw過高,可能意味著系統(tǒng)中存在競爭激烈的情況,需要進一步優(yōu)化調(diào)度策略 。
四、task_struct與進程管理的“化學(xué)反應(yīng)”
4.1 進程創(chuàng)建
在 Linux 系統(tǒng)中,進程的創(chuàng)建如同生命的誕生,充滿了奇妙的過程,而fork()系統(tǒng)調(diào)用則是這個過程的關(guān)鍵 “催化劑” 。當fork()系統(tǒng)調(diào)用被觸發(fā)時,一場精心編排的 “復(fù)制” 大戲便拉開了帷幕 。內(nèi)核首先會在內(nèi)存中為新的子進程精心分配一個全新的task_struct結(jié)構(gòu)體,就像為新生命準備了一個獨特的 “生命檔案” 。這個新的task_struct結(jié)構(gòu)體就像是一張白紙,等待著被賦予各種關(guān)鍵信息 。
接下來,子進程開始從父進程那里繼承一系列重要的信息 。子進程會繼承父進程的進程狀態(tài),就像孩子繼承了父母的某些特質(zhì) 。如果父進程處于運行狀態(tài),子進程在創(chuàng)建初期也會繼承這個狀態(tài),等待著被調(diào)度執(zhí)行 。在進程的親屬關(guān)系方面,子進程的real_parent和parent指針都會指向父進程,就像孩子與父母之間建立了緊密的聯(lián)系 。這種親屬關(guān)系的繼承確保了子進程能夠正確地融入進程家族樹,在需要時能夠向父進程尋求支持和資源 。
在內(nèi)存管理方面,子進程會與父進程共享內(nèi)存資源 。它們共享同一內(nèi)存描述符mm_struct,這意味著它們在用戶空間中看到的內(nèi)存布局是相同的 。這就好比兩個孩子住在同一所房子里,共享著房子里的各種設(shè)施 。不過,這種共享是基于寫時拷貝(Copy - On - Write,COW)機制的 。在初始階段,子進程和父進程共享內(nèi)存頁面,但當其中任何一方試圖對共享頁面進行寫操作時,系統(tǒng)會為寫操作的一方分配新的物理頁面,將共享頁面的內(nèi)容復(fù)制到新頁面中,然后進行寫操作 。這樣,既保證了內(nèi)存資源的高效利用,又確保了子進程和父進程在內(nèi)存操作上的獨立性 。
文件描述符表也會被子進程繼承 。這意味著父進程打開的文件,子進程同樣可以訪問 。如果父進程打開了一個日志文件用于記錄信息,子進程也能夠讀取和寫入這個文件 。這為父子進程之間的協(xié)作提供了便利,它們可以通過共享的文件進行數(shù)據(jù)傳遞和同步 。
例如,在一個多進程的服務(wù)器程序中,父進程負責(zé)監(jiān)聽網(wǎng)絡(luò)端口,接受客戶端的連接請求 。當有新的連接到來時,父進程通過fork()創(chuàng)建子進程,子進程繼承了父進程的網(wǎng)絡(luò)連接文件描述符,從而可以獨立地處理與客戶端的通信 。在這個過程中,子進程的task_struct結(jié)構(gòu)體從父進程那里繼承了必要的信息,使得子進程能夠順利地開始它的 “生命旅程”,與父進程協(xié)同工作,共同完成服務(wù)器的任務(wù) 。
4.2 進程調(diào)度
進程調(diào)度在 Linux 系統(tǒng)中就像是一場激烈的資源爭奪賽,而task_struct結(jié)構(gòu)體則是這場比賽中的關(guān)鍵 “情報站”,為調(diào)度器提供了豐富的信息,幫助調(diào)度器做出合理的決策 。調(diào)度器在選擇下一個要執(zhí)行的進程時,會仔細參考task_struct中的優(yōu)先級信息 。進程的優(yōu)先級就像運動員的比賽排名,優(yōu)先級高的進程會優(yōu)先獲得 CPU 資源 。實時進程的優(yōu)先級通常高于普通進程,這是因為實時進程對時間的要求非常嚴格,需要在規(guī)定的時間內(nèi)完成任務(wù) 。在一個實時視頻監(jiān)控系統(tǒng)中,用于處理視頻流的進程會被賦予較高的優(yōu)先級,以確保視頻的實時性和流暢性 。
進程的狀態(tài)也是調(diào)度器關(guān)注的重要信息 。處于TASK_RUNNING狀態(tài)的進程,如同已經(jīng)站在起跑線上的運動員,時刻準備著獲取 CPU 資源,進入執(zhí)行狀態(tài) 。而處于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE狀態(tài)的進程,則像是在休息或等待特定資源的運動員,調(diào)度器會暫時跳過它們,將 CPU 資源分配給更有執(zhí)行條件的進程 。
當進行上下文切換時,task_struct的作用更是不可或缺 。上下文切換就像是運動員在比賽中途進行換人,新上場的運動員需要迅速適應(yīng)比賽環(huán)境 。在這個過程中,task_struct保存了進程的各種上下文信息,包括 CPU 寄存器的值、堆棧指針等 。當一個進程被切換出去時,內(nèi)核會將該進程的 CPU 寄存器的值等上下文信息保存到它的task_struct中 。當這個進程再次被調(diào)度執(zhí)行時,內(nèi)核會從它的task_struct中讀取這些上下文信息,恢復(fù) CPU 寄存器的值和堆棧指針,使得進程能夠繼續(xù)從上次中斷的地方執(zhí)行 。這就像運動員在比賽中換人后,新上場的運動員能夠迅速了解比賽的進展情況,繼續(xù)完成比賽 。
在多核心 CPU 的系統(tǒng)中,調(diào)度器還會根據(jù)task_struct中的cpus_allowed等信息,決定將進程分配到哪個 CPU 核心上執(zhí)行 。這個信息就像是運動員的參賽場地選擇,調(diào)度器會根據(jù)進程的需求和 CPU 核心的負載情況,合理地安排進程在不同的 CPU 核心上運行,以提高系統(tǒng)的整體性能 。
4.3 進程終止
進程終止的過程就像是一場演出的落幕,雖然看似簡單,但背后卻涉及到一系列復(fù)雜而有序的操作,而task_struct在這個過程中扮演著關(guān)鍵的角色 。當一個進程完成了它的使命,準備終止時,內(nèi)核會首先對task_struct中的相關(guān)信息進行處理 。進程會釋放它所占用的各種資源,這就像是演員在演出結(jié)束后歸還借用的道具 。例如,進程會關(guān)閉它打開的文件描述符,釋放文件鎖,將文件資源歸還給系統(tǒng) 。在內(nèi)存管理方面,進程會釋放它所占用的內(nèi)存空間,包括堆內(nèi)存、棧內(nèi)存等 。對于使用了動態(tài)內(nèi)存分配的進程,如通過malloc()函數(shù)分配的內(nèi)存,在進程終止時,這些內(nèi)存會被回收,以避免內(nèi)存泄漏 。
進程的狀態(tài)也會被更新 。它會從當前的運行狀態(tài)轉(zhuǎn)換為EXIT_ZOMBIE狀態(tài),就像演員從舞臺上退下,進入了一種等待善后處理的狀態(tài) 。在EXIT_ZOMBIE狀態(tài)下,進程雖然已經(jīng)停止執(zhí)行,但它的task_struct結(jié)構(gòu)體仍然存在于系統(tǒng)中,因為它還需要向父進程傳遞一些重要的信息,如進程的退出狀態(tài)、資源使用情況等 。父進程可以通過wait()或waitpid()等系統(tǒng)調(diào)用來獲取這些信息 。這就像演出結(jié)束后,演員需要向?qū)а輩R報演出的情況 。
當父進程調(diào)用wait()或waitpid()時,內(nèi)核會處理子進程的task_struct 。內(nèi)核會讀取子進程的退出狀態(tài),這個狀態(tài)信息就像是演員的演出評價,父進程可以根據(jù)這個狀態(tài)了解子進程的執(zhí)行結(jié)果 。內(nèi)核會回收子進程的task_struct結(jié)構(gòu)體以及其他相關(guān)資源,將它們從系統(tǒng)中徹底移除 。這就像是演出結(jié)束后,清理舞臺和道具,為下一場演出做好準備 。
如果父進程沒有及時調(diào)用wait()來獲取子進程的終止信息,子進程就會一直處于EXIT_ZOMBIE狀態(tài),成為一個 “僵尸進程” 。僵尸進程雖然不占用CPU等執(zhí)行資源,但它會占用系統(tǒng)的內(nèi)存等資源,就像廢棄的道具占用著倉庫的空間 。長時間存在大量僵尸進程可能會導(dǎo)致系統(tǒng)資源的浪費,影響系統(tǒng)的性能 。因此,在編寫多進程程序時,父進程需要及時處理子進程的終止信息,避免僵尸進程的產(chǎn)生 。
五、案例實戰(zhàn)分析
為了更直觀地感受task_struct在實際中的應(yīng)用,我們通過一個簡單的內(nèi)核模塊代碼示例來一探究竟 。下面是一段用于讀取所有進程的task_struct結(jié)構(gòu)信息的內(nèi)核模塊代碼:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/sched/signal.h>
#include <linux/init.h>
static int __init hello_init(void) {
struct task_struct *pp;
printk("for_each_process begin\n");
for_each_process(pp) {
printk(KERN_INFO "process_info pid:%i comm:%s flags:%i", pp->pid, pp->comm, pp->flags);
}
return 0;
}
static void __exit hello_exit(void) {
printk("for_each_process end!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
在這段代碼中,我們首先包含了必要的頭文件,這些頭文件為我們提供了與內(nèi)核交互所需的各種定義和函數(shù)聲明 。hello_init函數(shù)是內(nèi)核模塊的初始化函數(shù),當模塊被加載到內(nèi)核中時,這個函數(shù)會被執(zhí)行 。在函數(shù)內(nèi)部,我們使用for_each_process宏來遍歷系統(tǒng)中的所有進程 。這個宏就像是一個向?qū)?,帶領(lǐng)我們逐一訪問系統(tǒng)中的每個進程 。對于每個遍歷到的進程,我們通過printk函數(shù)打印出其pid(進程 ID)、comm(進程名稱)和flags(進程標志)等信息 。pid就像進程的身份證號碼,獨一無二地標識著每個進程;comm則是進程的名字,讓我們能夠直觀地了解進程的用途;flags包含了進程的各種狀態(tài)標志為我們提供了關(guān)于進程狀態(tài)的重要線索 。
hello_exit函數(shù)是內(nèi)核模塊的退出函數(shù),當模塊從內(nèi)核中卸載時,這個函數(shù)會被執(zhí)行 。在函數(shù)中,我們使用printk函數(shù)打印出模塊卸載的提示信息 。
通過這個內(nèi)核模塊,我們可以清晰地看到系統(tǒng)中每個進程的一些關(guān)鍵信息,這些信息都來自于task_struct結(jié)構(gòu)體 。這就好比我們打開了一個進程信息的寶藏庫,通過task_struct結(jié)構(gòu)體,我們能夠獲取到進程的各種詳細信息,從而更好地了解系統(tǒng)中進程的運行狀態(tài)和行為 。