天天看點

我所了解的線程和程序我所了解的線程和程序(一)

我所了解的線程和程序(一)

0x00 沒什麼卵用的前言

最近剛巧和朋友讨論起程序和線程的概念,于是突發奇想寫了這篇部落格,希望不依托于具體的程式設計語言和平台簡述程序和線程的概念,是以本篇部落格不讨論實作細節,僅從宏觀上探讨,預計本文分為兩篇,分别對程序和線程進行介紹。筆者才疏學淺,難免有疏漏之處,望大神指正。不論是發現錯誤、給出修改意見還是讨論相關問題,可以給我發郵件聯系,我的郵箱:[email protected]

0x01 追根溯源——為什麼會有程序和線程的概念

在遙遠的上古時代,計算機大神們在草稿紙上寫程式,拿大腦模拟運作來Debug,用打孔紙帶把程式輸入計算機,這個時候尚未出現作業系統,整台計算機由一個使用者獨占,輸入什麼指令就執行什麼指令,一切資源由一個人獨占,自然也就不存在程序線程的概念。

我所了解的線程和程式我所了解的線程和程式(一)

随着時間的推移,程式員越來越多,自然創造出的程式就越來越多,随着要運作的任務的增加,計算機運算速度的增加,原始的手工代碼輸入方式卻仍舊大的改變,于是暴露出一個巨大的問題:許多程式僅僅隻運作幾分鐘,将程式輸入計算機卻動辄需要十幾分鐘乃至半個小時,極大的降低了計算機的使用效率,這怎麼能成?大神腦海中閃過無數個點子,卻得等着操作員慢慢地把寫好的程式輸入計算機,偉大的設計無法實作,這簡直是叫人痛不欲生的折磨,于是大神們想出來一個初級的解決方案:批處理系統。

大神們把寫好的程式依次輸入到計算機,儲存在錄音帶上(這個年代還沒有磁盤),當一個程式運作完計算機就自動的把錄音帶中的下一個程式加載入記憶體,這樣就省去了輸入程式的時間,提高了計算機的工作效率,當一盤錄音帶的程式運作完隻需要換一卷錄音帶即可。這樣許許多多的程式就一批接一批地不斷運作,于是起名為“批處理系統” ,windows中的cmd腳本翻譯為批處理也有這方面的原因。

現在來看把這個簡陋的東西稱作“系統”是不合實際的,因為所謂的系統并沒有現代作業系統的特征,僅僅是負責把磁盤中的程式依次裝入記憶體,但已經稱得上是極大的進步,是以我們才說現代作業系統的雛形在此時出現了。

到了60年代,計算機技術進一步發展,運作速度和操作方式都有了不小的進步,CPU可以指令他的輔助處理器來進行輸入輸出這種低端的操作,集中精力于程式的運作,但程式通常總得等待速度相對較慢的輔助處理器(通道)把資料從錄音帶讀入,錄音帶已經很慢了,更要命的是等待使用者的輸入,這就同樣浪費了寶貴的CPU資源,讓CPU長期閑置,處在一種無事可做的狀态。

CPU資源的浪費是一種不可饒恕的罪過,特别是那個計算機極其昂貴的年代,為了剝削可憐的計算機,于是大神們更新了批處理系統,開發出一種能夠更充分利用計算機資源的作業系統:多道批處理系統。

多道批處理系統同時在記憶體中放置多個程式,當一個程式等待較緩慢的輸入輸出操作執行完畢時,系統自動讓另一個程式運作,這樣就使得CPU可以幾乎不用等待地全速運轉(同時讓電表全速運轉),又一次極大的提升了效率。而到了多道批處理的時代,作業系統才算是得到了真正的完善,有了點現代作業系統的樣子。也正是這個時代開始有了早期程序的概念。

這時的程序基本和程式相對應,一個程式即是一個程序,擁有自己的代碼和資料。

在此之後進一步發展出了分時作業系統、實時作業系統等等諸多種類,而現代作業系統多數是曆史上種種作業系統類型加以融合的産物,而現代作業系統為了進一步提高運作效率,友善程式開發又提出了線程的概念,進一步縮小了作業系統和程式員控制程式執行的粒度。

是以總的來說,程序線程的發展是伴随作業系統發展而發展的,作業系統的設計又決定于當時時代計算機的性能,是以說作業系統發展史就是程式員們剝削計算機,榨幹計算機性能的計算機的苦難史和鬥争史。

0x02 程序初探——“程序”是個啥

在1978年的全國作業系統會議上,經讨論,老一輩的計算機專家給出了這樣的中文定義:

“程序是一個具有一定獨立功能的程式關于某個資料集合的一次運作活動”

看懂了吧,好我們繼續——個屁嘞!你看懂了了解了就已經是大神了,請立即點選右上角的小叉來節約寶貴的時間,沒看懂才是正常的,這是從計算機科學角度給出的一個極其抽象的嚴謹定義,要了解這個概念最好還是先執行個體化了,把玩一番再傳回頭品味。

我所了解的線程和程式我所了解的線程和程式(一)

之前我們說到程序的概念是伴随着多道批處理系統的産生而産生的,在當時來看是與程式相對應的概念,一個程式即是一個程序,一個程式包括兩部分——代碼(指令)和資料,那麼相應的,一個程序也就應該包含兩部分,一是代碼(指令),二是資料。是以基本上我們可以認為程序就是磁盤中程式載入記憶體的一個副本。

我們之前說,之是以發明程序,是為了在一個程序等待一個占用大量時間而不需要CPU參與的工作時,可以讓CPU執行其他工作來提高效率,是以相應的程序就有了兩種狀态——幹活和等待,不需要CPU時程序無聊的在一邊等資料,資料來了就繼續幹活,這就是程序的“兩種狀态模型”——即運作态和未運作态,類比一下就是你每搬了一天磚就得睡一覺休息休息。

當你打開任務管理器時,總會感覺記憶體不夠用,而你肯定不希望已經運作完了的程式繼續待在你寶貴的記憶體裡,占用着你的DDR4黃金等價的記憶體。當年設計多道批處理系統的設計師們同樣想到了這個問題,當年的記憶體比今天可是貴了不止一星半點。

于是:當一個程式被載入記憶體時建立一個相對應的程序,而當程式運作完畢停止,就應該釋放掉它所占用的空間,把這個程序從記憶體中清理掉,留下空間給其他要運作的程式使用,這就有了程序的建立和退出狀态,就好比你來一個公司應聘被錄取,辦理入職手續熟悉工作環境的狀态和被開除了搬東西滾蛋的狀态。

而另一方面,并不是程序等待的資料來了程序就可以馬上繼續運作,記憶體中同時存在着多個程序,可能當一個程序可以繼續運作時另一個程序正在幹的熱火朝天停不下來,怎樣排程CPU完全取決于作業系統心情(設計),是以你的程序很有可能還得等目前程序幹完了手頭的活才能繼續,這個能運作了的等待我們就稱它為:“就緒态”,而把不能運作等資料的等待稱為“阻塞态”,當等了許久好不容易被系統準許運作,程序繼續幹活的狀态就稱為“運作态”。

再打個比方:你好不容易完成了入職,老闆給你分派了工作你開始幹活,這就是“運作态”,幹了幾天你說得找項目經理問問客戶需求的細節,老闆說項目經理不在你等着吧,于是你工作沒法進行得等項目經理回來,得到了一段偷懶的時間,這時候你就處于“阻塞态”,項目經理回來了你問完了問題,經理說哎呀不巧上午你沒啥事,你工位電腦就借給隔壁小王用了,你再等等吧,這時候你能幹活了但經理讓别人先用着你的電腦,這段閑着的時間就是“就緒态”。

我所了解的線程和程式我所了解的線程和程式(一)

以上這建立、退出、就緒、運作、阻塞五種狀态就構成了程序的“五狀态模型”,至于為什麼就緒排在運作前面呢,因為一個程序建立好後往往正有别的程序正在運作,是以建立的程序總得在就緒态等一會兒。

如果你既喜歡chrome又沒有常關網頁的習慣,你會發現chrome總有辦法用光你的記憶體(chrome每打開一個網頁就建立一個程序),但事實上你不會同時看所有網頁,很多網頁你甚至不會再看僅僅是忘了關而已,但這些網頁的程序缺占用着你寶貴的記憶體,俗稱占着茅坑不拉屎。

現代作業系統為了解決這個問題,大多實作了虛拟記憶體機制,windows下有記憶體分頁檔案,Linux下有swap交換分區,這都是虛拟記憶體占用的磁盤空間,原理就是當一個程序(或程序的一個部分)一直用不到又沒被關閉的時候,就把這個程序從記憶體調到磁盤中,騰出記憶體空間供一直在忙碌的程序使用,而一旦被調到磁盤中,程序想繼續執行還得重新載入記憶體,程序位于磁盤中的狀态就稱為“挂起态”。

假設公司有幾台比較快的新電腦和一些卡的要命的老古董,經理發現你天天占着快的電腦卻啥都不幹,就把你安排到了老古董機器前,把好電腦讓給一直在忙着幹活的員工,不過值得注意的一點是:磁盤中被挂起的程序往往還能被調回記憶體,但總是無所事事而被“挂起”的員工往往不會有好下場。

但通常你之是以成為一個被挂起的員工往往是有原因的,要麼你是在等BOSS對你新想法準許的會議進行,你想工作卻不得不等你的老闆舉行會議,這樣你就處在“阻塞/挂起态”,要麼就是你原來工位上的哥們占着地盤不走,你又不好意思轟走他,就不得不等他忙完,那這你就處在“就緒/挂起态”。

對于計算機中的程序而言,當程序因為等待一個事件(比如IO讀寫)而被挂起就進入了“阻塞/挂起态”,而當程序等待的事件已經發生,載入記憶體就能運作,卻因為種種原因無法被立刻載入記憶體時,程序處在的狀态就是“就緒/挂起态”,這樣我們就完成了挂起态的介紹,進一步完善了之前的“五狀态程序模型”。

我所了解的線程和程式我所了解的線程和程式(一)

添加了挂起态的五狀态模型也正是現代作業系統廣泛采用的程序狀态模型,但各個作業系統有不同的實作,對五狀态模型也有不同的增改,初步學習程序了解添加了挂起态的五狀态模型就足以了。

0x03 程序控制——作業系統的工作

為了控制各個程序,對各個程序進行排程,作業系統需要一個特殊的資料結構——程序控制塊(可以了解為一個隻有成員變量沒有方法的類),這個資料結構中包含了諸多變量來描述一個程序的相關資訊,比如存放在記憶體中的位置、程序辨別資訊、處理器狀态資訊和程序控制資訊等。

程序控制塊是作業系統中最重要的資料結構,沒有之一,不接受反駁。作業系統對程序的一切控制都是基于程序控制塊進行的。

玩遊戲的時候總會遇到開挂的神仙,外挂本身也是一個程式,有自己的程序,按道理外挂程序不應該幹涉到遊戲程序,但外挂通過利用系統漏洞等方式,更改了遊戲程序中的資料和代碼,于是就實作了諸如無敵一類的操作,外挂玩家令人不齒,更叫人頭疼的是種種黑客軟體,往往被用于非法途徑以牟利。

為了避免一個程序通路其他程序的記憶體空間或是作業系統的記憶體空間,提高系統安全性,CPU廠商給CPU的種種指令進行了分級,部分會危害到作業系統和其他程序安全的指令被劃為特權指令,僅有作業系統可以執行,而另外的指令才是使用者程序可以直接執行的指令。要判斷究竟是誰在執行指令就要看執行指令的程序的狀态,以此分為使用者态和核心态(有時也成為系統态),特權指令隻有處在核心态才可以執行,進而提高了計算機的安全性。

當一個使用者程序要用的核心态才能執行的特權指令時,必須通過系統調用,把控制權交給作業系統,由作業系統先對指令安全性進行檢查,再再核心态下執行,執行完畢後把控制權再交還給使用者程序,這個過程無疑增大了開銷但大大提高了安全性,總體來看還是利大于弊的。

作業系統控制程序所做的最多的工作是程序的建立和切換。程序建立時,作業系統将為程序對應程式的代碼和資料配置設定記憶體空間,并載入記憶體,配置設定程序辨別符(程序ID),建立相應的程序控制塊,這個時候程序就處于建立态。運作中的程序常常遇到程序切換和模式切換,這兩種切換同樣是由系統進行排程的。

程序切換往往因為如下原因發生:一是時鐘中斷,當一個程序用完了系統分派給他的CPU時間片(時間片,即程序在中斷前可以執行的最大CPU時間段),那麼此時該程序就會暫時中斷,CPU控制權由作業系統交給下一個程序;二是I/O中斷,如果發生一個I/O活動,作業系統會檢查是否有程序正在等待這個事件并決定是否中斷正在運作的程序,把控制權交給等待這個事件的程序,這一點通常由程序優先級決定;三是記憶體失效,如果一個程序的一部分記憶體塊被置換到了虛拟記憶體(即磁盤)中,那當程序通路這塊不存在于記憶體中的資料時就會引起一個記憶體中斷,由作業系統先把這部分記憶體塊從磁盤加載到記憶體中,再繼續執行被中斷的程序。

程序切換時需要儲存目前處理器的上下文狀态(即寄存器資料),更新目前程序的程序控制塊,選擇下一個執行的程序并更新其程序控制塊,恢複該程序的處理器上下文,這個過程涉及了許多額外工作,開銷較大。而模式切換隻在由使用者态切換至核心态時發生,僅僅涉及程序運作狀态的更改,隻需更改記憶體控制塊和儲存恢複處理器上下文,因而開銷較小。

還有值得注意的一點是,作業系統同樣是由處理器執行的一個程式,與一般的使用者程式并沒有多大的不同,如果抽象的來看,作業系統不過是一個擁有極大特權的用于管理其他程序的程序。

繼續閱讀