Linux操作系統學習筆記(五)進程的核心——task_truct

一. 前言

  在前文中,我們分析了內核啓動的整個過程以及系統調用的過程,從本文開始我們會介紹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)。

  任何一個進程,如果只有主線程,那 pidtgid相同,group_leader 指向自己。但是,如果一個進程創建了其他線程,那就會有所變化了。線程有自己的pidtgid 就是進程的主線程的 pidgroup_leader 指向的進程的主線程。因此根據pidtgid是否相等我們可以判斷該任務是進程還是線程。

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是在進程接收到 SIGSTOPSIGTTINSIGTSTP或者 SIGTTOU 信號之後進入該狀態。

  TASK_TRACED 表示進程被 debugger 等進程監視,進程執行被調試程序所停止。當一個進程被另外的進程所監視,每一個信號都會讓進程進入該狀態。

  一旦一個進程要結束,先進入的是 EXIT_ZOMBIE 狀態,但是這個時候它的父進程還沒有使用wait() 等系統調用來獲知它的終止信息,此時進程就成了殭屍進程。EXIT_DEAD 是進程的最終狀態。EXIT_ZOMBIEEXIT_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 去掉。

img

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;

  從這裏的定義可以看出,大部分是關於用戶和用戶所屬的用戶組信息。

  • uidgid,註釋是 real user/group id。一般情況下,誰啓動的進程,就是誰的 ID。但是權限審覈的時候,往往不比較這兩個,也就是說不大起作用。
  • euidegid,註釋是 effective user/group id。一看這個名字,就知道這個是起“作用”的。當這個進程要操作消息隊列、共享內存、信號量等對象的時候,其實就是在比較這個用戶和組是否有權限。
  • fsuidfsgid,也就是 filesystem user/group id。這個是對文件操作會審覈的權限。

  在Linux中,我們可以通過chmod u+s program命令更改更改euidfsuid來獲取權限。

  除了以用戶和用戶組控制權限,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結構體。

img

  這個結構是對 task_struct 結構的補充。因爲 task_struct 結構龐大但是通用,不同的體系結構就需要保存不同的東西,所以往往與體系結構有關的,都放在 thread_info 裏面。在內核代碼裏面採用一個 unionthread_infostack 放在一起,在 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_structstack 指針在手,即可通過下面的函數找到這個線程內核棧:

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_structthread_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操作系統

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章