進程管理–學習筆記

來源:互聯網
上載者:User

這幾天在公司閑暇時間看了一下Robert Love的《linux核心設計與實現》(當然,我看的是中文翻譯版哈。看到英文就頭痛)進程管理、調度、系統調度、中斷這幾章,覺得這本書真的寫的很好,對我這種層級的人剛好,看起來沒那麼卡,不像最開始接觸linux的時候就抱起《linux核心源碼情景分析》來看,看得我“雲裡來霧裡去”,現在想起來真的覺得就是一口吃成大胖子,還沒學會走路就開始跑了,好了,廢話少說,這裡把進程管理這章好好複習一下,希望有興趣的人一起來學習!

 

1 幾個概念

什麼是進程

      進程就是處理執行期的程式。但進程不局限於一段可執行程式代碼,通常還包括其他資源,如存放全域變數的資料區段、開啟的檔案、掛起的訊號等,當然還包括地址空間及一個或幾個執行線程。

 

什麼是線程

      線程就是在進程中活動的對象。(呵呵,好抽象啊)。每個線程都擁有一個獨立的程式計數器、進程棧和一組進程寄存器。核心調度的對象是線程,而不是線程。在現在的系統大都都支援多線程應用程式,稍後會看到,在linux核心中,對線程的實現和進程並無特別區分。

 

建立進程

      要注意的是程式不是進程。進程是處於執行期的程式以及它所包含的資源的總稱。實際上完全可能存在兩個或多個不同的進程實際上執行同一個程式。並且它們還可以共用諸如開啟的檔案、地址空間之類的資源。在linux系統中,調用fork()系統調用(下次專門寫一篇linux系統調用的實現機制),該系統調用複製一個現有進程來建立一個全新的進程。調用fork()的進程稱為父進程,新產生的進程為子進程。在返回點這個相同位置上,父進程恢複執行,子進程開始執行。通常建立新惡進程為了執行新的不同的程式,會接著調用exec()這族函數;

 

進程退出

      程式通過exit()系統調用退出執行。這個函數會終結進程並將佔用的資源釋放掉。父進程可以通過wait4()(核心負責實現

wait4()系統調用。linux系統通過C庫通常要提供wait() waitpid() wait3() wait4()函數,下次介紹了系統調用的實現就明白C庫與系統調用的關係了)系統調用查詢子進程是否終結,這其實使得進程有了等待特定進程執行完畢的能力。進程退出執行後被設定為僵死狀態,直到它的父進程調用wait()或waitpid()為止。

 

任務

     經常我們能聽到任務(task)這個概念。其實,linux核心通常把進程也叫做任務,經常,我們一般把在核心中啟動並執行程式叫任務,在使用者空間啟動並執行程式叫進程。

 

2 進程描述符和任務隊列

     核心把進程放在叫做任務隊列(task list)的雙向迴圈鏈表中。鏈表中的每一項都是task_struct,稱為進程描述符(process decriptor)的結構,該結構定義在include/linux/sched.h檔案中。進程描述符包含一個具體進程的所有資訊。

task_struct

     task_struct相對較大,在32位機器上,大約為1.7K位元組。但它包含了核心管理一個進程的所有資訊,它包含的資料能完整描述一個正在執行的程式:它開啟的檔案、進程的地址空間、掛起的訊號、進程的狀態等。

 struct task_struct
{
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags;     /* per process flags, defined below */
    unsigned int ptrace;

    int lock_depth;         /* BKL lock depth */

    ....

}

 

Linux通過slab分配器分配task_struct結構,這樣能達到對象複用和緩衝的目的(可以避免動態分配和釋放所帶來的資源消耗)。在2.6核心中,各個進程的task_struct存放在核心棧的尾端。這樣,通過棧指標就能計算它們的位置。在linux中,在棧底或棧頂建立了一個新的結構struct thread_info:(<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
   int   uaccess_err;
}

 

                    進程核心棧 (說明一下:每個進程都有自己的核心棧。當進程從使用者態進入核心態時,CPU就自動地設定該進程的核心棧)

 ------------------------------------------最高記憶體位址

|                                                   |

|              棧首                               |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|-----------------------------------------|棧指標

|                                                   |

|                                                   |

|                                                   | 

|                                                   |

|-----------------------------------------| 

|                                                   |

|                                                   |

|    struct thread_struct                |

|-----------------------------------------|最低記憶體位址  current_thread_info()

         thread info 有一個指向進程描述符的指標

 

 

 每個任務的thread_info結構在它的核心棧的尾端分配。結構中task存放的是指向該任務實際task_struct指標。

 

這個地方我有點納悶,直接用task_struct不就行了嗎?為什麼還要thread_info,不可以把thread_info其他一些資訊放到task_struct裡面???

 

可以通過current_thread_info()->task返回當前進程資訊結構

 

進程狀態

進程描述符中state描述了進程當前的狀態,有下面五種狀態:

TASK_RUNNING                運行------進程可執行;它或者正在執行,或者在運行隊列中等待執行(我們知道如果cpu單核的話,一次只能執行一個任務,其他的任務在隊列中等待執行)

TASK_INTERRUPTIBLE      可中斷----進程正在睡眠,等待某些條件達成。一旦這些條件達成,核心就會把進程狀態設定為運行。此狀態也會因為接收到訊號而提前被喚醒並投入運行

TASK_UNINTERRUPTIBLE  不可中斷----除了不會因為接收到訊號而投入運行,其狀態與可中斷狀態相同。使用得較少

TASK_ZOMBIE                  僵死-------該進程結束了,但是父進程還沒有調用wait4()系統調用。為了父進程能夠獲知它的訊息,子進程的進程描述符仍然保留著

TASK_STOPPED                停止-------進程停止執行;進程沒有投入運行也不能投入運行。通常發生在接收到SIGSTOP SIGSTP SIGTTIN SIGTTOU等訊號的時候

 

設定當前進程的狀態

採用set_current_state(state)或set_task_state(curent, state)函數。上面我們說了current_thread_info()->task很容易就擷取了當前進程描述符,為什麼不直接擷取到了task->state = state設定呢?

那是因為如果在SMP(多核)系統中,需要保護,防止其他處理器做重新排序(進程運行隊列);

 

進程上下文

這是一個很重要的概念。當一個程式執行系統調用或觸發了某個異常時,它就陷入了核心空間,這時,我們稱核心“代碼進程執行”並處理進程上下文中。在此上下文中current宏是有效地。除非有更進階的進程搶佔了當前進程的執行,否則在核心退出的時候,程式恢複在使用者空間繼續執行。

 

系統調用和例外處理常式是對核心明確定義的介面。進程只有通過這些介面才能陷入核心。

 

LINUX進程之間存在一個明顯的繼承關係。所有進程都是PID為1的init進程的後代。核心在系統啟動的最後階段啟動init進程,該進程讀取系統的初始化指令碼並執行相關程式,最終完成系統的啟動全過程。

 

每個進程都有一個父進程,每個進程可以擁有一個或多個子進程。擁有同一個父進程的所有進程為兄弟。進程間的關係存放在進程描述符中。每個task_struct都包含一個父進程指標,還包含一個children的子進程鏈表。

struct task_struct *task = current->parent;   /*  擷取當前進程的父進程 */

struct list_head *list;

 

 

可以通過以下方式依次訪問子進程:

struct task_struct *task;

struct list_head *list;

 

list_for_each(list, &current->children)

{

    task = list_entry(list, struct task_struct, sibling)   /*sibling也是進程描述符的成員。核心是這樣解釋的:linkage in my parent's children list,父進程的子進程鏈表,也就是當前進程的兄弟進程鏈表*/

}

 

 init進程的進程描述符是作為init_task靜態分配的。(其實就是一個靜態變數吧)

下面代碼可得到所有進程之間的關係

struct task_struct *task;

 

for (task = current; task != &init_task; task = task->parent)

{

    ;

 

上面可以看到,可以從系統任何一個進程出發,找到任意指定的其他進程。但是我們不需要這麼麻煩,只要簡單的遍曆系統中的所有進程,因為任務隊列本來就是一個雙向迴圈鏈表。如下:

對於給定的進程,擷取鏈表下一個進程:

list_entry(task->tasks.next, struct task_struct, tasks) 

  (解釋一下這個什麼意思,回想一下前面的進程描述符結構,結構裡是不是有一個成員list_head tasks 。 list_entry是一個宏,由它可以由其成員tasks的指標task->tasks.next得到struct task_struct的地址

    不知道有沒有說清楚、。。)

 

同理,擷取前一個進程:

list_entry(task->tasks.prev, struct task_struct, tasks)

 

同樣,for_each_process(task)宏提供了一個依次訪問這個任務的能力:

struct task_struct *task;

 

for_each_process(task)

{

}

 

但是,在一個擁有大量進程的系統中,通過這樣的方法來遍曆所有進程是非常耗時的,所以沒有充足理由別這麼幹

 

 

 3 進程建立

 

 寫時拷貝(copy-on-write)

一開始我們介紹了進程的建立,傳統的fork()系統調用直接把所有資源複製給新建立的進程。這種實現過於簡單且效率低下。linux的fork()使用寫時拷貝(copy-on-write)頁實現

 

 寫時拷貝是一種可以延遲甚至免除拷貝資料的技術。核心並不複製整個進程地址空間,而是讓父進程和子進程共用同一個拷貝,只有在需要寫入的時候,資料才會被複製,從而使各個進程擁有各自的拷貝。

 fork()實際開銷就是複製父進程的頁表以及子進程建立唯一的進程描述符。

 

fork()

Linux通過clone()系統調用實現fork()。這個調用通過一系列的參數標誌來指明父、子進程需要共用的資源。fork()、vfork()、 __clone()庫函數都是根據各自需要的標誌去調用clone()。

然後由clone()去調用do_fork()。

do_fork完成了建立中的大部分工作,它定義在kernel/fork.c,該函數調用copy_process函數,然後讓進程開始運行。

copy_process()主要工作:

(1)調用dup_task_struct()為新進程建立一個核心棧、thread_info和task_struct,這些值與當前進程的值相同,此時父子進程描述符完成相同;

(2)檢查新建立子進程是否超過進程數目限制;

(3)子進程描述符很多成員清0或者設為初始值,區別父進程;

(4)子進程的狀態設定為TASK_UNINTERRUPTIBLE以保證它不會投入運行;

(5)調用copy_flags()以更新task_struct的flags成員。表明進程是否擁有超級使用者權限的PF_SUPERPRIV標誌清0。表明進程還沒調用exec()函數的PF_FORKNOEXEC標誌被設定;

(6)調用get_pid()為新進程擷取一個有效PID;

(7)根據傳遞給clone()的參數標誌,拷貝資源;

(8)讓父子進程平分剩餘的時間片

(9)返回一個指向子進程的指標

 

再回到do_fork()函數,如果copy_process()函數成功返回,新建立的子進程被喚醒並讓其投入運行。核心有意選擇子進程首先執行。因為一般子進程都會馬上調用exec()函數,這樣就可以避免寫時拷貝的額外開銷,

 

vfork()

vfork是fork還未實現寫時拷貝頁表現的一個最佳化,後來fork實現了就徹底沒用了,這裡也不介紹了;

 

 

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.