作業系統(四)—— 程序和執行緒基礎
概述
在前幾篇講記憶體管理的時候,提到地址空間是記憶體的抽象。那程序就是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"),這個操作就需要系統呼叫,上下文切換的過程如下
- 儲存 CPU 暫存器裡原來使用者態的指令位
- 為了執行核心態程式碼,CPU 暫存器需要更新為核心態指令的新位置。
- 跳轉到核心態執行核心任務。
- 當系統呼叫結束後,CPU 暫存器需要恢復原來儲存的使用者態,然後再切換到使用者空間,繼續執行程序。
所以一次系統呼叫發生了兩次上下文切換(使用者態 -> 核心態 -> 使用者態)
系統呼叫上下文切換,切換到核心態之後,並不需要虛擬記憶體等使用者空間的資源,而且也不會切換程序,只需要載入核心態的程式計數器和暫存器、堆、棧等資源。
程序間上下文切換
程序上下文切換的場景如下
- 為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個程序。這樣,當某個程序的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的程序執行。
- 程序在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以執行,這個時候程序也會被掛起,並由系統排程其他程序執行。
- 當程序通過睡眠函式 sleep 這樣的方法將自己主動掛起時,自然也會重新排程。
- 當有優先順序更高的程序執行時,為了保證高優先順序程序的執行,當前程序會被掛起,由高優先順序程序來執行
- 發生硬體中斷時,CPU 上的程序會被中斷掛起,轉而執行核心中的中斷服務程式。
由於程序是由核心管理的,因此程序的切換隻能發生在核心態,所以程序間上下文切換不光需要把使用者態的暫存器、程式計數器、堆疊,虛擬地址空間等資訊儲存起來,還需要把核心中的程序的狀態,堆疊資訊儲存起來。
系統呼叫和程序間上線文切換的區別
程序間上下文切換就比系統呼叫多了一步,切換到另一個程序的時候需要載入程序使用者態的資源,而系統呼叫,進入核心態的時候是不牽涉到使用者態的資源的,所以就無須載入使用者態的資源。
小結
作業系統為了安全,限制使用者程式的特權級別,就犧牲了效率。同樣,作業系統為了公平,讓每個程式都有執行的機會,搞了個分時排程,同樣犧牲了效率(不包括程式執行I/O等操作的情況,這種情況切換別的程序執行是提高效率的◠◡◠),上下文切換時間不只是浪費在需要儲存暫存器,堆疊等資訊和重新載入另一個程序的暫存器,堆疊資訊。還有之前介紹虛擬地址空間的時候,介紹過,為了加快虛擬地址和實體地址的對映,在CPU中有一個MMU,MMU中有一個TLB硬體,用來快取最近經常使用的對映關係,那如果發生上下文切換就需要從記憶體中的頁表中讀取對映關係,再快取到TLB中,這個過程也會浪費時間。
執行緒上下文切換
上面已經介紹過執行緒,同一個程序的執行緒會共享很多的資源,這些線上程上線文切換的時候是不需要儲存的,需要儲存的是執行緒自己私有的資料,就是上面介紹過的暫存器,棧,TLB等。
中斷上下文切換
中斷,在第一篇文章中已經介紹過,這裡就不介紹了,中斷上下文切換有點像系統呼叫,因為系統呼叫和中斷都是作業系統執行的,所以都發生在核心態,也就是說在處理中斷的時候不涉及到使用者態的虛擬地址空間、堆、棧等資訊。
總結
本文介紹了程序、執行緒、上下文切換 。程序介紹了程序的組成和程序的狀態,執行緒介紹了執行緒的組成,上下文切換分別介紹了程序上下文切換、執行緒上下文切換、中斷上線文切換,總的來說把基礎的知識給概括的介紹了一下,之後會介紹程序排程和執行緒安全相關的內容。
參考:
《現代作業系統》
《程式設計師的自我修養》
清華大學作業系統公開課