第1講:程序和執行緒

7hu95b發表於2024-06-10

掃盲課。對 Linux 系統下,程序和執行緒的基本概念和對比進行闡述。

一、程序

程序是處於執行期的程式及相關資源的總稱。作業系統為程序提供兩種虛擬機器制:虛擬處理器 & 虛擬記憶體,目的是讓程序有一種假象:“獨享處理器和整個記憶體空間”。

關於程序描述符 struct task_struct 放在後續內容中解釋。

(一)程序狀態機

這裡我們介紹兩個版本的“程序狀態機”。

(1)從邏輯上程序可分為五種狀態:建立、執行、就緒、阻塞、結束。
程序的五種邏輯狀態

(2)也可根據 task_struct 的五個標誌位進行分類:

  • task_running:圖中的“就緒佇列”和“執行佇列”。
  • task_interruptible:阻塞掛起,收到某些訊號後反應。
  • task_uninterruptible:忽略訊號,fork(clone呼叫)會用到。
  • __task_traced:被其他程序跟蹤除錯,比如 ptrace。
  • __task_stopped:停止。不僅是退出,除錯期間也會進入該狀態。
    程序狀態轉換

(二)常見的程序

(1)孤兒程序

  • 子程序退出時發現父程序已經退出了,會導致子程序的資源無法正確釋放而洩露。
  • 避免孤兒程序:在程序結束時會尋找新的父程序或託管給0號程序,即 init 程序。

(2)殭屍程序

  • 子程序在執行結束退出時,核心會釋放所有的資源;但是仍然會保留其程序描述符中的相關資訊,直到父程序透過 wait 或 waitpid 來獲取這些資訊時才釋放。如果不釋放這些資訊就會變成“殭屍程序”。
  • 注意:子程序銷燬時會臨時進入“僵死狀態”,而不是“殭屍程序”,注意區分。
  • 避免殭屍程序:子程序在退出時向父程序傳送訊號,讓父程序呼叫wait函式釋放。

(3)守護程序

  • 執行在後臺、獨立於執行終端、屬於1號程序管理。用於週期性的執行某種任務或等待處理某些發生的事件(比如 ssh 伺服器、ftp 伺服器等)。
  • 建立守護程序:fork呼叫後父程序退出,使子程序變成孤兒程序,歸1號程序管理。

二、執行緒

執行緒是程序中的一條執行流。Linux沒有實現專門的執行緒,Windows下專門實現了“輕量級程序”。

同一個程序內多個執行緒之間可以共享:程式碼段、資料段、堆區(開啟的檔案)等資源,但每個執行緒各自都有一套獨立的PID、棧(指標)、PC計數器、程式執行所需的暫存器,從而確保執行緒的控制流是相對獨立的。

主要是私有棧:用於儲存函式呼叫、區域性變數以及其他臨時資料,不能讓其他執行緒訪問。

主執行緒的私有棧是佔用程序虛擬記憶體空間的棧記憶體;其他執行緒的私有棧則是共享記憶體對映區的記憶體。

(一)程序崩潰

C/C++程序中的一個執行緒崩潰時,有可能會導致其所屬程序的所有執行緒崩潰。

  • 理論上每個執行緒都是獨立執行的,執行緒的崩潰通常只會影響到它本身,所以不會導致整個程序崩潰。
  • 執行緒之間共享地址空間和資料資源,導致這些共享資源處於不一致的狀態,該不確定性會讓OS直接結束當前程序。

因為各個執行緒的地址空間是共享的,所以某個執行緒對地址的非法訪問就會導致記憶體的不確定性,進而可能會影響到其他執行緒。既然這種操作是危險的,OS會認為這很可能導致一系列嚴重的後果,於是乾脆讓整個程序崩潰

有一些特殊情況下,執行緒的崩潰可能會影響整個程序。

  • 主執行緒的崩潰:主執行緒通常負責啟動和管理其他執行緒,所以它的崩潰會導致程序終止,進而導致子執行緒終止。
  • 關鍵資源的破壞:關鍵資源(如共享記憶體區域或檔案控制代碼)受損或無法使用,其他執行緒可能會受到影響而導致程序的異常終止。
  • 無法處理的異常:行為引發無法恢復的異常,例如訪問非法記憶體地址或遇到不可處理的硬體錯誤。

結束程序的方式:使用 kill 系統呼叫向程序傳送訊號。也支援自定義訊號處理回撥函式 sigHandler,在結束前“垂死掙扎”一番。

可以透過 kill -l 檢視所有訊號。比如常見的 kill -9 pid_num 就是一種訊號,-9 代表 SIGKILL 訊號,該訊號會忽略任何訊號處理函式,會馬上幹掉該程序。

Java 不會崩潰:JVM 有自定義的訊號處理函式,會執行資源清理後再 exit 退出。

舉例:StackOverflow 報錯,如果發生無限遞迴會導致棧幀(預設為 8M 大小)被佔滿。

當發生崩潰後,JVM 自定義的訊號處理函式會攔截 SIGSEGV 訊號、恢復程序的執行、丟擲 StackoverflowError 和 NPE 兩個錯誤供我們捕獲。

(二)分類:使用者執行緒 & 核心執行緒

執行緒控制塊TCB(Thread Control Block)

(1)使用者執行緒(邏輯執行緒)

  • 在使用者態下透過“執行緒管理庫”建立、管理、執行,不受核心空間的排程。Linux下的執行緒管理庫 pthread 用法之後的文章會具體分析。
  • 不能參與 CPU 的搶佔,只能共享程序的時間片,實現“執行緒的並行”。
  • 作業系統只能看到程序的PCB,看不到執行緒的TCB。因此作業系統不直接參與使用者執行緒的執行緒管理和排程,一切由使用者級執行緒庫函式管理,包括執行緒的建立、終止、同步和排程等

優點:(1)透過 TCB 可以在不支援執行緒技術的OS中使用執行緒;(2)切換時無需核心態和使用者態的切換,所以切換速度很快;

缺點:同一程序中只能同時有一個執行緒在執行,所以(1)不能充分利用多核CPU;(2)如果一個執行緒發起系統呼叫而阻塞,會導致整個程序中所有執行緒的阻塞。

(2)核心執行緒(物理執行緒)

  • 又稱為“守護程序”。執行在“核心態”,只能由核心進行管理排程,也是核心最小的排程單位。
  • 參與 CPU 的搶佔,可以實現真正的多核併發。由於它的資料結構和堆疊很小,所以它的建立、排程的開銷比程序小的多。

一個核心執行緒阻塞不影響同一程序下的其他執行緒,可以在多核處理器中執行。
舉例:中斷的下半部分為“軟中斷”,便是透過核心執行緒的方式進行實現的。

(3)對比

  • 核心支援:使用者執行緒可在一個不支援執行緒的OS中實現;核心執行緒則需要得到OS核心的支援。因此上一條、兩者建立聯絡的前提是OS必須支援核心執行緒。
  • CPU分配:核心只為使用者執行緒所在的程序分配一個處理器,使用者執行緒透過時間片排程的方式競爭該處理器;核心執行緒則可以在多個處理器上並行。
  • 排程:只有核心執行緒才是CPU排程的單位,使用者執行緒則透過所在的程序去排程。

根據上述原因,使用者執行緒必須要對映到核心執行緒上,才能讓核心看到 & 排程使用者執行緒執行,從而讓使用者態的多執行緒實現併發執行。

建立對映的方式有:一對一模型、混合執行緒模型(多對一、多對多)。也要考慮到OS對執行緒數量的限制、選擇合適的模型。

建立和管理大量的核心執行緒需要消耗系統資源,包括記憶體和排程開銷;並且執行緒之間的切換也需要時間和開銷,頻繁的上下文切換會導致開銷增加。

(三)多執行緒/執行緒池的簡單入門

根據CPU核數不同,多執行緒實際上分為“並行”和“併發”兩種。我們最感興趣的其實是“執行緒池”(Thread Pool)技術。它專門用於管理和複用多個執行緒,能極大減小執行緒的建立和銷燬的開銷。適用於需要處理大量短時間任務的場景,並能根據系統的承受能力調整可執行執行緒的數量。

一個簡單的執行緒池由三部分組成:

  • 執行緒池管理器(Thread Pool Manager):負責執行緒池的建立、銷燬和管理;並維護著執行緒佇列和任務佇列,並負責分配任務給空閒執行緒執行。
  • 執行緒池(Thread Pool):由一組執行緒組成,當執行緒池中的執行緒處於可用狀態時,便可以執行提交給執行緒池的任務。
  • 任務佇列(Task Queue):用於儲存待執行的任務,當執行緒池中的執行緒空閒時,會從任務佇列中獲取任務進行執行。

執行緒池的工作步驟有以下幾步:

  • 初始化執行緒池,建立一定數量的執行緒並將其置於可用狀態(阻塞狀態)。
  • 當有任務提交給執行緒池時,執行緒池從任務佇列中獲取任務。
  • 執行緒池分配空閒執行緒來執行任務。
  • 執行完任務後,執行緒繼續保持可用狀態,返回到執行緒池中,等待下一個任務。
  • 如果任務佇列為空且沒有待處理的任務,空閒執行緒等待新任務的到來。

三、程序和執行緒的區別

  • “程序”是資源分配的最小單位,“執行緒”是程式執行(CPU 排程)的最小單位。
  • 地址空間是否獨立。每一個程序都有它自己的獨立地址空間,一般不會發生讀寫同一塊記憶體(共享記憶體除外);執行緒共享所在程序的地址空間,對於臨界區資料需要進行同步或互斥處理。
  • 上下文切換的開銷不同。(1)程序的上下文切換開銷比較大:需要切換像虛擬記憶體等頁表,對 Cache 快取機制影響較大;(2)同一個程序的執行緒切換很快:只需要切換像“暫存器”和“棧”等私有資料即可;但如果是不同程序的執行緒進行切換,那麼和程序切換一樣

上下文切換會導致 CPU Cache 快取全部作廢,虛擬記憶體的切換會導致 Page Cache(TLB)全部失效,導致在一定時間範圍內、記憶體訪問的低效

  • 建立方式不同。兩者都執行 fork 系統呼叫(底層為 clone 系統呼叫),執行緒會傳入代表共享資源的引數標誌,建立速度更快(不涉及像記憶體、檔案等資源管理資訊);此外程序還有 COW 策略,以及建立子程序後執行 exec() 優先執行子程序。
  • 通訊方式。程序之間通訊記為 IPC(比如共享記憶體);執行緒因為共享記憶體和檔案資源,不需要經過核心,所以資料交換效率很高。

相關文章