課程內容#
什麼是進程#
- 進程是程序在內存中的鏡像,是正在運行的程序,是程序的實例化,是一個複雜的集合體
- 包含開辟的內存空間、用戶信息、組信息、權限、佔用的資源、正在跑的代碼、打開的文件等等
- 與之對應
- ① 什麼是程序
- 程序是編譯好的可執行的二進制文件,放在磁碟上
- 就是一個普通文件,有 x 權限
- 程序的集合是應用
- 程序是編譯好的可執行的二進制文件,放在磁碟上
- ② 什麼是線程
- 線程代表一系列有順序的、需要 CPU 執行的指令
- 一個進程可能由一個或多個線程組成,同時執行指令
- [PS] 進程是 CPU資源分配的基本單位,線程是 CPU調度的基本單位
- ① 什麼是程序
fork#
創建一個子進程 [進程接口]
- man fork
- 原型 + 描述
- 返回值 [類型:pid_t]:進程 id
- 通過複製調用 fork 的進程 [父進程] 創建一個新進程 [子進程],父進程和子進程運行在相互獨立的內存空間
- fork 完成時,兩者有一樣的內容,之後內存的寫、文件映射不會互相影響
- 當內存發生變化時,才會發生真正的拷貝 [寫拷貝的概念]
- 否則共用的是同一份內存空間
- 父子主要有以下的不同點:
- 孩子有自己唯一的 PID,並且不和任何已存在的 PID 相同
- 孩子認為的父親 PID [getppid] 與真實的父親 PID 相同
- 孩子不會繼承父親的內存鎖
- 孩子的資源使用量和 CPU 使用時間都會重置為 0
- 孩子不會繼承待執行的信號、信號量、記錄鎖、計時器、異步 IO 操作
- 返回值
- 成功:在父進程中返回孩子的 PID,子進程中返回 0
- 父親無法再通過其他方式獲得該孩子的 PID,兒子可通過 getppid 獲得父親的 PID
- 失敗:返回 - 1,並設置 errno [沒有創建子進程]
wait#
等待進程狀態的改變
- man wait
- 原型
- wstatus [int *]:返回子進程的狀態
- 如子進程中 return、exit 的值
- 需要使用宏來解析,如 WIFEXITED (wstatus),詳見代碼演示 —wait— 二
- 描述
- 等待對象:調用進程的孩子
- 狀態改變情況:孩子被終止、被信號中斷、被信號喚醒
- 當有被終止的孩子時,
- wait 命令可以使系統釋放孩子相關的資源
- 否則 [不執行 wait 命令],被終止的子進程就會變成殭屍進程[👇]
- 死了的孩子沒有被父進程察覺,其資源沒有被釋放
- 可使用 top 查看
- zombie 即殭屍進程
- 只要一個孩子已經改變狀態,wait 命令就會被立馬返回
- 否則,會阻塞直到有孩子改變狀態或者信號中斷
- 返回值
- 返回被終止的孩子 PID 或者 - 1 [出錯,並設置 errno]
exec 族#
執行一個文件 [一切皆文件]
- man exec
- 原型
- 有很多兄弟
- 描述
- 將用一個全新的進程鏡像【替換】當前的進程鏡像
- [讓孩子有一個全新的世界]
- 第一個參數都是要執行文件的名字
- path:完整路徑
- file:可以是 PATH 環境變量裡的命令或完整路徑
- 整個族可概括為:"exec + l/v + p/e/pe"
- 參數名 arg,表示是前面參數 path 的參數
- l-list,所有參數放入一整個字符串中 [參數的傳遞方式]
- 按慣例,arg0 應與要執行文件的名字有關聯
- 必須以 (char *) NULL 結尾
- v-vector,所有參數放入一個字符串數組中 [參數的傳遞方式]
- 必須以 null 指針結尾
- p-path,可執行文件的查找範圍包括 PATH 環境變量
- 複製了 Shell 查找命令的過程
- e-env,允許指定環境變量
- 變量 - 數值對
- 返回值
- 只在錯誤發生時返回 - 1
flock#
在打開的文件上操作建議鎖
[本質上是為了保護數據]
- man 2 flock
- 原型 + 描述
- 通過文件描述符 fd 操作
- 主要三種操作
- LOCK_SH:共享鎖
- LOCK_EX:互斥鎖
- 互斥鎖:如果有一個人訪問,其他人就不能訪問了
- 舉例:很多人上一個衛生間
- LOCK_UN:解鎖
- 返回值
- 0,成功;-1,失敗
代碼演示#
fork#
一、複製緩衝區、行緩衝
- 輸出結果
- ❗ fork 後面已經沒有輸出函數了,為什麼輸入 suyelu,會輸出兩個suyelu?
- 【事實】雖然 fork 後代碼複製了一份給子進程,但是子進程只會執行 fork 後的代碼
- 【關鍵】緩衝區被複製了,裡面還存有 suyelu
- printf 中沒有換行符,而標準 I/O 是行緩衝I/O,第 13 行執行完並不會刷新緩衝區
- 當程序結束時,才觸發刷新緩衝區的條件
- [PS] zsh 下可能只會輸出一次 suyelu,可能是 zsh 的優化?bash 下有兩個
二、父子進程相互獨立
- 輸出結果
- ❗ 父進程一定先執行嗎?
- 不一定,父子進程完全獨立,不相干,本質上誰先執行是由內核調度決定
- 但父進程極大概率先執行,因為內核調度的每個進程有一個運行時間,父進程生了孩子後,它的運行時長還沒到
- [PS]
- 1 號進程 <pid 為 1 的進程> 是 init 進程,其它進程都是由它生出來的
- 與人類世界相反,計算機世界的第一個進程一直活著,等著給子進程收屍
三、創建 10 個子進程,並打印自己的序號
10 個子進程是親兄弟
- 如果不加 18 行的 break
- 將產生 2^10 個進程:1 -> 2 -> 4 -> 8 -> 16 -> 32 -> ... -> 2^10
- 統計運行的父、子進程數量:ps -ef | grep -v grep | grep Ten | wc -l
- [Ten 為可執行程序名]
- sleep 時長不會疊加
- 進程遇到 sleep,系統調度換到其它進程運行,最後等待時間只體現約 10s
- i 變量被子進程帶走後就獨立了,不會因父進程中 i 變量的改變而改變
wait#
一、製造殭屍進程
- 不用 wait 感知子進程的終止,即會產生殭屍進程
- 用戶有多種查看殭屍進程的方式 [讓程序在後台運行:./a,out &]
- 基於 ps,查看有 defunct 或 Z 標記的進程
- 基於 top
- 利用 pstree 可以看到殭屍進程的血緣關係
- [PS] 殺殭屍進程需要殺其父進程;該父進程的父進程是 zsh,程序結束後,zsh 會告知系統對父子進程收屍
二、感知子進程返回狀態
- 程序在運行約 2s 後,輸出如下:
- ❗ 為什麼子進程 return 1,父進程 wait 得到的 status 是 256
- 16 位 int 型變量值為 256 👉 其二進制對應第 8 位為 1、其餘位均為 0
- 再參考下圖 [Linux-UNIX 系統編程手冊 (上冊)—26.1.3 節],問題有了答案
- 其實,在 man 手冊中提到了可使用宏來檢查狀態
- WEXITSTATUS (wstatus) 則可以解析退出狀態
- 在源碼中,每個宏對應了下面的位操作
- 所以在 printf 狀態時,根據需求,通過宏處理以下即可
exec 族#
【替換為全新的進程】
- 子進程在第 17 行被替換為全新的進程 [vim],之後的代碼永遠不會被執行
- fork 後直接 exec:不會在 fork 時複製父進程的內存空間又在 exec 時馬上啟用 [寫拷貝概念:當內存發生變化時,才會發生真正的拷貝]
- wait (NULL) 負責收屍
- execlp 的第二個參數可以任意,但和第一個參數有關聯更有意義
- 在下面可以體現該參數的某方面意義
- 如果將 exec 代碼換成第 17 行,第二個參數隨意取名
- 生成可執行文件 Test 的源文件 test.c 如下:
- 輸出 argv [0] 的值
- 執行上下兩份代碼的結果如下:
- 可見,第二個參數體現在了 argv [0] 變量裡
附加知識點#
- 使用 while (1){} 時在循環體裡加 sleep,對 CPU 更友好
- 否則可能導致 CPU 利用率蹿升、空轉、過熱
- pstree 可以方便地看到進程的繼承關係,-p 可以顯示 pid
- 查看殭屍進程:ps、ps -aux、ps -ef、top 均可
- 死鎖:兩個以上的運算單元,雙方都在等待對方停止運行,以獲取系統資源,但是沒有一方提前退出
- 計算機中的同步與生活中的不太一樣
- 不是做同樣的操作
- 而是事件發生的順序是確定的,是有因果關係的
思考點#
Tips#
- du [-h]:查看當前目錄以及所有子目錄的大小 [human-readable]
- 對於多進程的輸出,使用 more 會將不同進程的輸出獨立顯示
- 推薦電影:《她》2013