一步步理解Linux進程(3)–核心中進程的實現

來源:互聯網
上載者:User
0. 說明
作者:Gao Peng<gaopenghigh@gmail.com> 本文章由Gao Peng編寫,轉載請註明出處。 原文地址:
http://blog.csdn.net/gaopenghigh/article/details/8831692 1. 進程描述符(process descriptor) 核心中,進程的所有資訊存放在一個叫做“進程描述符”(process descriptor)的struct中,結構名叫做task_struct,該結構定義在<linux/sched.h>檔案中。核心又把所有進程描述符放在一個叫做“任務隊列(task list)的雙向迴圈列表中。關於雙向迴圈列表的實現,參見《核心中雙向列表的實現》(http://blog.csdn.net/gaopenghigh/article/details/8830293)。 task_stuct的如下: 2. 進程的狀態 task_struct中的state欄位描述了該進程當前所處的狀態,進程可能的狀態必然是下面5種當中的一種:     1. TASK_RUNNING(運行) -- 進程正在執行,或者在運行隊列中等待執行。這是進程在使用者空間中唯一可能的狀態。     2. TASK_INTERRUPTIBLE(可中斷) -- 進程正在睡眠(阻塞),等待某些條件的達成。一個硬體中斷的產生、釋放進程正在等待的系統資源、傳遞一個訊號等都可以作為條件喚醒進程。     3. TASK_UNINTERRUPTIBLE(不可中斷) -- 與可中斷狀態類似,除了就算是收到訊號也不會被喚醒或準備投入運行,對訊號不做響應。這個狀態通常在進程必須在等待時不受幹擾或等待事件很快就會發生時出現。例如,當進程開啟一個裝置檔案,相應的裝置驅動程式需要探測某個硬體裝置,此時進程就會處於這個狀態,確保在探測完成前,裝置驅動程式不會被中斷。     4. __TASK_TRACED -- 被其它進程跟蹤的進程,比如ptrace對程式進行跟蹤。     5. __TASK_STOPPED -- 進程停止執行。通常這種狀態發生在接收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU等訊號的時候。此外,在調試期間接收到任何訊號,都會使進程進入這種狀態。 可以通過set_task_state()函數來改變一個進程的狀態:  set_task_state(task, state); 3. 線程描述符 與task_struct結構相關的一個小資料結構是thread_info(線程描述符)。對於每一個進程,核心把它核心態的進程堆棧和進程對應的thread_info這兩個資料結構緊湊地存放在一個單獨為進程分配的儲存地區內。X86上,struct_info在檔案<linux/thread_info.h>定義如下(LINUX_SRC/arch/x86/include/asm/thread_info.h)
struct thread_info {    struct task_struct  *task;      /* main task structure */    struct exec_domain  *exec_domain;   /* execution domain */    __u32           flags;      /* low level flags */    __u32           status;     /* thread synchronous flags */    __u32           cpu;        /* current CPU */    int         preempt_count;  /* 0 => preemptable,                           <0 => BUG */    mm_segment_t        addr_limit;    struct restart_block    restart_block;    void __user     *sysenter_return;#ifdef CONFIG_X86_32    unsigned long           previous_esp;   /* ESP of the previous stack in                           case of nested (IRQ) stacks */    __u8            supervisor_stack[0];#endif    unsigned int        sig_on_uaccess_error:1;    unsigned int        uaccess_err:1;  /* uaccess failed */};

核心用一個thread_union來方便地表示一個進程的thread_info和核心棧:(<linux/sched.h>)

union thread_union {    struct thread_info thread_info;    unsigned long stack[THREAD_SIZE/sizeof(long)];};

thread_info和進程的核心態堆棧在記憶體中的存放方式如:
圖中,esp寄存器是CPU棧指標,用來存放棧頂單元的地址。兩者緊密結合起來存放的主要好處是:核心很容易從esp寄存器的值獲得當前在CPU上正在啟動並執行進程的thread_info的地址(例如,如果thread_union結構長度為2^13位元組即8K,那麼只要屏蔽掉esp的低13位即可得到對應的thread_info的基址),繼而可以得到該進程的task_struct的地址。對於像x86那樣寄存器較少的硬體體繫結構,只要通過棧指標就能得到當前進程的task_struct結構,避免了使用額外的寄存器專門記錄。(對於PowerPC結構,它有一個專門的寄存器儲存當前進程的task_struct地址,需要時直接取用即可。) 核心中大部分處理進程的代碼都是直接通過task_struct進行的,可以通過current宏尋找到當前正在運行進程的task_struct。硬體體系不同,該宏的實現不同,它必須針對專門的影印件體繫結構做處理。想PowerPC可以直接去取寄存器的值,對於x86,則在核心棧的尾端建立thread_info結構,通過計算位移間接地尋找task_struct結構,比如上面說的“那麼只要屏蔽掉esp的低13位”。

4. 進程的標識和定位(PID散列表) 我們知道進程通過PID來標識,PID有個預設的最大值,一般是32768,這個值在<linux/threads.h>中定義: #define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) 也可以通過修改/proc/sys/kernel/pid_max的值來提高上限。 為了能高效地從PID匯出對應的進程描述符指標(而不是順序掃描鏈表),核心引入了4個散列表。(是4個的原因是由於進程描述符中包含了4個不同類型的PID的欄位,每種類型的PID需要自己的散列表),這四個散列表存放在task_struct結構中的名為pids的成員表示的數組中。 Hash表的類型   欄位名     說明 PIDTYPE_PID   pid      進程的PID PIDTYPE_TGID  tgid     線程組領頭進程的PID PIDTYPE_PGID  pgrp     進程組領頭進程的PID PIDTYPE_SID   session  會話領頭進程的PID 在task_struct中與這4個散列表有關的內容是:
/* 在linux-3.4.5/include/linux/sched.h中 */struct task_struct {    ... ...    /* PID/PID hash table linkage. */                                                   struct pid_link pids[PIDTYPE_MAX];    ... ...};/* 在linux-3.4.5/include/linux/pid.h中 */enum pid_type                                                                   {                                                                                   PIDTYPE_PID,                                                                    PIDTYPE_PGID,                                                                   PIDTYPE_SID,                                                                    PIDTYPE_MAX                                                                 };struct pid                                                                         {                                                                                      atomic_t count;                                                                    unsigned int level;                                                                /* lists of tasks that use this pid */                                             struct hlist_head tasks[PIDTYPE_MAX];                                              struct rcu_head rcu;                                                               struct upid numbers[1];                                                        };                                                                                                                            struct pid_link                                                                    {                                                                                      struct hlist_node node;                                                            struct pid *pid;                                                               };

在3.4.5版本的核心中,pid和tgid直接是task_struct結構的成員: pid_t pid; pid_t tgid;

注意,對於核心來說,線程只是共用了一些資源的進程,也用進程描述符來描述。getpid(擷取進程ID)系統調用返回的也是tast_struct中的tgid, 而tast_struct中的pid則由gettid系統調用來返回。在執行ps命令的時候不展現子線程,也是有一些問題的。比如程式a.out運行時,建立 了一個線程。假設主線程的pid是10001、子線程是10002(它們的tgid都是10001)。這時如果你kill 10002,是可以把10001和10002這兩個線程一起殺死的,儘管執行ps命令的時候根本看不到10002這個進程。如果你不知道linux線程背後的故事,肯定會覺得遇到靈異事件了。 pgrp和session是通過下面這兩個函數取得的:
static inline struct pid *task_pgrp(struct task_struct *task){    return task->group_leader->pids[PIDTYPE_PGID].pid;}static inline struct pid *task_session(struct task_struct *task){    return task->group_leader->pids[PIDTYPE_SID].pid;}

而group_leader是線程組的組長線程的進程描述符,在task_struct中定義如下:

struct task_struct *group_leader;   /* threadgroup leader */

一個PID只對應著一個進程,但是一個PGID,TGID和SID可能對應著多個進程,所以在pid結構體中,把擁有同樣PID(廣義的PID)的進程放進名為tasks的成員表示的數組中,當然,不同類型的ID放在相應的數組元素中。

考慮下面四個進程:     進程A: PID=12345, PGID=12344, SID=12300     進程B: PID=12344, PGID=12344, SID=12300,它是進程組12344的組長進程     進程C: PID=12346, PGID=12344, SID=12300     進程D: PID=12347, PGID=12347, SID=12300 分別用task_a, task_b, task_c和task_d表示它們的task_struct,則它們之間的聯絡是: task_a.pids[PIDTYPE_PGID].pid.tasks[PIDTYPE_PGID]指向有進程A-B-C構成的列表 task_a.pids[PIDTYPE_SID].pid.tasks[PIDTYPE_SID]指向有進程A-B-C-D構成的列表

核心初始化期間動態地為4個散列表分配空間,並把它們的地址存入pid_hash數組。 核心用pid_hashfn宏把PID轉換為表索引(kernel/pid.c):
#define pid_hashfn(nr, ns)  \    hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)

這個宏就負責把一個PID轉換為一個index,關於hash_long函數以及核心中的hash演算法,可以參考《Linux核心中hash函數的實現》(
http://blog.csdn.net/gaopenghigh/article/details/8831312
) 現在我們已經可以通過pid_hashfn把PID轉換為一個index了,接下來我們再來想一想其中的問題。首先,對於核心中所用的hash演算法,不同的PID/TGID/PGRP/SESSION的ID(沒做特殊聲明前一般用PID作為代表),有可能會對應到相同的hash表索引,也就是衝突(colliding)。於是一個index指向的不是單個進程,而是一個進程的列表,這些進程的PID的hash值都一樣。task_struct中pids表示的四個列表,就是具有同樣hash值的進程組成的列表。比如進程A的task_struct中的pids[PIDTYPE_PID]指向了所有PID的hash值都和A的PID的hash值相等的進程的列表,pids[PIDTYPE_PGID]指向所有PGID的hash值與A的PGID的hash值相等的進程的列表。需要注意的是,與A同組的進程,他們具有同樣的PGID,更具上面所解釋的,這些進程構成的鏈表是存放在A的pids[PIDTYPE_PGID].pid.tasks指向的列表中。 下面的圖片說明了hash和進程鏈表的關係,圖中TGID=4351和TGID=246具有同樣的hash值。(圖中的欄位名稱比較老,但大意是一樣的,只要把pid_chain看做是pid_link結構中的node,把pid_list看做是pid結構中的tasks即可)

處理PID散列表的函數和宏: do_each_task_pid(nr, type, task) while_each_task_pid(na, type, task)     迴圈作用在PID值等於nr的PID鏈表上,鏈表的類型由參數type給出,task指向當前被掃描的元素的進程描述符。 find_task_by_pid_type(type, nr)     在type類型的散列表中尋找PID等於nr的進程。 find_tsk_by_pid(nr)     同find_task_by_pid_type(PIDTYPE_PID, nr) attach_pid(task, type, nr)     把task指向的PID等於nr的進程描述符插入type類型的散列表中。 detach_pid(task, type)     從type類型的PID進程鏈表中刪除task所指向的進程描述符。 next_thread(task)     返回PIDTYPE_TGID類型的散列錶鏈表中task指示的下一個進程。

5. fork()的實現 Linux通過clone()系統調用實現fork()。這個調用通過一系列的參數標誌來指明父、子進程需要共用的資源。fork(), vfork(), __clone()庫函數都根據各自需要的參數標誌去調用clone(), 然後由clone()去調用do_fork(). 之前我們討論過Linux中的線程就是共用了一些資源的進程,所以只要設定對參數,即指定特別的clone_flags就能建立一個線程,就能通過clone()系統調用建立線程。 Linux的fork()使用“寫時拷貝”(copy-on-write)來實現,這是一種可以延遲甚至免除拷貝資料的技術。也就是說,核心此時並不賦值整個進程地址空間,而是讓父進程和子進程共用同一個拷貝。只有在需要寫入時,資料才會被複製,從而使各個進程擁有各自的拷貝,在此之前,只是以唯讀方式共用。在頁根本不會被寫入的情況下(比如fork()之後立即調用exec())它們就無需複製了。 clone()參數的標誌如下: 參數標誌               含義 CLONE_FILES          父子進程共用開啟的檔案 CLONE_FS             父子進程共用檔案系統資訊 CLONE_IDLETASK       將PID設定為0(只供idle進程使用)TODO CONE_NEWNS           為子進程建立新的命名空間(即它自己的已掛載檔案系統視圖) CLONE_PARENT         指定子進程魚父進程擁有同一個父進程。即設定子進程的父進程(進程描述符中的parent和real_parent欄位)為調用進程的父進程 CLONE_PTRACE         繼續調試子進程。如果父進程被跟蹤,則子進程也被跟蹤 CLONE_SETTID         將TID回寫致使用者空間。TODO CLONE_SETTLS         為子進程建立新的TLS CLONE_SIGHAND        父子進程共用訊號處理函數及被阻斷的訊號 CLONE_SYSVSEM        父子進程共用SYstem V SEM_UNDO 語義。TODO CLONE_THREAD         父子進程放入相同的線程組 CLONE_VFOK           調用vfork(),所以父進程準備睡眠等待子進程將其喚醒 CLONE_UNTRACED       防止跟蹤進程在子進程上強制執行CLONE_PTRACE,即使CLONE_PTRACE標誌失去作用 CLONE_STOP           以TASK_STOPED狀態開始進程 CLONE_CHILD_CLEARTID 清除子進程的TID TODO CLONE_CHILD_SETTID   設定子進程的TID CLONE_PARENT_SETTID  設定父進程的TID CLONE_VM             父子進程共用地址空間

負責建立進程的函數的階層入: do_fork() 核心的函數do_fork主要工作內容如下: 做一些許可權和使用者態命名空間的檢查(TODO), 調用copy_process函數得到新進程的進程描述符,如果copy_process返回成功,則喚醒新進程並讓其投入運行。核心有意選擇子進程首先執行。因為一般子進程會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。現在我們再來看一看copy_process()函數到底做了些什麼。 copy_process() copy_process()建立子進程的進程描述符以及執行它所需要的所有其它核心資料結構,但並不真的執行子進程,這個函數非常複雜,它的主要步驟如下: 1. 檢查參數clone_flags所傳遞標誌的一致性。標誌必須遵守一定的規則,不符合這些規則的話它就返回錯誤代號。 2. 調用security_task_create()以及稍後調用的security_task_alloc()執行所有附加的安全檢查。 3. 調用dup_task_struct()為新進程建立一個核心棧、thread_info結構和task_struct, 這些值與當前進程的值相同。此時,子進程和父進程的進程描述符是完全相同的。dup_task_struct()的工作如下:     a. prepare_to_copy(), 如果有需要,把一些寄存器(FPU)2的內容儲存到父進程的thread_info結構中,稍後,這些值會複製到子進程的thread_info結構中;     b. tsk = alloc_task_struct();獲得task_struct結構,ti = alloc_thread_info();獲得task_info結構, 把父進程的值考到新的結構中,並把tsk和ti連結上:tsk->thread_info = ti;ti->task = tsk;     c. 把新進程描述符的使用計數器(tsk->usage)置為2,用來表示進程描述符正在被使用而且其相應的進程處於活動狀態。     d. 返回tsk. 4. 檢查並確保新建立這個子進程後,目前使用者所擁有的進程數目沒有超出給它分配的資源的限制。 5. 檢查系統中的進程數量是否超過max_threads變數的值。系統管理員可以通過寫/proc/sys/kernel/threads-max檔案來改變這個值。 6. 新進程著手使自己與父進程區別開來。進程描述符內的許多成員都要被清0或設為初始值。那些不是從父進程繼承而來的成員,主要是些統計資訊,大部分的資料依然未被修改。 + 根據傳遞給clone()的參數標誌,copy_process()拷貝或共用開啟的檔案、檔案系統資訊、訊號處理函數、進程地址空間和命名空間等。一般情況下,這些資源是會在給定進程的所有線程之間共用的,不能共用的資源則拷貝到新進程裡面去。 7. 調用sched_fork()函數,把子進程狀態設定為TASK_RUNNING,並完成一些進程調度需要的初始化。 8. 根據clone_flags做一些進程親子關係的初始化,比如clone_flags中有CLONE_PARENT|CLONE_THREAD,則把子進程的real_parent的值等於父進程的real_parent的值。 9. 根據flags對是否需要跟蹤子進程做一些初始化; 10. 執行SET_LINKS()宏把新進程描述符插入進程鏈表,SET_LINKS宏定義在<linux/sched.h>中。 11. 判斷子進程是否要被跟蹤,做一些設定。 12. 調用attch_pid()把新進程描述符插入到pidhash[PIDTYPE_PID]散列表。 13. 做一些掃尾工作,修改一些計數器的值,並返回新進程的進程的進程描述符指標tsk。在copy_process()函數成功返回後,do_fork()函數將喚醒新建立的進程並投入運行。

6. 核心線程 核心通過核心線程(kernel thread)來完成一些經常需要在後台完成的操作,比如刷緩衝,進階電源管理等等。核心線程和普通進程的區別在於核心線程沒有獨立的地址空間,它只在核心態運行。 核心線程只能由其它核心線程建立,核心是通過從kthreadd核心進程中衍生出所有新的核心線程來自動處理這一點的。建立一個新的核心線程的方法如下:
/* <linux/kthread.h> */struct task_struct *kthread_create(int (*threadfn)(void *data),                   void *data,                   const char namefmt[], ...);

新的任務是由kthread核心進程通過clone()系統調用建立的,新進程建立後處於不可運行狀態,需要用wake_up_process()來喚醒,喚醒後,它運行threadfn函數,以data作為參數,新核心線程的名字叫namefmt。 進程0 所有進程都是由一個pid=0的祖先繁衍出來的,它叫做進程0,它是Linux在初始化階段從無到有造出來的,使用的是靜態資料結構(其他進程的資料結構都是動態分配的)。 進程1 進程0會建立一個新進程:進程1,也就是init進程,init進程被調度到後會運行init()方法,完成核心的初始化,然後用execve()系統調用裝入可執行程式init,於是init進程從核心線程轉變為普通進程。

7. 進程終結 所有進程的終止都是由do_exit()函數來處理的(位於<kernel/exit.c>)。這個函數從核心資料結構中刪除對終止進程的大部分引用,注意task_struct和thread_info並沒有被刪除,這些資訊在父進程調用wait系列函數時仍然需要。 需要注意的是,do_exit()中會調用exit_notify()函數向父進程發送訊號,給它的子進程重新尋找養父,養父為線程組中的其它進程,找不到時則為init進程,並把進程狀態(task_struct結構中的exit_state成員)設成EXIT_ZOMBIE。然後,do_exit()調用schedule()切換到新的進程。 這樣與進程相關聯的所有資源都被釋放掉了(當然預其它進程共用的資源不會被釋放)。進程不可運行(實際上也沒有地址空間讓它運行)並處於EXIT_ZOMBIE狀態。它佔用的所有記憶體就是核心棧、thread_info結構和task_struct結構。當父進程發出了與被終止進程相關的wait()類系統調用之後,這些記憶體才會被釋放。

參考資料: 《深入理解Linux核心》 《Linux核心設計與實現》

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.