以下文章來源于程式喵大人 ,作者程式喵大人
下面隆重推出我嘔心瀝血,耗時半個月完成的精心力作:
什么是行程?
標準定義:行程是一個具有一定獨立功能的程式在一個資料集合上依次動態執行的程序,行程是一個正在執行程式的實體,包括程式計數器、暫存器和程式變數的當前值,
簡單來說行程就是一個程式的執行流程,內部保存程式運行所需的資源,
在作業系統中可以有多個行程在運行,可對于CPU來說,同一時刻,一個CPU只能運行一個行程,但在某一時間段內,CPU將這一時間段拆分成更短的時間片,CPU不停的在各個行程間游走,這就給人一種并行的錯覺,像CPU可以同時運行多個行程一樣,這就是偽并行,
行程和程式有什么聯系?
一個行程是某種型別的一個活動,它有程式、輸入、輸出以及狀態,單個處理器可以被若干行程共享,它使用某種調度演算法決定何時停止一個行程的作業,并轉而為另一個行程提供服務,
程式是產生行程的基礎
程式的每次運行產生不同的行程
行程是程式功能的體現
通過多次執行,一個程式可對應多個行程;通過呼叫關系,一個行程可包括多個程式
行程和程式有什么區別?
行程是動態的,程式是靜態的:程式是有序代碼的集合,行程是程式的執行,
行程是暫時的,程式是永久的:行程是一個狀態變化的程序,程式可長久保存,
行程和程式的組成不同:行程的組成包括程式、資料和行程控制塊(行程狀態資訊),
行程有什么特點?
動態性:可動態的創建和結束行程
并發性:可以被獨立的調度并占用處理機并發運行
獨立性:不同行程的作業不相互影響
制約性:因訪問共享資源或行程間同步而產生制約
行程如何創建?
有什么事件會觸發行程的創建呢?
系統初始化:當啟動作業系統時,通常會創建很多行程,有些是同用戶互動并替他們完成作業的前臺行程,其它的都是后臺行程,后臺行程和特定用戶沒有關系,但也提供某些專門的功能,例如接收郵件等,這種功能的行程也稱為守護行程,計劃任務是個典型的守護行程,它每分鐘運行一次來檢查是否有作業需要它完成,如果有作業要做,它就會完成此作業,然后進入休眠狀態,直到下一次檢查時刻的到來,
正在運行的程式執行了創建行程的系統呼叫:在一個行程中又創建了一個新的行程,這種情況很常見,
用戶請求創建一個新行程:這種情況相信每個人都見過,用電腦時雙擊某個應用圖示,就會有至少一個行程被創建,
一個批處理作業的初始化:這種情形不常見,僅在大型機的批處理系統中應用,用戶在這種系統中提交批處理作業,在作業系統認為有資源可運行另一個作業時,它創建一個新的行程,并運行其輸入佇列中的下一個作業,
歸根到底:在UNIX系統中,只有fork系統呼叫才可以創建新行程,使用方式如下:
#include<stdio.h>#include<unistd.h>intmain(){pid_tid = fork();if(id <0) { perror("fork\n"); }elseif(id ==0) {// 子行程printf("子行程\n"); }else{// 父行程printf("父行程\n"); }return0;}
行程創建之后,父子行程都有各自不同的地址空間,其中一個行程在其地址空間的修改對另一個行程不可見,子行程的初始化空間是父行程的一個副本,這里涉及兩個不同地址空間,不可寫的記憶體區是共享的,某些UNIX的實作使程式正文在兩者間共享,因為它是不可修改的,
還有一種寫時復制共享技術,子行程共享父行程的所有記憶體,一旦兩者之一想要修改部分記憶體,則這塊記憶體被復制確保修改發生在當前行程的私有記憶體區域,
行程為何終止?
有什么事件會觸發行程的終止呢?
正常退出(自愿):行程完成了作業正常終止,UNIX中退出行程的系統呼叫是exit,
出錯退出(自愿):行程發現了錯誤而退出,可以看如下代碼:
#include<stdio.h>#include<stdlib.h>voidFunc(){if(error) {// 有錯誤就退出程式exit(1); }}intmain(){ Func();}
嚴重錯誤(非自愿):行程發生了嚴重的錯誤而不得不退出,通常是程式的錯誤導致,例如執行了一條非法指令,參考不存在的記憶體,或者除數是0等,出現這些錯誤時行程默認會退出,而有些時候如果用戶想自行處理某種型別的錯誤,發生不同型別錯誤時行程會收到不同型別的信號,用戶注冊處理不同信號的函式即可,
被其它行程殺死(非自愿):其它行程執行kill系統呼叫通知作業系統殺死某個行程,
作業系統如何進行行程管理?
這里就不得不提到一個資料結構:行程控制塊(PCB),作業系統為每個行程都維護一個PCB,用來保存與該行程有關的各種狀態資訊,行程可以抽象理解為就是一個PCB,PCB是行程存在的唯一標志,作業系統用PCB來描述行程的基本情況以及運行變化的程序,行程的任何狀態變化都會通過PCB來體現,
PCB包含行程狀態的重要資訊,包括程式計數器、堆疊指標、記憶體分配狀況、所打開檔案的狀態、賬號和調度資訊,以及其它在行程由運行態轉換到就緒態或阻塞態時必須保存的資訊,從而保證該行程隨后能再次啟動,就像從未中斷過一樣,后一小節會具體介紹PCB,
提到行程管理,有一個概念我們必須要知道,就是中斷向量,中斷向量是指中斷服務程式的入口地址,一個行程在執行程序中可能會被中斷無數次,但是每次中斷后,被中斷的行程都要回傳到與中斷發生前完全相同的狀態,
中斷發生后作業系統最底層做了什么呢?
1)硬體壓入堆疊程式計數器等;
2)硬體從中斷向量裝入新的程式計數器;
3)匯編語言程序保存暫存器值;
4)匯編語言程序設定新的堆疊;
5)C中斷服務例程運行(典型的讀和緩沖輸入);
6)調度程式決定下一個將運行的行程;
7)C程序回傳到匯編代碼;
8)匯編語言程序開始運行新的當前行程,
行程控制塊中存盤了什么資訊?
行程標識資訊:如本行程的標識,本行程的父行程標識,用戶標識等,
處理機狀態資訊保護區:用于保存行程的運行現場資訊:
用戶可見暫存器:用戶程式可以使用的資料,地址等暫存器
控制和狀態暫存器:程式計數器,程式狀態字
堆疊指標:程序呼叫、系統呼叫、中斷處理和回傳時需要用到它
行程控制資訊:
調度和狀態資訊:用于作業系統調度行程使用
行程間通信資訊:為支持行程間與通信相關的各種標識、信號、信件等,這些資訊存在接收方的行程控制塊中
存盤管理資訊:包含有指向本行程映像存盤空間的資料結構
行程所用資源:說明由行程打開使用的系統資源,如打開的檔案等
有關資料結構連接資訊:行程可以連接到一個行程佇列中,或連接到相關的其他行程的PCB
行程如何進行生命周期管理?
行程創建:
創建行程有三個主要事件:
系統初始化
用戶請求創建一個新行程
一個正在運行的行程執行創建行程的系統呼叫
行程運行:內核選擇一個就緒的行程,讓它占用處理機并運行,這里就涉及到了行程的調度策略,選擇哪個行程調度?為什么選擇調度這個行程呢?(莫慌,下面會介紹哈)
行程等待:
在以下情況下行程會等待(阻塞):
請求并等待系統服務,無法馬上完成
啟動某種操作,無法馬上完成
需要的資料沒有到達,
注意:行程只能自己阻塞自己,因為只有行程自身才能知道何時需要等待某種事件的發生,
行程喚醒:
行程只能被別的行程或作業系統喚醒,喚醒行程的原因有:
被阻塞行程需要的資源可被滿足
被阻塞行程等待的事件到達
將該行程的PCB插入到就緒佇列
行程結束:
在以下四種情況下行程會結束:
自愿型正常退出
自愿型錯誤退出
強制型致命錯誤退出
強制型被其它行程殺死退出
行程都有什么狀態?
不同系統設定的行程狀態是不同的,多數系統中的行程在生命結束前有三種基本狀態,行程只會處于三種基本狀態之一:
運行狀態:行程正在處理機上運行時就處在運行狀態,該時刻行程時鐘占用著CPU;
就緒狀態:萬事俱備,只欠東風,行程已經獲得了除處理機之外的一切所需資源,一旦得到處理機就可以運行;就緒態中的行程其實可以運行,但因為其它行程正在占用著CPU而暫時停止運行;
等待狀態(阻塞狀態):行程正在等待某一事件而暫停運行,等待某個資源或者等待輸入輸出完成,除非某種外部事件發生,否則阻塞態的行程不能運行;
行程狀態變化圖如下:
在作業系統發現行程不能繼續運行下去時,行程因為等待輸入而被阻塞,行程從運行態轉換到阻塞態!調度程式選擇了另一個行程執行時,當前程式就會從運行態轉換到就緒態!被調度程式選擇的程式會從就緒態轉換到運行態!當阻塞態的行程等待的一個外部事件發生時,就會從阻塞態轉換到就緒態,此時如果沒有其他行程運行時,則立刻從就緒態轉換到運行態!
某些系統設定下行程還會有其它狀態:
創建狀態:行程正在被創建還沒被轉到就緒狀態之前的狀態;
結束狀態:行程正在從系統中消失時的狀態,
有些與行程管理相關的系統呼叫讀者有必要了解一下:
什么是行程掛起?為什么會出現行程掛起?
行程掛起就是為了合理且充分的利用系統資源,把一個行程從記憶體轉到外存,行程在掛起狀態時,意味著行程沒有占用記憶體空間,處在掛起狀態的行程映射在磁盤上,行程掛起通常有兩種狀態:
阻塞掛起狀態:行程在外存并等待某事件的出現;
就緒掛起狀態:行程在外存,但只要進入記憶體即可運行,
有什么與行程掛起相關的狀態轉換?
行程掛起可能有以下幾種情況:
阻塞到阻塞掛起:沒有行程處于就緒狀態或就緒行程要求更多記憶體資源時,會進行這種轉換,以提交新行程或運行就緒行程;
就緒到就緒掛起:當有高優先級阻塞行程或低優先級就緒行程時,系統會選擇掛起低優先級就緒行程;
運行到就緒掛起:對于搶占式分時系統,當有高優先級阻塞掛起行程因事件出現而進入就緒掛起時,系統可能會把運行行程轉到就緒掛起狀態;
阻塞掛起到就緒掛起:當有阻塞掛起行程有相關事件出現時,系統會把阻塞掛起行程轉換為就緒掛起行程,
有行程掛起那就有行程解掛:指一個行程從外存轉到記憶體,相關狀態有:
就緒掛起到就緒:沒有就緒行程或就緒掛起行程優先級高于就緒行程時,就會進行這種轉換;
阻塞掛起到阻塞:當一個行程釋放足夠記憶體時,系統會把一個高優先級阻塞掛起行程轉換為阻塞行程,
什么是行程調度?作業系統對于行程調度都有什么策略?
當系統中有多個行程同時競爭CPU,如果只有一個CPU可用,那同一時刻只會有一個行程處于運行狀態,作業系統必須要選擇下一個要運行的是哪個行程,在作業系統中,完成選擇作業的這部分稱為調度程式,該程式使用的演算法稱作調度演算法,
什么時候進行調度?
系統呼叫創建一個新行程后,需要決定是運行父行程還是運行子行程
一個行程退出時需要做出調度決策,需要決定下一個運行的是哪個行程
當一個行程阻塞在I/O和信號量或者由于其它原因阻塞時,必須選擇另一個行程運行
當一個I/O中斷發生時,如果中斷來自IO設備,而該設備現在完成了作業,某些被阻塞的等待該IO的行程就成為可運行的就緒行程了,是否讓新就緒的行程運行,或者讓中斷發生時運行的行程繼續運行,或者讓某個其它行程運行,這就取決于調度程式的抉擇了,
調度演算法可以分類:
非搶占式調度演算法:挑選一個行程,然后讓該行程運行直至被阻塞,或者直到該行程自動釋放CPU,即使該行程運行了若干個小時,它也不會被強迫掛起,這樣做的結果是,在時鐘中斷發生時不會進行調度,在處理完時鐘中斷后,如果沒有更高優先級的行程等待,則被中斷的行程會繼續執行,簡單來說,調度程式必須等待事件結束,
非搶占方式引起行程調度的條件:
行程執行結束,或發生某個事件而不能繼續執行
正在運行的行程因有I/O請求而暫停執行
行程通信或同步程序中執行了某些原語操作(wait、block等)
搶占式調度演算法:挑選一個行程,并且讓該行程運行某個固定時段的最大值,如果在該時段結束時,該行程仍在運行,它就被掛起,而調度程式挑選另一個行程運行,進行搶占式調度處理,需要在時間間隔的末端發生時鐘中斷,以便CPU控制回傳給調度程式,如果沒有可用的時鐘,那么非搶占式調度就是唯一的選擇,簡單來說,就是當前運行的行程在事件沒結束時就可以被換出,防止單一行程長時間獨占CPU資源,下面會介紹很多搶占式調度演算法:優先級演算法、短作業優先演算法、輪轉演算法等,
調度策略:不同系統環境下有不同的調度策略演算法,調度演算法也是有KPI的,對調度演算法首先提的需求就是:
公平:調度演算法需要給每個行程公平的CPU份額,相似的行程應該得到相似的服務,對一個行程給予較其它等價的行程更多的CPU時間是不公平的,被普通水平的應屆生工資倒掛也是不公平的!
執行力:每一個策略必須強制執行,需要保證規定的策略一定要被執行,
平衡:需要保證系統的所有部分盡可能都忙碌
但是因為不同的應用有不同的目標,不同的系統中,調度程式的優化也是不同的,大體可以分為三種環境:
批處理系統
批處理系統的管理者為了掌握系統的作業狀態,主要關注三個指標:
吞吐量:是系統每小時完成的作業數量
周轉時間:指從一個作業提交到完成的平均時間
CPU利用率:盡可能讓CPU忙碌,但又不能過量
調度演算法:
先來先服務
先來后到嘛,就像平時去商店買東西需要排隊一樣,使用該演算法,行程按照它們請求CPU的順序來使用CPU,該演算法最大的優點就是簡單易于實作,太容易的不一定是好的,該演算法也有很大的缺點:平均等待時間波動較大,時間短的任務可能排隊排在了時間長的任務后面,舉個生活中的例子,排著隊去取快遞,如果每個人都很快取出來快遞還好,如果前面有幾個人磨磨唧唧到快遞柜前才拿出手機打開app,再找半分鐘它的取件碼,就會嚴重拖慢后面的人取快遞的速度,同理排著隊的行程如果每個行程都很快就運行完還好,如果其中有一個得到了CPU的行程運行時候磨磨唧唧很長時間都運行不完,那后面的行程基本上就沒有機會運行了!
最短作業優先
該調度演算法是非搶占式的演算法,每個行程執行期間不會被打斷,每次都選擇執行時間最短的行程來調度,但問題來了,作業系統怎么可能知道行程具體的執行時間呢,所以該演算法注定是基于預測性質的理想化演算法,而且有違公平性,而且可能導致運行時間長的任務得不到調度,
最短剩余時間優先
該調度演算法是搶占式的演算法,是最短作業優先的搶占版本,在行程運行期間,如果來了個更短時間的行程,那就轉而去把CPU時間調度給這個更短時間的行程,它的缺點和最短作業優先演算法類似,
互動式系統
對于互動系統最重要的指標就是回應時間和均衡性啦:
回應時間:一個請求被提交到產生第一次回應所花費的時間,你給別人發微信別人看后不回復你或者幾個小時后才回復你,你是什么感受,這還是互動式嗎?
均衡性:減少平均回應時間的波動,需要符合固有期望和預期,你給別人發微信,他有時候秒回復,有時候幾個小時后才回復,在互動式系統中,可預測性比高差異低平均更重要,
調度演算法:
輪轉調度
每個行程被分配一個時間段,稱為時間片,即CPU做到雨露均沾,輪流翻各個行程的牌子,這段時間寵幸行程A,下一段時間寵幸行程B,再下一段時間寵幸行程C,確保每個行程都可以獲得CPU時間,如果CPU時間特別短的話,在外部看來像是同時寵幸了所有行程一樣,那么問題來了,這個時間片究竟多長時間好呢?如果時間片設的太短會導致過多的行程切換,頻繁的背景關系切換會降低CPU效率,而如果時間片設的太長又可能對短的互動請求的回應時間變長,通常將時間片設為20-50ms是個比較合理的折中,大佬們的經驗規則時維持背景關系切換的開銷處于1%以內,
優先級調度
上面的輪轉調度演算法是默認每個行程都同等重要,都有相同優先級,然而有時候行程需要設定優先級,例如某些播放視頻的前臺行程可以優先于某些收發郵件的后臺守護行程被調度,在優先級調度演算法中,每個優先級都有相應的佇列,佇列里面裝著對應優先級的行程,首先在高優先級佇列中進行輪轉調度,當高優先級佇列為空時,轉而去低優先級佇列中進行輪轉調度,如果高優先級佇列始終不為空,那么低優先級的行程很可能就會饑餓到很久不能被調度,
多級佇列
多級佇列演算法與優先級調度演算法不同,優先級演算法中每個行程分配的是相同的時間片,而在多級佇列演算法中,不同佇列中的行程分配給不同的時間片,當一個行程用完分配的時間片后就移動到下一個佇列中,這樣可以更好的避免背景關系頻繁切換,舉例:有一個行程需要100個時間片,如果每次調度都給分配一個時間片,則需要100次背景關系切換,這樣CPU運行效率較低,通過多級佇列演算法,可以考慮最開始給這個行程分配1個時間片,然后被換出,下次分給它2個時間片,再換出,之后分給它4、8、16、64個時間片,這樣分配的話,該行程只需要7次交換就可以運行完成,相比100次背景關系切換運行效率高了不少,但顧此就會失彼,那些需要互動的行程得到回應的速度就會下降,
最短行程優先
互動式系統中應用最短行程優先演算法其實是非常適合的,每次都選擇執行時間最短的行程進行調度,這樣可以使任務的回應時間最短,但這里有個任務,還沒有運行呢,我怎么知道行程的運行時間呢?根本沒辦法非常準確的再當前可運行行程中找出最短的那個行程,有一種辦法就是根據行程過去的行為進行預測,但這能證明是個好辦法嗎?
保證調度
這種調度演算法就是向用戶做出明確的可行的性能保證,然后去實作它,一種很實際的可實作的保證就是確保N個用戶中每個用戶都獲得CPU處理能力的1/N,類似的,保證N個行程中每個行程都獲得1/N的CPU時間,
彩票調度
彩票調度演算法基本思想是為行程提供各種資源(CPU時間)的彩票,一旦需要做出調度決策時,就隨機抽出一張彩票,擁有該彩票的行程獲得該資源,很明顯,擁有彩票越多的行程,獲得資源的可能性越大,該演算法在程式喵看來可以理解為股票演算法,將CPU的使用權分成若干股,假設共100股分給了3個行程,給這些行程分別分配20、30、50股,那么它們大體上會按照股權比例(20:30:50)劃分CPU的使用,
公平分享調度
假設有系統兩個用戶,用戶1啟動了1個行程,用戶2啟動了9個行程,如果使用輪轉調度演算法,那么用戶1將獲得10%的CPU時間,用戶2將獲得90%的CPU時間,這對用戶來說公平嗎?如果給每個用戶分配50%的CPU時間,那么用戶2中的行程獲得的CPU時間明顯比用戶1中的行程短,這對行程來說公平嗎?這就取決于怎么定義公平啦?
實時系統
實時系統顧名思義,最關鍵的指標當然是實時啦:
滿足截止時間:需要在規定deadline前完成作業;
可預測性:可預測性是指在系統運行的任何時刻,在任何情況下,實時系統的資源調配策略都能為爭奪資源的任務合理的分配資源,使每個實時任務都能得到滿足,
調度演算法分類:
硬實時
必須在deadline之前完成作業,如果delay,可能會發生災難性或發生嚴重的后果;
軟實時
必須在deadline之前完成作業,但如果偶爾delay了,也可以容忍,
調度演算法:
單調速率調度
采用搶占式、靜態優先級的策略,調度周期性任務,
每個任務最開始都被配置好了優先級,當較低優先級的行程正在運行并且有較高優先級的行程可以運行時,較高優先級的行程將會搶占低優先級的行程,在進入系統時,每個周期性任務都會分配一個優先級,周期越短,優先級越高,這種策略的理由是:更頻繁的需要CPU的任務應該被分配更高的優先級,
最早截止時間調度
根據截止時間動態分配優先級,截止時間越早的行程優先級越高,
該演算法中,當一個行程可以運行時,它應該向作業系統通知截止時間,根據截止時間的早晚,系統會為該行程調整優先級,以便滿足可運行行程的截止時間要求,它與單調速率調度演算法的區別就是一個是靜態優先級,一個是動態優先級,
如何配置調度策略?
調度演算法有很多種,各有優缺點,作業系統自己很少能做出最優的選擇,那么可以把選擇權交給用戶,由用戶根據實際情況來選擇適合的調度演算法,這就叫策略與機制分離,調度機制位于內核,調度策略由用戶行程決定,將調度演算法以某種形式引數化,由用戶行程來選擇引數從而決定內核使用哪種調度演算法,
作業系統怎么完成行程調度?
行程的每次變化都會有相應的狀態,而作業系統維護了一組狀態佇列,表示系統中所有行程的當前狀態;不同的狀態有不同的佇列,有就緒佇列阻塞佇列等,每個行程的PCB都根據它的狀態加入到相應的佇列中,當一個行程的狀態發生變化時,它的PCB會從一個狀態佇列中脫離出來加入到另一個狀態佇列,
注意圖中同一種狀態為什么有多個佇列呢?因為行程有優先級概念,相同狀態的不同佇列的優先級不同,
什么是執行緒?
執行緒是行程當中的一條執行流程,這幾乎就是行程的定義,一個行程內可以有多個子執行流程,即執行緒,可以從兩個方面重新理解行程:
從資源組合的角度:行程把一組相關的資源組合起來,構成一個資源平臺環境,包括地址空間(代碼段、資料段),打開的檔案等各種資源
從運行的角度:代碼在這個資源平臺上的執行流程,然而執行緒貌似也是這樣,但是行程比執行緒多了資源內容串列樣式:那就有一個公式:行程 = 執行緒 + 共享資源
為什么使用執行緒?
因為要并發編程,在許多情形中同時發生著許多活動,而某些活動有時候會被阻塞,通過將這些活動分解成可以準并行運行的多個順序流程是必須的,而如果使用多行程方式進行并發編程,行程間的通信也很復雜,并且維護行程的系統開銷較大:創建行程時分配資源建立PCB,撤銷行程時回收資源撤銷PCB,行程切換時保存當前行程的狀態資訊,所以為了使并發編程的開銷盡量小,所以引入多執行緒編程,可以并發執行也可以共享相同的地址空間,并行物體擁有共享同一地址空間和所有可用資料的能力,這是多行程模型所不具備的能力,
使用執行緒有如下優點:
可以多個執行緒存在于同一個行程中
各個執行緒之間可以并發的執行
各個執行緒之間可以共享地址空間和檔案等資源
執行緒比行程更輕量級,創建執行緒撤銷執行緒比創建撤銷行程要快的多,在許多系統中,創建一個執行緒速度是創建一個行程速度的10-100倍,
如果多個執行緒是CPU密集型的,并不能很好的獲得更好的性能,但如果多個執行緒是IO密集型的,執行緒存在著大量的計算和大量的IO處理,有多個執行緒允許這些活動彼此重疊進行,從而會加快整體程式的執行速度,
但也有缺點:一旦一個執行緒崩潰,會導致其所屬行程的所有執行緒崩潰,由于各個執行緒共享相同的地址空間,那么讀寫資料可能會導致競爭關系,因此對同一塊資料的讀寫需要采取某些同步機制來避免執行緒不安全問題,
什么時候用行程、執行緒?
行程是資源分配單位,執行緒是CPU調度單位;
行程擁有一個完整的資源平臺,而執行緒只獨享必不可少的資源,如暫存器和堆疊;
執行緒同樣具有就緒阻塞和執行三種基本狀態,同樣具有狀態之間的轉換關系;
執行緒能減少并發執行的時間和空間開銷:
執行緒的創建時間比行程短
執行緒的終止時間比行程短
同一行程內的執行緒切換時間比行程短
由于同一行程的各執行緒間共享記憶體和檔案資源,可直接進行不通過內核的通信
結論:可以在強調性能時候使用執行緒,如果追求更好的容錯性可以考慮使用多行程,google瀏覽器據說就是用的多行程編程,在多CPU系統中,多執行緒是有益的,在這樣的系統中,通常情況下可以做到真正的并行,
C/C++中如何使用多執行緒編程?
POSIX使用如下執行緒封裝函式來操作執行緒:
C++中有std::thread和async,可以很方便的操作多執行緒,示例代碼如下:
執行緒是如何實作的?
執行緒的實作可分為用戶執行緒和內核執行緒:
用戶執行緒:在用戶空間實作的執行緒機制,它不依賴于作業系統的內核,由一組用戶級的執行緒庫函式來完成執行緒的管理,包括行程的創建終止同步和調度等,
用戶執行緒有如下優點:
由于用戶執行緒的維護由相應行程來完成(通過執行緒庫函式),不需要作業系統內核了解內核了解用戶執行緒的存在,可用于不支持執行緒技術的多行程作業系統,
每個行程都需要它自己私有的執行緒控制塊串列,用來跟蹤記錄它的各個執行緒的狀態資訊(PC,堆疊指標,暫存器),TCB由執行緒庫函式來維護;用戶執行緒的切換也是由執行緒庫函式來完成,無需用戶態/核心態切換,所以速度特別快;允許每個行程擁有自定義的執行緒調度演算法;但用戶執行緒也有缺點:阻塞性的系統呼叫如何實作?如果一個執行緒發起系統呼叫而阻塞,則整個行程在等待,當一個執行緒開始運行后,除非它主動交出CPU的使用權,否則它所在行程當中的其它執行緒將無法運行;由于時間片分配給行程,與其它行程比,在多執行緒執行時,每個執行緒得到的時間片較少,執行會較慢
內核執行緒:是指在作業系統的內核中實作的一種執行緒機制,由作業系統的內核來完成執行緒的創建終止和管理,
特點:
在支持內核執行緒的作業系統中,由內核來維護行程和執行緒的背景關系資訊(PCB TCB);
執行緒的創建終止和切換都是通過系統呼叫內核函式的方式來進行,由內核來完成,因此系統開銷較大;
在一個行程當中,如果某個內核執行緒發起系統呼叫而被阻塞,并不會影響其它內核執行緒的運行;
時間片分配給執行緒,多執行緒的行程獲得更多CPU時間;
tips
由于在內核中創建或撤銷執行緒的代價比較大,某些系統采取復用的方式回收執行緒,當某個執行緒被撤銷時,就把它標記不可運行,但是內核資料結構沒有受到任何影響,如果后續又需要創建一個新執行緒時,就重新啟動被標記為不可運行的舊執行緒,從而節省一些開銷,
注意
盡管使用內核執行緒可以解決很多問題,但還有些問題,例如:當一個多執行緒的行程創建一個新的行程時會發生什么?新行程是擁有與原行程相同數量的執行緒還是只有一個執行緒?在很多情況下,最好的選擇取決于行程計劃下一步做什么?如果它要呼叫exec啟動一個新程式,或許一個執行緒正合適,但如果它繼續運行,那么最好復制所有的執行緒,
輕量級行程:它是內核支持的用戶執行緒模型,一個行程可以有多個輕量級行程,每個輕量級行程由一個單獨的內核執行緒來支持,
在Linux下是沒有真正的執行緒的,它所謂的執行緒其實就是使用行程來實作的,就是所謂的輕量級行程,其實就是行程,都是通過clone介面呼叫創建的,只不過兩者傳遞的引數不同,通過引數決定子行程和父行程共享的資源種類和數量,進而有了普通行程和輕量級行程的區別,
什么是背景關系切換?
背景關系切換指的是作業系統停止當前運行行程(從運行狀態改變成其它狀態)并且調度其它行程(就緒態轉變成運行狀態),作業系統必須在切換之前存盤許多部分的行程背景關系,必須能夠在之后恢復他們,所以行程不能顯示它曾經被暫停過,同時切換背景關系這個程序必須快速,因為背景關系切換操作是非常頻繁的,那背景關系指的是什么呢?指的是任務所有共享資源的作業現場,每一個共享資源都有一個作業現場,包括用于處理函式呼叫、區域變數分配以及作業現場保護的堆疊頂指標,和用于指令執行等功能的各種暫存器,
注意
這里所說的行程切換導致背景關系切換其實不太準確,準確的說應該是任務的切換導致背景關系切換,這里的任務可以是行程也可以是執行緒,準確的說執行緒才是CPU調度的基本單位,但是因為各個資料都這么解釋背景關系切換,所以上面也暫時這么介紹,只要讀者心里有這個概念就好,
行程間通信有幾種方式?
由于各個行程不共享相同的地址空間,任何一個行程的全域變數在另一個行程中都不可見,所以如果想要在行程之間傳遞資料就需要通過內核,在內核中開辟出一塊區域,該區域對多個行程都可見,即可用于行程間通信,有讀者可能有疑問了,檔案方式也是行程間通信啊,也要在內核開辟區域嗎?這里說的內核區域其實是一段緩沖區,檔案方式傳輸資料也有內核緩沖區的參與(零拷貝除外),
如何開辟這種公共區域來進行行程間通信呢?
匿名管道
匿名管道就是pipe,pipe只能在父子行程間通信,而且資料只能單向流動(半雙工通信),
使用方式:
1)父行程創建管道,會得到兩個檔案描述符,分別指向管道的兩端;
2)父行程創建子行程,從而子行程也有兩個檔案描述符指向同一管道;
3)父行程可寫資料到管道,子行程就可從管道中讀出資料,從而實作行程間通信,下面的示例代碼中通過pipe實作了每秒鐘父行程向子行程都發送訊息的功能,
我們平時也經常使用關于管道的命令列:
ls| less
該命令列的流向圖如下:
1:創建管道
2:為ls創建一個行程,設定stdout為管理寫端
3:為less創建一個行程,設定stdin為管道讀端
高級管道
通過popen將另一個程式當作一個新的行程在當前行程中啟動,它算作當前行程的子行程,高級管道只能用在有親緣關系的行程間通信,這種親緣關系通常指父子行程,下面的GetCmdResult函式可以獲取某個Linux命令執行的結果,實作方式就是通過popen,
命名管道
匿名管道有個缺點就是通信的行程一定要有親緣關系,而命名管道就不需要這種限制,
命名管道其實就是一種特殊型別的檔案,所謂的命名其實就是檔案名,檔案對各個行程都可見,通過命名管道創建好特殊檔案后,就可以實作行程間通信,
可以通過mkfifo創建一個特殊的型別的檔案,引數讀者看名字應該就了解,一個是檔案名,一個是檔案的讀寫權限:
當回傳值為0時,表示該命名管道創建成功,至于如何通信,其實就是個讀寫檔案的問題!
訊息佇列
佇列想必大家都知道,像FIFO一樣,這里可以有多個行程寫入資料,也可以有多個行程從佇列里讀出資料,但訊息佇列有一點比FIFO還更高級,它讀訊息不一定要使用先進先出的順序,每個訊息可以賦予型別,可以按訊息的型別讀取,不是指定型別的資料還存在佇列中,本質上MessageQueue是存放在內核中的訊息鏈表,每個訊息佇列鏈表會由訊息佇列識別符號表示,這個訊息佇列存于內核中,只有主動的洗掉該訊息佇列或者內核重啟時,訊息佇列才會被洗掉,
在Linux中訊息佇列相關的函式呼叫如下:
示例代碼如下:
代碼中為了演示方便使用訊息佇列進行的執行緒間通信,該代碼同樣用于行程間通信,訊息佇列的實作依賴于內核的支持,上述代碼可能在某些系統(WSL)上不能運行,在正常的Ubuntu上可以正常運行,
訊息佇列VS命名管道
訊息佇列>命名管道
1)訊息佇列收發訊息自動保證了同步,不需要由行程自己來提供同步方法,而命名管道需要自行處理同步問題;
2)訊息佇列接收資料可以根據訊息型別有選擇的接收特定型別的資料,不需要像命名管道一樣默認接收資料,
訊息佇列<命名管道
訊息佇列有一個缺點就是發送和接收的每個資料都有最大長度的限制,
共享記憶體
可開辟中一塊記憶體,用于各個行程間共享,使得各個行程可以直接讀寫同一塊記憶體空間,就像執行緒共享同一塊地址空間一樣,該方式基本上是最快的行程間通信方式,因為沒有系統呼叫干預,也沒有資料的拷貝操作,但由于共享同一塊地址空間,資料競爭的問題就會出現,需要自己引入同步機制解決資料競爭問題,
共享記憶體只是一種方式,它的實作方式有很多種,主要的有mmap系統呼叫、Posix共享記憶體以及System V共享記憶體等,通過這三種“工具”共享地址空間后,通信的目的自然就會達到,
信號
信號也是行程間通信的一種方式,信號可以在任何時候發送給某一個行程,如果行程當前并未處于執行狀態,內核將信號保存,直到行程恢復到執行態再發送給行程,行程可以對信號設定預處理方式,如果對信號設定了阻塞處理,則信號的傳遞會被延遲直到阻塞被取消,如果行程結束,那信號就被丟棄,我們常用的CTRL+C和kill等就是信號的一種,也達到了行程間通信的目的,行程也可以對信號設定signal捕獲函式自定義處理邏輯,這種方式有很大的缺點:只有通知的作用,通知了一下訊息的型別,但不能傳輸要交換的任何資料,
Linux系統中常見的信號有:
SIGHUP:該信號在用戶終端結束時發出,通常在中斷的控制行程結束時,所有行程組都將收到該信號,該信號的默認操作是終止行程;
SIGINT:程式終止信號,通常的CTRL+C產生該信號來通知終止行程;
SIGQUIT:類似于程式錯誤信號,通常的CTRL+\產生該信號通知行程退出時產生core檔案;
SIGILL:執行了非法指令,通常資料段或者堆疊溢位可能產生該信號;
SIGTRAP:供除錯器使用,由斷電指令或其它陷阱指令產生;
SIGABRT:使程式非正常結束,呼叫abort函式會產生該信號;
SIGBUS:非法地址,通常是地址對齊問題導致,比如訪問一個4位元組長的整數,但其地址不是4的倍數;
SIGSEGV:合理地址的非法訪問,訪問了未分配的記憶體或者沒有權限的記憶體區域;
SIGPIPE:管道破裂信號,socket通信時經常會遇到,行程寫入了一個無讀者的管道;
SIGALRM:時鐘定時信號,由alarm函式設定的時間終止時產生;
SIGFPE:出現浮點錯誤(比如除0操作);
SIGKILL:殺死行程(不能被捕捉和忽略);
信號量
想必大家都聽過信號量,信號量就是一個特殊的變數,程式對其訪問都是原子操作,每個信號量開始都有個初始值,最簡單最常見的信號量是只能取0和1的變數,也叫二值信號量,
信號量有兩個操作,P和V:
P:如果信號量變數值大于0,則變數值減1,如果值為0,則阻塞行程;
V:如果有行程阻塞在該信號量上,則喚醒阻塞的行程,如果沒有行程阻塞,則變數值加1
互斥量用于互斥,信號量用于同步,互斥指的是某一資源同一時間只允許一個訪問者訪問,但無法限制訪問順序,訪問是無序的,而同步在互斥的基礎上可以控制訪問者對資源的順序,
套接字:就是網路傳輸,不用多說,網路通信都可以多機通信呢,更不用說行程間通信啦,你能看到程式喵的文章也是套接字的功勞,
檔案:顯而易見,多個行程可以操作同一個檔案,所以也可以通過檔案來進行行程間通信,
關于行程和執行緒的知識點介紹就到這里,程式喵為了寫本文花費了半月以上時間,相信你弄懂這19個問題,面試不用愁!
另外如果你想更好的提升你的編程能力,學好C語言C++編程!彎道超車,快人一步!筆者這里或許可以幫到你~
C語言C++編程學習交流圈子,QQ群1030652847【點擊進入】微信公眾號:C語言編程學習基地
分享(原始碼、專案實戰視頻、專案筆記,基礎入門教程)
歡迎轉行和學習編程的伙伴,利用更多的資料學習成長比自己琢磨更快哦!
編程學習書籍分享:

編程學習視頻分享:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/230505.html
標籤:其他
下一篇:hello world
