目錄
前言:
一、進程優先級:
1.通過nice值修改優先級:?
二、進程切換:
三、上下文數據
四、Linux真實調度算法:
五、bitmap位圖:
六、命令總結:
總結:
前言:
我們已經知道了進程的一些屬性,和進程的各種狀態及孤兒進程,那么接下來我們就需要知道進程的調度方法和優先級,少年,開始吧!
一、進程優先級:
進程優先級也就是獲取某種資源的先后順序。為什么存在?本質就是目標資源較少。
在task_struct中的優先級屬性是使用幾個int類型的變量表示的。優先級數字越小,優先級越高,也就和我們的排名一樣。
為了方便觀察,我們依舊寫一個死循環的程序:
當我們想查看一個進程的優先級信息,我們可以使用ps -l查看優先級信息:
如果你是新開的Xshell窗口,需要加上-a選項來查看全局的進程。
1.通過nice值修改優先級:?
我們一般是通過修改nice值對優先級進行修改的:
插一嘴:UID是什么?
我們平時文件的擁有者,所屬組都是一個字符串,系統做對比的時候所需時間就很多,所以OS會對每一個用戶維護一個叫做UID的東西。也就是用戶ID(USER identify)。我們可以使用ls -ln來查看。-n選項是把能顯示成數字的就顯示成數字,尤其是用戶。
所以操作系統使用UID來區分進程是誰啟動的,所有操作都是進程操作,進程會記錄誰啟動的我;而文件會記錄下擁有者,所屬組和對應權限。所以進程去啟動相關文件時,就會進行UID的對比,從而實現權限控制。
進程的優先級是為了競爭CPU資源做準備的。?接下來我們修改進程優先級,可以通過指令也可以通過代碼修改。
注意這里把nice拉到最大100(原來1767進程PRI為80,NI為0):
之后我們再次把nice值拉到最低-100,并再次觀察結果:
可以看到并沒有從上次的99加上nice值(-20)得到79,而是從最開始的80加上(-20)得到60最終優先級。?
為什么nice在可控范圍以內呢?因為我們使用的是分時OS,在進程調度的時候要盡量公平。
優先級的存在是為了更合理競爭相關資源。
二、進程切換:
當一個進程的時間片到了,進程就要被切換;Linux是基于時間片,進行調度輪轉的。但是一個進程的時間片到了并不一定就跑完了,可在任何地方被重新調度切換。
三、上下文數據
當一個進程的時間片到了,OS要保留上下文數據;當其又被調度的時候,OS要恢復上下文數據。
進程在運行的時候,會有很多臨時數據,這些數據都在寄存器中保存。CPU內部的寄存器數據,是進程執行時的瞬時狀態信息。
我們需要來具體了解幾個寄存器,如果各位不知道寄存器是什么,可以先臨時了解,就是存放各種信息的東西可以是地址,也可以是數據,寄存器在CPU上。
其中指令指針寄存器(也稱IP、PC程序計數器),它里面記錄下次要執行的命令的地址;IR(Instruction Register)即指令寄存器,存放的是當前執行代碼的地址。?
進程在運行的時候,會有很多臨時數據,這些數據都在寄存器中保存。CPU內部的寄存器數據,是進程執行時的瞬時狀態信息,只寫信息我們可以當做進程的上下文數據。
學過硬件的都知道,每個進程不是都需要用到寄存器嗎?IP寄存器即使這次存儲了進程A的下次執行地址,但是A的時間片到了,進程B執行,IP內的內容應該是B下次執行的地址;B的時間片到了,CPU調度進程A,有是怎么找進程A的執行地址呢?
我們之前已經講過PCB(task_struct)里面有一個屬性專門保存進程的上下文數據,所以在進程A執行完以后,寄存器中的內容會存放在PCB的上下文數據中,對于B也是,所以下次再次調度A就可以根據PBC來恢復上下文,繼續順序執行代碼。
我們找到第一版本的Linux內核代碼,其中tast_struct中的一個結構體屬性是tss(任務狀態段),之后看里面的屬性:
可以看到PCB中包含tss_struct(可以理解為保存上下文數據的屬性)?
這里面可以看到很多熟悉的寄存器。?所以上下文保存在PCB中,也就是內存中,而不是寄存器中。
我們之前講到過,進程是通過runqueue的task_struct*head找到第一個進程并讓CPU調度,當時間片結束就放在隊尾,也就是FIFO(先進先出)的方式。但是我們又講到了優先級,所以實際上,進程調度并不是FIFO,而是盡量的公平調度。
四、Linux真實調度算法:
所以我們來講解Linux的真實調度算法:
runqueue隊列中,有兩個指針維護調度的進程列表。*active活躍的進程列表,*expired過期的進程列表。
進程列表是一個指針數組,前100個是實時進程,我們無法對其改變優先級;后40個是分時進程,我們可以改變優先級。這也就解釋了之前的nice值為什么只有40個。
進程真正的優先級范圍為[60,99],而優先級為60對應的數組下標為100。
比如此時有一個優先級為61的進程要放入queue中,會61 - 60(startpri) + 100 = 101
如果再來一個進程優先級為61進程,則會連接在第一個進程后面:
這也就是一個哈希桶,但是其實runqueue中包含兩個哈希桶,維護兩個,接下來我們來具體了解。
為什么要這樣設計?比如此時active哈希桶中有一個進程:?
調度一般分為三種情況:
1.運行退出
2.不退出但時間片到了
3.有新進程產生了
第一種情況進程退出程序就釋放掉了,我們不做討論。
我們首先討論有新進程產生。此時有一個問題,如果一直有新的進程產生,并且創建的新進程優先級一直都是最高的,是不是會和之前講的調度器要非常均衡的進行進程調度所矛盾?優先級很高,表示一定要一直優先嗎?優先級很低,表示要一直等待嗎?
這樣的話優先級低的進程就永遠不會被調度,這叫做進程饑餓問題。
所以新進程會放入expired(過期)哈希桶中,而且時間片到了的進程,也會被放入expired哈希桶中。
所以active哈希桶中,進程只會越來越少,不會增多。?因為時間片會到,進程會退出。
當active為空時,OS會將active和expired指針指向的內容交換(swap(&active, &expired)),之后使用相同的調度算法,輪轉執行。
注意:實時進程(0-99)不收調度影響
回到之前queue定義中的nr_active是代表對應當前哈希桶中有多少進程,?它決定要不要交換,當nr_active為0就需要交換。
五、bitmap位圖:
而其中的bitmap[5],注意這是一個整形。5個整形也就有5*32=160個bit位,剛好覆蓋140。因為實際上還是從上到下進行遍歷看桶是否為空,這樣還是很慢的。而bitmap就可以快速定位,一次查找32個bit位看是否為空。這是位圖的概念。
類似這樣:
for(int i = 0; i < 5; ++i)
{if (bitmap[i] == 0) {continue;}else {//確定在32個比特位那個位置有進程}
}
這就是Linux的進程O(1)算法,?所有的進程都要有鏈表連接,而且進程可以在調度隊列中,阻塞隊列中。
Linux中的鏈式結構為雙鏈表結構。但是它并不是我們想象的那樣將數據和指針都放在一個節點中;其實內核中只有鏈接字段,沒有屬性字段。
所以一個進程既可以在全局鏈表中,也可以在任何一個其他數據結構中,只要加上節點字段即可。?
我們進程其實只知道task_struct中的link字段地址,那么它是如何找到結構體的起始地址呢?這其實就是結構體部分的內容。
struct S
{char c1;int a;char c2;
};#define OFFSETOF(struct_name,member_name) (int)&(((struct_name*)0)->member_name)int main()
{struct S s = { 'a', 10, 'b' };//printf("%d\n", OFFSETOF(struct S, c1));//printf("%d\n", OFFSETOF(struct S, a));//printf("%d\n", OFFSETOF(struct S, c2));printf("%p\n", &s);printf("%p\n", (struct S*)( (char*) & s.a - OFFSETOF(struct S, a)));//這是作者自己實現的OFFSETOF宏,各位可以直接使用offsetof宏來求偏移量//使用offsetof要引用stddef.h同文件printf("%p\n", (struct S*)((char*)&s.a - offsetof(struct S, a)));return 0;
}
這里為什么要轉換為char*?這樣可以按照字節為單位去操作,更好的計算。各位可以試試其他類型,會發現結果并不符合預期。
六、命令總結:
ps -l:查看當前系統中進程狀態,將能顯示為數字的都顯示為數字。
總結:
大佬不愧為大佬,這個調度算法和位圖的設計簡直無敵,接下來我們就會講解命令行參數和環境變量,這部分知識也更為炸裂,各位敬請期待!