本章節目標:
- 認識馮諾依曼系統
- 作業系統概念與定位,理解"管理"
- 深⼊理解進程概念,瞭解PCB
- 學習進程狀態,學會創建進程,掌握僵⼫進程和孤⼉進程,及其形成原因和危害
- 瞭解進程調度,Linux進程優先級,理解進程競爭性與獨⽴性,理解並⾏與併發
- 理解進程切換,以及Linux2.6 kernel,O(1)調度算法架構
- 理解環境變量,熟悉常見環境變量及相關指令, getenv/setenv函數
- 理解C記憶體空間分配規律,瞭解進程記憶體映像和應⽤程序區別, 認識虛擬地址空間
馮諾伊曼系統結構
現實生活中常見的計算機大部分都遵循馮諾伊曼系統體系
輸入設備包含:鍵盤、鼠標、掃描儀、攝像頭、網卡、硬盤(ROM)等
輸出設備包含:顯示器、影印機、喇叭、網卡、硬盤等
而在這裡的存儲器指的就是記憶體。輸入設備與輸出設備統稱為外部設備。由於它們的速度遠慢於CPU,因此通常不會直接與 CPU持續交互,而是透過I/O控制器、緩衝區或DMA先將資料傳入記憶體,再由CPU與記憶體進行交互。
為什麼要有記憶體?
存儲分級的問題:
為了提高效率,輸入設備的資料通常會透過緩衝或快取的方式預先載入到記憶體。由於程式在處理資料時往往具有時間局部性與空間局部性,因此大部分情況下 CPU 可以直接從記憶體中存取所需資料,而不必頻繁等待輸入設備。
- 時間局部性(Temporal Locality):如果某個記憶體位置被訪問過,那麼在不久之後很可能會再次被訪問。
- 空間局部性(Spatial Locality):如果某個記憶體位置被訪問,那麼它附近的記憶體位置很可能會在不久之後被訪問。
計算機中,資料流動的本質就是拷貝!!! -> 因此可以知道計算機效率的問題是由設備的拷貝效率決定的
從你登錄上某通訊軟體開始和某位朋友聊天開始,資料的流動過程
作業系統
任何計算機系統都包含⼀個基本的程序集合,稱為作業系統(OS)。籠統的理解,作業系統包括
- 內核(進程管理,記憶體管理,檔案管理,驅動管理)-> 任何計算機都需要這四個功能
- 其他程序(例如函數庫,shell程序等等)

設計OS的目的
- 對下,與硬體交互,管理所有的軟硬體資源 -> 手段
- 對上,為⽤⼾程序(應⽤程序)提供⼀個良好的執⾏環境(穩定、高效、安全) -> 目的

OS 內部,核心最重要的是什麼?── 資料結構
作業系統(OS)本質上就是一個軟體,一款負責管理軟硬體資源的軟體。它遵循一個基本的思路:先進行描述,再進行組織。
回想C++的「類別」與STL的關係
- 類別的設計是 先描述(定義它的功能與特性)
- 然後再透過STL等工具進行 組織與運用。這樣的做法,目的都是為了把現實世界的問題轉換成計算機世界中可操作的規律。
從開發角度來看,作業系統對外表現為一個整體,但它會暴露部分介面給上層使用,這些介面就是系統呼叫(System Call)。
系統呼叫本身功能比較基礎,對使用者的要求也相對較高。為了方便更多開發者使用,部分開發者會對系統呼叫進行 封裝,形成各種函式庫。這些庫的出現,進一步降低了使用難度,也有利於更高層的應用程式開發。
那麼,作業系統是如何管理進程的呢?其核心思路其實很簡單:先將進程進行描述,再把它們組織起來。
因為在作業系統中,可能同時需要運行許多程式: - 每一個程式都必須被加載到記憶體
- 當多個進程並存時,作業系統必須對它們進行有效管理
而要做到「管理」,前提就是「先描述」。換句話說,要讓作業系統認識一個進程,就必須先有一個用來描述它的結構體物件。
在作業系統中,這個結構體就是 PCB(Process Control Block,進程控制塊)!
當一個程式被加載到記憶體,真正成為「進程」時,系統會為它建立對應的PCB,進而透過PCB進行後續的管理與調度。
在還沒有啟動進程前的第一個軟體是什麼? --- 作業系統(作業系統也在記憶體中)
所以什麼是進程?進程就是內核的資料結構 + 自己程序的代碼和資料
進程
PCB 的概念
- PCB(Process Control Block,進程控制塊)是作業系統中用來描述和管理進程的一個資料結構
- 它保存了進程執行所需的所有資訊,例如:進程狀態、程式計數器、寄存器內容、記憶體管理資訊、開啟的檔案、排程相關資訊等等
- 這是一個抽象概念,不同作業系統的實作方式和名稱可能不同
可以理解成,當創建一個新的進程時,就會建立一個對應的PCB,而Linux中,新的進程往往是通過父進程(bash -- 命令行解釋器)的方式創建出來。
Linux 的實作
- 在 Linux 內核中,進程控制塊的具體實作就是一個名為 task_struct 的結構體
- task_struct 中包含了 Linux 內核用來管理進程(以及輕量級執行緒,因為 Linux 沒有嚴格區分 process/thread)的所有資訊
例如其中包含:
- 進程識別符(pid)
- 父進程/子進程關係
- 記憶體描述符(mm_struct)
- 檔案描述符表
- 調度優先級與排程策略
- 狀態資訊(running、sleeping 等)
上下文數據(程式計數器、CPU 暫存器、堆疊指標等)
- 程式計數器 -- 下一個即將要執行的指令的地址
- 暫存器是共享的,但暫存器中的數據是進程私有的
一個進程執行代碼,佔有CPU,要一直等到該代碼執行完畢後才讓出CPU? --- 不是(如果是while(1)就卡死了)
-> 當代計算機,都會給每一個進程分配一個時間片,時間片執行完畢就會讓出CPU,讓另一個進程執行 -- 基於時間片的輪轉調度
-> 一個進程還沒執行完可能就會讓出CPU --- 因爲時間片到達,就存在進程切換和調度的動作
一個進程如何獲取自己的識別符? -> 要獲取task_struct 自己內部的屬性OS就需要提供給我們一個系統調用 -- getpid()、getppid()
About fork() -- fork()後會共享fork後的程式碼
如何創建一個進程(子進程)?通過程式碼的方式 -- fork() -- 本質是OS內部多了一個子進程 -> task_struct + 程式碼和數據 ; 進程會即時的顯示在 /proc中
fork()返回值有兩個?是的,但為什麼給子進程的是0,而給父進程的是子進程的PID
- 因爲父進程和子進程的關係通常是 1 : n -- 返回給父進程子進程的PID是為了標識指定的子進程,以便未來可以控制特定的子進程
- fork()函數怎麼可能返回兩次阿? -- 上面說了fork後會共享fork後的程式碼,其實就是執行了兩次return (return的本質就是寫入)
- 一個id怎麼既能接受 == 0 又接受 > 0 ? (後面談) -- 關聯寫實拷貝、虛擬地址部分
進程狀態
- 作業系統教材中對於狀態(運行、阻塞、掛起)的說明

什麼叫阻塞?阻塞和運行的本質是看你的task_struct在誰的佇列中(runqueue or waitqueue)!!!!
- 凡是在runqueue中的都是運行狀態!!!
什麼是掛起?
task_struct 在記憶體中,由於記憶體不足,而導致進程對應的數據或代碼被交換到的swap分區(磁盤)
- 但是過度的swap會導致系統變慢
- 如果進程本身就處於阻塞狀態,又被掛起,就稱為阻塞掛起
- 如果阻塞掛起後記憶體還是不足,作業系統實在沒辦法只能將運行狀態的進程也掛起,就稱為運行掛起
- 如果還是不行呢?那麼Linux系統就會選擇性殺掉特定的進程
Linux中具體進程狀態表示
- R運⾏狀態(running): 並不意味著進程⼀定在運⾏中,它表明進程要麼是在運⾏中要麼在運⾏佇列⾥
- S睡眠狀態(sleeping): 意味著進程在等待事件完成(這⾥的睡眠有時候也叫做可中斷睡眠(interruptible sleep))
- D磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束
- T停⽌狀態(stopped): 可以通過發送 SIGSTOP 信號給進程來停⽌(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運⾏
X死亡狀態(dead):這個狀態只是⼀個返回狀態,你不會在任務列表⾥看到這個狀態
- 進程不會直接進入X狀態,會先進入Z狀態,之後再變成X狀態
僵死狀態(Zombies):是⼀個⽐較特殊的狀態。當進程退出並且⽗進程(使⽤wait()系統調⽤)沒有讀取到⼦進程退出的返回代碼時就會產⽣僵死(屍)進程
- 僵死進程會以終⽌狀態保持在進程表中,並且會⼀直在等待⽗進程讀取退出狀態代碼。
- 只要⼦進程退出,⽗進程還在運⾏,但⽗進程沒有讀取⼦進程狀態,⼦進程進⼊Z狀態
- Z狀態指的就是,只保留進程的task_struct,未來讓父進程或OS幫我們獲取子進程的退出數據
- 進程退出了,退出信息是什麼?
main函數的返回值或收到的信號值 - 進程退出了,退出信息保存在哪?
進程自己的task_struct結構中 - 檢測Z狀態進程、回收Z狀態進程本質是在做什麼?
本質是在獲取task_struct中exit_state、exit_code、exit_signal這部分的數據,獲取完後讓task_struct處於X釋放掉 - 怎麼回收?誰來回收?
通常是父進程來回收,透過wait函數來獲取到子進程的status,這也是為什麼要返回子進程的pid給父進程
進程狀態查看
ps axj / ps aux
- a:顯⽰⼀個終端所有的進程,包括其他⽤⼾的進程。
- x:顯⽰沒有控制終端的進程,例如後臺運⾏的守護進程。
- j:顯⽰進程歸屬的進程組ID、會話ID、⽗進程ID,以及與作業控制相關的信息
- u:以⽤⼾為中⼼的格式顯⽰進程信息,提供進程的詳細信息,如⽤⼾、CPU和記憶體使⽤情況等
在此先拋出的概念
- 前後臺進程 --- TODO
- kill -9 --- 發送信號
- stat + --- 帶加號表示前臺進程,否則為後臺進程
細談殭屍進程(僵死進程)
殭屍進程的危害
當一個進程退出時,其退出的狀態需要維持,因爲要告訴父進程說你交給我的任務我完成的怎麼樣。但如果父進程一直不讀取,那子進程就會一直維持在殭屍狀態!而維護退出狀態本⾝就是要⽤資料維護,也屬於進程基本信息,所以保存在task_struct(PCB)中,換句話說,Z狀態⼀直不退出,PCB⼀直都要維護!
那我們知道一個父進程可能會創建許多子進程,如果父進程一直不回收退出的子進程,就可能導致記憶體資源浪費的問題 -- 也就是記憶體洩漏
孤兒進程
這裡討論的是,如果父進程比子進程早退出呢?那子進程退出時進入殭屍狀態由誰來管理?
如果父進程比子進程早退出,那麼這些子進程就稱為孤兒進程
孤兒進程會被系統(1號進程 -- init/systemd)「領養」,1號進程處理孤兒進程的善後,也就是說,孤兒進程會被系統自動回收
-> 1號進程接收孤兒進程後,孤兒進程會轉變成後台進程!
*關於前後台進程,只要是能從鍵盤上獲取數據的,就是前台進程,反之則為後台進程。
- 前台進程無論何時都只能有一個!
- 後台進程可以有無數個
進程優先級(理論重要性 >>>>> 實操)
何謂優先級?
權限說明了「能不能」的問題,而優先級就是來說明能的情況下的先後問題 --- 進程能得到某種資源的前提下,獲取該資源的先後順序!
- 為什麼要有優先級?
因爲資源有限!優先級就是用來分配資源的! Linux作業系統下是如何做的? --- 進程調度 --- task_struct中的兩個整形變量

- UID : 代表執⾏者的⾝份
- PID : 代表這個進程的代號
- PPID :代表這個進程是由哪個進程發展衍⽣⽽來的,亦即⽗進程的代號
- PRI :代表這個進程可被執⾏的優先級,其值越⼩越早被執⾏
- NI :代表這個行程的nice值
PRI是優先級,那NI呢?NI是一個NICE值,其用來修正進程的優先級數值,在Linux下要調整進程優先級,就是調整NI值
(1) NICE的範圍?[-20,19]
(2) 為什麼是這個範圍?
這是 Linux(源自 UNIX)調度器設計時的約定,為了提供一個平衡範圍- 負值代表「提高優先級」
- 正值代表「降低優先級」
- 總共 40 個級別,這樣設計是為了 1. 給系統管理員足夠的微調空間 2. 避免讓範圍過大導致難以管理。
除此之外,因爲目前的作業系統都是分時系統 -- 為了要確保給進程分配的時間片是相對公平、公正的,讓每一個進程都能較均 衡的在一段時間內獲取CPU的資源。
(3) 進程優先級變化範圍是多少?[60,99]
查看進程優先級的命令
- top : 進⼊top後按「r」‒>輸⼊進程PID‒>輸⼊nice值
nice : nice [選項] 命令 [參數]
nice -n 10 ./myprocess- 啟動時設定進程的 NI 值
只能在創建新進程時使用
# 以 NI=10 啟動程式(較低優先級) nice -n 10 ./myprocess # 以 NI=-5 啟動程式(較高優先級,需要 root 權限) sudo nice -n -5 ./myprocess # 預設情況下,nice 會降低優先級 nice ./myprocess # 相當於 nice -n 10 ./myprocess
renice [選項] 優先級 [選項] 進程ID
選項:- -n <數值>:設定新的 NI 值
- -p
:指定進程 ID - -u <使用者>:指定使用者的所有進程
- -g <群組>:指定群組的所有進程
和nice的不同之處 - 修改執行中進程的 NI 值
可以修改已存在的進程
# 修改指定 PID 的進程 NI 值 renice 10 1234 # 將 PID 1234 的進程 NI 值設為 10 # 使用 -n 選項(更明確) renice -n 10 -p 1234 # 修改使用者的所有進程 renice -n 5 -u username # 修改多個進程 renice -n 10 -p 1234 5678 9012 # 提高優先級(需要 root 權限) sudo renice -n -5 -p 1234
補充概念-競爭、獨⽴、並⾏、並發
- 競爭性: 系統進程數⽬眾多,⽽CPU資源只有少量,甚⾄1個,所以進程之間是具有競爭屬性的。為了⾼效完成任務,更合理競爭相關資源,便具備了優先級
- 獨⽴性: 多進程運⾏,需獨享各種資源,多進程運⾏期間互不⼲擾
- 並⾏: 多個進程在多個CPU下分別,同時進⾏運⾏,這稱之為並⾏
- 並發: 多個進程在⼀個CPU下採⽤進程切換的⽅式,在⼀一段時間之內,讓多個進程都得以推進,稱之為並發
Linux2.6內核進程O(1)調度佇列

活動佇列
時間⽚還沒結束的所有行程都依照優先權放在該佇列
• nr_active: 總共有多少運⾏狀態的進程
• queue[140]: ⼀個元素就是⼀個進程佇列,相同優先權的進程依照FIFO規則進⾏排隊調度,所以,數組下標就是優先權!
從這個結構中,選擇⼀個最適合的進程,過程是如何做的呢?
- 從0下表開始遍歷queue[140]
- 找出第⼀個⾮空佇列,該佇列必定為優先權最⾼的佇列
- 拿到選取佇列的第⼀個進程,開始運⾏,調度完成!
- 遍歷queue[140]時間複雜度是常數!但還是太低效了!
bitmap[5]:⼀共140個優先權,⼀共140個進程佇列,為了提⾼找出⾮空佇列的效率,就可以⽤5*32個⽐特位表⽰佇列是否為空,這樣,便可以⼤⼤提⾼查找效率!
小結論:
- 優先級數字,本身就是數組的下標(優先級+40)
- 根據優先級選擇的過程,本身是一個hash的過程
- 一旦確定是哪個佇列的,接下來就是FIFO
- 進程飢餓問題:長時間內不斷有進程進入,導致其他進程無法被調度
過期佇列
過期佇列與活動佇列結構⼀模⼀樣!
過期佇列上放置的進程,都是時間⽚耗盡的進程,當活動佇列上的進程都被處理完畢之後,對過期佇列的進程進⾏時間⽚重新計算。
active指標和expired指標
active指標永遠指向活動佇列;expired指標永遠指向過期佇列。
可是活動佇列上的進程會越來越少,過期佇列上的進程會越來越多,因為進程時間⽚到期時⼀直都存在的。
-> 沒關係,在適當的時候,只要能夠交換active指針和expired指針的內容,就相當於有具有了⼀批新的活動進程。
結論:
在系統當中找出⼀個最適合調度的進程的時間複雜度是⼀個常數,不隨著進程增加⽽導致時間成本增加,我們稱之為進程調度O(1)演算法!