1. 程式人生 > >碼農翻身講作業系統2:程序,執行緒與作業系統那些事

碼農翻身講作業系統2:程序,執行緒與作業系統那些事

我聽說我的祖先們生活在專用計算機裡, 一生只幫助人類做一件事情,比說微積分運算 了、人口統計了 、生成密碼、甚至通過織布機印花 !

如果你想在這些專用“計算機”上乾點別的事兒,例如安裝個遊戲玩玩, 那是絕對不可能的, 除非你把它拆掉, 然後建一個全新的機器。 而我這些祖先們勉強可以稱為“程式”。

後來有個叫馮諾依曼的人, 非常了不起, 他提出了“儲存程式”的思想, 並且把計算機分為五大部件: 運算器、控制器、儲存器、輸入裝置、輸出裝置。

各種各樣不同功能的程式寫好以後,和程式使用的資料一起存放在計算機的儲存器中,即“儲存程式”;然後,計算機按照儲存的程式逐條取出指令加以分析,並執行指令所規定的操作。

這樣一來,原來的專用計算機變成了通用的計算機,不管你是計算導彈彈道的,模擬核爆炸的,還是計算個人所得稅的, 統統都可以在一臺機器上執行, 我就是其中的一員: 專門計算員工的薪水。

程序的誕生

我所在的計算機是個批處理系統, 每次上機時, 我和其他程式都排好隊, 一個接一個的進入記憶體執行。

每個月末是發薪日, 我都要執行一次,這樣我每月都能見一次CPU阿甘, 這個沉默寡言,但是跑的非常快的傢伙。

我知道記憶體看阿甘不順眼,還告了它一狀,說他一遇到IO操作的時候,就歇著喝茶 ,從來不管不問記憶體和硬碟的忙的要死的慘境。 
(碼農翻身注:參見《CPU阿甘》和《CPU阿甘之煩惱》)

其實我倒是覺得挺好, 這時候正好和阿甘海闊天空的聊天,他閱程式無數, 知道很多內部訊息,每一個位元組都清清楚楚, 和他聊天實在是爽。

又到了月末發薪水的時候, 我剛一進入記憶體,便看到這麼一個公告:

公告 
為了建立和諧社會,促進效率和公平, 充分發揮每一個人的能力,經系統黨委慎重研究決定:本系統自即日起,正式從“批處理系統”轉為“多道程式系統”, 希望各部門通力配合,一起完成切換工作。 
系統黨委 
xxxx年xx月xx日

我正想著啥是多道程式系統, 阿甘便打電話給記憶體要我的指令開始運行了。

和之前一樣,執行到了第13869123行,這是個IO指令, 我歡天喜地的準備和阿甘開聊了。

阿甘說: 哥們, 準備儲存現場吧, 我要切換到另外一個程式來執行啦!

“啊 ? 我這正執行著呢! 咱們不喝茶了?

“喝啥茶啊,馬上另外一個程式就來了!”

” 那我什麼時候回來再見你?” 我問道。

“等這個IO指令完成,然後作業系統老大會再給你機會執行的。”

“那誰來記住我當前正在執行第13869123行? 還有剛把兩個資料從記憶體裝載到了你的暫存器,就是那個EAX, EBX , 你一切換豈不都丟了? ” 我有點著急。

阿甘說:“所以要暫時儲存起來啊, 不僅僅是這些,還有你的那些函式在呼叫過程中形成的棧幀和棧頂, 我這裡用暫存器EBP和ESP維護著, 都得儲存起來。” 
(碼農翻身注:參見《CPU阿甘之函式呼叫的祕密》)

“還有” 阿甘接著說,“你開啟的檔案控制代碼,你的程式段和資料段的地址, 你已經使用CPU的時間,等待CPU的時間。。。。。。 以及其他好多好多的東西,統統都要儲存下來。”

我瞪大了眼睛: “這也太麻煩了吧, 原來我只需要關心我的指令和資料, 現在還得整這麼多稀奇古怪的東西”

“沒辦法,這就叫做上下文切換, 把你的工作現場儲存好,這樣下一次執行的時候才能恢復啊。 對了,老大給你們統一起了一個新的名稱: 程序 ! 剛才那些需要儲存的東西叫做叫做 程序控制塊 (Processing Control Block, PCB ),”

我想了想,這個名字還挺貼切的, 一個真正進行的程式! 只是這個正在進行的程式隨時可以被打斷啊。

我只好儲存好上下文,撤出CPU, 回到記憶體裡歇著去了,與此同時另外一個程式開始佔據CPU執行。

其實我這個程式,奧,不對,我這個程序被放到一個阻塞佇列裡,等到IO的資料來了以後,又被趕到了就緒佇列中, 最後才有機會再次執行,再次見到CPU阿甘。 
(碼農翻身: 程序的就緒, 阻塞,執行這三個狀態的轉換和《我是一個執行緒》中描述的非常類似)

阿甘從我的PCB中取出各種儲存的資訊,恢復了執行時現場, 可是忙活了好一陣, 沒辦法,這就是程式切換必須要付出的代價。

我有點同情阿甘了, 從此以後,他很難再悠閒和和我們海闊天空,每時每刻都處於高速的奔跑中。

得益於阿甘的高速度, 雖然在同一時刻只有一個程式在執行, 但是有很多程式在短時間內不斷的切換, 在外界看來, 似乎多個程式在同時執行。

尤其是那些速度超慢的人類, 他們開著電腦一邊聽歌,一邊上網,一邊QQ, 很是自在, 理所當然的認為這些程式就是同時在執行。 豈不知阿甘是讓音樂播放器上執行幾十毫秒, 然後打斷,讓瀏覽器程序執行幾十毫秒,再打斷,讓QQ也執行幾十毫秒,如此迴圈往復。

唉,阿甘真是能者多勞啊, 這個計算機系統也算是達到了我們黨委的目標:兼顧了效率和公平。

執行緒

有了程序就萬事大吉了嗎? 人類的慾望是無止境的,很快就出現了新情況, 舉個例子來說吧,我有一個兄弟,是個文書處理軟體, 他和我不一樣, 他有介面, 人類在用的時候能看到, 這實在是很幸福, 不像我總是在背後默默工作,幾乎無人知曉。

這哥們有個智慧的小功能,就是在人類編輯文件的時候能自動儲存, 防止辛辛苦苦敲的文字由於斷電什麼的丟掉。

可是這個功能導致了人類的抱怨, 原因很簡單,自動儲存文字是和IO打交道,那硬碟有多慢你也知道, 這個時候整個程序就被掛起了, 給人類的感覺就是: 程式死了,鍵盤和滑鼠不響應了! 無法繼續輸入文字, 但是過一會兒就好了。

並且這種假死一會兒就會出現一次(每當自動儲存的時候), 讓人不勝其煩。

系統黨委研究了很久, 他們當然可以用兩個程序來解決問題, 一個程序負責和使用者互動, 另外一個程序負責自動儲存, 但是,這兩個程序之間完全是獨立的,每個人都有自己的一畝三分地(地址空間), 完全互不知曉, 程序之間通訊的開銷實在是太大, 他們沒有辦法高效的操作那同一份文件資料。

後來還是勞模阿甘想出了一招 : 可以採用多程序的偉大思想啊!

把一個程序當成一個資源的容器, 讓裡邊執行幾個輕量級的程序, 就叫執行緒吧, 這些執行緒共享程序的所有資源, 例如地址空間,全域性變數,檔案資源等等。

但是每個執行緒也有自己獨特的部分, 那就是要記住自己執行到哪一行指令了, 有自己的函式呼叫堆疊,自己的狀態等等, 總而言之,就是為了能像切換程序那樣切換執行緒。

這裡寫圖片描述

拿我那個哥們的情況來說, 一個程序儲存著文件的資料, 程序中有兩個執行緒, 一個負責和使用者互動, 另外一個專門負責定時的自動儲存, IO導致的阻塞就不會影響另外一個了。

注意,這兩個執行緒都能訪問程序的所有東西, 他們兩個要小心,不要發起衝突才好 – 這是人類程式設計師要做的事情了, 不歸我們管。

爭吵

阿甘的建議被採納了, 其實這幾乎是唯一的解決問題方式了, 但是由誰來管理引起了激烈爭吵。

系統黨委有一波人堅持要在使用者空間實現執行緒, 換通俗的話說就是讓那些程序在自個兒內部去管理執行緒, 他們的理由也很充分: 
你們自己實現了執行緒,可以自己定製自己的排程演算法,多靈活啊;

所有的執行緒切換都在程序內完成, 不用請求我們作業系統核心來處理,效率多高啊;

況且你們可以在那些核心不支援執行緒的作業系統中執行, 移植性多好啊。

這裡寫圖片描述

我們清楚的知道這是核心想做甩手掌櫃, 因為他們選擇性的忽略了一個致命的問題: 如果由我們實現執行緒,則作業系統核心還是認為我們只是一個程序而已,對裡邊的執行緒一無所知,對程序的排程還是以程序為最小單位。

一旦出現阻塞的系統呼叫,不僅僅阻塞那個執行緒,還會阻塞整個程序!

例如文書處理器那個程序, 如果負責定時儲存的執行緒發起了IO呼叫, 核心會認為,這是由程序發起的,於是就把整個程序給掛起了, 雖然和使用者互動的程序還是可以執行,也被強制的隨著程序掛起來,不響應了, 這多麼悲催啊, 又回到了老問題上去了。

所以我們堅決不能答應, 我們則一致的要求:在核心中實現執行緒 ! 核心需要知道程序中執行緒的存在,核心需要維護執行緒表,並且負責排程!

這裡寫圖片描述

黨委的人傲慢的說: 你們不嫌累嗎, 每次建立一個執行緒都得通過我們核心, 多慢啊。

我們說:只有這樣,一個執行緒的IO系統呼叫才不會阻塞我們整個程序啊, 你們完全可以選擇同一個程序的另外一個執行緒去執行。

雙發僵持不下,最後只好妥協, 那就是:混合著實現吧。

使用者空間的程序可以建立執行緒(使用者執行緒), 核心也會建立執行緒(核心執行緒), 使用者執行緒對映到核心執行緒上。

這裡寫圖片描述

問題基本解決了,但也帶來了新的問題,我們的系統也變的越來越複雜, 尤其是程序之間的通訊和執行緒之間的同步, 會那些程式設計師們帶來無窮無盡的煩惱, 這是後話了, 有機會下次再說吧。

注: 本文的插圖來源於《現代作業系統》和《作業系統概念》(恐龍書)這兩本書, 我重畫了一下。 對作業系統感興趣的同學可以看看這兩本書。

(完)


一個執行緒的一生

  我是一個執行緒,我一出生就被編了個號: 0×3704,然後被領到一個昏暗的屋子裡, 這裡我發現了很多和我一模一樣的同伴。

  我身邊的同伴0×6900 待的時間比較長, 他帶著滄桑的口氣對我說:

  “我們執行緒的宿命就是處理包裹。把包裹處理完以後還得馬上回到這裡,否則可能永遠回不來了。”

  我一臉懵懂,包裹,什麼包裹?

  “不要著急,馬上你就會明白了, 我們這裡是不養閒人的。”

  果然,沒多久,屋子的門開了, 一個面貌凶惡的傢伙吼道:

  “0×3704 ,出來!”

  我一出來就被塞了一個沉甸甸的包裹,上面還有附帶著一個寫滿了操作步驟的紙。

  “快去,把這個包裹處理了。”

  “去哪兒處理?”

  “跟著指示走, 先到就緒車間”

  果然,地上有指示箭頭,跟著它來到了一間明亮的大屋子,這裡已經有不少執行緒了, 大家都很緊張,好像時刻準備著往前衝。

  我剛一進來,就聽見廣播說:“0×3704,進入車間”

  我趕緊往前走, 身後很多人議論說:

  ”他太幸運了, 剛進入就緒狀態就能執行”

  “是不是有關係?”

  “不是,你看人家的優先順序多高啊, 唉~”

  前邊就是車間, 這裡簡直是太美了, 怪不得老執行緒總是嘮叨著說:要是能一直待在這裡就好了。

  這裡空間大,視野好,空氣清新,鳥語花香,還有很多從來沒見過的人,像服務員一樣等著為我服務。

  他們也都有編號, 更重要的是每個人還有個標籤,上面寫著:硬碟,資料庫,記憶體,網絡卡...

  我現在理解不了,看看操作步驟吧:

  第一步:從包裹中取出引數

  開啟包裹, 裡邊有個 HttpRequest 物件,可以取到 userName, password 兩個引數。

  第二步:執行登入操作

  奧,原來是有人要登入啊,我把 userName/password 交給資料庫服務員,他拿著資料, 慢騰騰的走了。

  他怎麼這麼慢?不過我是不是正好可以在車間裡多待一會兒? 反正也沒法執行第三步。

  就在這時,車間裡的廣播響了: 

  “0×3704,我是 CPU,記住你正在執行的步驟,馬上帶包裹離開”

  我慢騰騰的開始收拾。。。

  “快點, 別的執行緒馬上就要進來了”

  離開這個車間, 又來到一個大屋子,這裡很多執行緒慢騰騰的在喝茶,打牌。

  “哥們,你們沒事幹了?”

  “你新來的吧,你不知道我在等資料庫服務員給我資料啊,據說他們比我們慢好幾十萬倍, 在這裡好好歇吧”

  “啊? 這麼慢? 我這裡有人在登入系統, 能等這麼長時間嗎”

  “放心,你沒聽說過人間一天,CPU 一年嗎, 我們這裡是用納秒,毫秒計時的,人間等待一秒,相當於我們好幾天呢,來的及”

  乾脆睡一會吧 , 不知道過了多久 ,大喇叭又開始廣播了:

  “0×3704, 你的資料來了,快去執行”

  我轉身就往 CPU 車間跑,發現這裡的們只出不進!

  後面傳來陣陣鬨笑聲:

  “果然是新人,不知道還得去就緒車間等”

  於是趕緊到就緒車間,這次沒有那麼好運了,等了好久才被再次叫進 CPU 車間。

  在等待的時候, 我聽見有人小聲議論:

  “聽說了嗎,最近有個執行緒被 kill 掉了”

  “為啥啊?”

  “這傢伙賴在 CPU 車間不走,把 CPU 利用率一直搞成 100%,後來就被 kill 掉了”

  “Kill 掉以後弄哪兒去了”

  “可能被垃圾回收了吧”

  我心裡打了個寒噤 , 趕緊接著處理,收下的動作塊多了,第二步登入成功了。

  第三步:構建登入成功後的主頁

  這一步有點費時間, 因為有很多 HTML 需要處理, 不知道程式碼誰寫的,處理起來很煩人。

  我正在緊張的製作 HTM 呢, CPU 有開始叫了:

  “0×3704,我是 CPU,記住你正在執行的步驟,馬上帶包裹離開”

  “為啥啊”

  “每個執行緒只能在 CPU 上執行一段時間,到了時間就得讓別人用了,你去就緒車間待著, 等著叫你吧”

  就這樣, 我一直在“就緒-執行”這兩個狀態,不知道輪轉了多少次,終於安裝步驟清單把工作做完了。

  最後順利的把包含 HTML 的包裹發了回去。

  至於登入以後幹什麼事兒 ,我就不管了。

  馬上就要回到我那昏暗的房間了,真有點捨不得這裡。

  不過相對於有些執行緒, 我還是幸運的, 他們執行完以後就徹底的銷燬了,而我還活著!

  回到了小黑屋, 老執行緒0×6900 問:

  “怎麼樣?第一天有什麼感覺?”

  “我們的世界規則很複雜,首先你不知道什麼時候會被挑中執行;第二,在執行的過程中隨時可能被打斷,讓出 CPU 車間;第三,一旦出現硬碟,資料庫這樣耗時的操作也得讓出 CPU,去等待;第四,就是資料來了,你也不一定馬上執行,還得等著 CPU 挑選”

  “小夥子理解的不錯啊”

  “我不明白為什麼很多執行緒都執行完就死了, 為什麼咱們還活著?”

  “你還不知道,長生不老是我們的特權,我們這裡有個正式的名稱,叫做執行緒池!”

  平淡的日子就這麼一天天過去,作為一個執行緒,我每天的生活都是取包裹,處理包裹,然後回到我們昏暗的家:執行緒池。

  有一天我回來的時候,聽到有個兄弟說,今天要好好休息下,明天就是最瘋狂的一天。

  我看了一眼日曆,明天是 11 月 11 號。

  果然,零點剛過,不知道那些人類怎麼了,瘋狂的投遞包裹,為了應付蜂擁而至的海量包裹,執行緒池裡沒有一個人能閒下來,全部出去處理包裹,CPU 車間利用率超高,硬碟在嗡嗡轉,網絡卡瘋狂的閃,即便如此,還是處理不完,堆積如山。

  我們也沒有辦法,實在是太多太多了,這些包裹中大部分都是瀏覽頁面,下訂單,買,買,買。

  不知道過了多久,包裹山終於慢慢的消失了。

  終於能夠喘口氣, 我想我永遠都不會忘記這一天。

  通過這個事件,我明白了我所處的世界:這是一個電子商務的網站!

  我每天的工作就是處理使用者的登入,瀏覽, 購物車,下單,付款。

  我問執行緒池的元老0×6900:“我們要工作到什麼時候?”

  “要一直等到系統重啟的那一刻”,0×6900 說。

  “那你經歷過系統重啟嗎?”

  “怎麼可能?系統重啟就是我們的死亡時刻, 也就是世界末日,一旦重啟,整個執行緒池全部銷燬,時間和空間全部消失,一切從頭再來”

  “那什麼時候會重啟?”

  “這就不好說了,好好享受眼前的生活吧…..”

  其實生活豐富多彩,我最喜歡的包裹是上傳圖片,由於網路慢,所以能在就緒車間,CPU 車間待很長很長時間,可以認識很多好玩的執行緒。

  比如說上次認識了 memecached 執行緒,他給我說通過他快取了很多的使用者資料, 還是分散式的! 很多機器上都有!

  我說怪不得後來的登入操作快了那麼多, 原來是不再從資料庫取資料了你那裡就有啊,哎,對了,你是分散式的,你去過別的機器沒有?

  他說怎麼可能,我每次也只能通過網路往那個機器傳送一個 GET, PUT 命令才存取資料而已,別的一概不知。

  再比如說上次在等待的時候遇到了資料庫連線的執行緒,我才知道它他那裡也是一個連線池,和我們執行緒池幾乎一模一樣。

  他說有些包裹太變態了,竟然檢視一年的訂單資料,簡直把我累死了。

  我說拉倒吧你,你那是純資料,你把資料傳給我以後,我還得組裝成 HTML,工作量不知道比你大多少倍。

  他說一定你要和 memecached 搞好關係,直接從他那兒拿資料,儘量少直接呼叫資料庫,我們 JDBC connection 也能活的輕鬆點。

  我說好啊好啊,關鍵是你得提前把資料搞到快取啊,要不然我先問一遍快取,沒有資料,我這不還得找你嗎?

  生活就是這樣,如果你自己不找點樂子,還有什麼意思?

  有一天我遇到一個可怕的事情, 差一點死在外邊,回不了執行緒池了……

  其實這次遇險我應該能夠預想到才對, 太大意了。

  前幾天我處理過一些從 http 發來的存款和取款的包裹,老執行緒0×6900 特意囑咐我:

  “處理這些包裹的時候要特別小心,你得一定要先獲得一把鎖,在對賬戶存款或者取款的時候一定要把賬戶給鎖住,要不然別的執行緒就會在你等待的時候趁虛而入,搞破壞,我年輕那會兒很毛糙,就捅了簍子”

  為了“恐嚇”我,好心的0×6900 還給了我兩個表格:

  1、沒有加鎖的情況  

  2、加鎖的情況  

  我看的膽顫心驚, 原來不加鎖會帶來這麼嚴重的事故。

  從此以後看到存款,取款的包裹就倍加小心,還好,沒有出過事故。

  今天我收到的一個包裹是轉賬,從某著名演員的賬號給某著名導演賺錢,具體是誰我就不透漏了,數額可真是不小。

  我按照老執行緒的吩咐,肯定要加鎖啊,先對著名演員賬號加鎖,在對著名導演賬號加鎖。

  可我萬萬沒想到的是,還有一個執行緒,對,就是0×7954,竟然同時在從這個導演到往這個演員轉賬。

  於是乎,就出現了這麼個情況:

  

  剛開始我還不知道什麼情況,一直坐在等待車間傻等,可是等的時間太長了,長達幾十秒!我可從來沒有經歷過這樣的事件。

  這時候我就看到了執行緒0×7954,他悠閒的坐在那裡喝咖啡,我和他聊了起來:

  “哥們,我看你已經喝了 8 杯咖啡了,怎麼還不去幹活?”

  “你不喝了 9 杯茶了嗎?” 0×7954 回敬到。

  “我在等一個鎖, 不知道哪個孫子一直不釋放”

  “我也在等鎖啊,我要是知道哪個孫子不釋放鎖我非揍死他不可 ” 0×7954 毫不示弱。

  我偷偷的看了一眼,這傢伙懷裡不就抱著我正在等的某導演的鎖嘛?

  很明顯,0×7954 也發現了我正抱著他正在等待的鎖。

  很快我們兩個就吵了起來,互不相讓:

  “把你的鎖先給我,讓我先做完”

  “不行,從來都是做完工作才釋放鎖,現在絕對不能給你”

  從爭吵到打起來,就那麼幾秒鐘的事兒。

  更重要的是,我們倆不僅僅持有這個著名導演和演員的鎖,還有很多其他的鎖,導致等待的執行緒越來越多,圍觀的人們把屋子都擠滿了。

  最後事情真的鬧大了,我從來沒見過的終極大 Boss“作業系統”也來了。

  大 Boss 畢竟是見多識廣,他看了一眼,哼了一聲,很不屑的說:

  “又出現死鎖了”

  “你們倆要 Kill 掉一個, 來吧,過來抽籤”

  這一下子把我給嚇尿了,這麼嚴重啊!

  我戰戰兢兢的抽了籤,開啟一看,是個”活”字。

  唉,小命終於保住了。

  可憐的0×7954 被迫交出了所有的資源以後,很不幸的被 kill 掉,消失了。

  我拿到了導演的鎖,可以開始幹活了。

  大 Boss 作業系統如一陣風似的消失了,身後只傳來他的聲音:

  “記住, 我們這裡導演>演員,無論認識情況都要先獲得導演的鎖”

  由於不僅僅是隻有導演和演員,還有很多其他人,Boss 留下了一個表格,裡邊是個演算法,用來計算資源的大小,計算出來以後,永遠按照從大到小的方式來獲得鎖:

  我回到執行緒池,大家都知道了我的歷險,圍著我問個不停。

  凶神惡煞的執行緒排程員把大 Boss 的演算法貼到了牆上。

  每天早上,我們都得像無節操的房屋中介,美容美髮店的服務員一樣,站在門口,像被耍猴一樣大聲背誦:

  “多個資源加鎖要牢記,一定要按 Boss 的演算法比大小,然後從最大的開始加鎖”

  又過了很多天,我和其他執行緒們發現了一個奇怪的事情:包裹的處理越來越簡單。不管任何包裹,不管是登入, 瀏覽,存錢….. 處理的步驟都是一樣的,返回一個固定的 html 頁面。

  有一次我偷偷的看了一眼,上面寫著:

  “本系統將於今晚 00:00 至 4:00 進行維護升級, 給你帶來的不便我們深感抱歉”

  我去告訴了老執行緒0×6904,他嘆了一口氣說:

  “唉,我們的生命也到頭了,看來馬上就要重啟系統,我們就要消失了,再見吧兄弟。”

  系統重啟的那一刻終於到來了。

  我看到屋子裡的東西一個個的不見了,等待車間,就緒車間,甚至 CPU 車間都慢慢的消失了。

  我身邊的執行緒兄弟也越來越少,最後只剩我自己了。

  我在空曠的原野上大喊:還有人嗎?

  無人應答。

  我們這一代執行緒池完成了使命。

  下一代執行緒池將很快重生。

程序通訊機制詳解

多程序:

首先,先來講一下fork之後,發生了什麼事情。

由fork建立的新程序被稱為子程序(child process)。該函式被呼叫一次,但返回兩次。兩次返回的區別是子程序的返回值是0,而父程序的返回值則是新程序(子程序)的程序 id。將子程序id返回給父程序的理由是:因為一個程序的子程序可以多於一個,沒有一個函式使一個程序可以獲得其所有子程序的程序id。對子程序來說,之所以fork返回0給它,是因為它隨時可以呼叫getpid()來獲取自己的pid;也可以呼叫getppid()來獲取父程序的id。(程序id 0總是由交換程序使用,所以一個子程序的程序id不可能為0 )。

fork之後,作業系統會複製一個與父程序完全相同的子程序,雖說是父子關係,但是在作業系統看來,他們更像兄弟關係,這2個程序共享程式碼空間,但是資料空間是互相獨立的,子程序資料空間中的內容是父程序的完整拷貝,指令指標也完全相同,子程序擁有父程序當前執行到的位置兩程序的程式計數器pc值相同,也就是說,子程序是從fork返回處開始執行的),但有一點不同,如果fork成功,子程序中fork的返回值是0,父程序中fork的返回值是子程序的程序號,如果fork不成功,父程序會返回錯誤。
可以這樣想象,2個程序一直同時執行,而且步調一致,在fork之後,他們分別作不同的工作,也就是分岔了。這也是fork為什麼叫fork的原因

至於那一個最先執行,可能與作業系統(排程演算法)有關,而且這個問題在實際應用中並不重要,如果需要父子程序協同,可以通過原語的辦法解決。

常見的通訊方式:

1. 管道pipe:管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程序間使用。程序的親緣關係通常是指父子程序關係。
2. 命名管道FIFO:有名管道也是半雙工的通訊方式,但是它允許無親緣關係程序間的通訊。
4. 訊息佇列MessageQueue:訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
5. 共享儲存SharedMemory:共享記憶體就是對映一段能被其他程序所訪問的記憶體,這段共享記憶體由一個程序建立,但多個程序都可以訪問。共享記憶體是最快的 IPC 方式,它是針對其他程序間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如訊號兩,配合使用,來實現程序間的同步和通訊。
6. 訊號量Semaphore:訊號量是一個計數器,可以用來控制多個程序對共享資源的訪問。它常作為一種鎖機制,防止某程序正在訪問共享資源時,其他程序也訪問該資源。因此,主要作為程序間以及同一程序內不同執行緒之間的同步手段。
7. 套接字Socket:套解口也是一種程序間通訊機制,與其他通訊機制不同的是,它可用於不同及其間的程序通訊。
8. 訊號 ( sinal ) : 訊號是一種比較複雜的通訊方式,用於通知接收程序某個事件已經發生。

訊號:

訊號是Linux系統中用於程序之間通訊或操作的一種機制,訊號可以在任何時候傳送給某一程序,而無須知道該程序的狀態。如果該程序並未處於執行狀態,則該訊號就由核心儲存起來,知道該程序恢復執行並傳遞給他為止。如果一個訊號被程序設定為阻塞,則該訊號的傳遞被延遲,直到其阻塞被取消時才被傳遞給程序。

Linux提供了幾十種訊號,分別代表著不同的意義。訊號之間依靠他們的值來區分,但是通常在程式中使用訊號的名字來表示一個訊號。在Linux系統中,這些訊號和以他們的名稱命名的常量被定義在/usr/includebitssignum.h檔案中。通常程式中直接包含<signal.h>就好。

訊號是在軟體層次上對中斷機制的一種模擬,是一種非同步通訊方式,訊號可以在使用者空間程序和核心之間直接互動。核心也可以利用訊號來通知使用者空間的程序來通知使用者空間發生了哪些系統事件。訊號事件有兩個來源:

1)硬體來源,例如按下了cltr+C,通常產生中斷訊號sigint

2)軟體來源,例如使用系統呼叫或者命令發出訊號。最常用的傳送訊號的系統函式是kill,raise,setitimer,sigation,sigqueue函式。軟體來源還包括一些非法運算等操作。

一旦有訊號產生,使用者程序對訊號產生的相應有三種方式:

1)執行預設操作,linux對每種訊號都規定了預設操作。

2)捕捉訊號,定義訊號處理函式,當訊號發生時,執行相應的處理函式。

3)忽略訊號,當不希望接收到的訊號對程序的執行產生影響,而讓程序繼續執行時,可以忽略該訊號,即不對訊號程序作任何處理。

  有兩個訊號是應用程序無法捕捉和忽略的,即SIGKILL和SEGSTOP,這是為了使系統管理員能在任何時候中斷或結束某一特定的程序。

上圖表示了Linux中常見的命令

1、訊號傳送:

訊號傳送的關鍵使得系統知道向哪個程序傳送訊號以及傳送什麼訊號。下面是訊號操作中常用的函式:

例子:建立子程序,為了使子程序不在父程序發出訊號前結束,子程序中使用raise函式傳送sigstop訊號,使自己暫停;父程序使用訊號操作的kill函式,向子程序傳送sigkill訊號,子程序收到此訊號,結束子程序。

2、訊號處理

當某個訊號被髮送到一個正在執行的程序時,該程序即對次特定的訊號註冊相應的訊號處理函式,以完成所需處理。設定訊號處理方式的是signal函式,在程式正常結束前,在應用signal函式恢復系統對訊號的

預設處理方式。

3.訊號阻塞

有時候既不希望程序在接收到訊號時立刻中斷程序的執行,也不希望此訊號完全被忽略掉,而是希望延遲一段時間再去呼叫訊號處理函式,這個時候就需要訊號阻塞來完成。

 

例子:主程式阻塞了cltr+c的sigint訊號。用sigpromask將sigint假如阻塞訊號集合。

管道:

管道允許在程序之間按先進先出的方式傳送資料,是程序間通訊的一種常見方式。

管道是Linux 支援的最初Unix IPC形式之一,具有以下特點:

1) 管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道

2) 匿名管道只能用於父子程序或者兄弟程序之間(具有親緣關係的程序);

3) 單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。

管道分為pipe(無名管道)和fifo(命名管道)兩種,除了建立、開啟、刪除的方式不同外,這兩種管道幾乎是一樣的。他們都是通過核心緩衝區實現資料傳輸。

  • pipe用於相關程序之間的通訊,例如父程序和子程序,它通過pipe()系統呼叫來建立並開啟,當最後一個使用它的程序關閉對他的引用時,pipe將自動撤銷。
  • FIFO即命名管道,在磁碟上有對應的節點,但沒有資料塊——換言之,只是擁有一個名字和相應的訪問許可權,通過mknode()系統呼叫或者mkfifo()函式來建立的。一旦建立,任何程序都可以通過檔名將其開啟和進行讀寫,而不侷限於父子程序,當然前提是程序對FIFO有適當的訪問權。當不再被程序使用時,FIFO在記憶體中釋放,但磁碟節點仍然存在。

管道的實質是一個核心緩衝區,程序以先進先出的方式從緩衝區存取資料:管道一端的程序順序地將程序資料寫入緩衝區,另一端的程序則順序地讀取資料,該緩衝區可以看做一個迴圈佇列,讀和寫的位置都是自動增加的,一個數據只能被讀一次,讀出以後再緩衝區都不復存在了。當緩衝區讀空或者寫滿時,有一定的規則控制相應的讀程序或寫程序是否進入等待佇列,當空的緩衝區有新資料寫入或慢的緩衝區有資料讀出時,就喚醒等待佇列中的程序繼續讀寫。

無名管道:

pipe的例子:父程序建立管道,並在管道中寫入資料,而子程序從管道讀出資料

命名管道:

和無名管道的主要區別在於,命名管道有一個名字,命名管道的名字對應於一個磁碟索引節點,有了這個檔名,任何程序有相應的許可權都可以對它進行訪問。

而無名管道卻不同,程序只能訪問自己或祖先建立的管道,而不能訪任意訪問已經存在的管道——因為沒有名字。

Linux中通過系統呼叫mknod()或makefifo()來建立一個命名管道。最簡單的方式是通過直接使用shell

mkfifo myfifo

 等價於

mknod myfifo p

以上命令在當前目錄下建立了一個名為myfifo的命名管道。用ls -p命令檢視檔案的型別時,可以看到命名管道對應的檔名後有一條豎線"|",表示該檔案不是普通檔案而是命名管道。

使用open()函式通過檔名可以開啟已經建立的命名管道,而無名管道不能由open來開啟。當一個命名管道不再被任何程序開啟時,它沒有消失,還可以再次被開啟,就像開啟一個磁碟檔案一樣。

可以用刪除普通檔案的方法將其刪除,實際刪除的事磁碟上對應的節點資訊。

例子:用命名管道實現聊天程式,一個張三端,一個李四端。兩個程式都建立兩個命名管道,fifo1,fifo2,張三寫fifo1,李四讀fifo1;李四寫fifo2,張三讀fifo2。

用select把,管道描述符和stdin假如集合,用select進行阻塞,如果有i/o的時候喚醒程序。(粉紅色部分為select部分,黃色部分為命名管道部分)

 

在linux系統中,除了用pipe系統呼叫建立管道外,還可以使用C函式庫中管道函式popen函式來建立管道,使用pclose關閉管道。

例子:設計一個程式用popen建立管道,實現 ls -l |grep main.c的功能

分析:先用popen函式建立一個讀管道,呼叫fread函式將ls -l的結果存入buf變數,用printf函式輸出內容,用pclose關閉讀管道;

接著用popen函式建立一個寫管道,呼叫fprintf函式將buf的內容寫入管道,執行grep命令。

popen的函式原型:

FILE* popen(const char* command,const char* type);

引數說明:command是子程序要執行的命令,type表示管道的型別,r表示讀管道,w代表寫管道。如果成功返回管道檔案的指標,否則返回NULL。

使用popen函式讀寫管道,實際上也是呼叫pipe函式呼叫建立一個管道,再呼叫fork函式建立子程序,接著會建立一個shell 環境,並在這個shell環境中執行引數所指定的程序。

訊息佇列:

訊息佇列,就是一個訊息的連結串列,是一系列儲存在核心中訊息的列表。使用者程序可以向訊息佇列新增訊息,也可以向訊息佇列讀取訊息。

訊息佇列與管道通訊相比,其優勢是對每個訊息指定特定的訊息型別,接收的時候不需要按照佇列次序,而是可以根據自定義條件接收特定型別的訊息。

可以把訊息看做一個記錄,具有特定的格式以及特定的優先順序。對訊息佇列有寫許可權的程序可以向訊息佇列中按照一定的規則新增新訊息,對訊息佇列有讀許可權的程序可以從訊息佇列中讀取訊息。

訊息佇列的常用函式如下表:

程序間通過訊息佇列通訊,主要是:建立或開啟訊息佇列,新增訊息,讀取訊息和控制訊息佇列。

例子:用函式msget建立訊息佇列,呼叫msgsnd函式,把輸入的字串新增到訊息佇列中,然後呼叫msgrcv函式,讀取訊息佇列中的訊息並列印輸出,最後再呼叫msgctl函式,刪除系統核心中的訊息佇列。(黃色部分是訊息佇列相關的關鍵程式碼,粉色部分是讀取stdin的關鍵程式碼)

共享記憶體:

共享記憶體允許兩個或多個程序共享一個給定的儲存區,這一段儲存區可以被兩個或兩個以上的程序對映至自身的地址空間中,一個程序寫入共享記憶體的資訊,可以被其他使用這個共享記憶體的程序,通過一個簡單的記憶體讀取錯做讀出,從而實現了程序間的通訊。

採用共享記憶體進行通訊的一個主要好處是效率高,因為程序可以直接讀寫記憶體,而不需要任何資料的拷貝,對於像管道和訊息隊裡等通訊方式,則需要再核心和使用者空間進行四次的資料拷貝,而共享記憶體則只拷貝兩次:一次從輸入檔案到共享記憶體區,另一次從共享記憶體到輸出檔案。

一般而言,程序之間在共享記憶體時,並不總是讀寫少量資料後就解除對映,有新的通訊時在重新建立共享記憶體區域;而是保持共享區域,直到通訊完畢為止,這樣,資料內容一直儲存在共享記憶體中,並沒有寫回檔案。共享記憶體中的內容往往是在解除對映時才寫回檔案,因此,採用共享記憶體的通訊方式效率非常高。

共享記憶體有兩種實現方式:1、記憶體對映 2、共享記憶體機制

1、記憶體對映

記憶體對映 memory map機制使程序之間通過對映同一個普通檔案實現共享記憶體,通過mmap()系統呼叫實現。普通檔案被對映到程序地址空間後,程序可以

像訪問普通記憶體一樣對檔案進行訪問,不必再呼叫read/write等檔案操作函式。

例子:建立子程序,父子程序通過匿名對映實現共享記憶體。

分析:主程式中先呼叫mmap對映記憶體,然後再呼叫fork函式建立程序。那麼在呼叫fork函式之後,子程序繼承父程序匿名對映後的地址空間,同樣也繼承mmap函式的返回地址,這樣,父子程序就可以通過對映區域進行通訊了。

2、UNIX System V共享記憶體機制

IPC的共享記憶體指的是把所有的共享資料放在共享記憶體區域(IPC shared memory region),任何想要訪問該資料的程序都必須在本程序的地址空間新增一塊記憶體區域,用來對映存放共享資料的實體記憶體頁面。

和前面的mmap系統呼叫通過對映一個普通檔案實現共享記憶體不同,UNIX system V共享記憶體是通過對映特殊檔案系統shm中的檔案實現程序間的共享記憶體通訊。

例子:設計兩個程式,通過unix system v共享記憶體機制,一個程式寫入共享區域,另一個程式讀取共享區域。

分析:一個程式呼叫fotk函式產生標準的key,接著呼叫shmget函式,獲取共享記憶體區域的id,呼叫shmat函式,對映記憶體,迴圈計算年齡,另一個程式讀取共享記憶體。

(fotk函式在訊息佇列部分已經用過了,

根據pathname指定的檔案(或目錄)名稱,以及proj引數指定的數字,ftok函式為IPC物件生成一個唯一性的鍵值。)

key_t ftok(char* pathname,char proj)