天天看點

Linux的程序管理

目錄

1、概述

2、程序描述符

2.1 程序描述符的配置設定

2.2 程序描述符的存放

2.3 程序狀态

2.4 程序上下文

2.5 程序家族樹

3、程序的建立

4、程序的終結

5、線程的實作

1、概述

    程序是執行期的代碼。但是程序不止包括這樣一段可執行的代碼,還包括程序執行相關的各種資源:

  • 打開的檔案;
  • 挂起的信号;
  • 核心内部的資料;
  • 處理器狀态;
  • 一個或多個具有記憶體映射的位址空間;
  • 一個或多個執行線程;
  • 存放全局變量的資料段;
  • ......

    程序在建立它的時候開始存活,在Linux系統中,這通常的fork()系統調用的結果。fork()系統調用通過複制一個現有程序來建立一個新的程序,調用fork()的程序稱為父程序,新建立的程序稱為子程序。在fork()系統調用結束時,父程序恢複執行,子程序開始執行。fork()系統調用從核心傳回兩次:一次是回到父程序,一次是回到子程序。Linux核心中,fork()實際上是通過clone()調用實作的。

    fork()調用後,通過exec()調用可以建立新的位址空間,并把程式載入其中。最終,程式通過exit()調用退出執行,并釋放其占用的資源。父程序可以通過wait4()調用等待子程序的執行結果。程式執行完成後被設定為僵死狀态,直到它的父程序調用wait()或waitpid()調用。

    執行線程簡稱線程,是在程序中活動的對象。每個線程都有一個獨立的程式計數器、程序棧和一組程序寄存器。核心排程的對象是線程而不是程序。對于Linux而言,線程就是一種特殊的程序。

2、程序描述符

    程序描述符用來描述一個具體的程序的所有資訊。程序描述符的資料結構叫做task_struct,定義在<linux/sched.h>檔案中。

    task_struct資料大小約為1.7KB(32位機器),包含了核心管理一個程序所需要的全部資訊:

  • 程序的id(pid);
  • 打開的檔案;
  • 程序的位址空間;
  • 挂起的資訊;
  • 程序的狀态;
  • ......

2.1 程序描述符的配置設定

    Linux通過slab配置設定器動态配置設定task_struct結構,配置設定時隻需要在程序核心棧的尾端建立一個新的結構thread_info即可。其中,task域存放的是指向task_struct的指針。

Linux的程式管理

2.2 程序描述符的存放

    核心通過唯一的程序辨別值PID(Process Identification Value)來辨別每個程序。pid的類型是隐含類型pid_t,實際上就是int。為了相容老版本,PID數的最大值是32768(有符号short int最大值),但是可以在<linux/threads.h>中增加到400萬。核心把每個程序的pid存放到程序描述符中。

    核心大部分處理程序的操作,都是通過task_struct實作的,是以核心需要能擷取到指向task_struct的指針。有些記憶體結構有專門的寄存器存放task_struct的指針,而x86等寄存器不富裕的體系,隻能在核心棧的尾端建立thread_info,通過計算記憶體偏移來簡接查找task_struct。

2.3 程序狀态

    程序描述符的state域描述了程序的狀态。程序包含5種狀态:

  • TASK_RUNNING:運作,程序是可執行的。可能正在執行,也可能正在任務隊列等待執行。這是程序在使用者空間中執行的唯一可能的狀态;
  • TASK_INTERRUPTIBLE:可中斷,此時的程序正在睡眠(也叫阻塞),等待某些條件的達成。處于此狀态的程序會因為提前接收到信号而被喚醒;
  • TASK_UNINTERRUPTIBLE:不可中斷,除了接收到信号也不會提前被喚醒,其他的和可中斷相同。這個狀态通常用在等待時不受幹擾,或者等待的事情很快發生的場景;
  • __TASK_TRACED:被其他程序跟蹤的程序,例如通過ptrace對調試程序進行跟蹤;
  • __TASK_STOPPED:停止,程序停止執行,程序沒有投入運作,也不能投入運作。通常這種狀态發生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号時;在調試期間接受到任何信号,也會進入這個狀态。
Linux的程式管理

2.4 程序上下文

    當一個程式執行了系統調用,或者觸發了某個異常,就陷入了核心空間,此時稱核心“代表程式執行”,并處于程序上下文。除非期間有更高優先級的程序要執行,并且排程器做出了相應調整,否則在核心退出的時候,程式恢複在使用者空間繼續運作。

    系統調用和異常處理是對核心明确定義的接口,程序隻有通過這些接口才能陷入核心,即程序對核心的所有通路都需要通過這些接口。

2.5 程序家族樹

    Linux的所有程序都是PID為1的初始程序的後代。核心在系統啟動的最後階段啟動init程序。該程序讀取系統的初始化腳本,并執行相關的程式,最終完成系統啟動的整個過程。

    系統中每個程序都有一個父程序,和零個或多個子程序。擁有同一父程序的程序,稱為兄弟程序。程序間的關系存放在程序描述符中,每個task_struct都包含一個parent指針,指向父程序所在的task_struct,以及一個名為children的指針連結清單,指向所有的子程序的task_struct。

3、程序的建立

    Unix把程序的建立拆分成了兩個函數:

  • fork():Linux通過clone()系統調用實作了fork(),通過拷貝目前程序建立一個子程序。子程序和父程序的差別隻在于PID、PPID(父程序的PID)和一些統計量。
  • exec():将可執行檔案載入位址空間開始執行。

    Linux的fork()使用寫時拷貝(copy-on-write)頁實作資源的複制。資源的複制,隻有在需要寫入的時候才進行,在此之前以隻讀方式共享。這種技術使位址空間的頁拷貝推遲到實際發生寫入的時候才進行,如果頁根本不會被寫入,例如fork()後立即執行exec(),則無需複制。這樣可以避免拷貝大量根本不會被使用的資料,這也是Unix快速執行的能力的一種重要支撐。

    fork()的實際開銷就是複制父程序的頁表,以及給子程序建立唯一的程序描述符。程序頁表是程序私有的結果,映射了核心态和使用者态的線性位址。

4、程序的終結

    當一個程序終結時,核心會回收程序的全部資源,并通知父程序。

1、do_exit()方法回收資源并更新狀态:

  1. 回收位址空間:如果沒有其他程序占用,就釋放掉位址空間;
  2. 回收檔案描述符、檔案系統資料:引用計數分别遞減,如果減為0,就釋放;
  3. 執行退出代碼:執行作業系統規定的退出動作,并把退出代碼存放到task_struct中的exit_code中供父程序随時檢索;
  4. 設定退出狀态:task_struct中的exit_state設定為EXIT_ZOMBIE,程序不再接受排程;
  5. 給子程序重新尋找父程序,新的父程序是線程組的其他線程,或者init程序;
  6. 調用schedule()方法切換到新的程序。

2、此時,程序所占有的記憶體資源隻剩下核心棧、thread_info和task_struct。此時程序存在的唯一目的,就是向它的父程序提供資訊。父程序檢索到相關資訊後,或者通知核心那是無關資訊後,核心會釋放剩下的這些資源。

5、線程的實作

    Linux核心并沒有線程的概念,它把線程當做程序來實作,線程僅僅被視為與其他程序共享某些資源的程序。線程有自己的task_struct,是以從核心的角度,線程隻是一個普通的程序,僅僅是和其他程序共享了一些資源,比如位址空間等。

1、線程的建立

    建立線程和建立程序相似,也是調用clone()實作,隻不過傳遞的一些參數,用來控制共享一些資源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
           

    這些代碼是用來和父程序共享:位址空間、檔案系統資源、檔案描述符和信号處理程式。

    相對之下,建立程序的指令:

clone(SIGCHLD)
           

2、核心線程

    核心線程是獨立運作在核心空間的标準程序,核心在背景執行操作,使用核心線程來完成。核心線程和普通程序的差別,隻在于核心線程沒有獨立的位址空間。它們隻在核心空間運作,不會切換到使用者空間。核心程序和普通程序一樣,可以被排程,也可以被搶占。

    核心線程隻能由核心線程來建立,核心從kthreadadd核心程序衍生出新的核心程序。在Linux系統中,可以使用ps -ef指令看到很多核心線程。

繼續閱讀