linux源碼分析 - 進程

本文爲原創,轉載請註明:http://www.cnblogs.com/tolimit/linux

 

  最近在回想一些知識點的時候,以爲對進程這一塊有些模糊,特別寫一篇隨筆對進程信息進行鞏固和複習。shell

 

程序和進程

  以我我的的理解就是,程序是一段二進制編碼甚至是一個簡單的可執行文件,而當程序這段二進制編碼放入內存運行時,它就會產生一個或多個進程。編程

 

 

CPU時間片

  對於CPU來講,它的工做就是不停地執行指令,而因爲CPU執行指令的速度很是快,它能夠用5ms的時間專門用於執行進程A,5ms的時間專門用於執行進程B,5ms的時間專門用於執行進程C,而後這樣不停交替執行進程A、B、C。在咱們看來就像進程A、B、C在同時執行同樣,而實際上同一個時間點只有一個進程正在CPU上運行。數組

 

 

進程描述符

  就是用於描述一個進程的結構體,每一個進程有且只有一個進程描述符,它裏面包含了這個進程相關的全部信息。session

struct task_struct {

    ......

    /* 進程狀態 */
    volatile long state;
    /* 指向內核棧 */
    void *stack;
    /* 用於加入進程鏈表 */
    struct list_head tasks;
    ......

    /* 指向該進程的內存區描述符 */
    struct mm_struct *mm, *active_mm;

    ........

    /* 進程ID,每一個進程(線程)的PID都不一樣 */
    pid_t pid;
    /* 線程組ID,同一個線程組擁有相同的pid,與領頭線程(該組中第一個輕量級進程)pid一致,保存在tgid中,線程組領頭線程的pid和tgid相同 */
    pid_t tgid;
    /* 用於鏈接到PID、TGID、PGRP、SESSION哈希表 */
    struct pid_link pids[PIDTYPE_MAX];

    ........

    /* 指向建立其的父進程,若是其父進程不存在,則指向init進程 */
    struct task_struct __rcu *real_parent;
    /* 指向當前的父進程,一般與real_parent一致 */
    struct task_struct __rcu *parent;

    /* 子進程鏈表 */
    struct list_head children;
    /* 兄弟進程鏈表 */
    struct list_head sibling;
    /* 線程組領頭線程指針 */
    struct task_struct *group_leader;

    /* 在進程切換時保存硬件上下文(硬件上下文一共保存在2個地方: thread_struct(保存大部分CPU寄存器值,包括內核態堆棧棧頂地址和IO許可權限位),內核棧(保存eax,ebx,ecx,edx等通用寄存器值)) */
    struct thread_struct thread;

    /* 當前目錄 */
    struct fs_struct *fs;

    /* 指向文件描述符,該進程全部打開的文件會在這裏面的一個指針數組裏 */
    struct files_struct *files;

    ........

  /* 信號描述符,用於跟蹤共享掛起信號隊列,被屬於同一線程組的全部進程共享,也就是同一線程組的線程此指針指向同一個信號描述符 */
  struct signal_struct *signal;
  /* 信號處理函數描述符 */
  struct sighand_struct *sighand;

  /* sigset_t是一個位數組,每種信號對應一個位,linux中信號最大數是64
   * blocked: 被阻塞信號掩碼
   * real_blocked: 被阻塞信號的臨時掩碼
   */
  sigset_t blocked, real_blocked;
  sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
  /* 私有掛起信號隊列 */
  struct sigpending pending;


    ........
}

  這裏只截取了部分以後須要說明的字段。在內核中,會有一個進程鏈表經過使用進程描述符中的tasks結構把全部進程的進程鏈表連接起來。多線程

 

 

進程內核棧

  咱們在編程的時候知道,在進程地址空間中有個棧,用於程序的順利執行,而當程序陷入內核態以後,就不可以使用應用態的棧了,因此,對於每一個進程(準確說是對於每一個線程),它在內核中也有一個內核態的棧區,在內核中,把棧和thread_info(線程描述符)結構結合起來放在一塊兒,這塊存儲區域一般爲8192字節,也就是兩個頁框。thread_info結構大小爲52字節,也就是說,進程的可用的棧大小爲8140個字節。由於進程在內核態中所須要執行的代碼量並不算多,因此這個8K的內核棧已經足夠使用。在編譯內核時也能夠設置整個內核棧爲一個頁框大小(4KB),不過在這種狀況下,內核在處理硬中斷和軟中斷時就不使用進程的內核棧棧,而是使用額外的兩個個棧:硬中斷請求棧(每一個CPU一個,大小4K),軟中斷請求棧(每一個CPU一個,大小4K)。不過值得注意的是,在進行異常處理時仍是會使用進程的內核棧。函數

  如上圖能夠看到,進程的內核棧是向下增加的,也就是棧底在高位地址,棧頂在低位地址。對於這個內核棧的做用,咱們能夠總結一下:編碼

  • 進程陷入內核後用於代替應用層的棧區進行使用。
  • 中斷髮生時用於保存進程上下文現場,而且用於中斷嵌套的現場保存和返回。
  • 當發生進程切換時,部分寄存器的值會保存在進程的內核棧中。
  • thread_info中保存着一些重要的字段用於維持進程的正常運行。

 

 

輕量級進程

  linux使用輕量級進程對多線程應用提供支持,其實它的建立也是基於fork()系統調用,只是在進程描述符的初始化當中有所區別。首先,輕量級進程也是一個進程,它有它本身的pid,有它本身的內核棧和進程描述符,甚至還有它本身的調度策略,而輕量級進程和普通進程不一樣的就是它沒有本身的進程地址空間,而且要響應線程組內其餘線程接收到的信號(但能夠經過修改信號屏蔽字屏蔽某些信號)。輕量級進程使用的是父進程的內存地址空間,也就是在task_struct結構中的mm和active_mm指針都指向父進程的mm指針所指地址。而信號描述符指針signal會指向父進程指向的地址。而在應用層,線程有本身的棧,我想這個應該是由glibc實現的。spa

  輕量級進程和普通進程區別:線程

  • 沒有本身的進程地址空間,使用父進程的進程地址空間
  • 與組內全部進程共享信號,但有本身的信號屏蔽字

 

 

進程狀態

  • TASK_RUNNING:可運行狀態,進程要麼在CPU上執行,要麼準備執行。
  • TASK_INTERRUPTIBLE:可中斷的等待狀態,進程被掛起(睡眠),直到某個條件爲真,產生一個硬中斷、釋放進程正等待的系統資源、或傳遞一個信號都是能夠喚醒進程的條件。
  • TASK_UNINTERRUPTIBLE:不可中斷的等待狀態,與可中斷等待狀態相似,只是不能被信號喚醒。在一些特殊狀況下會使用,例如:當進程打開一個設備文件,設備驅動會開始探測相應的硬件時會用到這種狀態。
  • TASK_STOPED:暫停狀態,當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信號後進入。
  • TASK_TRACED:跟蹤狀態,進程執行由debugger程序暫停,當一個進程被另外一個進程監控時,任何信號均可以把這個進程置於TASK_TRACED狀態。

  還有兩個狀態是既能夠存放在進程描述符的state字段中,也能夠存放在exit_state字段中。從這兩個字段能夠看出,只有當進程執行被終止時,進程的狀態纔會爲這兩種狀態中的一種:

  • EXIT_ZOMBIE:僵死狀態,進程將被終止,但父進程尚未發佈wait4()或者waitpid()系統調用來返回關於死亡進程的信息。發佈wait()類系統調用以前,內核不能丟棄包含在死進程描述符中的數據,由於父進程可能還須要它。
  • EXIT_DEAD:僵死撤銷狀態,進程被終止後的最終狀態,父進程發佈wait4()或者waitpid()系統調用後,內核刪除此進程描述符。

 

  對於一個普通進程,它的執行狀態以下圖所示:

  咱們使用一個簡單地例子說明這種狀態的轉變,咱們有個程序A,它的工做就是作一些計算,而後把計算結構寫入磁盤文件中。咱們在shell中運行它,起初它就是TASK_RUNNING狀態,也就是運行態,CPU會不停地分配時間片供咱們的進程A運行,每次時間片耗盡後,進程A都會轉變到就緒態(實際上仍是TASK_RUNNING狀態,只是此時在等待CPU分配時間片,暫時不在CPU上運行)。當進程A使用fwrite或write將數據寫入磁盤文件時,就會進入阻塞態(TASK_INTERRUPTIBLE狀態),而磁盤將數據寫入完畢後,會經過一箇中斷告知內核,內核此時會將進程A的狀態由阻塞態(TASK_INTERRUPTIBLE)轉變爲就緒態(TASK_RUNNING)等待CPU分配時間片運行。而最後當進程A須要退出時,內核先會將其設置爲僵死狀態(EXIT_ZOMBIE),這時候它所使用的內存已經被釋放,只保留了一個進程描述符供父進程使用,最後當父進程(也就是咱們起初啓動它的shell)經過wait()類系統調用通知內核後,內後會將進程A設置爲僵死撤銷狀態(EXIT_DEAD),並釋放其進程描述符。到這裏進程A的整個運行週期完整結束。

 

 

PID和tgid字段

  PID是一個數字,用於標識一個進程,就像學生的學號同樣,每一個進程都有一個惟一的編號,保存在進程描述符的pid字段中。通常的,在系統運行期間,PID都是被順序編號,好比進程A的PID爲10,那下個建立的進程的PID則爲11。不過PID的值有一個上限,當內核使用的PID達到這個上限後就會循環開始找已閒置的小PID號。在缺省狀態下,最大PID值爲32767(PID_MAX_DEFAULT - 1);能夠經過修改/proc/sys/kernel/pid_max這個文件來減少PID上限值。而在64位系統中,PID可擴大到4194303。

  內核是經過一個叫pidmap的位圖來管理已分配的PID號和閒置的PID號。在32位系統中,pidmap的大小就是一個頁框的大小(4KB),而一個頁框大小爲32768位,也就是每一位表明一個PID號,1表明此PID已經被分配,0表明此PID號未被使用;而在64位系統下,pidmap會使用多個頁框。

  在POSIX標準中規定了一個多線程應用程序中全部的線程都必須有相同的PID,在linux內核中,是使用輕量級進程實現線程的功能,可是輕量級進程也是一個進程,他們的PID都不相同,爲了實現這一點,內核在進程描述符中引入了tgid字段。在linux的線程組概念中,一個線程組中全部線程使用的該線程組領頭線程相同的PID,也就是該組第一個輕量級進程的PID,並保存到進程描述符的tgid字段中,以下圖:

  

  在編程過程當中,咱們使用的getpid()函數返回的值實際上是當前進程的tgid而不是pid的值,而因爲線程組中領頭線程和pid和tgid相同,於是getpid()對這類進程所起到的做用和通常進程是同樣的。

  接下來講說內核如何將全部的PID和進程描述符組織在一塊兒,方便系統查找和使用。在系統運行過程當中,可能會有成百上千的進程在運行,這時候進程的查找效率就相當重要了,好比系統管理員使用kill 1024命令去終止PID=1024的進程,內核會從這個PID導出對應的進程描述符進行處理。內核爲了提升查找效率,專門使用了4個哈希表用於索引進程描述符。爲何要4個,由於咱們能夠用pid、tgid、pgrp、session去找進程,這幾個哈希表說明以下:

  在內核中,這四個哈希表一共佔16個頁框,也就是每一個哈希表佔4個頁框,他們每一個能夠擁有2048個表項,內核會把把這四個哈希表的地址保存到pid_hash數組中。如今問題來了,拿pid的哈希表爲例,怎麼在2048個表項中保存32767個PID值,其實內核會對每一個已經分配的PID值進行一個處理,獲得的結果的數值就是對應的表項,處理結果相同的PID被串成一個鏈表,以下:

 

  當咱們使用kill 29384命令時,內核會根據29384處理得出199,而後以199爲下標,獲取PID哈希表中對應的鏈表頭,並在此鏈表中找出PID=29384的進程。進程描述符中使用struct pid_link pids[PIDTYPE_MAX]鏈入這四個哈希表。對於另外三個哈希表,道理同樣。

 

 

進程間關係

  在系統中,除了進程0,一個進程是由另外一個進程建立,它們都具備父子關係。若是一個進程建立多個子進程,則子進程之間有兄弟關係。在整個系統啓動期間,會初始化系統的第一個進程init_task,這個進程屬於內核中的一個進程,它算是全部進程的祖先,以後它會啓動PID爲1的init進程和PID爲2的kthreadd,這兩個進程以後啓動的全部進程,而init_task以後會轉變爲一個idle進程用於CPU空閒時運行。在進程描述符中,使用real_parent、parent、children、sibling這幾個指針將進程關係組織在一塊兒,咱們看看這幾個指針的說明:

  而若是一個進程P0建立了進程P一、P二、P3,進程P3又建立了進程P4,它們整個鏈表狀況是這樣的

 

 

組織進程

  全部處於TASK_RUNNING狀態的進程都會被放入CPU的運行隊列,它們有可能在不一樣CPU的運行隊列中。

  系統沒有爲TASK_STOPED、EXIT_ZOMBIE和EXIT_DEAD狀態的進程創建專門的鏈表,由於處於這些狀態的進程訪問比較簡單,可經過PID和經過特定父進程的子進程鏈表進行訪問。

  全部TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都會被放入相應的等待隊列,系統中有不少種等待隊列,有些是等待磁盤操做的終止,有些是等待釋放系統資源,有些是等待時間通過固定的間隔,每一個等待隊列它的喚醒條件不一樣,好比等待隊列1是等待系統釋放資源A的,等待隊列2是等待系統釋放資源B的。所以,等待隊列表示一組睡眠進程,當某一條件爲真時,由內核喚醒這條等待隊列上的進程。咱們看看內核中一個簡單的sleep_on()函數:

/* wq爲某個等待隊列的隊列頭 */
void sleep_on (wait_queue_head_t *wq)
{
    /* 聲明一個等待隊列結點 */
    wait_queue_t wait;

    /* 用當前進程初始化這個等待隊列結點 */
    init_waitqueue_entry (&wait, current);

    /* 設置當前進程狀態爲TASK_UNINTERRUPTIBLE */
    current->state = TASK_UNINTERRUPTIBLE;

    /* 將這個表明着當前進程的等待隊列結點加入到wq這個等待隊列 */
    add_wait_queue (wq, &wait);

    /* 請求調度器進行調度,執行完schedule後進程會被移除CPU運行隊列,只有等待隊列喚醒後纔會從新回到CPU運行隊列 */
    schedule ();

    /* 這裏進程已經被等待隊列喚醒,從新移到CPU運行隊列,也就是等待的條件已經爲真,喚醒後第一件事就是將本身從等待隊列wq中移除 */
    remove_wait_queue (wq, &wait);  
}

  這時候又有一個問題,好比有等待隊列是等待系統釋放資源A,而等待隊列中全部的進程都是但願可以佔有這個資源A的,就像咱們編程中用到的信號量,這時候系統的作法不是將這個等待隊列中全部的進程都進行喚醒,而是隻喚醒一個。內核區分這種互斥進程的原理就是這個等待隊列中全部的等待隊列結點wait_queue_t中的flags被設置爲1(默認是0)。

相關文章
相關標籤/搜索