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