文章目錄
- 進程和程序
- 操作系統如何控制和調度程序
- 進程控制塊–PCB
- 子進程
- 進程狀態
- 僵尸進程
- 孤兒進程
- 守護進程(精靈進程)
- 進程地址空間
- 引言
- 頁表
進程和程序
- 程序: 一系列有序的指令集合(就是我們寫的代碼)。
- 進程: 進程就是程序的一次執行,是系統進行資源分配和調度的獨立單位。
一個程序可以創建多個進程,每個進程的文本段相同,但是數據段、堆、堆棧段卻不同。
進程的特性:
- 動態性:進程是動態的;程序則是靜態的。
- 并發性:多個進程能在同一時間段內同時運行。
- 獨立性:系統中獨立獲得資源和進行調度的基本單位。
- 異步性:各進程按不可預知的速度各自運行。
程序最初以某種可執行格式駐留在外存上(如:磁盤)。操作系統運行程序時將需要用到的代碼和所有靜態數據加載(load
)到內存中(惰性執行,暫時用不到的代碼不加載),方便 CPU 運行進程時使用。
操作系統如何控制和調度程序
實際中,一個正常的系統可能會有上百個進程同時在運行,而我們只有少量的物理 CPU
可以使用,因此,如何滿足諸多進程對于 CPU
的需求便成了重中之重。
按照馮諾依曼體系結構,所有的數據想要被CPU進行處理,第一步就是要將代碼和數據加載到內存中。
操作系統通過 虛擬化CPU ,讓一個進程只運行一個時間片,然后切換到其他進程,通過 快速切換 和 優先級調度 運行所有的程序,造成了同時運行的假象。這就是 時分共享CPU技術 ,也就是 CPU分時機制 。
但是,這里還存在著幾個問題,CPU是如何在內存中找到每個程序的?CPU在來回調度時,如何能夠從上一次運行的位置繼續運行?如何能夠保證繼續處理上一條沒有處理完的數據?
操作系統為了能夠完成上述操作,設置了一個用于描述進程信息的數據結構—— PCB
。
進程控制塊–PCB
操作系統為了能夠使每個程序能夠獨立運行,在操作系統中為其配置了一個數據結構,也就是我們通常所說的 PCB(Process Control Block)
,這個數據結構在 Linux
下是:task_struct
task_struct 中的內容:
- 標示符: 描述本進程的唯一標示符,用來區別其他進程。
- 狀態: 任務狀態,退出代碼,退出信號等。
- 優先級: 相對于其他進程的優先級。
- 程序計數器: 程序中即將被執行的下一條指令的地址。
- 內存指針: 包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針。
- 上下文數據: 進程執行時處理器的寄存器中的數據。
- I/O狀態信息: 包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。
- 記賬信息: 可能包括處理器時間總和、使用的時鐘數總和、時間限制、記賬號等其他信息。
PCB有兩種組織方式:
- 鏈接方式:將同一狀態的進程PCB鏈成一個隊列,多個狀態對應多個不同的隊列。
- 索引方式:將同一狀態的進程歸入一個索引表,多個狀態對應多個不同的索引。
鏈接方式:
索引方式:
PCB是操作系統對一個運行中的程序(也就是進程)的描述,操作系統通過這個描述來實現對程序的運行調度:
回到前面提出的問題:
- CPU通過PCB中的內存指針來找到程序在內存中的地址
- 通過上下文數據來記錄運行中程序的各種信息
- 通過程序計數器來找到這個程序即將執行的下一條指令的地址
子進程
我們可以通過 fork
在一個 已經創建的進程內 創建一個 新的進程 ,這個 新的進程 就是 原先進程的 子進程 。
在子進程創建的時候,它從父進程的PCB中復制了很多數據,如內存指針、上下文數據、程序計數器等,所以它的代碼、數據以及運行的位置,都與父進程一模一樣。
由于代碼段是只讀的,所以兩者的代碼都一樣,不可修改,而兩者雖然虛擬地址相同,但物理地址不同,所以兩者的數據都各自獨立。
總結一下就是:父子進程代碼共享,數據各自開辟空間。 (利用寫時拷貝技術)
在 Linux
中,我們可以通過 fork 函數
來創建子進程
pid_t fork(void)
我們創建子進程,是希望它和父進程執行不一樣的操作,那么我們該怎么實現呢?
最簡單的方法就是通過
fork
的返回值來進行代碼分流,父進程的返回值是子進程的pid
,而子進程的返回值是0
,通過對返回值的判斷,即可完成代碼的分流。
但是這種方法的代碼十分冗余,還有一種更加優秀的方法——程序替換。
進程狀態
進程有三種基本狀態:
- 執行狀態(
running
):- 進程正在
CPU
上執行; - 只能有一個進程處于執行狀態(單
CPU
);
- 進程正在
- 就緒狀態(
ready
):- 進程已獲得除
CPU
外的所有資源,等待分配CPU
就可執行; - 可以有多個進程處于就緒狀態,組成就緒隊列。
- 進程已獲得除
- 阻塞狀態(
waiting
):- 進程因自身原因(如:等待I/O資源)而暫停執行,也稱 “等待狀態” 或 “睡眠狀態” 。
- 可以有多個進程處于阻塞狀態,組成阻塞隊列
但是在 Linux
中,將狀態細分到了六種:
- R運行狀態(running): 并不意味著進程一定在運行中,它表明進程要么是在運行中要么在運行隊列里。
- S睡眠狀態(sleeping): 意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(
interruptible sleep
)。 - **D磁盤休眠狀態(Disk sleep):**有時候也叫不可中斷睡眠狀(
uninterruptible sleep
),在這個狀態的進程通常會等待IO
的結束。 - T停止狀態(stopped): 可以通過發送
SIGSTOP
信號給進程來停(T
)進程。這個被暫停的進程可以通過發送SIGCONT
信號讓進程繼續運行。 - X死亡狀態(dead): 這個狀態只是一個返回狀態,你不會在任務列表里看到這個狀態
- Z僵死狀態(Zombies): 進程已經退出了但是資源還沒有完全被釋放的一種狀態。
僵尸進程
當子進程退出的時候,如果父進程沒有讀取到子進程的返回值,這時子進程就進入了 僵死狀態 。
這時就處于一個很尷尬的局面,子進程實際上已經退出了,但是父進程認為它還在執行,所以并沒有釋放它的資源,所以子進程會一直卡在進程表中,等待父進程讀取退出狀態代碼。此時的 子進程 就被稱為 僵尸進程 ,它持有的資源一直無法釋放,也無法再將其殺死。
對于僵尸進程,即使是 kill -9
對其也沒有作用。這時只有兩種解決方法:
- 進程等待
- 退出父進程
父進程退出,子進程保存退出的狀態就沒有任何意義了,因此就被釋放了。 但是這并不是一個合理的方式,如果為了解決僵尸進程而刻意退出還不應該退出的父進程,不是很好的解決方法,我們應該避免僵尸進程的產生。
從上面可以看出,僵尸進程是非常危險的,因為我們無法通過正常途徑將其解決,同時它會一直占用著我們的資源,同時 PCB
還需要對它的狀態進行維護。并且一個用戶所能創建的進程數量是有限的,如果一個父進程創建了大量的子進程而不進行回收,當達到上限時,我們就無法創建新的程序。
孤兒進程
如果父進程先于子進程退出,那么沒有父進程的子進程會怎么樣呢?持有資源不被回收?就像僵尸進程一樣一直占用資源?
實際上,失去了父進程后的子進程被稱為 “孤兒進程” ,但并不是沒有父進程,而是會被 1
號進程 init
統一收養,然后由 Init
進程回收。
守護進程(精靈進程)
守護進程:一種特殊的孤兒進程,父進程是一號進程,運行在后臺,與終端和登陸會話脫離關系,不受影響。
守護進程通常是一種運行在系統后臺的批處理程序,默默的做一些循環往復的事情。
進程地址空間
引言
我們利用一個全局變量val,看看修改子進程中的變量val,父進程會不會發生變化,他們的地址又是否相同:
因為子進程運行的位置和父進程一樣,所以先讓父進程睡眠一會,讓子進程先修改。
奇怪的事情發生了,明明子進程已經修改了 val
,但是父進程的卻沒變,同時明明父子進程中全局變量 val
的大小都不一樣,但是他們的地址確還是一樣的,這就有些不符合邏輯了,因為一個地址中不可能有兩個同名的變量。
這里就讓我們確定了一件事情,我們在代碼中所看到的地址,并不是真正的地址,而是虛擬內存地址。
頁表
操作系統再引入虛擬地址空間的時候還引入了一種東西,叫做 頁表 。
通過頁表來映射虛擬地址和物理地址的關系,不同的進程有不同的頁表。上面例子中訪問的 val 地址就是 val 在頁表的編號,在頁表中查找該編號對應的物理內存從而訪問 val 數據。
- 通過在虛擬地址來使數據進行連續的存儲,然后再通過頁表映射到物理內存上,來實現離散式的存儲,提高了內存的利用率。
- 同時頁表可以針對某個地址設置訪問權限,讓某個地址設置為只讀,通過這種方法來實現內存的訪問控制。
- 為了能夠使進程具有獨立性,彼此之間不會相互干預,每一個進程都會有它自己的頁表和虛擬地址空間。
現在我們探討幾個問題:
為什么父子進程的代碼相同,且無法修改?
- 因為通過頁表將代碼段的權限設置為只讀,所以無法修改。
為什么父子進程數據各自開辟空間?
- 其實父子進程一開始物理地址和虛擬地址都是相同的,但是當任意一個進程中數據發生變化的時候,這個時候操作系統會找到另外一塊物理空間,將數據全部拷貝過去給發生修改的進程使用,并且修改原來的物理空間的權限,使原來的物理空間給另一個進程使用。