操作系统随笔(二)
如果你還沒有讀過第一篇隨筆,請點擊這里→操作系統隨筆(一)
文章目錄
- @[toc]
- 2 進程和線程
- 2.1 進程
- 2.1.1 進程模型
- 2.1.2 進程的創建
- 2.1.3 進程的終止
- 2.1.4 進程的層次結構
- 2.1.5 進程的狀態
- 2.1.6 進程的實現
- 2.2 線程
- 2.2.1 進程的使用
- 2.2.2 經典的線程模型
- 2.2.3 POSIX線程
- 2.2.4 在用戶空間中實現線程
- 2.2.5 在內核中實現線程
- 2.2.6 混合實現
- 2.2.7 調度程序激活機制
- @[toc]
- 2.1 進程
- 2.1.1 進程模型
- 2.1.2 進程的創建
- 2.1.3 進程的終止
- 2.1.4 進程的層次結構
- 2.1.5 進程的狀態
- 2.1.6 進程的實現
- 2.2 線程
- 2.2.1 進程的使用
- 2.2.2 經典的線程模型
- 2.2.3 POSIX線程
- 2.2.4 在用戶空間中實現線程
- 2.2.5 在內核中實現線程
- 2.2.6 混合實現
- 2.2.7 調度程序激活機制
2 進程和線程
在操作系統中,最核心的概念是進程,這是對正在運行程序的一個抽象。操作系統的其他內容都是圍繞進程的概念來展開的。
2.1 進程
在只有一個用戶的PC機開機的時候,實際上會秘密啟動很多進程。例如,啟動一個進程用來等待進入的電子郵件;或者啟動另一個防病毒進程周期性地檢查是否有病毒庫更新。或者更好笑的是,一開機就是垃圾捆綁軟件,什么2345,什么網頁游戲,這些都是進程。這么多進程的活動都是需要管理的,于是有一個支持多進程的多道程序系統在這里顯得就很有用了。
在任何多道程序設計系統中,CPU能夠很快地切換進程,這個很快是幾百毫秒哦。這也就讓人產生一種并行的錯覺,在一秒鐘內怎么開了這么多進程?同時開的嗎?不是,實際上在一瞬間只能有一個進程讓CPU服務,只是進程切換地太快了,這就是偽并行。這和真正意義上的并行是有區別的,這也導致了此情形可以用來作為判別是否為多處理器系統的指標。
2.1.1 進程模型
在進程模型中,計算機上所有可運行的軟件,通常也包括操作系統,被組織成若干順序進程,簡稱進程(process),進程是程序的一次執行過程。
每個進程都擁有自己的虛擬CPU,當然,實際上真正的CPU在各進程之間來回切換。我們在之前的1.1.2 中時間復用技術曾經提到過,當一個資源在時間上復用時,不同的程序或用戶輪流使用它。實際上對于CPU來說也是如此,在時間上進行復用的時候,不同的進程輪流使用它。這種快速地切換是需要特定的設計的,我們稱為多道程序設計。
如下圖,在一段時間內,CPU為多個進程服務,但是觀察c圖,實際上在某個瞬間CPU只服務一個進程。
當然在上述的思考中,我們僅僅討論的是單核CPU,而不是多核。如果是多核CPU,根據我們之前所說,多核CPU可以看成一個大CPU里面裝了多個小的CPU;甚至于有的電腦還不止一個CPU,對于一些并行計算機,多處理器的情況也是很常見的。
對于大多數進程來說并不受CPU多道程序設計或其他進程相對速度的影響,因為每個進程占用所需CPU的時間是不同的,所以我們無法確定在快速切換進程的過程中,需要給每個進程多久的處理時間處理完才切換。這時候就迫切的需要干一件事,既然我無法確定一個進程需要多久的CPU,那我干脆每個進程占有CPU的時間都相同,但是在對一個進程處理未完的情況下,我需要有一種物件能夠保存其未處理完的狀態,就像別人玩單機游戲玩到一半去喝水一樣。在后面的小節中,我們會給出這個物件。
2.1.2 進程的創建
有4種主要事件會導致進程的創建:
- 系統初始化
- 正在運行的程序執行了創建進程的系統調用
- 用戶請求創建一個新進程
- 一個批處理作業的初始化
啟動操作系統時,通常會創建若干個進程,有些事可以同用戶交互并且替他們工作的前臺進程,其余的為后臺進程。如果想要查看進程,在windows操作系統中可以使用任務管理器,在linux系統中可以用ps指令。
在像windows和ubuntu這樣的交互式系統中,點擊某個圖標都可以啟動一個程序,啟動的時候就相當于開啟了一個新的進程。
在一個進程開始的時候,可以不打開窗口,也可以打開一個或多個窗口,用戶可以用鼠標和鍵盤在窗口內與進程交互,比如打開QQ的時候和別人用鍵盤打字聊天。
還有一種情況是在大型的批處理系統中,在操作系統認為有資源可以運行另一個作業時,它創建一個新的進程,并運行其輸入隊列中的下一個作業。
我們知道系統調用的作用之一是控制進程。在Unix系統中,只有一個系統調用可以用來創建進程,即fork。而在Windows中則是用Win32函數調用CreateProcess來負責進程的創建和程序裝入進程的過程。除了CreateProcess,Win32還有大約100個其他的函數用于進程的管理。
2.1.3 進程的終止
有4種主要事件會導致進程的終止:
- 正常退出
- 出錯退出
- 嚴重錯誤
- 被其他進程殺死
正常退出就沒什么好說了,Unix用的是exit,Windows調用的是ExitProcess。
出錯退出一般還好說,如果用戶在Linux鍵入命令cc foo.c要編譯文件foo.c,但是該文件不存在,那么編譯器就會退出。
如果是嚴重錯誤,比如分母為0,數組越界,空指針異常這類錯誤,那么進程會收到信號然后中斷。
最后一種在linux很常見的就是kill命令,利用kill 進程號可以殺死一個進程,而在Win32用的則是TerminateProcess函數。
2.1.4 進程的層次結構
在前面,我們曾經提到父子進程這個名詞,那么什么是父子進程呢?
在Unix中,通過fork函數創建的新進程是原進程的子進程,而調用fork函數的進程是fork函數創建出來的新進程的父進程。也就是說,通過fork函數創建的新進程與原進程是父子關系,fork就相當于一個憑證,有fork,就有父子關系。
但是這也有一個問題,我們學過java的都知道,繼承的父類和子類共享屬性,那么對應到這里的父子進程是否也有共享資源的說法呢?
事實是,父進程和子進程的共享方式采用的是寫時復制,即兩個進程在讀資源的時候的確是共享,但是在寫資源的時候,寫資源的那個進程先把資源拷貝一份然后進行操作,操作完然后在覆蓋到原來的資源上,在這個過程中我們可以發現,可寫的內存時不可以被共享的。
經過上面的說明,我們可以大概知道這么個事,子進程是父進程創建出來的。父進程可以有多個子進程,但是子進程只可以有一個父進程。在Unix中,父進程和所有子進程組成了一個進程組,當一個信號傳入進程組,進程組的每個進程成員皆可以捕獲該信號,并且采取相應的動作。
但是在Windows則沒有這些說法,所有的進程地位都是相同的。
2.1.5 進程的狀態
進程之間時常要相互作用,如Linux命令:cat chapter1 | grep tree,這個命令啟動了兩個進程,一個是cat,它將chapter文件進行輸出;一個是grep,它在cat輸出的文件中去搜索含有tree的那些單詞。
從這個過程我們可以發現一件事,如果cat進程還沒好,grep進程就無法運行。當一個進程在邏輯上不能繼續運行時,他就會被阻塞。
經過上面的敘述,我們引入最簡單的三種狀態:
| 運行態 | 該時刻進程實際占用CPU |
| 就緒態 | 可運行,但是因為其他進程正在運行而暫時停止 |
| 阻塞態 | 除非某種外部事件發生,否則進程不能運行 |
對于就緒態來說,很多人可能會和阻塞態搞混;實際上,就緒態是萬事俱備只欠CPU,即資源都準備好了但是沒有CPU給它用,而阻塞態則像我們上面引入的例子,因為某個事情還沒做好而導致其處于一個等待(阻塞)狀態,這也是為什么有時候阻塞態被稱為等待態的原因。
特別典型的例子是C++中我們可以使用
system("pause");來讓該進程處于阻塞狀態。
轉換上圖的2和3是由進程調度程序引起的,進程調度程序是操作系統的一部分。實際上進程調度也被叫做低級調度,當系統認為一個運行進程占用處理器太久了,他就會讓其他進程去占用CPU,此時發生轉換2;當系統已經讓所有的程序都占用過CPU了,公平了,那么這個時候就會“重新洗牌”,第一個進程再次占有CPU,此時發生轉換3;當進程等待的一個外部事件發生時,則發生轉換4,當CPU此時空閑,則可以發生轉換3,該進程立刻運行,否則該進程將處于就緒態,等待CPU空閑。
2.1.6 進程的實現
為了實現進程模型,操作系統維護著一張表格,即進程表。每個進程都占有一條元組(數據庫的說法),每條元組即PCB(進程控制塊),該元組中包含了進程狀態的重要信息,包括程序計數器、堆棧指針、內存分配情況、所打開文件的狀態、賬號和調度信息以及其他在進程由運行態轉換為就緒態或阻塞態時必須保存的信息,從而保證該進程隨后能夠再次啟動。
一個進程在執行過程中可能被中斷數千次,但由于PCB的存在,即使中斷,也可以返回到發生中斷前完全相同的狀態。
2.2 線程
在很久以前還沒有引入進程之前,系統中的各個程序只能串行執行。比如你想要邊聽歌邊開QQ,這是不可能做到的,只能先做一件事再做一件事。
后來引入進程后,系統中的各個程序可以并發執行。也就是說,可以同時聽歌和開QQ。但是,即使引入了進程,也不能在QQ中同時視頻聊天和傳輸文件。這是因為操作系統每一次執行都是按照進程為單位來執行的。
從上面的例子來看,進程是程序的一次執行。但是這些功能顯然不可能是由一個程序順序處理就能實現的。
有的進程可能需要“同時做很多事”,而傳統的進程只能串行地執行一系列程序。為此,引入了線程來提高并發度。
在傳統中,進程是程序執行流的最小單位,也就是說,CPU每次執行任務,最少執行一個進程。而后在現在,CPU每次執行任務,最少執行一個線程,線程是進程的子集。也就是說,引入線程后,線程成為了程序執行流的最小單位。
綜上所述,我們可以把線程理解為“輕量級進程”。線程是一個基本的CPU執行單元,也是程序執行流的最小單位。引入線程之后,不僅是進程之間可以并發,進程內的各線程之間也可以并發,從而進一步提升了系統的并發度,使得一個進程內也可以并發處理各種任務(如QQ視頻、文字聊天、傳文件)。引入線程后,進程只作為除CPU之外的系統資源的分配單元(如打印機、內存地址空間等都是分配給進程的)。
2.2.1 進程的使用
如上所說,為了追求多功能的同時運行,輕量級進程顯得尤為重要,線程比進程更輕量級,它們比進程更容易創建,也更容易撤銷。
舉一個例子說明為什么線程更容易創建,也更容易撤銷。假如我們用電腦寫一本書,通常的做法是創建一個doc文件直接寫,這樣的話如果中間要查詢某個東西非常方便,你只需要用WPS自帶的查找或者Office的查找都可以完成這個工作,這就是一個進程內含有多個線程的現實模型。
但是如果你采用創建一個文件夾,一個文件夾內含有多個章節的文件,那么你每次要處理某個章節的一小段都需要完成打開文件,修改內容,關閉文件等一系列操作,十分麻煩。
綜上所述,我們可以總結如下:
| 傳統進程機制中,進程是資源分配、調度的基本單位 | 傳統進程機制中,只能進程間并發 | 傳統的進程間并發,需要切換進程的運行環境,系統開銷很大。 |
| 引入線程后,進程是資源分配的基本單位,線程是調度的基本單位 | 引入線程后,各線程間也能并發,提高了并發度 | 線程間并發,如果是同一進程內的線程切換,則不需要切換進程環境,系統開銷小,也就是說引入線程后,并發所帶來的系統開銷減小。 |
2.2.2 經典的線程模型
進程模型基于兩種獨立的概念:資源分組處理和執行。引入線程后,由于一個進程含有多個線程,所以功能的執行依賴于線程的切換,而不必使用開銷更大的進程切換,線程的切換即涉及到線程的調度;而對于多個線程來說,其使用的是同一個資源,程序所需的資源分配的基本單位是進程而不是線程,多個線程共享同一個資源。
在每個線程中,通常帶有一個程序計數器、寄存器和堆棧指針,這和進程是十分類似的。線程給進程模型增加的內容即同一個進程可以有多個線程,其切換我們在前面也提到過,CPU允許多線程切換納秒級完成。
和傳統進程一樣,線程也有進程所擁有的進程狀態。在Windows中線程的創建時通過調用庫函數thread_create創建的,調用庫函數thread_exit進行退出。
2.2.3 POSIX線程
為了實現可移植的線程程序,IEEE定義了線程的標準。它定義的線程包叫做pthread,大部分UNIX系統支持該標準。這個標準定義了超過60個函數調用。常見的幾個如下所示:
2.2.4 在用戶空間中實現線程
有兩種主要的方法實現線程包:在用戶空間中和內核中。
第一種方法是把整個線程包放在用戶空間中,內核對線程包一無所知。這樣的話就會出現一個問題,即使用戶開多條線程,內核還是以為只有一個進程,因為它并不知道進程內發生了啥,所以這時候就會出現單線程進程。即使多個線程處理機也是分配其中一個。
以上的情況比較明顯地體現是在Java中利用thread開啟多線程,多條線程的執行并不是并行地,而是并發地,你可以在兩條線程中各自打印一點東西,然后同時啟動你就能了解到效果了。
在用戶空間管理線程時,每個進程需要有其專用的線程表,用拉力跟蹤該進程中的線程。這些表和內核中的進程表類似,不過它們僅僅記錄各個線程的屬性,如每個線程的程序計數器、堆棧指針、寄存器和狀態等。
用戶級線程有一個優點是,它允許每個進程有自己定制的調度算法,并且其具有較好的擴展性,你可以多開幾條線程,但是在內核空間中萬一開多了線程是會出現問題的。
其另外一個優點是,在用戶級線程下,線程的切換開銷小,其無需切換為核心態;如果是內核級切換線程,需要陷入內核,開銷較大。
有一個問題我們前面提到,如果是在用戶空間下實行多線程,那么實際上處理器的占用取決于內核中有多少條線程。如果用戶空間里有6條線程,而內核中只有一條線程,那么實際處理線程時,處理器只會用到一個。也就是說,如果在用戶空間下實行多線程,那么一旦有一條線程阻塞,那么其他所有線程都將阻塞。
系統調用實際上是可以全部改成非阻塞的,但是這需要修改操作系統。還有一種替代方案是某個調用如果阻塞了就提前通知,但是這個處理方法需要重寫部分系統調用庫,所以也不太好,但是可惜的是,沒有第三種方法了。
用戶級線程包的最后一個問題是,如果一個線程開始運行,那么在該進程中的其他線程就不能運行,除非第一個線程自動放棄CPU,這和我們前面所講的是一樣的。需要知道的是,在一個單獨的進程內部是沒有時鐘中斷的,所以也就不可能以輪轉調度的方式調度線程。所以除非某個線程能夠按照自己的意志進行運行時系統,要不然調度程序是沒有任何機會的。
2.2.5 在內核中實現線程
如圖所示,此時如果是在內核中實現線程,那么不需要運行時系統了。內核實現線程的情況下,進程表和線程表都由內核控制。根據我們上一小節所說,內核控制的線程可以按照線程數來分配處理器,當一個線程阻塞時,內核會自動切換另外一個線程。
雖然使用內核線程可以解決阻塞等諸多問題,但也不是一勞永逸,內核級線程的管理工作由操作系統內核完成。線程調度、切換等工作都由內核負責,因此內核級線程的切換必然需要在核心態下才能完成,線程管理的成本高,開銷大。
2.2.6 混合實現
在前面,我們說的兩種情況如圖所示:
這兩個圖在有的書中也被叫做多對一模型和一對多模型。
但是既然這兩種模型各有各的缺點,為什么不聯合起來呢?將用戶級線程和某些內核線程多路復用起來,這在一些書上叫做多對多模型,如圖所示:
采用這種方法的特點是:內核還是只能識別內核級線程,這也就導致了即使用戶級線程再多,處理器的分配數量還是依照內核級線程來確定,但是不會再有阻塞問題,也不會再有內核線程開多出毛病的問題。
2.2.7 調度程序激活機制
盡管內核級線程在一些關鍵點上優于用戶級線程,但是內核級線程速度慢是硬傷。為了保持其優良特性并且改進其速度,研究人員研究出了調度程序激活機制。
調度程序激活機制的本質即:既然能在用戶空間中有這么大的便利,那我把內核的權限給你不就行了,用戶線程如果發出的系統調用是安全的,那么就行使內核賦予的權限去處理即可,如果實在不能處理,再去陷入內核交給內核去處理。這樣的話,由于避免了在用戶空間和內核空間之間的不必要轉換,從而提高了效率。
在2.2.4前面我們說到過,多對一模型中,由于只有一個內核級線程,所以一旦線程堵塞就完蛋了。這時候如果堵塞,內核就會通知該進程的運行時系統,并且在堆棧中以參數形式傳遞有問題的線程編號和所發生事件的一個描述。內核通過在一個已知的其實地址啟動運行時系統,從而發出了通知,這種機制被叫做上行調用。一旦如此激活,運行時系統就重新調度其線程。
在某個用戶線程運行的同時發生一個硬件中斷時,被中斷的CPU切換進內核態。如果該線程沒有什么大問題,在相關的事件發生后并處理完成,那么線程會通過與PCB同樣功能的TCB(線程控制塊)去重新啟動自己所在的線程。
總結
- 上一篇: ducker桌面版更改安装位置_Ubun
- 下一篇: win11系统下安装java 8的教程