天天看點

linux核心-程序

程序是多道程式設計系統的作業系統的基本概念,通常把程序定義為一個程式的執行執行個體。

程序

程式在一個資料執行個體上的一次執行過程,資源配置設定的基本機關

輕量級程序

在計算機作業系統中,輕量級程序(LWP)是一種實作多任務的方法。與普通程序相比,LWP與其他程序共享所有(或大部分)它的邏輯位址空間和系統資源;與線程相比,LWP有它自己的程序辨別符,優先級,狀态,以及棧和局部存儲區,并和其他程序有着父子關系;這是和類Unix作業系統的系統調用vfork()生成的程序一樣的。另外,線程既可由應用程式管理,又可由核心管理,而LWP隻能由核心管理并像普通程序一樣被排程。Linux核心是支援LWP的典型例子。兩個輕量級程序基本上可以共享一些資源,隻要一個程序修改,另外一個程序就檢視修改,達到同步。

線程:程序的進一步劃分,排程的基本機關,一個程序的多個線程共享程序的資源。

程序描述符

為了管理程序,核心必須對每個程序所做的事情進行清楚的描述。例如核心必須知道程序的優先級,在cpu上運作還是被阻塞等等,這就是程序描述符的作用

程序描述符中包含:程序的基本資訊,指向記憶體區描述符的指針 目前目錄 指向檔案描述符的指針 所接收的信号等等

程序的狀态:

可運作狀态:程序要麼在cpu上執行,要麼準備執行

可中斷狀态:程序被挂起,知道某個條件為真,産生一個硬體中斷,釋放程序正等待的系統資源,或傳遞一個信号都是可以喚醒程序的條件

不可中斷狀态:把信号傳遞給一個睡眠程序也不能改變它的狀态,這種狀态很少用到,但在某些情況下有用。例如,當程序打開一個裝置檔案,其相應的裝置探測程式開始探測相應的裝置時會用到這種狀态。探測完成前,裝置驅動程式不能被中斷。

暫停狀态:程序的執行被暫停

跟蹤狀态:程序的執行由debugger程式暫停。當一個程序被另一個程序監控時,任何信号都可以把這個程序至于跟蹤狀态。

僵死狀态:程序被終止,但父程序還沒有調用wait4或waitpid傳回死亡程序的資訊。

僵死撤銷狀态:由父程序發出wait4或waitpid調用,因而程序由系統删除。為了防止其他執行程序也執行wait()調用,而把程序狀态改為僵死撤銷狀态

辨別一個程序:一般來說,能被獨立排程的每個執行上下文都必須擁有自己的程序描述符;是以,即使共享核心大部分資料結構的輕量級程序,也有他們自己的task_struct結構。類unix系統允許使用者使用一個叫做程序辨別符pid的數字來辨別程序,pid的值存放在程序描述符的PID字段中。pid被順序編号,預設情況下pid最大值為32767,pid号循環使用,核心通過管理一個pid使用位圖表示目前配置設定的pid号和閑置的pid号。

另一方面,unix程式員希望同一個組中的線程有共同的pid。例如把指定的pid的信号發送給組内的所有線程。事實上,posix1003.1.c标準規定一個多線程應用程式中的所有線程都必須具有相同的pid。linux引入線程組的概念,一個線程組中的所有線程使用改線程組的領頭線程的pid,也就是改組的第一個輕量級程序的pid,它被存入到程序描述符的tgid字段。

程序描述符的處理:核心必須能夠同時處理很多程序,并吧程序描述符放在動态的記憶體中,而不是放在永久的核心記憶體區。對每個程序來說,linux都把兩個不同的資料結構緊湊的存放在一個單獨為程序配置設定的存儲區域中,一個是核心态的程序堆棧,另個一緊挨着的是程序描述符的小資料結構膠線程描述符thread-info,這塊存儲域的大小通常為8k;考慮效率問題,核心讓這8k空間占連續的兩個頁框并讓第一個頁框的起始位址是2的13次方的倍數。c語言中使用聯合結構體表示一個程序的線程描述符描述符和核心棧

union thread_info

{

struct thread_info thread_info;

unsigned long stack[2048];

}

thread_info大小為52個位元組,是以核心堆棧能擴充到8140個位元組。

thread_info從低位址開始存放,stack從高位址開始存放。esp為棧頂指針。

辨別目前程序:核心很容易從esp寄存器的值獲得目前在cpu上正在運作程序的thread-info結構的位址。事實上,因為thread-info結構的大小為8k,則核心屏蔽掉esp的低13位就可以獲得thread-info的基位址。

程序最常用的是程序描述符的指針,current_thread_info->task,

task字段在threadinfo中的偏移量為0,是以執行完這後,就可獲得包含在cpu上運作程序的描述符的指針。

雙向連結清單:對于每個連結清單,必須實作一組原語操作:初始化連結清單,插入和删除一個元素,掃描連結清單等等。linux核心定義了list_head結構,字段next和prev分别表示通用雙向連結清單的向前或向後的指針元素。

程序連結清單:程序連結清單的投是init_task程序,踏實所謂的0程序或swapper程序的程序描述符。

TASK_RUNNING狀态的程序連結清單:當核心尋找一個新程序在cpu上運作時,必須隻考慮可運作程序。早起的linux版本把所有的可運作程序放在一個可運作隊列的連結清單中,由于維持連結清單中的程序按優先級排序開銷過大,是以,早期的排程程式不得不為選擇最佳的排程消耗過多的時間。

linux隊列實作的運作隊列有所不同。其目的是讓排程程式能在固定時間内選出最佳的可運作程序。後面會在程序排程中回詳細講到這種新的運作隊列。

提高程式運作速度的一個方法是根據優先級建立多個可運作隊列,在多處理機系統中,每個cpu都有它自己的運作隊列,即他自己的程序連結清單集。排程程式的速度的确提高了,但是運作隊列的連結清單卻是以拆分了很多個。

程序之間的關系:程式建立的程序具有父子關系。如果一個程序建立多個子程序時,則子程序之間具有兄弟關系。樹形關系采用孩子兄弟表示法。

程序之間還有其他關系:一個程序可能是一個程序組或登入會話的領頭程序,也可能是一個線程組的領頭程序。還可能跟蹤其他程序的執行。

pidhash表以及連結清單

在幾種情況下,核心必須能從程序pid導出對應的程序的程序描述符的指針。例如kill系統調用時提供服務是會發生以下情況,當程序p1希望向程序p2發送一個信号是,p1調用kill系統調用,其參數是p2的pid,核心從這個pid導出其對應的程序描述符,然後從p2的程序描述符中取出記錄挂起信号的資料結構的指針。

順序掃描程序連結清單并檢查程序描述符的pid字段是可行,但非常低效的。為了加速查早,引入了4個散清單,程序描述符包含了表示不同類型pid的字段,而且每種類型的pid需要它自己的散清單。

4個散清單:程序的pid;線程組領頭程序pid;程序組領頭程序pid;會話領頭程序pid;

核心初始化期間動态為4個散清單配置設定空間,并把他們的位址存入pid_hash數組,一個散清單的長度依賴于可用的RAM容量。例如:一個系統擁有512M的RAM,那麼每個散清單就被存在4個頁框中,可以擁有2048個表項。

沖突解決:鍊位址法。

如何組織程序:運作隊列連結清單把處于TASK_RUNNING狀态的所有程序組織在一起。當要把其他狀态的程序分組時,不同的狀态要求不同的處理,linux選擇下列方式之一:

沒有為TASK_STOPPED、EXIT_ZOMBIE或EXIT_DEAD狀态的程序建立專門的連結清單,由于對出于暫停、僵死,死亡狀态的程序通路比較簡單,是以不必為這三種程序分組。

等待隊列:程序必須經常等待某些事件的發生,例如,等待一個磁盤操作的終止,等待釋放系統資源,或等待事件經過固定的間隔。等待隊清單示一組睡眠的程序,當某一條件為真時,由核心喚醒他們。因為等待隊列是由中斷處理函數和核心修改的,是以必須對其雙向連結清單進行保護以免對齊進行同時通路,因為同時通路會導緻不可預測的結果。同步,是通過等待隊列的投中的lock自旋鎖達到的。等待隊列中的每個元素代表一個睡眠程序,該程序等待某一事件的發生;它的描述符位址存放在task字段中,task_list字段中保護的是指針,由這個指針把一個元素連結到等待相同僚件的程序連結清單中。然而要喚醒等待隊列是有時并不友善,如果有兩個或多個隊列等待統一資源,僅喚醒等待隊列中的一個程序才有意義。這個程序占有資源,而其他程序睡眠。

是以有兩種程序:互斥程序由核心有選擇的喚醒,而非互斥程序總是由核心在事件發生時喚醒。等待通路臨界資源的程序就是互斥程序的例子,等待相關事件的程序是非互斥的。

程序資源限制:每個及承諾都有一組相關的資源限制,限制指定了程序所能使用的系統資源數量。這些限制避免使用者過分使用系統資源。包括:

程序位址空間的最大數;記憶體資訊轉儲檔案大小;程序使用cpu的最長時間;堆大小的最大值;檔案大小的最大值;檔案鎖的最大值;非交換記憶體的最大值;posix消息隊列的最大位元組數;打開檔案描述符的最大值;使用者能擁有的程序最大數;程序所擁有的頁框最大數;程序挂起信号的最大數;棧大小的最大值。

程序切換:為了控制程序的執行,核心必須有能力挂起正在cpu上執行的程序,并恢複以前挂起的某個程序執行,這種行為叫程序的上下文切換。

硬體上下文:盡管每個程序可以擁有屬于自己的位址空間,但所有程序必須共享cpu寄存器。是以,在恢複一個程序執行前,核心必須確定每個寄存器裝入了挂起時程序的值。程序恢複執行前必須裝入寄存器的一組資料稱為上下文。在linux中,程序硬體上下文的一部分放在TSS段,而剩餘部分存放在核心态的堆棧。早期的linux版本利用硬體完成上下文切換,基于以下原因,linux2.6使用軟體執行上下文切換:

1、通過一組mov指令逐漸完成切換,這樣能更好地控制所裝入資料的合法性。尤其,這使檢查ds,es段寄存器的值成為可能。

2、新舊方法時間上大緻相同。然而,盡管目前的切換代碼還有改進的餘地,卻不能對硬體上下文進行優化。程序切換隻發生在核心态。在執行程序切換之前,使用者态程序使用的所有寄存器的内容全部儲存在核心态堆棧中。

TSS任務狀态段:用來存放硬體上下文,盡管并不用硬體上下文切換,但還是為每個cpu建立了一個tss段:

1、當x86的一個cpu從使用者态切換到核心态時,它就從TSS段擷取核心态堆棧的位址

2、當使用者程序試圖通過in或out指令通路一個I/O端口時,cpu需要通路存放在TSS段的I/O許可權的位圖。

TSS反應了cpu上的目前程序的特權級,但不必為沒有運作在cpu上的程序保留TSS。

每個程序切換出去,核心就把其硬體上下文儲存在程序描述符的thread_struct的thread字段,這個資料結構包含了大部分cpu寄存器,但不包括eax,ebx等等這些通用的寄存器,他們的值保留在核心堆棧中。

建立程序:傳統的unix程序以統一的方式對待所有子程序:子程序複制父程序的所有資源,這種方法使程序的建立非常慢且非常低效,因為子程序要拷貝父程序的整個位址空間,實際上子程序幾乎不必讀或修改父程序的資源,很多情況下,子程序立即調用execve(),并清除父程序仔細拷貝過來的位址空間。

現代unix引入三種不同的機制來解決這個問題:

1、寫時複制技術允許父子程序讀相同的實體頁。隻要兩者中有一個試圖寫一個實體頁,核心就把這個頁的内容拷貝到一個新的實體頁,并把這個實體頁配置設定給正在寫的程序。

2、輕量級程序允許父子程序共享每個程序在核心的很多資料結構。

3、vfork系統調用建立的程序能共享父程序的記憶體位址空間。為了防止父程序重寫子程序時需要的資料,阻塞父程序的執行,一直到子程序退出或執行一個新程式為止。

核心線程與普通線程相比:

1、核心線程隻運作在核心态,而普通程序既可以運作在核心态,也可以運作在使用者态

2、因為核心線程隻運作在核心态,因為他們隻是用大于PAGE_OFFSET的線性位址空間。另一方面,不管在使用者态還是在核心态,普通程序可以使用4G的線性位址空間。

繼續閱讀