一、進程狀態
什么是進程狀態?
進程狀態指的是在操作系統中進程在生命周期中所處的不同階段。
進程狀態有哪些呢?
我們可以看到上述圖片
進程狀態分為:創建狀態、就緒狀態、運行狀態、阻塞狀態和終止狀態
所有的操作系統在實現進程狀態變化的時候都要符合上面的理論。
進程在運行的時候是需要資源的,也就是說進程之間是有競爭的,它們競爭的資源分為兩類:外設資源和CPU資源
(輸入輸出設備統稱為外設資源,運算器和控制器統稱為CPU資源)
大部分的操作系統內部都要給每個CPU設置一個調度隊列
多個CPU就要多個調度隊列,操作系統對調度隊列進行的管理方式就是先描述在組織
綜上所述,我們就知道了在操作系統中就要有一個struct runqueue
struct runqueue
{//隊列屬性int num; //隊列個數struct task_stuct* head;
}
所以說CPU的調度隊列是怎么樣拿到該進程的呢?
struct task_struct
{//所有的屬性struct list_head tasks;
}
struct list_head
{struct list_head* next;struct list_head* prev;
}
看上述的代碼,在struct task_struct里面有一個struct list_head tasks的變量,struct list_head里面包含兩個指針分別是next和prev指針,它是通過struct list_head來實現雙鏈表的。所以綜上所述我們就可以知道,只要我們得到struct task_struct結構體中的struct list_head tasks變量的地址,我們就可以利用偏移量來得到task_struct的起始地址。
既然我們知道task_struct的起始地址 = struct list_head變量的地址 - 偏移量
了,那偏移量該怎么得到呢?
假設在地址為0處有一個struct task_struct變量,去訪問該變量中的struct list_head變量并進行取地址:
&((struct task_struct*)0->tasks)
。我們要知道的是,對于每一個進程來書,struct task_struct結構體的大小是固定的,所以說struct list_head在struct task_struct中的偏移量也是固定的。那么struct task_struct* start = &stract list_head task - &((struct task_struct*)0->tasks)
.
所以說在linux中PCB實現雙鏈表的這種方式就再也和類型沒有關系了,struct task_struct既可以屬于鏈表,同時也可以屬于其他數據結構。
在linux中是沒有就緒狀態的,所以我們認為就緒狀態和運行狀態不分家
。在有些系統中,認為PCB處在運行隊列中但是未分配CPU資源叫做就緒狀態,正在CPU中執行的進程叫做運行狀態。
每個進程都要競爭CPU資源,所以就得把進程鏈入到調度隊列中。我們把上述這個行為叫做進程在CPU的調度隊列當中進行排隊。進行排隊時,該進程的狀態就叫做(r)運行狀態
。
運行狀態:該進程的PCB必須處在CPU的調度隊列中,只要在調度隊列中,進程就叫做運行狀態,隨時等待CPU的調度執行
阻塞狀態:當進程無法繼續執行,需要等待某些非CPU資源就緒時,就會進入阻塞狀態
(進程進入阻塞狀態,PCB會被移除運行隊列,進入等待隊列)
掛起狀態:當內存空間嚴重不足時,操作系統會將進程的代碼,數據,堆棧等內存資源移出內存,換出磁盤的交換區里,但進程的PCB仍然保留在內存中,這就叫掛起狀態
新建狀態:操作系統為進程分配必要的資源,并為進程分配唯一的標識符
下面的狀態在kernel源代碼里定義:
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
R運行狀態(running): 并不意味著進程?定在運行中,它表明進程要么是在運行中要么在運行隊列?。
? S睡眠狀態(sleeping): 意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(interruptible sleep))。
? D磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束。
? T停止狀態(stopped): 可以通過發送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運行。
? X死亡狀態(dead):這個狀態只是?個返回狀態,你不會在任務列表?看到這個狀態。
看下面這個代碼,代碼運行時我們發現它的狀態怎么會是S(睡眠狀態)
狀態呢?不應該是R(運行狀態)
嗎?
接下來我們再改一下代碼
很奇怪的就是為什么將while中的printf屏蔽了就變成了R狀態呢?
原因就是printf是向顯示器打印內容的,訪問的是外設資源,外設的速度很慢,很大概率是無法就緒的,所以就是S狀態。但是有少數的概率會顯示R狀態
其實S這個狀態是一種淺度睡眠的狀態,我們可以使用ctrl+c來殺掉淺度睡眠,而D這種狀態則是深度睡眠,其不對任何事件進行響應,連操作系統都不能殺掉,除非是說進程自己醒過來。
僵尸進程
模擬實現僵尸進程
運行代碼得到的結果如下:
我們看上述代碼會發現子進程比父進程先退出,父進程就會先處于一個僵尸狀態
,代碼、數據釋放掉,但是會保留struct task_struct,PCB會自動記錄進程退出時的退出信息,方便父進程讀取退出碼。
假設父進程一直不處理,一直處于僵尸進程,PCB一直存在于內存中,這就會導致內存的泄露。
那如果我們是父進程先退出呢?子進程會怎么樣?
孤兒進程
模擬實現孤兒進程
運行結果如下:
我們查看進程狀態發現一個很奇怪的東西,就是這個1號進程是什么呢?
子進程被操作系統一號領養了,一號是一個叫做init的進程
在linu中init進程是什么?
它是系統啟動后運行的第一個用戶態進程,通常具有進程 ID(PID)為 1。init 進程的主要作用是初始化系統環境、啟動其他關鍵服務和守護進程,并管理系統的運行級別。
為什么要領養子進程呢?
因為孤兒進程也會退出,但是父進程早早就沒了,所以會被系統領養,本質上是為了回收孤兒進程防止內存泄漏。
進程退出不應該會變成僵尸進程嗎?為什么沒有看到父進程的僵尸狀態呢?
子進程的父進程的父進程是bash(外殼程序),父進程一旦退出,就會被bash回收,所以看不到父進程的僵尸狀態,具體過程看下面兩張圖。
二、進程的優先級
什么是進程的優先級?
在操作系統中,進程的優先級是用于決定進程調度順序的一個重要參數。優先級越高,進程獲得 CPU 時間片的機會就越大。總而言之就是進程得到某種資源的先后順序
我們使用ps -al
命令查看進程的優先級
UID : 代表執行者的身份
PID : 代表這個進程的代號
PPID :代表這個進程是由哪個進程發展衍生而來的,亦即父進程的代號
PRI :代表這個進程可被執行的優先級,其值越小越早被執行
NI :代表這個進程的nice值
PRI越小代表該進程優先級越高,它就是一個整數,存在于task_struct中,而NI是一個修正數據:PRI(new) = PRI(old) + nice
在linux中nice的取值范圍是-20~19,一共40個級別。
使用top來改變PRI,步驟如下:
top指令后,-r進入pid,接著輸入pid
補充進程的性質:
競爭性: 系統進程數目眾多,而CPU資源只有少量,甚至1個,所以進程之間是具有競爭屬性的。為了高效完成任務,更合理競爭相關資源,便具有了優先級
獨立性: 多進程運行,需要獨享各種資源,多進程運行期間互不干擾
并行: 多個進程在多個CPU下分別,同時進行運行,這稱之為并行
并發: 多個進程在?個CPU下采用進程切換的方式,在?段時間之內,讓多個進程都得以推進,稱之為并發
三、進程切換
CPU內有很多寄存器,這些寄存器就是一套儲存空間,寄存器只有一套,屬于CPU本身,但是寄存器內部的數據可以有很多。進程在被切換的時候,進程PCB會保存該進程的上下文數據,以便進程再被調度的時候可以恢復數據。
看上述圖片中紅色括號中的queue數組,這個是什么呢?
其實queue數組類型是一個struct list_head類型,簡單的理解就是認為它是一個struct task_struct*類型,它可以使用struct list_head中的節點指針對進程進行關聯,這個進程的大小是140,下標[0,99]我們不需要理會,[100,139]剛好是40個,然而我們進程范圍是[60,99]剛剛好也是40個,所以我們知道操作系統根據進程的優先級將進程掛接在對應的下標上。
在linux中對進程調度的時候,用到一個bitmap[5]
,利用位圖的方式對queue數組進行遍歷,這樣能提高效率
。但是為什么是5呢?因為一個整型4個byte,32個bit,那么5個就是160個bit,剛剛好對應下標140.
對應的比特位的位置表示的是數組中第幾個隊列,比特位的內容表示的是隊列是否為空。
我們再看向上面的圖片會發現有兩個指針分別是:struct prio_array*
,struct prio_array* expired
,這兩個指針分別是活躍隊列
和過期隊列
。
CPU調度的時候直接從
active指針找到對應的queue[140],新增進程或者時間片到的進程,從CPU上剝離下來,被剝離下來的進程只能重新入隊列,入過期隊列。
既然如此為什么要有nice值呢?
如果我們要改PRI值的時候,我們能直接改嗎?答案是不能的,因為我們直接改PRI值那么進程就會被立即遷移到對應的隊列上,而有了nice值的話就不會影響active隊列中進程的調度,在expired隊列中對應的進程的優先級做出調整。
等到CPU調度完所有進程,所有進程都會跑到過期隊列。一旦active沒有進程了,這時只需要交換active,expired指針內容就可以了。