1. 程式人生 > 實用技巧 >作業系統(四)—— 程序和執行緒基礎

作業系統(四)—— 程序和執行緒基礎

概述

  在前幾篇講記憶體管理的時候,提到地址空間是記憶體的抽象。那程序就是CPU的抽象,一個程式執行起來以後就是一個程序,執行緒是程序創建出來的,其本身並不能獨立執行,一個程序可以創建出來多個執行緒,他們之間會共享程序的堆空間,公共變數等。本文就詳細介紹一個程序和執行緒基礎。

程序

程序定義

一個具有一定獨立功能的程式在一個數據集合上的一次執行過程。這是清華大學作業系統公開課上給的定義。覺得不用做過多解釋,相信大家應該都明白。

程序的組成

  • 程式計數器
  • 開啟的檔案
  • 獨立的虛擬地址空間

程序控制塊(PCB)

作業系統用於管理程序所用的資訊集合。linux中task_struct描述符結構體表示一個程序的控制塊,這個task_struct結構記錄了這個程序所有的context (程序上下文資訊)

struct task_struct{
    //列出部分欄位
    volatitle long state;//表示程序當前的狀態 ,-1表示不可執行,0表示可執行,>0表示停止
    void                *stack; //程序核心棧
    unsigned int            ptrace;

    pid_t               pid;//程序號
    pid_t               tgid;//程序組號
    struct              mm_struct *mm,*active_mm //使用者空間 核心空間

    struct              list_head thread_group;
//該程序的所有執行緒連結串列 struct list_head thread_node; int leader;//表示程序是否為會話主管 struct thread_struct thread;//該程序在特定CPU下的狀態 //等等欄位:包括一些 表示 使用的檔案描述符、該程序被CPU排程的次數、時間、父子、兄弟程序等資訊 }

在linux中,程序的資訊在/proc資料夾下儲存著。如果想看某個pid的資訊,可以使用ps或者top等命令獲取。程序控制塊在記憶體的組織形式有兩種方式,一種是連結串列方式,每個PCB以連結串列的方式連線在一起,另一種是索引的方式,索引的指標指向程序控制塊。系統中的所有程序的資訊都會儲存到程序控制塊的連結串列中。

程序的狀態

由於現代作業系統,大多是採用時間片的方式來執行程序,就是每個程序執行一段時間,相互交替執行,所以程序就有了很多的中間狀態,下面就介紹一下這些狀態。

         

                   圖片來源:現代作業系統

  • 執行狀態:程式正常執行,佔用CPU時間片
  • 阻塞狀態:如果程式執行一個I/O操作,CPU往往會進行上下文切換,執行別的程序,那當前程序就變成了阻塞狀態
  • 就緒狀態:程式已經準備好,可以執行,但是CPU時間片還沒有分配給當前程序

這裡說明一下什麼是時間片,舉個例子,CPU給每個程序的執行時間是20ns,那如果某個程序20ns沒有執行完,會切換到別的程序執行,那20ns就是時間片的大小。

在解釋一下上面狀態之間的切換。

  • 1,執行狀態到阻塞狀態,程序執行某個操作必須等待,比如程式執行一個I/O操作,CPU會把當前程序進入阻塞狀態執行別的程序
  • 2,執行狀態到就緒狀態,分配給當前程序的時間片用完
  • 3,就緒狀態到執行狀態,被程序排程程式選中,開始執行
  • 4,阻塞狀態到就緒狀態,程式等待某個事件的到來

上面只介紹了三種狀態,其實還有一種狀態,殭屍狀態,下面就簡略介紹一下

父程序可以通過fork來建立子程序,當子程序結束的時候,可以直接呼叫exit退出,這個時候程序的資源就會全部被回收,但是程序控制塊是作業系統管理的,這個還沒有被回收,由於程序使用者態資源已經全部釋放了,無法再回到使用者態發出系統呼叫,回收程序控制塊(PCB),只能交給他的父程序進行回收,在程式執行了exit,而父程序還沒有回收程序控制塊這段時間程序既不是就緒狀態,也不是等待狀態,而是處於一種殭屍狀態(就是半死不死的狀態)。上面的解釋是清華大學作業系統公開課老師給的一種解釋,不太明白為什麼在程序資源被回收完之後,不把程序的唯一標示PCB也給回收掉,而要交給父程序進行回收,有明白的胖友,歡迎指教。

執行緒

執行緒其實是一種輕量級程序,通常一個程序由多個執行緒組成,各個執行緒共享程序的記憶體空間(包括資料,程式碼,堆,開啟的檔案,訊號等),一個典型的執行緒和程序的關係圖如下

    

         圖片來源:程式設計師的自我修養

執行緒的訪問許可權

執行緒之間雖然可以共享同一個程序的很多資源,但是執行緒仍然有私有儲存空間,如下

  • 棧(並非完全無法被其他執行緒訪問,但是一般情況下仍然認為是執行緒私有,以上這段話來自程式設計師的自我修養,不太懂為什麼執行緒的棧,別的執行緒也可以訪問
  • 執行緒區域性儲存(Thread Local Storage 簡稱TLS),是作業系統提供的一個很有限的空間,java中的ThreadLocal就是基於此設計的,一會再談這個
  • 暫存器

上面幾個儲存空間,我想介紹一個TLS,在牛客網上有這麼一個題。

連結:https://www.nowcoder.com/questionTerminal/a0c59b5a3e71436a86c3cc1f6392e55f
來源:牛客網
(多選題)
對於執行緒區域性儲存TLS(thread local storage),以下表述正確的是
A. 解決多執行緒中的對同一變數的訪問衝突的一種技術
B. TLS會為每一個執行緒維護一個和該執行緒繫結的變數的副本
C. 每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了
D. Java平臺的java.lang.ThreadLocal是TLS技術的一種實現

牛客上給的正確答案是ABD,C之所以錯誤是如果TLS中儲存的是變數的引用,多個執行緒併發修改時,還是有同步的問題,所以C錯誤,具體解釋看這篇文章,而B說每個執行緒維護一個變數副本,意思是說每個執行緒都會獲取到相同的具有初始化值的變數副本,至於之後每個執行緒怎麼改,是相互獨立的。

這篇文章更詳細的介紹了TLS,有興趣的可以看看:有必要澄清一下究竟TLS(or java's thread local)的作用是什麼

從資料的角度來看,是否私有如下表

  

多執行緒和多程序的區別

  • 多執行緒之間堆記憶體共享,而程序相互獨立,執行緒間通訊可以直接基於共享記憶體來實現,比程序的常用的那些多程序通訊方式更輕量(這個牽涉到執行緒間和程序間通訊,本文沒有講)
  • 在上下文切換來說,不管是多執行緒還是都程序都涉及到暫存器、棧的儲存,但是執行緒不需要切換 頁面對映(虛擬記憶體空間)、檔案描述符等,所以執行緒的上下文切換也比多程序輕量
  • 多程序比多執行緒更安全,一個程序基本上不會影響另外一個程序
    在實際的開發中,一般不同任務間(可以把一個執行緒、程序叫做一個任務)需要通訊,使用多執行緒的場景比多程序多。但是多程序有更高的容錯性,一個程序的crash不會導致整個系統的崩潰,在任務安全性較高的情況下,採用多程序。

上下文切換

  由於程序排程還沒有寫,不過大家應該都聽說過作業系統分時排程,那既然要切換不同的程序,就要把之前執行的程序一些資訊給儲存起來,什麼資訊呢,比如暫存器中的臨時變數,程式計數器等,然後載入另外一個程序的臨時變數,程式計數器,並跳轉到指定的地方執行,這個過程就叫做上下文切換。

上下文切換的型別

  • 程序上下文切換
  • 執行緒上下文切換
  • 中斷上下文切換

下面就詳細介紹一下這三種上下文切換。

程序上下文切換

程序上下文分為程序內上下文切換和程序間上下文切換,先介紹程序內上下文切換。

程序內上下文切換--系統呼叫

在現代作業系統中,有兩種執行狀態,使用者態和核心態,使用者態執行著特權級別比較低的程式,只能訪問部分資源,核心態執行著特權級別比較高的程式,可以訪問所有的硬體和功能,比如作業系統,而處於使用者態的程式如果想要執行某個特權級別比較高的操作,就需要呼叫作業系統暴露的介面,這個過程叫做系統呼叫,而系統呼叫需要程式從使用者態切換到核心態執行,這個過程會發生上下文切換。

舉個例子,printf("hello world"),這個操作就需要系統呼叫,上下文切換的過程如下

  1. 儲存 CPU 暫存器裡原來使用者態的指令位
  2. 為了執行核心態程式碼,CPU 暫存器需要更新為核心態指令的新位置。
  3. 跳轉到核心態執行核心任務。
  4. 當系統呼叫結束後,CPU 暫存器需要恢復原來儲存的使用者態,然後再切換到使用者空間,繼續執行程序。

所以一次系統呼叫發生了兩次上下文切換(使用者態 -> 核心態 -> 使用者態)

系統呼叫上下文切換,切換到核心態之後,並不需要虛擬記憶體等使用者空間的資源,而且也不會切換程序,只需要載入核心態的程式計數器和暫存器、堆、棧等資源。

程序間上下文切換

程序上下文切換的場景如下

  • 為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個程序。這樣,當某個程序的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的程序執行。
  • 程序在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以執行,這個時候程序也會被掛起,並由系統排程其他程序執行。
  • 當程序通過睡眠函式 sleep 這樣的方法將自己主動掛起時,自然也會重新排程。
  • 當有優先順序更高的程序執行時,為了保證高優先順序程序的執行,當前程序會被掛起,由高優先順序程序來執行
  • 發生硬體中斷時,CPU 上的程序會被中斷掛起,轉而執行核心中的中斷服務程式。

由於程序是由核心管理的,因此程序的切換隻能發生在核心態,所以程序間上下文切換不光需要把使用者態的暫存器、程式計數器、堆疊,虛擬地址空間等資訊儲存起來,還需要把核心中的程序的狀態,堆疊資訊儲存起來。

系統呼叫和程序間上線文切換的區別

程序間上下文切換就比系統呼叫多了一步,切換到另一個程序的時候需要載入程序使用者態的資源,而系統呼叫,進入核心態的時候是不牽涉到使用者態的資源的,所以就無須載入使用者態的資源。

小結

作業系統為了安全,限制使用者程式的特權級別,就犧牲了效率。同樣,作業系統為了公平,讓每個程式都有執行的機會,搞了個分時排程,同樣犧牲了效率(不包括程式執行I/O等操作的情況,這種情況切換別的程序執行是提高效率的◠◡◠),上下文切換時間不只是浪費在需要儲存暫存器,堆疊等資訊和重新載入另一個程序的暫存器,堆疊資訊。還有之前介紹虛擬地址空間的時候,介紹過,為了加快虛擬地址和實體地址的對映,在CPU中有一個MMU,MMU中有一個TLB硬體,用來快取最近經常使用的對映關係,那如果發生上下文切換就需要從記憶體中的頁表中讀取對映關係,再快取到TLB中,這個過程也會浪費時間。

執行緒上下文切換

上面已經介紹過執行緒,同一個程序的執行緒會共享很多的資源,這些線上程上線文切換的時候是不需要儲存的,需要儲存的是執行緒自己私有的資料,就是上面介紹過的暫存器,棧,TLB等。

中斷上下文切換

中斷,在第一篇文章中已經介紹過,這裡就不介紹了,中斷上下文切換有點像系統呼叫,因為系統呼叫和中斷都是作業系統執行的,所以都發生在核心態,也就是說在處理中斷的時候不涉及到使用者態的虛擬地址空間、堆、棧等資訊。

總結

本文介紹了程序、執行緒、上下文切換 。程序介紹了程序的組成和程序的狀態,執行緒介紹了執行緒的組成,上下文切換分別介紹了程序上下文切換、執行緒上下文切換、中斷上線文切換,總的來說把基礎的知識給概括的介紹了一下,之後會介紹程序排程和執行緒安全相關的內容。

                

參考:

《現代作業系統》

《程式設計師的自我修養》

清華大學作業系統公開課

一文讓你明白CPU上下文切換

淺析作業系統的程序、執行緒區別