深入了解Linux內核:task_struct結構詳解
在 Linux 系統那如浩瀚宇宙般復雜又精妙的內核世界里,隱藏著無數掌控全局的 “關鍵密碼”,而今天要帶大家認識的 task_struct 結構,無疑是其中最為耀眼的一顆明星。當你打開電腦,啟動 Linux 系統,瞬間仿佛開啟了一場盛大的狂歡派對,無數的進程在幕后馬不停蹄地忙碌著,有的負責渲染精美的圖形界面,有的保障網絡連接順暢無阻,還有的默默守護著系統的安全防線。而這每一個進程,它們的 “身世檔案”、“成長軌跡” 以及 “一舉一動”,統統都被記錄在一個神奇的結構體 ——task_struct 之中。
它就像是一位超級幕后管家,知曉進程何時誕生,由哪個用戶啟動,占用了多少寶貴的系統資源,又該在何時退場謝幕。無論是深入探究系統性能瓶頸,精準調試詭異的程序錯誤,還是試圖理解 Linux 內核如何有條不紊地調度千軍萬馬般的進程,掌握 task_struct 結構,都如同握住了一把開啟內核智慧寶庫的萬能鑰匙。此刻,就請緊跟我的腳步,一起深入剖析這個 Linux 內核中至關重要的 task_struct 結構,探尋進程背后那些不為人知的精彩故事吧!
一、引言
在前文中,我們分析了內核啟動的整個過程以及系統調用的過程,從本文開始我們會介紹Linux系統各個重要的組成部分。這一切就從進程和線程開始,在 Linux 里面,無論是進程,還是線程,到了內核里面,我們統一都叫任務(Task),由一個統一的結構 task_struct 進行管理。
這個結構非常復雜,本文將細細分析task_struct結構。主要分析順序會按照該架構體中的成員變量和函數的作用進行分類,主要包括:
- 任務ID
- 親緣關系
- 任務狀態
- 任務權限
- 運行統計
- 進程調度
- 信號處理
- 內存管理
- 文件與文件系統
- 內核棧
二、Task_struct結構
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中的兩個內核棧相關成員變量了。
⑴通過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 位上,修改了這個問題,變成了定長的。
⑵通過內核棧找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:進程的“靈魂檔案”
2.1 定義與地位
在 Linux 內核的代碼世界里,task_struct被精心定義為一個結構體,其內部成員眾多,宛如一個龐大而有序的信息倉庫 。在<linux/sched.h>頭文件中,我們能一窺它的定義全貌:
struct task_struct {
volatile long state; /* 進程狀態 */
void *stack; /* 指向內核棧的指針 */
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; /* 指向內存描述符 */
struct mm_struct *active_mm; /* 指向活躍的內存描述符 */
// 此處省略大量其他成員
};
task_struct就如同進程的 “靈魂檔案”,從進程誕生的那一刻起,它便如影隨形,詳細記錄著進程的各種屬性和狀態信息。無論是進程的唯一標識 —— 進程 ID(PID),還是進程所處的運行狀態(如運行、就緒、睡眠等),亦或是進程與其他進程之間的親屬關系(父進程、子進程、兄弟進程等),以及進程所占用的內存資源、打開的文件描述符等關鍵信息,都被一一存儲在這個結構體中。可以毫不夸張地說,task_struct是進程在 Linux 內核中的 “代言人”,內核正是通過對task_struct結構體的管理和操作,實現了對進程的創建、調度、終止等一系列生命周期的有效管控 。
2.2 內存布局奧秘
在 Linux 內核中,task_struct的內存分配與內核棧有著緊密的聯系 。通常情況下,內核會為每個進程分配一個大小固定的內存區域,這個區域同時包含了task_struct結構體和進程的內核棧。以常見的 x86 架構為例,內核會一次性分配兩個連續的物理頁面(每個頁面大小通常為 4KB,共 8KB)來存儲這兩部分內容 。其中,task_struct結構體大約占用底部的 1KB 空間,而剩余的 7KB 空間則用于存放進程的內核棧 。
這種內存布局方式并非隨意為之,而是有著深刻的設計考量 。一方面,將task_struct與內核棧放在一起,能夠減少內存碎片的產生,提高內存的使用效率 。當進程創建時,一次性分配連續的內存空間,避免了多次分配內存可能導致的內存碎片化問題 。另一方面,這種布局方式也方便了內核在進行進程上下文切換時對相關信息的快速訪問 。在進程上下文切換過程中,內核需要保存和恢復進程的各種狀態信息,包括 CPU 寄存器的值、堆棧指針等 。將task_struct與內核棧相鄰放置,使得內核能夠通過簡單的指針運算,快速找到并操作這些關鍵信息,從而大大提高了進程上下文切換的效率 。
例如,當進程從用戶態切換到內核態時,CPU 需要將當前的堆棧指針切換到內核棧的地址 。由于task_struct與內核棧在內存中是連續的,內核可以根據task_struct中保存的棧指針信息,迅速定位到內核棧的起始地址,完成堆棧指針的切換 。同樣,在進程返回用戶態時,內核也能夠輕松地恢復用戶棧的地址和相關狀態信息 。這種緊密的內存布局關系,就像是一場精心編排的舞蹈,task_struct與內核棧相互配合,共同保障了進程在 Linux 內核中的高效運行 。
2.3 task_struct 與系統調用
(1)以 fork 系統調用為例看 task_struct 的復制與初始化
在 Linux 中,fork 系統調用是創建新進程的重要方式。當執行 fork 調用時,內核會為新創建的子進程分配一個新的 task_struct 結構體,這是整個創建子進程流程中極為關鍵的起始步驟。
起初,內核會盡可能多地將父進程 task_struct 里的內容復制到子進程的 task_struct 中。不過,這里要注意并非所有內容都會馬上被原樣復制,像內存頁相關的部分,就會采用寫時拷貝(COW)機制來優化性能,避免不必要的資源浪費以及保證數據在后續使用時的獨立性和安全性。
例如,在一個文本編輯進程執行 fork 操作創建子進程時,父進程 task_struct 中記錄的該文本編輯程序代碼段、數據段等相關內存區域信息會先嘗試復制給子進程,而那些具體的內存頁面數據可能暫不實際復制,只是設置好寫時拷貝相關的機制,等到子進程要對相應內存數據進行修改操作時,才會真正去復制一份屬于子進程自己的數據副本,以保證父子進程后續數據操作的互不干擾。
完成基本的復制后,內核還需要進行一系列必要的修改來確保子進程能夠獨立于父進程運行。其中會設置子進程的 PID(進程 ID),使其擁有一個新的唯一的進程 ID,而父進程的 PID 保持不變;同時,子進程的 PPID(父進程 ID)會被設置為調用 fork 的進程的 PID。除此之外,像進程組、會話等關系也會相應地更新或初始化,信號處理器、文件描述符表等同樣要進行適當的調整,以符合子進程后續獨立運行的需求。
(2)對進程管理的影響與意義
task_struct 在 fork 系統調用創建子進程時的這種復制與初始化機制,對進程管理有著多方面重要的影響和意義。
從資源分配角度來看,通過寫時拷貝機制,在子進程創建初期可以避免大量不必要的內存數據復制開銷,多個子進程可以在初期共享父進程的內存資源,只有當真正需要修改數據時才各自分配獨立的內存空間,這樣能更高效地利用系統內存資源,尤其是在創建多個相似子進程的場景下,能顯著節省內存開銷,使得系統整體資源分配更加合理且靈活。
在進程調度方面,新創建并初始化好的子進程會被加入到內核的調度隊列中(通常是就緒隊列),等待 CPU 的調度執行。每個子進程擁有獨立的 task_struct 結構體,意味著調度器可以依據各個進程(包括父子進程以及不同的子進程之間) task_struct 里記錄的不同狀態、優先級等調度相關信息,來公平且合理地分配 CPU 時間片,確保系統中各個進程都能有序地獲得執行機會,保障系統的并發處理能力和整體運行效率。
從進程獨立性和安全性來講,盡管子進程是以父進程為模板進行 task_struct 的復制和初始化,但經過修改關鍵標識信息以及后續內存數據寫時拷貝等操作后,子進程能夠獨立運行,不會因為自身的操作(比如修改內存數據、接收信號等)而影響到父進程或者其他子進程的正常運行,保障了每個進程在系統中的獨立性,同時也避免了因進程間不合理的相互干擾而可能引發的安全問題,增強了整個系統進程管理的穩定性和安全性。
三、結構成員深度剖析
3.1 進程狀態標識
在task_struct結構體中,state和exit_state成員肩負著標識進程狀態的重要使命 。state成員通過一系列預定義的常量值,細致地描述了進程當前的運行狀態 。其中,TASK_RUNNING狀態猶如賽道上蓄勢待發的選手,表示進程要么正在 CPU 上全力奔跑(執行),要么已經站在起跑線上,時刻準備著獲取 CPU 的青睞(就緒) 。當我們在系統中運行一個簡單的計算程序時,在程序執行的過程中,該進程就處于TASK_RUNNING狀態 。如果系統中同時存在多個處于TASK_RUNNING狀態的進程,它們就會像一群渴望上場比賽的選手,等待著調度器按照一定的規則安排它們輪流在 CPU 這個賽道上奔跑 。
TASK_INTERRUPTIBLE狀態則像是一位暫時休息的選手,進程處于可中斷的睡眠狀態,它正在耐心等待某個特定事件的發生,比如等待讀取文件的數據、等待網絡請求的響應等 。當一個進程發起文件讀取操作時,由于磁盤 I/O 速度相對較慢,在數據讀取完成之前,進程會進入TASK_INTERRUPTIBLE狀態,暫時讓出 CPU 資源,進入睡眠狀態 。此時,如果有信號傳來,就如同有人呼喊這位休息的選手,它會被喚醒,從睡眠狀態中蘇醒過來,加入到可運行狀態的隊伍中,等待再次獲得 CPU 資源,繼續執行后續的操作 。
與TASK_INTERRUPTIBLE狀態類似,TASK_UNINTERRUPTIBLE狀態下的進程也處于睡眠狀態,但它是深度睡眠,如同一位陷入沉睡的選手,不會被信號輕易喚醒,只有當它所等待的特定事件完成時,才會被喚醒 。在某些情況下,進程可能會等待特定的硬件資源,比如等待磁盤設備完成初始化,此時進程會進入TASK_UNINTERRUPTIBLE狀態,以確保在硬件資源準備好之前,不會被其他因素干擾 。
__TASK_STOPPED狀態表示進程被暫時喊停,處于停止執行的狀態,就像比賽中的選手因為某些特殊原因被裁判要求暫停比賽 。通常,當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU等信號時,就會進入這種狀態 。例如,當我們在調試程序時,使用調試工具向進程發送SIGSTOP信號,進程就會停止執行,方便我們進行調試操作 。
__TASK_TRACED狀態則表示進程正在被像調試器這樣的 “裁判助手” 密切監視著,進程的一舉一動都在監控之下 。在調試程序時,調試器會將進程設置為__TASK_TRACED狀態,以便實時獲取進程的運行信息,幫助開發者找出程序中的問題 。
而exit_state成員主要用于記錄進程在終止階段的相關狀態 。EXIT_ZOMBIE狀態意味著進程已經完成了它的使命,執行被終止,但它的 “后事” 還未處理完畢,父進程還沒有使用wait()等系統調用來獲取它的終止信息 。就像一位選手完成了比賽,但還沒有和教練(父進程)進行最后的交接 。EXIT_DEAD狀態則表示進程已經徹底結束,所有的資源都已被釋放,如同選手已經離開賽場,一切都已塵埃落定 。
進程狀態的轉換就像一場精心編排的舞蹈,隨著系統中各種事件的發生而有序進行 。當一個處于TASK_INTERRUPTIBLE狀態的進程等待的事件完成時,它會像被喚醒的選手一樣,從睡眠狀態轉換為TASK_RUNNING狀態,重新獲得執行的機會 。當進程接收到終止信號時,它會從當前狀態轉換為EXIT_ZOMBIE狀態,等待父進程的處理 。父進程調用wait()系統調用后,進程才會最終進入EXIT_DEAD狀態,完成它的整個生命周期 。
3.2 身份標識
pid和tgid作為task_struct結構體中的身份標識成員,在進程和線程的識別與管理中扮演著至關重要的角色 。pid,即進程 ID,是系統為每個進程分配的獨一無二的 “身份證號碼”,它就像班級里每個學生的學號,用于唯一標識一個進程 。在 Linux 系統中,pid是一個整型數值,從 1 開始依次遞增,每個新創建的進程都會被賦予一個比之前進程pid更大的唯一值 。當我們在系統中運行多個程序時,通過pid可以準確地區分不同的進程,操作系統也能夠根據pid對進程進行各種操作,如發送信號、終止進程等 。例如,使用kill命令時,就需要指定進程的pid來向該進程發送終止信號 。
tgid,即線程組 ID,與線程組的概念緊密相連 。在 Linux 系統中,線程被視為輕量級進程,它們共享同一進程的資源,如內存空間、文件描述符等 。線程組是由一個或多個線程組成的集合,這些線程共同協作完成特定的任務 。tgid用于標識線程組,同一線程組中的所有線程都擁有相同的tgid,它就像一個團隊的標志,將同一組的線程緊密聯系在一起 。對于只有主線程的進程來說,pid和tgid的值是相等的,因為此時進程就是一個單一的線程組,主線程既是進程的代表,也是線程組的唯一成員 。但當一個進程創建了多個線程時,情況就有所不同了 。每個線程都有自己獨立的pid,就像團隊中的每個成員都有自己的個性標識,但它們都共享同一個tgid,以表明它們屬于同一個線程組 。
這種進程和線程的標識機制,為操作系統的高效管理提供了有力支持 。在多線程編程中,通過pid和tgid,操作系統能夠清晰地識別每個線程的身份,合理地分配 CPU 資源,確保線程組內的線程能夠協同工作,同時也能對不同線程組的進程進行有效的調度和管理 。例如,在一個多線程的網絡服務器程序中,主線程負責監聽網絡連接,而多個工作線程負責處理接收到的請求 。通過pid和tgid,操作系統可以準確地調度這些線程,保證服務器能夠高效地處理大量并發請求 。
3.3 優先級與調度
在task_struct結構體中,prio、static_prio、normal_prio和rt_priority等成員在進程調度的舞臺上扮演著關鍵角色,它們共同決定了進程在 CPU 資源競爭中的優先級順序 。
static_prio是進程的靜態優先級,它就像一個學生的基礎成績,在進程創建時就被確定下來,并且在進程的生命周期中相對穩定 。靜態優先級的值越小,代表進程的優先級越高 。對于實時進程,其靜態優先級范圍通常是 0 - 99,而普通進程的靜態優先級范圍是 100 - 139 。例如,一個用于實時視頻播放的進程,為了保證視頻播放的流暢性,可能會被賦予較低的靜態優先級,以確保它能夠優先獲得 CPU 資源 。靜態優先級可以通過nice()或者setpriority等系統調用在用戶空間進行修改,新創建的進程會繼承父進程的靜態優先級 。
rt_priority表示進程的實時優先級,主要用于實時進程 。實時進程對時間的要求非常嚴格,需要在規定的時間內完成任務 。實時優先級的范圍是 1 - 99,值越大表示優先級越高 。在一個工業控制系統中,用于控制生產設備的實時進程,其rt_priority可能會被設置得較高,以確保能夠及時響應設備的各種信號,保證生產的正常進行 。普通進程的rt_priority通常為 0 。
normal_prio是歸一化優先級,它是根據靜態優先級、實時優先級和調度策略綜合計算得出的 。調度器在進行調度決策時,會參考歸一化優先級來確定進程的執行順序 。對于普通進程,其歸一化優先級通常就是靜態優先級;而對于實時進程,歸一化優先級則與實時優先級相關 。
prio是進程的動態優先級,它是調度器實際用于調度的優先級 。動態優先級在運行時可以根據進程的運行情況進行調整 。例如,當一個進程長時間占用 CPU 資源時,調度器可能會降低它的動態優先級,以便讓其他進程也有機會獲得 CPU 資源 。相反,當一個進程處于等待狀態,等待某個資源的時間較長時,調度器可能會適當提高它的動態優先級,以提高系統的整體性能 。
不同的調度策略對這些優先級成員有著不同的運用方式 。在完全公平調度(CFS)策略下,調度器會根據進程的虛擬運行時間(vruntime)和歸一化優先級來分配 CPU 時間,確保每個進程都能公平地獲得 CPU 資源 。而在實時調度策略中,如SCHED_FIFO(先進先出調度)和SCHED_RR(時間片輪轉調度),實時優先級rt_priority起著關鍵作用,高優先級的實時進程會優先獲得 CPU 資源,并且在沒有更高優先級實時進程的情況下,會一直占用 CPU,直到完成任務或者主動讓出 CPU 。
3.4 內存管理指針
在task_struct結構體中,mm和active_mm成員猶如進程內存管理世界的 “導航儀”,在進程用戶空間內存管理的復雜旅程中發揮著關鍵作用 。
mm指針指向一個mm_struct結構體,這個結構體就像是進程內存世界的 “大管家”,詳細記錄了進程用戶空間的內存布局和管理信息 。它包含了進程的代碼段、數據段、堆、棧等內存區域的映射信息,以及頁表、虛擬內存區域(VMA)列表等重要數據 。當一個進程運行時,它所需要的代碼和數據都存儲在這些內存區域中 。例如,一個 C 語言程序在運行時,程序的可執行代碼會存儲在代碼段,全局變量和靜態變量會存儲在數據段,動態分配的內存會在堆中進行管理,而函數調用時的局部變量和參數則會存儲在棧中 。mm_struct結構體通過對這些內存區域的有效管理,確保進程能夠正確地訪問和使用內存資源 。
active_mm主要用于處理內核線程的內存管理問題 。對于普通用戶進程來說,active_mm通常指向與該進程關聯的mm_struct,就像一個專屬的內存管理助手,時刻為進程提供內存管理服務 。然而,內核線程的情況有所不同 。內核線程只在內核空間中運行,不需要訪問用戶空間內存,因此它們通常沒有自己獨立的mm_struct 。在這種情況下,active_mm會指向最后一個運行在該 CPU 上的用戶進程的mm_struct 。這就好比內核線程在內存管理方面沒有自己的 “家”,但它可以借用最后一個在該 CPU 上運行的用戶進程的 “家” 來進行一些必要的內存操作 。例如,當內核線程執行某些需要訪問內存的操作時,它可以通過active_mm找到合適的內存上下文,從而確保內存操作的正確執行 。
這種內存管理機制,既保證了普通進程能夠高效地管理和使用自己的用戶空間內存,又巧妙地解決了內核線程在內存管理方面的特殊需求,使得整個系統的內存管理更加靈活和高效 。在系統中同時運行多個進程和內核線程的情況下,通過mm和active_mm的協同工作,能夠有條不紊地進行內存分配、回收和訪問控制,為系統的穩定運行提供了堅實的保障 。
3.5 親屬關系指針
在task_struct結構體中,real_parent、parent、children和sibling等成員如同一張無形的關系網,清晰地描繪了進程間的親屬關系,在進程管理和信號傳遞的舞臺上發揮著不可或缺的作用 。
real_parent指針指向進程真正的父進程,就像孩子指向自己的親生父母 。在正常情況下,當一個進程被創建時,它的real_parent會指向創建它的父進程 。例如,當我們在終端中通過命令行啟動一個新的進程時,這個新進程的real_parent就是終端進程 。然而,在某些特殊情況下,如使用調試工具(如 GDB)調試進程時,情況會有所不同 。假設在 bash 中使用 GDB 來調試一個進程,此時進程的parent是 GDB,因為 GDB 負責監控和控制進程的執行;而real_parent仍然是 bash,因為 bash 是最初創建該進程的父進程 。這種區分在進程管理和信號傳遞中非常重要,它確保了進程能夠正確地繼承父進程的資源和屬性,并且在需要時能夠向正確的父進程發送信號 。
parent指針同樣指向父進程,但它主要用于接收SIGCHLD信號和wait4()系統調用的報告 。當一個進程終止時,它會向自己的parent發送SIGCHLD信號,通知父進程自己的狀態發生了變化 。父進程可以通過wait4()系統調用來獲取子進程的終止信息,如子進程的退出狀態、資源使用情況等 。這就像孩子完成任務后向家長匯報情況,家長可以根據這些信息進行相應的處理 。
children是一個鏈表頭,它將所有屬于該進程的子進程串聯在一起,形成了一個家族樹 。鏈表中的每個元素都是該進程的子進程,通過children鏈表,父進程可以方便地管理和訪問自己的子進程 。例如,父進程可以遍歷children鏈表,對每個子進程進行資源分配、狀態查詢等操作 。
sibling指針則用于將當前進程插入到兄弟進程鏈表中,它就像連接兄弟姐妹之間的紐帶 。擁有同一父進程的所有進程互為兄弟進程,它們通過sibling指針相互關聯 。通過這個鏈表,進程可以快速找到自己的兄弟進程,實現進程間的協作和通信 。例如,在一個多進程的應用程序中,兄弟進程之間可能需要共享某些資源或者傳遞數據,通過sibling鏈表,它們可以方便地找到彼此并進行交互 。
在進程管理和信號傳遞中,這些親屬關系指針起著至關重要的作用 。當一個進程接收到信號時,它會根據自己的親屬關系將信號傳遞給合適的進程 。例如,當父進程接收到SIGCHLD信號時,它可以通過children鏈表找到對應的子進程,并進行相應的處理 。這種基于親屬關系的信號傳遞機制,確保了信號能夠準確地到達目標進程,提高了系統的響應速度和穩定性 。
3.6 時間與統計信息
在task_struct結構體中,utime、stime、start_time等時間相關成員,以及nvcsw、nivcsw等統計信息成員,就像一個個精準的記錄員,詳細地記錄著進程的時間開銷和運行統計信息,為系統的性能分析和進程管理提供了重要的數據支持 。
utime表示進程在用戶態下消耗的時間,就像運動員在比賽中實際奔跑的時間 。它記錄了進程執行用戶代碼所花費的時間,這個時間不包括進程在系統調用和內核態下的時間 。例如,一個計算密集型的進程在進行復雜的數學運算時,utime會隨著運算的進行而不斷增加 。通過統計utime,我們可以了解進程在用戶態下的執行效率,判斷進程是否存在性能瓶頸 。
stime則記錄了進程在內核態下消耗的時間,如同運動員在比賽中準備和調整的時間 。當進程進行系統調用,如讀取文件、分配內存等操作時,會進入內核態,stime會統計這部分時間 。系統調用通常涉及到內核資源的訪問和管理,stime的統計可以幫助我們了解進程對內核資源的使用情況,評估系統調用的開銷 。
start_time記錄了進程的啟動時間,就像比賽的開始時間 。它是一個時間戳,表示進程從創建到開始執行的時間點 。通過start_time,我們可以計算進程的運行時長,了解進程在系統中的存活時間 。在系統性能分析中,運行時長是一個重要的指標,它可以幫助我們判斷進程是否長時間占用系統資源,是否需要進行優化 。
nvcsw和nivcsw屬于統計信息成員,分別表示自愿上下文切換次數和非自愿上下文切換次數 。自愿上下文切換是指進程主動放棄 CPU,例如進程在等待 I/O 操作完成時,會主動讓出 CPU,此時nvcsw會增加 。這就像運動員在比賽中主動休息,調整狀態 。非自愿上下文切換則是指進程被調度器強制剝奪 CPU,例如當有更高優先級的進程需要運行時,當前進程會被切換出去,nivcsw會增加 。這就像運動員在比賽中被裁判要求暫停,讓其他選手上場 。通過統計這兩個值,我們可以了解進程在 CPU 競爭中的表現,評估調度器的性能 。如果一個進程的nivcsw過高,可能意味著系統中存在競爭激烈的情況,需要進一步優化調度策略 。
四、task_struct與進程管理的“化學反應”
4.1 進程創建
在 Linux 系統中,進程的創建如同生命的誕生,充滿了奇妙的過程,而fork()系統調用則是這個過程的關鍵 “催化劑” 。當fork()系統調用被觸發時,一場精心編排的 “復制” 大戲便拉開了帷幕 。內核首先會在內存中為新的子進程精心分配一個全新的task_struct結構體,就像為新生命準備了一個獨特的 “生命檔案” 。這個新的task_struct結構體就像是一張白紙,等待著被賦予各種關鍵信息 。
接下來,子進程開始從父進程那里繼承一系列重要的信息 。子進程會繼承父進程的進程狀態,就像孩子繼承了父母的某些特質 。如果父進程處于運行狀態,子進程在創建初期也會繼承這個狀態,等待著被調度執行 。在進程的親屬關系方面,子進程的real_parent和parent指針都會指向父進程,就像孩子與父母之間建立了緊密的聯系 。這種親屬關系的繼承確保了子進程能夠正確地融入進程家族樹,在需要時能夠向父進程尋求支持和資源 。
在內存管理方面,子進程會與父進程共享內存資源 。它們共享同一內存描述符mm_struct,這意味著它們在用戶空間中看到的內存布局是相同的 。這就好比兩個孩子住在同一所房子里,共享著房子里的各種設施 。不過,這種共享是基于寫時拷貝(Copy - On - Write,COW)機制的 。在初始階段,子進程和父進程共享內存頁面,但當其中任何一方試圖對共享頁面進行寫操作時,系統會為寫操作的一方分配新的物理頁面,將共享頁面的內容復制到新頁面中,然后進行寫操作 。這樣,既保證了內存資源的高效利用,又確保了子進程和父進程在內存操作上的獨立性 。
文件描述符表也會被子進程繼承 。這意味著父進程打開的文件,子進程同樣可以訪問 。如果父進程打開了一個日志文件用于記錄信息,子進程也能夠讀取和寫入這個文件 。這為父子進程之間的協作提供了便利,它們可以通過共享的文件進行數據傳遞和同步 。
例如,在一個多進程的服務器程序中,父進程負責監聽網絡端口,接受客戶端的連接請求 。當有新的連接到來時,父進程通過fork()創建子進程,子進程繼承了父進程的網絡連接文件描述符,從而可以獨立地處理與客戶端的通信 。在這個過程中,子進程的task_struct結構體從父進程那里繼承了必要的信息,使得子進程能夠順利地開始它的 “生命旅程”,與父進程協同工作,共同完成服務器的任務 。
4.2 進程調度
進程調度在 Linux 系統中就像是一場激烈的資源爭奪賽,而task_struct結構體則是這場比賽中的關鍵 “情報站”,為調度器提供了豐富的信息,幫助調度器做出合理的決策 。調度器在選擇下一個要執行的進程時,會仔細參考task_struct中的優先級信息 。進程的優先級就像運動員的比賽排名,優先級高的進程會優先獲得 CPU 資源 。實時進程的優先級通常高于普通進程,這是因為實時進程對時間的要求非常嚴格,需要在規定的時間內完成任務 。在一個實時視頻監控系統中,用于處理視頻流的進程會被賦予較高的優先級,以確保視頻的實時性和流暢性 。
進程的狀態也是調度器關注的重要信息 。處于TASK_RUNNING狀態的進程,如同已經站在起跑線上的運動員,時刻準備著獲取 CPU 資源,進入執行狀態 。而處于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE狀態的進程,則像是在休息或等待特定資源的運動員,調度器會暫時跳過它們,將 CPU 資源分配給更有執行條件的進程 。
當進行上下文切換時,task_struct的作用更是不可或缺 。上下文切換就像是運動員在比賽中途進行換人,新上場的運動員需要迅速適應比賽環境 。在這個過程中,task_struct保存了進程的各種上下文信息,包括 CPU 寄存器的值、堆棧指針等 。當一個進程被切換出去時,內核會將該進程的 CPU 寄存器的值等上下文信息保存到它的task_struct中 。當這個進程再次被調度執行時,內核會從它的task_struct中讀取這些上下文信息,恢復 CPU 寄存器的值和堆棧指針,使得進程能夠繼續從上次中斷的地方執行 。這就像運動員在比賽中換人后,新上場的運動員能夠迅速了解比賽的進展情況,繼續完成比賽 。
在多核心 CPU 的系統中,調度器還會根據task_struct中的cpus_allowed等信息,決定將進程分配到哪個 CPU 核心上執行 。這個信息就像是運動員的參賽場地選擇,調度器會根據進程的需求和 CPU 核心的負載情況,合理地安排進程在不同的 CPU 核心上運行,以提高系統的整體性能 。
4.3 進程終止
進程終止的過程就像是一場演出的落幕,雖然看似簡單,但背后卻涉及到一系列復雜而有序的操作,而task_struct在這個過程中扮演著關鍵的角色 。當一個進程完成了它的使命,準備終止時,內核會首先對task_struct中的相關信息進行處理 。進程會釋放它所占用的各種資源,這就像是演員在演出結束后歸還借用的道具 。例如,進程會關閉它打開的文件描述符,釋放文件鎖,將文件資源歸還給系統 。在內存管理方面,進程會釋放它所占用的內存空間,包括堆內存、棧內存等 。對于使用了動態內存分配的進程,如通過malloc()函數分配的內存,在進程終止時,這些內存會被回收,以避免內存泄漏 。
進程的狀態也會被更新 。它會從當前的運行狀態轉換為EXIT_ZOMBIE狀態,就像演員從舞臺上退下,進入了一種等待善后處理的狀態 。在EXIT_ZOMBIE狀態下,進程雖然已經停止執行,但它的task_struct結構體仍然存在于系統中,因為它還需要向父進程傳遞一些重要的信息,如進程的退出狀態、資源使用情況等 。父進程可以通過wait()或waitpid()等系統調用來獲取這些信息 。這就像演出結束后,演員需要向導演匯報演出的情況 。
當父進程調用wait()或waitpid()時,內核會處理子進程的task_struct 。內核會讀取子進程的退出狀態,這個狀態信息就像是演員的演出評價,父進程可以根據這個狀態了解子進程的執行結果 。內核會回收子進程的task_struct結構體以及其他相關資源,將它們從系統中徹底移除 。這就像是演出結束后,清理舞臺和道具,為下一場演出做好準備 。
如果父進程沒有及時調用wait()來獲取子進程的終止信息,子進程就會一直處于EXIT_ZOMBIE狀態,成為一個 “僵尸進程” 。僵尸進程雖然不占用CPU等執行資源,但它會占用系統的內存等資源,就像廢棄的道具占用著倉庫的空間 。長時間存在大量僵尸進程可能會導致系統資源的浪費,影響系統的性能 。因此,在編寫多進程程序時,父進程需要及時處理子進程的終止信息,避免僵尸進程的產生 。
五、案例實戰分析
為了更直觀地感受task_struct在實際中的應用,我們通過一個簡單的內核模塊代碼示例來一探究竟 。下面是一段用于讀取所有進程的task_struct結構信息的內核模塊代碼:
#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");
在這段代碼中,我們首先包含了必要的頭文件,這些頭文件為我們提供了與內核交互所需的各種定義和函數聲明 。hello_init函數是內核模塊的初始化函數,當模塊被加載到內核中時,這個函數會被執行 。在函數內部,我們使用for_each_process宏來遍歷系統中的所有進程 。這個宏就像是一個向導,帶領我們逐一訪問系統中的每個進程 。對于每個遍歷到的進程,我們通過printk函數打印出其pid(進程 ID)、comm(進程名稱)和flags(進程標志)等信息 。pid就像進程的身份證號碼,獨一無二地標識著每個進程;comm則是進程的名字,讓我們能夠直觀地了解進程的用途;flags包含了進程的各種狀態標志為我們提供了關于進程狀態的重要線索 。
hello_exit函數是內核模塊的退出函數,當模塊從內核中卸載時,這個函數會被執行 。在函數中,我們使用printk函數打印出模塊卸載的提示信息 。
通過這個內核模塊,我們可以清晰地看到系統中每個進程的一些關鍵信息,這些信息都來自于task_struct結構體 。這就好比我們打開了一個進程信息的寶藏庫,通過task_struct結構體,我們能夠獲取到進程的各種詳細信息,從而更好地了解系統中進程的運行狀態和行為 。