Linux 多任務編程——進程管理

引言:

       在Linux的內核的五大組成模塊中,進程管理模塊時非常重要的一部分,它雖然不像內存管理、虛擬文件系統等模塊那樣複雜,也不像進程間通信模塊那樣條理化,但作爲五大內核模塊之一,進程管理對我們理解內核的運作、對於我們以後的編程非常重要。同時,作爲五大組成模塊中的核心模塊,它與其他四個模塊都有聯繫。下面就對進程模塊進行想寫的介紹,首先要了解進程及其相關的概念。其次介紹進程的創建、切換、撤銷等基本操作。除此之外,還給出了Linux內核是如何對進程進行調度管理的。

一、進程及其相關概念

       進程:進程可以理解爲程序執行的一個實例,它包括可執行程序以及與其相關的系統資源,比如打開的文件、掛起的信號、內核內部數據、處理器狀態、內存地址空間及包含全局變量的數據段等。從內核的角度看,進程也可以稱爲任務。

       進程描述符:與進程相關的事情非常多,比如進程的狀態、進程的優先級、進程的地址空間、允許該進程訪問的文件等等,Linux內核爲此專門設計了一個類型爲task_struct的結構體,稱之爲進程描述符。進程描述符中包含了內核管理進程的所有信息,可以說,只要得到一個進程的進程描述符,就可以知道一個進程的所有信息。

       進程狀態:進程描述符task_struct結構體中有一個state字段,表示進程當前的所處狀態。從進程的創建到進程的刪除,它可以經過5種不同的狀態,分別是可運行狀態、可中斷的等待狀態、不可中斷的等待狀態、暫停狀態、跟蹤狀態。除此之外,當進程被終止時,還可能會變爲僵死狀態、僵死撤消狀態。內核可以使用宏set_current_state(state)設置當前進程的狀態,用set_task_state(task,state)設置某進程的狀態。

       進程標示符:進程描述task_struct結構體中的pid字段可以標識唯一標識一個進程,稱之爲進程標識符PID。當創建一個新進程時,PID是按照順序從小到大分配給新進程的。內核通過管理一個pidmap_array位圖來表示當前已分配的PID和閒置的PID號。注意:在多線程組中,所有的線程共享相同的PID。除了進程標識符外,內核對進程的大部分訪問時通過進程描述符指針進行的。

       進程關係:進程之間的關係有親屬關係和非親屬關係。親屬關係包括父子關係和兄弟關係等。其中由tast_struct結構體中的parent/children/real_parent/sibling等字段描述。除了親屬關係外,還有其他關係,比如,一個進程是一個進程組或登錄會話的領頭進程,可能是一個線程組的領頭進程,這些關係由group_leader/tgid/signal->pgrp等字段描述。

       進程資源:爲了防止進程過度的使用系統資源,內核爲每個進程使用資源的數量進行了一些限制。其中包括進程地址空間的最大數、進程使用CPU的最大時間、堆的最大值、文件大小的最大值、文件鎖數量的最大值、消息隊列的最大字節數、打開文件描述符的最大數、進程擁有的頁框最大數等。

 

 二、進程的創建、切換、撤銷

       進程的創建:在Linux環境編程時,一般採用fork()函數來創建新的進程,當然,那是在用戶空間的函數,它會調用內核中的clone()系統調用,由clone()函數繼續調用do_fork()完成進程的創建。

       傳統Unix系統中,創建的子進程複製父進程所擁有的資源,這種方法效率低,因爲子進程需要拷貝父進程的整個地址空間。但是,子進程幾乎不必讀或修改父進程擁有的所有資源,因爲很多情況下,子進程創建後會立即調用exec()一族的函數,並清除父進程仔細拷貝過來的地址空間。現代Unix系統用三種方式解決了這個問題:1、寫實複製技術允許父子進程讀相同的物理頁。2、輕量級進程允許父子進程共享每進程在內核的很多數據結構。3、vfork()系統調用創建的進程能共享父進程的內存地址空間,爲了防止父進程重寫子進程需要的數據,阻塞父進程的執行,一直到子進程退出或執行一個新的程序爲止。整個進程創建過程可能涉及到如下函數:

       fork()/vfork()/_clone----------->clone()--------->do_fork()---------->copy_process()

       上面的創建過程結束之後,就有了處於可運行狀態的完整的子進程,新的子進程有了PID、進程描述符等各種數據結構,要想實際運行它,還需要調度程序把CPU交給新創建的子進程。

      除了進程外,還有內核線程(用kernet_thread創建)的概念。在Linux中,內核線程與普通進程存在以下兩個方面的不同

       1、內核線程只運行在內核態,而普通進程既可以運行在內核態,也可運行在用戶態。

       2、因爲內核線程只運行在內核態,它只使用大於PAGE_OFFSET的線性地址空間。另一方面,不管在用戶態還是在內核態,普通進程可以用4GB的線性地址空間。

       撤銷進程:進程終止後,需要通知內核以便內核釋放進程所擁有的資源,包括內存、打開文件以及其他資源,如信號量。進程終止的一般方式是調用exit()庫函數,該函數釋放C函數庫所分配的資源,執行編程者所註冊的每個函數,並結束從系統回收進程的那個系統調用。

       除了進程自己終止自己外,內核可以有選擇地強迫整個線程組死掉。這發生在:當進程接收到一個不能處理或忽視的信號時,或者當內核正在代表進程運行時再內核態產生一個不可恢復的CPU異常時。

        有兩個終止用戶態應用的系統調用:exit_group()系統調用,它終止整個線程組,即整個基於多線程的應用。do_group_exit()是實現這個系統調用的主要內核函數。exit()系統調用,它終止一個線程,而不管該線程所屬線程組中的所有其他進程。do_exit()是實現這個系統調用的主要內核函數。

       進程切換:進程切換又稱爲任務切換、上下文切換。它是這樣一種行爲,爲了控制進程的執行,內核掛起當前在CPU上運行的進程,並恢復以前掛起的某個進程的執行。

        跟函數的調用類似,進程切換時,一般要在CPU上裝載要執行進程的進程上下文。進程的硬件上下文指:可執行程序上下文的一個子集,是進程恢復執行前裝入寄存器的一組數據。其中一部分放在TSS段,即任務狀態段,剩餘部分存放在內核態堆棧中。進程的切換隻發生在內核態,在執行進程切換之前,用戶態進程使用的所有寄存器內容都已保存在內核態堆棧上。

        進程的切換有兩種方法,一種是硬件切換,一種是軟件切換。軟件切換就是利用程序逐步執行切換,它的優點是,可以對切換時裝入的數據進行合法性檢查,執行時間雖與硬件切換大致相同,但仍有可改進的地方。

     進程切換使用schedule()函數完成在本質上,每個進程切換由兩部分組成:1、切換頁全局目錄以安裝一個新的地址空間。2、切換內核態堆棧和硬件上下文,因爲硬件上下文提供了內核執行新進程所需要的所有信息,包括CPU寄存器,主要有switch_to函數完成。

 

三、進程調度

       調度策略:調度策略就是這樣一組規則:決定什麼時候以怎樣的方式選擇一個新進程運行的規則。Linux的調度基於分時技術:多個進程以“時間多路複用”方式運行,因爲CPU的時間被分成“片”,給每個可運行進程分配一片。調度策略也是根據進程的優先級對它們進行分類。在Linux中,進程的優先級是動態的。調度程序跟蹤進程正在做什麼,並週期性地調整它們的優先級。根據不同的分類標準,可以把進程分成不同的類型。比如可以把一個進程看作是“I/O受限”或“CPU受限”。也可把進程區分爲以下三類:交互式進程、批處理進程、實時進程。Linux的進程是搶佔式的,無論是處於內核態還是用戶態。時間片的長短對系統性能是很關鍵的:它既不能太長也不能太短。如果平均時間片太短,由進程切換引起的系統額外開銷就變得非常高。如果平均時間片太長,進程看起來就不再是併發執行的。對時間片大小的選擇始終是一種折中。Linux採用單憑經驗的方法,即選擇儘可能長、同時能保持良好響應時間的一個時間片。

       調度算法:早起的Linux中,調度算法是根據進程的優先級選擇“最佳”進程來執行,它的缺點是時間開銷與“可運行進程數量”有關。現代的Linux中,調度算法可以在固定時間內(與可運行進程數量無關)選中要運行的進程。首先,我們必須知道進程可以分爲實時進程與普通進程。每個LInux進程總是按照如下的調度類型被調度:先進先出的實時進程、時間片輪轉的實時進程、普通的分時進程。調度算法根據進程是普通進程還是實時進程而有很大不同。

       普通進程的調度:每個普通進程都有它自己的靜態優先級(值是從100到139),調度程序使用靜態優先級來估價系統中這個進程與其他普通進程之間調度的程度。靜態優先級決定進程的基本時間片,即進程用完了以前的時間片時,系統分配給進程的時間片長度。普通進程除了靜態優先級,還有動態優先級。動態優先級是調度程序在選擇新進程來運行的時候使用的數。平均睡眠時間是進程在睡眠狀態所消耗的平均納秒數。即使具有較高靜態優先級的普通進程獲得了較大的CPU時間片,也不應該使靜態優先級較低的進程無法運行。爲了避免這個問題,提出了活動進程和過期進程的概念,活動進程指進程的時間片還未用完,過期進程指進程的時間片以用完,即使過期進程的優先級更高,也不能繼續運行,除非等到所有活動進程都過期以後。

      實時進程的調度:每個實時進程都與一個試試優先級相關,實時優先級是一個範圍從1到99的值。跟普通進程不同,實時進程總是被當作活動進程。

      調度程序所使用的主要數據結構:數據結構runqueue和進程描述符

       數據結構runqueue:runqueue數據結構中最重要的字段是與可運行進程 的鏈表相關的字段。其中的arrays字段是活動進程和過期進程的兩個集合,active字段是指向活動進程鏈表的指針,expired字段是指向過期進程鏈表的指針。

       進程描述符:每個進程描述符都包括幾個與調度相關的字段。其中的time_slice字段是在進程的時間片中還剩餘的時鐘節拍數。它由copy_process函數設置:父進程的剩餘節拍數被劃分爲兩等分,一份給父進程,一份給子進程。

 

四、調度程序所使用的函數

       調度程序依靠幾個函數來完成調度工作,其中最重要的函數如下:

        try_to_wake_up()函數通過把進程狀態設置爲TASK_RUNNING,並把該進程插入本地CPU的運行隊列來喚醒睡眠或停止的進程。

        recalc_task_prio()函數更新進程的平均睡眠時間和動態優先級。

        schedule()憾事實現調度程序,它的任務時從運行隊列的鏈表中找到一個進程,並隨後將CPU分配給這個進程。schedule()可以由幾個內核控制路徑調用,可以採用直接調用或延遲調用的方式

 

總結:

       Linux內核中的進程管理模塊非常重要,它是連接其他4大模塊的重要橋樑,它也非常複雜,理解它的一些基本原理,對於理解Linux內核非常重要,上面只是對它進行了一些簡單的描述,並沒有深入到具體實現細節,希望已有有機會能深入分析實現的細節。


--------------------- 
作者:Mike__Jiang 
來源:CSDN 
原文:https://blog.csdn.net/tennysonsky/article/details/45168799 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

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