面經 cisco

1. 優先級反轉問題及解決方法php

(1)什麼是優先級反轉 css

簡單從字面上來講,就是低優先級的任務先於高優先級的任務執行了,優先級搞反了。那在什麼狀況下會生這種狀況呢? html

假設三個任務準備執行,A,B,C,優先級依次是A>B>C; vue

首先:C處於運行狀態,得到CPU正在執行,同時佔有了某種資源; node

其次:A進入就緒狀態,由於優先級比C高,因此得到CPU,A轉爲運行狀態;C進入就緒狀態; linux

第三:執行過程當中須要使用資源,而這個資源又被等待中的C佔有的,因而A進入阻塞狀態,C回到運行狀態; ios

第四:此時B進入就緒狀態,由於優先級比C高,B得到CPU,進入運行狀態;C又回到就緒狀態; c++

第五:若是這時又出現B2,B3等任務,他們的優先級比C高,但比A低,那麼就會出現高優先級任務的A不能執行,反而低優先級的B,B2,B3等任務能夠執行的奇怪現象,而這就是優先反轉。 git

(2)如何解決優先級反轉 程序員

高優先級任務A不能執行的緣由是C霸佔了資源,而C若是不能得到CPU,不釋放資源,那A也只好一直等在那,因此解決優先級反轉的原則確定就是讓C儘快執行,儘早把資源釋放了。基於這個原則產生了兩個方法:

2.1 優先級繼承

當發現高優先級的任務由於低優先級任務佔用資源而阻塞時,就將低優先級任務的優先級提高到等待它所佔有的資源的最高優先級任務的優先級。

2.2 優先級天花板

優先級天花板是指將申請某資源的任務的優先級提高到可能訪問該資源的全部任務中最高優先級任務的優先級.(這個優先級稱爲該資源的優先級天花板)

2.3 二者的區別

優先級繼承:只有一個任務訪問資源時一切照舊,沒有區別,只有當高優先級任務由於資源被低優先級佔有而被阻塞時,纔會提升佔有資源任務的優先級;而優先級天花板,不管是否發生阻塞,都提高,即誰先拿到資源,就將這個任務提高到該資源的天花板優先級。

 

2. 紅黑樹比通常樹好在哪,爲何樹要平衡

平衡二叉樹和紅黑樹最差狀況分析

1.經典平衡二叉樹

平衡二叉樹(又稱AVL樹)是帶有平衡條件的二叉查找樹,使用最多的定理爲:一棵平衡二叉樹是其每一個節點的左子樹和右子樹的高度最多差爲1的二叉查找樹。由於他是二叉樹的一種具體應用,因此他一樣具備二叉樹的性質。例如,一棵滿二叉樹在第k層最多可擁有個節點(性質1)。一棵樹的高度爲其從根節點到最底層節點通過的路徑數(例如只含一個節點的樹的高度爲0)(性質2)。而且已被證實,一棵含有N個節點的平衡二叉樹的高度最多(粗略來講)爲。下面咱們來嘗試總結如何獲得一個高度差最大的平衡二叉樹(查找性能最差)。

由平衡二叉樹的定義可知,左子樹和右子樹最多能夠相差1層高度,那麼多個在同一層的子樹就能夠依次以相差1層的方式來遞減子樹的高度,以下圖所示是一個擁有4棵子樹的樹的層高最大差情形(圖1):

 

圖1 擁有4顆子樹的平衡二叉樹最大高度差

該圖虛線框中的子樹,最左端的節點樹高度爲0,最右端的節點樹的高度爲2,所以該平衡二叉樹的內部子樹最大高度差爲2

利用這樣的性質,咱們就能夠依次遞推,1棵子樹最大高度差爲0層,23棵子樹最大高度差爲1層,四、五、六、7棵子樹最大高度差爲2層,8至15棵子樹最大高度差爲3層,16至31棵子樹最大高度差爲4......

假設n爲子樹的數量,m爲最大高度差,可得:

 

進一步分析,假設一棵高爲h的平衡二叉樹的最大高度差爲m(假設最小的子樹高度爲0),且m的高度差由n(此處只考慮分界點情形,即n爲2的冪級數)棵子樹達成。由平衡二叉樹的性質(性質1和性質2),可得以下式:


化簡後最終能夠獲得一個簡單的結論(彷佛這個結論在本文以前沒人關注過,此處僅僅考慮了最差狀況,不過對於實際應用和性能分析已經徹底夠用,詳細完整的數學證實感興趣的讀者能夠嘗試證實):

也就是說,一棵高爲h的平衡二叉樹,其內部子樹的最大高度差能夠達到(結果取整,不捨入)。舉例來講,一棵高度爲8的平衡二叉樹,其內部子樹的最大高度差爲4,同理,一棵高度爲9的平衡二叉樹,其內部子樹的最大高度差爲5,以下示(圖2):

圖2 高度爲 9的平衡二叉樹的最大高度差

2.紅黑樹

歷史上AVL樹流行的一種變種是紅黑樹(red black tree)。紅黑樹也是許多編程語言底層實現採納較多的數據結構(例如JavaTreeSetTreeMap實現)。紅黑樹是具備下列着色性質的二叉查找樹:

1.每個節點或爲黑色或爲紅色。

2.根節點時黑色的。

3.一個紅色節點的兒子節點必須所有是黑色。

4.從任意一個節點到一個null的每一條路徑必須包含相同數目的黑色節點。

以上着色法則的一個結論是:紅黑樹的高度最多爲,這個結論彷佛還不如經典平衡二叉樹。下面咱們來分析由以上着色法則規定的紅黑樹的最差情形。

由規則4可知,只要在一個節點的一側子樹儘量多的使用紅色節點,而另外一側儘量少甚至不使用紅色節點,就能夠拉開左右子樹的高度差,以下圖所示(圖3):

圖3 紅黑樹高度差造成緣由

則咱們能夠顯而易見的獲得一個結論:一棵含有k個紅色節點的紅黑樹,理論上其內部子樹最大高度差就能夠達到k

既然紅黑樹理論上缺陷如此大,那爲何實際應用中反而採納較多?深刻研究紅黑樹的具體實現方式,能夠發現,紅黑樹在實際應用中的實現形式已經超出其本來定義的規則。紅黑樹的理論闡述不夠完善,也是其難於理解和新手難以本身動手實現的緣由之一。(注:本文采納Mark Allen Weiss的《Data Structures and Algorithm Analysis in Java》一書當中對於紅黑樹的實現方式,這也是實際中使用最多的實現方式)。

通過個人概括總結,現實中的紅黑樹在實現過程當中增長了如下限制條件:

5.新插入的節點必須爲紅色。

6.任意節點其左右子樹最多相差2層紅節點。

7.插入過程(僅限於插入點那條路徑上)中不容許任一節點有2個紅色兒子節點。

增長了以上三條限制條件的紅黑樹甚至都不須要再多加分析了。規則67創建在規則5和紅黑樹複雜的插入調整的基礎之上,規則6恰巧直接阻止了經典平衡二叉樹出現最差情形的可能性,規則7甚至是對紅黑樹到AA樹(AA樹實現尚不完善,目前實際性能較差,本文不作深刻討論)的一種過渡。如下圖展現了連續向紅黑樹中插入右節點(依次插入30,40,50,60,70,80,90,100)的變化過程(圖4,圖5):

圖4 依次向紅黑樹插入 30,40,50,60情形
圖5 繼續向紅黑樹插入 70,80,90,100情形

3.性能測試

本文的測試環境爲Window7操做系統,代碼所有使用Java編寫,jdk版本爲1.8.0,測試均採用隨機數生成器產生的九位數數字數據。爲儘量排除編譯器優化以及操做系統調度形成的影響,每一項測試均運行100遍取平均時間,例如對於萬級測試:生成100次不一樣的1000個隨機九位數,將每次生成的結果分別對平衡二叉樹、紅黑樹、AA樹進行樹的構造,構造完成後從新循環這10000個數進行平均查找時間的統計,在查找期間還會生成節點數據量十分之一(此處即1000)個假數據來保證查找不到的狀況也被統計進查找時間,也就是說萬級測試是在操做1100000次後得出的結果。最終測試結果以下:

注:AA樹的各項性能均介於紅黑樹和平衡二叉樹之間。可是AA樹的深度受隨機生成的數字影像波動較大(最差狀況出現次數多,而且最差狀況樹高最高)。

 

由此總結以下(若是前文都沒看懂沒有關係,記住下面的結論):

性能:紅黑樹>平衡二叉樹>AA樹;

編程實現難度:紅黑樹>平衡二叉樹>AA樹。

雖然各類樹的實現以及具體應用千差萬別,可是沒有最好的數據結構只有最合適的數據結構,此處比較的三種查找樹在實際應用中性能上只有細微的差異(最差狀況出現的機率畢竟很是小),在生產實踐中徹底不會帶來性能的明顯缺陷,所以選擇合理的實現方式,保證程序的功能健全性,纔是選擇數據結構的最重要選擇因素。

 

 

紅黑樹屬於平衡二叉樹。它不嚴格是由於它不是嚴格控制左、右子樹高度或節點數之差小於等於1,但紅黑樹高度依然是平均log(n),且最壞狀況高度不會超過2log(n)。

紅黑樹(red-black tree) 是一棵知足下述性質的二叉查找樹:

1. 每個結點要麼是紅色,要麼是黑色。

2. 根結點是黑色的。

3. 全部葉子結點都是黑色的(實際上都是Null指針,下圖用NIL表示)。葉子結點不包含任何關鍵字信息,全部查詢關鍵字都在非終結點上。

4. 每一個紅色結點的兩個子節點必須是黑色的。換句話說:從每一個葉子到根的全部路徑上不能有兩個連續的紅色結點

5. 從任一結點到其每一個葉子的全部路徑都包含相同數目的黑色結點

 

紅黑樹相關定理

1. 從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長。

      根據上面的性質5咱們知道上圖的紅黑樹每條路徑上都是3個黑結點。所以最短路徑長度爲2(沒有紅結點的路徑)。再根據性質4(兩個紅結點不能相連)和性質1,2(葉子和根必須是黑結點)。那麼咱們能夠得出:一條具備3個黑結點的路徑上最多隻能有2個紅結點(紅黑間隔存在)。也就是說黑深度爲2(根結點也是黑色)的紅黑樹最長路徑爲4,最短路徑爲2。從這一點咱們能夠看出紅黑樹是 大體平衡的。 (固然比平衡二叉樹要差一些,AVL的平衡因子最多爲1)

 

2. 紅黑樹的樹高(h)不大於兩倍的紅黑樹的黑深度(bd),即h<=2bd

      根據定理1,咱們不難說明這一點。bd是紅黑樹的最短路徑長度。而可能的最長路徑長度(樹高的最大值)就是紅黑相間的路徑,等於2bd。所以h<=2bd。

 

3. 一棵擁有n個內部結點(不包括葉子結點)的紅黑樹的樹高h<=2log(n+1)

      下面咱們首先證實一顆有n個內部結點的紅黑樹知足n>=2^bd-1。這能夠用數學概括法證實,施概括於樹高h。當h=0時,這至關因而一個葉結點,黑高度bd爲0,而內部結點數量n爲0,此時0>=2^0-1成立。假設樹高h<=t時,n>=2^bd-1成立,咱們記一顆樹高 爲t+1的紅黑樹的根結點的左子樹的內部結點數量爲nl,右子樹的內部結點數量爲nr,記這兩顆子樹的黑高度爲bd'(注意這兩顆子樹的黑高度必然一 樣),顯然這兩顆子樹的樹高<=t,因而有nl>=2^bd'-1以及nr>=2^bd'-1,將這兩個不等式相加有nl+nr>=2^(bd'+1)-2,將該不等式左右加1,獲得n>=2^(bd'+1)-1,很顯然bd'+1>=bd,因而前面的不等式能夠 變爲n>=2^bd-1,這樣就證實了一顆有n個內部結點的紅黑樹知足n>=2^bd-1。

        在根據定理2,h<=2bd。即n>=2^(h/2)-1,那麼h<=2log(n+1)

        從這裏咱們可以看出,紅黑樹的查找長度最多不超過2log(n+1),所以其查找時間複雜度也是O(log N)級別的。



紅黑樹的操做

 

由於每個紅黑樹也是一個特化的二叉查找樹,所以紅黑樹上的查找操做與普通二叉查找樹上的查找操做相同。然而,在紅黑樹上進行插入操做和刪除操做會致使不 再符合紅黑樹的性質。恢復紅黑樹的屬性須要少許(O(log n))的顏色變動(實際是很是快速的)和不超過三次樹旋轉(對於插入操做是兩次)。 雖然插入和刪除很複雜,但操做時間仍能夠保持爲 O(log n) 次 。

 

紅黑樹的優點

 

紅黑樹可以以O(log2(N))的時間複雜度進行搜索、插入、刪除操做。此外,任何不平衡都會在3次旋轉以內解決。這一點是AVL所不具有的。

並且實際應用中,不少語言都實現了紅黑樹的數據結構。好比 TreeMap, TreeSet(Java )、 STL(C++)等。

 

 

3. tcp爲何要3次握手,若是不是會出什麼問題,舉例說明

爲何不能用兩次握手進行鏈接?

咱們知道,3次握手完成兩個重要的功能,既要雙方作好發送數據的準備工做(雙方都知道彼此已準備好),也要容許雙方就初始序列號進行協商,這個序列號在握手過程當中被髮送和確認。
    如今把三次握手改爲僅須要兩次握手,死鎖是可能發生的。做爲例子,考慮計算機S和C之間的通訊,假定C給S發送一個鏈接請求分組,S收到了這個分組,併發 送了確認應答分組。按照兩次握手的協定,S認爲鏈接已經成功地創建了,能夠開始發送數據分組。但是,C在S的應答分組在傳輸中被丟失的狀況下,將不知道S 是否已準備好,不知道S創建什麼樣的序列號,C甚至懷疑S是否收到本身的鏈接請求分組。在這種狀況下,C認爲鏈接還未創建成功,將忽略S發來的任何數據分 組,只等待鏈接確認應答分組。而S在發出的分組超時後,重複發送一樣的分組。這樣就造成了死鎖。

 

設想:若是隻有兩次握手,那麼第二次握手後服務器只向客戶端發送ACK包,此時客戶端與服務器端創建鏈接。在這種握手規則下: 
        假設:若是發送網絡阻塞,因爲TCP/IP協議定時重傳機制,B向A發送了兩次SYN請求,分別是x1和x2,且由於阻塞緣由,致使x1鏈接請求和x2鏈接請求的TCP窗口大小和數據報文長度不一致,若是最終x1達到A,x2丟失,此時A同B創建了x1的鏈接,這個時候,由於AB已經鏈接,B沒法知道是請求x1仍是請求x2同B鏈接,若是B默認是最近的請求x2同A創建了鏈接,此時B開始向A發送數據,數據報文長度爲x2定義的長度,窗口大小爲x2定義的大小,而A創建的鏈接是x1,其數據包長度大小爲x1,TCP窗口大小爲x1定義,這就會致使A處理數據時出錯。
        很顯然,若是A接收到B的請求後,A向B發送SYN請求y3(y3的窗口大小和數據報長度等信息爲x1所定義),確認了鏈接創建的窗口大小和數據報長度爲x1所定義,A再次確認回答創建x1鏈接,而後開始相互傳送數據,那麼就不會致使數據處理出錯了。

 

 

 

4. timewait狀態的理解

這裏寫圖片描述

1. time_wait狀態如何產生?
由上面的變遷圖,首先調用close()發起主動關閉的一方,在發送最後一個ACK以後會進入time_wait的狀態,也就說該發送方會保持2MSL時間以後纔會回到初始狀態。MSL值得是數據包在網絡中的最大生存時間。產生這種結果使得這個TCP鏈接在2MSL鏈接等待期間,定義這個鏈接的四元組(客戶端IP地址和端口,服務端IP地址和端口號)不能被使用。

2.time_wait狀態產生的緣由

1)爲實現TCP全雙工鏈接的可靠釋放

由TCP狀態變遷圖可知,假設發起主動關閉的一方(client)最後發送的ACK在網絡中丟失,因爲TCP協議的重傳機制,執行被動關閉的一方(server)將會重發其FIN,在該FIN到達client以前,client必須維護這條鏈接狀態,也就說這條TCP鏈接所對應的資源(client方的local_ip,local_port)不能被當即釋放或從新分配,直到另外一方重發的FIN達到以後,client重發ACK後,通過2MSL時間週期沒有再收到另外一方的FIN以後,該TCP鏈接才能恢復初始的CLOSED狀態。若是主動關閉一方不維護這樣一個TIME_WAIT狀態,那麼當被動關閉一方重發的FIN到達時,主動關閉一方的TCP傳輸層會用RST包響應對方,這會被對方認爲是有錯誤發生,然而這事實上只是正常的關閉鏈接過程,並不是異常。

2)爲使舊的數據包在網絡因過時而消失

爲說明這個問題,咱們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP鏈接:(local_ip, local_port, remote_ip,remote_port),因某些緣由,咱們先關閉,接着很快以相同的四元組創建一條新鏈接。本文前面介紹過,TCP鏈接由四元組惟一標識,所以,在咱們假設的狀況中,TCP協議棧是沒法區分先後兩條TCP鏈接的不一樣的,在它看來,這根本就是同一條鏈接,中間先釋放再創建的過程對其來講是「感知」不到的。這樣就可能發生這樣的狀況:前一條TCP鏈接由local peer發送的數據到達remote peer後,會被該remot peer的TCP傳輸層當作當前TCP鏈接的正常數據接收並向上傳遞至應用層(而事實上,在咱們假設的場景下,這些舊數據到達remote peer前,舊鏈接已斷開且一條由相同四元組構成的新TCP鏈接已創建,所以,這些舊數據是不該該被向上傳遞至應用層的),從而引發數據錯亂進而致使各類沒法預知的詭異現象。做爲一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種狀況的發生,這正是TIME_WAIT狀態存在的第2個緣由。

3)總結
具體而言,local peer主動調用close後,此時的TCP鏈接進入TIME_WAIT狀態,處於該狀態下的TCP鏈接不能當即以一樣的四元組創建新鏈接,即發起active close的那方佔用的local port在TIME_WAIT期間不能再被從新分配。因爲TIME_WAIT狀態持續時間爲2MSL,這樣保證了舊TCP鏈接雙工鏈路中的舊數據包均因過時(超過MSL)而消失,此後,就能夠用相同的四元組創建一條新鏈接而不會發生先後兩次鏈接數據錯亂的狀況。

3.time_wait狀態如何避免

首先服務器能夠設置SO_REUSEADDR套接字選項來通知內核,若是端口忙,但TCP鏈接位於TIME_WAIT狀態時能夠重用端口。在一個很是有用的場景就是,若是你的服務器程序中止後想當即重啓,而新的套接字依舊但願使用同一端口,此時SO_REUSEADDR選項就能夠避免TIME_WAIT狀態。

 

關於tcp中time_wait狀態的4個問題

time_wait是個常問的問題。tcp網絡編程中最不easy理解的也是它的time_wait狀態,這也說明了tcp/ip四次揮手中time_wait狀態的重要性。
如下經過4個問題來描寫敘述它


問題

1.time_wait狀態是什麼

2.爲何會有time_wait狀態

3.哪一方會有time_wait狀態

4.怎樣避免time_wait狀態佔用資源


1.time_wait狀態是什麼

簡單來講:time_wait狀態是四次揮手中server向client發送FIN終止鏈接後進入的狀態。

下圖爲tcp四次揮手過程
這裏寫圖片描寫敘述
可以看到time_wait狀態存在於client收到serverFin並返回ack包時的狀態
當處於time_wait狀態時,咱們沒法建立新的鏈接,因爲port被佔用。


2.爲何會有time_wait狀態

time_wait存在的緣由有兩點
1.可靠的終止TCP鏈接。
2.保證讓遲來的TCP報文段有足夠的時間被識別並丟棄。

1.可靠的終止TCP鏈接,若處於time_wait的client發送給server確認報文段丟失的話,server將在此又一次發送FIN報文段,那麼client必須處於一個可接收的狀態就是time_wait而不是close狀態。
2.保證遲來的TCP報文段有足夠的時間被識別並丟棄,linux 中一個TCPport不能打開兩次或兩次以上。當client處於time_wait狀態時咱們將沒法使用此port創建新鏈接,假設不存在time_wait狀態,新鏈接可能會收到舊鏈接的數據。

 

3.哪一方會有time_wait狀態

time_wait狀態是通常有client的狀態。

而且會佔用port
有時產生在server端,因爲server主動斷開鏈接或者發生異常


4.怎樣避免time_wait狀態佔用資源

假設是client,咱們通常不用操心,因爲client通常選用暫時port。再次建立鏈接會新分配一個port。

除非指定client使用某port,只是通常不需要這麼作。

假設是server主動關閉鏈接後異常終止。則因爲它老是使用用一個知名serverport號,因此鏈接的time_wait狀態將致使它不能從新啓動。只是咱們可以經過socket的選項SO_REUSEADDR來強制進程立刻使用處於time_wait狀態的鏈接佔用的port。
經過socksetopt設置後,即便sock處於time_wait狀態,與之綁定的socket地址也可以立刻被重用。

此外也可以經過改動內核參數/proc/sys/net/ipv4/tcp_tw/recycle來高速回收被關閉的socket,從而是tcp鏈接根本不進入time_wait狀態,進而贊成應用程序立刻重用本地的socket地址。

 

 

談談TCP中的TIME_WAIT

在服務端可能會常常遇到有不少處於TIMEWAIT狀態的TCP鏈接。若是上網一搜索,能夠找到有不少關於處理TIMEWAIT不正確的博文(包括本文),不少文章就放了幾個調整參數。至於這些參數有什麼用,爲何要調整爲那個值就沒有深刻地介紹了。這就好像生了病不去找醫生了解病情,而是隨便從別人的藥箱裏面找點藥來吃,看看有沒有效果,也無論別人的藥是否過時,是否對症。因此,本文也來湊個熱鬧,來談談TIME_WAIT。

爲何要有TIME_WAIT?

TIME_WAIT是TCP主動關閉鏈接一方的一個狀態,TCP斷開鏈接的時序圖以下:image

當主動斷開鏈接的一方(Initiator)發送FIN包給對方,且對方回覆了ACK+FIN,而後Initiator回覆了ACK後就進入TIME_WAIT狀態,一直將持續2MSL後進入CLOSED狀態。

那麼,咱們來看若是Initiator不進入TIME_WAIT狀態而是直接進入CLOSED狀態會有什麼問題?

考慮這種狀況,服務器運行在80端口,客戶端使用的鏈接端口是12306,數據傳輸完畢後服務端主動關閉鏈接,可是沒有進入TIME_WAIT,而是直接計入CLOSED了。這時,客戶端又經過一樣的端口12306與服務端創建了一個新的鏈接。假如上一個鏈接過程當中網絡出現了異常,致使了某個包重傳並延時到達了服務端,這時服務端就沒法區分這個包是上一個鏈接的仍是這個鏈接的。因此,主動關閉鏈接一方要等待2MSL,而後才能CLOSE,保證鏈接中的IP包都要麼傳輸完成,要麼被丟棄了。

TIME_WAIT會帶來什麼問題

系統中TIME_WAIT的鏈接數不少,會致使什麼問題呢?這要分別針對客戶端和服務器端來看的。

首先,若是是客戶端發起了鏈接,傳輸完數據而後主動關閉了鏈接,這時這個鏈接在客戶端就會處於TIMEWAIT狀態,同時佔用了一個本地端口。若是客戶端使用短鏈接請求服務端的資源或者服務,客戶端上將有大量的鏈接處於TIMEWAIT狀態,佔用大量的本地端口。最壞的狀況就是,本地端口都被用光了,這時將沒法再創建新的鏈接。

針對這種狀況,對應的解決辦法有2個:
1. 使用長鏈接,若是是http,能夠使用keepalive
2. 增長本地端口可用的範圍,好比Linux中調整內核參數:net.ipv4.ip_local_port_range

對於服務器而已,因爲服務器是被動等待客戶端創建鏈接的,所以即便服務器端有不少TIME_WAIT狀態的鏈接,也不存在本地端口耗盡的問題。大量的TIME_WAIT的鏈接會致使以下問題:
1. 內存佔用:由於每個TCP鏈接都會有佔用一些內存。
2. 在某些Linux版本上可能致使性能問題,由於數據包到達服務器的時候,內核須要知道數據包是屬於哪一個TCP鏈接的,在某些Linux版本上可能會遍歷全部的TCP鏈接,因此大量TIME_WAIT的鏈接將致使性能問題。不過,如今的內核都對此進行了優化(待確認)。

那系統中處於TIME_WAIT狀態的TCP鏈接數有上限嗎?有的,這是經過net.ipv4.tcp_max_tw_buckets參數來控制的,默認值爲180000。當超過了之後,系統就開始關閉這些鏈接,同時會在系統日誌中打印日誌。此時,能夠將這個值調大一些,從這個參數的默認值就能夠看出,對服務器而已,處於TIME_WAIT狀態的TCP鏈接多點也沒有什麼關係,只是多佔用些內存而已。

常見的TIMEWAIT錯誤參數

若是用TIME_WAIT做爲關鍵字到網絡上搜索,會獲得不少關於如何減小TIME_WAIT數量的建議,其中有些建議是有錯誤或者有風險的,列舉以下:

  1. net.ipv4.tcp_syncookies = 1,這個參數表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊。這個和TIME_WAIT沒有什麼關係。
  2. net.ipv4.tcp_tw_reuse = 1,這個參數表示重用TIME_WAIT的鏈接,重用的條件是TCP的4元組(源地址、源端口、目標地址、目標端口)要徹底一致,並且開啓了net.ipv4.tcp_timestamps,且新創建鏈接的使用的timestamp要大於當前鏈接的timestamp。因此,開啓了這個參數對減小TIME_WAIT的TCP鏈接有點用,但條件太苛刻,因此實際用處不大。
  3. net.ipv4.tcp_tw_recycle = 1,這個參數表示開啓TIME_WAIT回收功能,開啓了這個參數後,將大大減少TIME_WAIT進入CLOSED狀態的時間。可是開啓了這個功能了風險很大,可能會致使處於NAT後面的某些客戶端沒法創建鏈接。由於,開啓這個功能後,它要求來自同一個IP的TCP新鏈接的timestamp要大於以前鏈接的timestamp。

TIMEWAIT的「正確」處理方法

簡單總結一下我對於TIME_WAIT狀態TCP鏈接的理解和處理方法:
1. TIME_WAIT狀態的設計初衷是爲了保護咱們的。服務端沒必要擔憂系統中有幾w個處於TIME_WAIT狀態的TCP鏈接。能夠調大net.ipv4.tcp_max_tw_buckets這個參數。
2. 使用短鏈接的客戶端,須要關注TIME_WAIT狀態的TCP鏈接,建議是採用長鏈接,同時調節參數net.ipv4.ip_local_port_range,增長本地可用端口的範圍。
3. 能夠開啓net.ipv4.tcp_tw_reuse這個參數,可是實際用處有限。
4. 不要開啓net.ipv4.tcp_tw_recycle這個參數,它帶來的問題比用處大。
5. 某些Linux的發行版能夠調節TIME_WAIT到CLOSED的等待時間(好比Ali的Linux內核提供了參數net.ipv4.tcp_tw_timeout
),能夠稍微調小一點這個參數。

 

 

5. linux內核進程調度

1.    Linux進程和線程如何建立、退出?進程退出的時候,本身沒有釋放的資源(如內存沒有free)會怎樣?

解答:

Linux進程經過fork來建立

Linux線程經過pthread_create建立,

 

2.    什麼是寫時拷貝?

解答:

寫時拷貝(copy-on-write, COW)就是等到修改數據時才真正分配內存空間,這是對程序性能的優化,能夠延遲甚至是避免內存拷貝,固然目的就是避免沒必要要的內存拷貝。

Linux 的fork系統調用就使用了寫時拷貝技術,具體細節以下:

如今有一個父進程P1,這是一個主體,那麼它是有靈魂也是有身體的。如今在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部分,相應地,內核要爲這四個部分分配給自的物理塊。即正文段塊、數據段塊、堆塊、棧塊。

1)如今P1用fork()函數爲進程建立一個子進程P2

內核:

(1) 複製P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。

(2) 爲這四個部分分配物理塊,P2的:正文段(爲P1的正文段的物理塊,其實就是不爲P2分配正文段塊,讓P2的正文段指向P1的正文段塊),數據段(P2本身的數據段塊,爲其分配對應的塊),堆(P2本身的堆塊),棧(P2本身的棧塊)。以下圖所示,同左到右大的方向箭頭表示複製內容:

2)寫時複製技術

寫時複製技術:內核只爲新生成的子進程建立虛擬空間結構,它們複製於父進程的虛擬空間結構,可是不爲這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應的段的行爲發生時,再爲子進程相應的段分配物理空間。

3)vfork

vfork的作法更加簡單粗暴,內核連子進程的虛擬地址空間也不建立了,直接共享了父進程的虛擬空間,固然了,這種作法就順水推舟的共享了父進程的物理空間

總結

傳統的fork()系統調用直接把全部的資源複製給新建立的進程。這種實現過於簡單而且效率低下,由於它拷貝的數據也許並不共享,更糟的狀況是,若是新進程打算當即執行一個新的映像,那麼全部的拷貝將是無用功。

Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種能夠推遲甚至免除拷貝數據的技術。內核此時並不複製整個地址空間,而是讓父進程和子進程共享一個拷貝。只有在須要寫入的時候,數據纔會複製,從而使各個進程擁有各自的拷貝。也就是說,資源的複製只有在須要寫入的時候才進行,在此以前,只是以只讀方式共享。這種技術使地址空間的頁的拷貝被推遲到實際發生寫入的時候。

 

3.    Linux的線程如何實現,與進程的本質區別是什麼?

解答:

進程是資源分配和管理的單位,線程是調度的基本單位,進程有獨立的地址空間,擁有PCB,其中包含進程標識符(非負整數)、進程資源、進程調度信息、進程間通訊相關資源、處理機狀態(便於調度後恢復原狀態)等,線程具備單獨的堆棧和寄存器,保存本身容許的相關上下文,具備TCB。各線程還共享如下進程資源和環境: 

1)文件描述符表 
2)每種信號的處理方式(SIG_IGN,SIG_DFL,用戶自定義) 
3)當前工做目錄 
4)用戶id和組id 
但有些資源是線程獨享的: 
1)線程id 
2)上下文,包括各類寄存器的值,程序計數器和棧指針 
3)棧空間 
4)errno變量 
5)信號屏蔽字 
6)調度優先級 

4.    Linux可否知足硬實時的需求?

5.    進程如何睡眠等資源,此後又如何被喚醒?

6.    進程的調度延時是多少?

7.    調度器追求的吞吐率和響應延遲之間是什麼關係?CPU消耗型和I/O消耗型進程的訴求?

8.    Linux怎麼區分進程優先級?實時的調度策略和普通調度策略有什麼區別?

9.    nice值的做用是什麼?nice值低有什麼優點?

10.  Linux能夠被改形成硬實時嗎?有什麼方案?

11.  多核、多線程的狀況下,Linux如何實現進程的負載均衡?

12.  這麼多線程,究竟哪一個線程在哪一個CPU核上跑?有沒有辦法把某個線程固定到某個CPU跑?

13.  多核下如何實現中斷、軟中斷的負載均衡?

14.  如何利用cgroup對進行進程分組,並調控各個group的CPU資源?

15.  CPU利用率和CPU負載之間的關係?CPU負載高必定用戶體驗差嗎?

 

 

帶着問題上路

一切的學習都是爲了解決問題,而不是爲了學習而學習。爲了學習而學習,這種行爲實在是太傻了,由於最終也學很差。因此咱們要弄清楚進程調度和內存管理究竟能解決什麼樣的問題。

Linux進程調度以及配套的進程管理回答以下問題:

1.    Linux進程和線程如何建立、退出?進程退出的時候,本身沒有釋放的資源(如內存沒有free)會怎樣?

2.    什麼是寫時拷貝?

3.    Linux的線程如何實現,與進程的本質區別是什麼?

4.    Linux可否知足硬實時的需求?

5.    進程如何睡眠等資源,此後又如何被喚醒?

6.    進程的調度延時是多少?

7.    調度器追求的吞吐率和響應延遲之間是什麼關係?CPU消耗型和I/O消耗型進程的訴求?

8.    Linux怎麼區分進程優先級?實時的調度策略和普通調度策略有什麼區別?

9.    nice值的做用是什麼?nice值低有什麼優點?

10.  Linux能夠被改形成硬實時嗎?有什麼方案?

11.  多核、多線程的狀況下,Linux如何實現進程的負載均衡?

12.  這麼多線程,究竟哪一個線程在哪一個CPU核上跑?有沒有辦法把某個線程固定到某個CPU跑?

13.  多核下如何實現中斷、軟中斷的負載均衡?

14.  如何利用cgroup對進行進程分組,並調控各個group的CPU資源?

15.  CPU利用率和CPU負載之間的關係?CPU負載高必定用戶體驗差嗎?

Linux內存管理回答以下問題:

1.    Linux系統的內存用掉了多少,還剩餘多少?下面這個free命令每個數字是什麼意思?

2.    爲何要有DMA、NORMAL、HIGHMEM zone?每一個zone的大小是由誰決定的?

3.    系統的內存是如何被內核和應用瓜分掉的?

4.    底層的內存管理算法buddy是怎麼工做的?它和內核裏面的slab分配器是什麼關係?

5.    頻繁的內存申請和釋放是否會致使內存的碎片化?它的後果是什麼?

6.    Linux內存耗盡後,系統會發生怎樣的狀況?

7.    應用程序的內存是何時拿到的?malloc()成功後,是否真的拿到了內存?應用程序的malloc()與free()與內核的關係到底是什麼?

8.    什麼是lazy分配機制?應用的內存爲何會延後以最懶惰的方式拿到?

9.    我寫的應用究竟耗費了多少內存?進程的vss/rss/pss/uss分別是什麼概念?虛擬的,真實的,共享的,獨佔的,究竟哪一個是哪一個?

10.  內存爲何要作文件系統的緩存?如何作?緩存什麼時候放棄?

11.  Free命令裏面顯示的buffers和cached分別是什麼?兩者有何區別?

12.  交換分區、虛擬內存到底是什麼鬼?它們針對的是什麼性質的內存?什麼是匿名頁?

13.  進程耗費的內存、文件系統的緩存什麼時候回收?回收的算法是否是相似LRU?

14.  怎樣追蹤和判決發生了內存泄漏?內存泄漏後如何查找泄漏源?

15.  內存大小這樣影響系統的性能?CPU、內存、I/O三角如何互動?它們如何綜合決定系統的一些關鍵性能?

以上問題,若是您都能回答,那麼恭喜您,您是一個概念清楚的人,Linux出現吞吐低、延遲大、響應慢等問題的時候,你能夠找到一個可能的方向。若是您只能回答低於1/3的問題,那麼,Linux對您仍然是一片空白,出現問題,您只會陷入瞎貓子亂抓,而撈不到耗子的困境,或者胡亂地意測問題,陷入不斷的低水平重試。

試圖回答這些問題

本文的目的不是回答這些問題,由於回答這些問題,須要洋洋灑灑數百頁的文檔,而本文檔不會超過10頁。因此,本文的目的是試圖給出一個回答這些問題的思考問題的出發點,咱們倡導面對任何問題的時候,先要弄明白系統的設計目標。

吞吐vs.響應

首先咱們在思考調度器的時候,咱們要理解任何操做系統的調度器設計只追求2個目標:吞吐率大和延遲低。這2個目標有點相似零和遊戲,由於吞吐率要大,勢必要把更多的時間放在作真實的有用功,而不是把時間浪費在頻繁的進程上下文切換;而延遲要低,勢必要求優先級高的進程能夠隨時搶佔進來,打斷別人,強行插隊。可是,搶佔會引發上下文切換,上下文切換的時間自己對吞吐率來說,是一個消耗,這個消耗能夠低到2us或者更低(這看起來沒什麼?),可是上下文切換更大的消耗不是切換自己,而是切換會引發大量的cache miss。你明明weibo跑的很爽,如今切過去微信,那麼CPU的cache是不太容易命中微信的。

不搶確定響應差,搶了吞吐會降低。Linux不是一個徹底照顧吞吐的系統,也不是一個徹底照顧響應的系統,它做爲一個軟實時的操做系統,其實是想達到某種平衡,同時也提供給用戶必定的配置能力,在內核編譯的時候,Kernel Features  --->  Preemption Model選項實際上可讓咱們編譯內核的時候,是傾向於支持吞吐,仍是支持響應:

越往上面選,吞吐越好,越好下面選,響應越好。服務器你一個月也可貴用一次鼠標,而桌面則顯然要求必定的響應,這樣能夠保證UI行爲的表現較好。可是Linux即使選擇的是最後一個選項「Preemptible Kernel (Low-Latency Desktop)」,它仍然不是硬實時的。由於,在Linux有三類區間是不能夠搶佔調度的,這三類區間是:

  • 中斷
  • 軟中斷
  • 持有相似spin_lock這樣的鎖而鎖住該CPU核調度的狀況
以下圖,一個綠色的普通進程在T1時刻持有spin_lock進入一個critical section(該核調度被關),綠色進程T2時刻被中斷打斷,然後T3時刻IRQ1裏面喚醒了紅色的RT進程(若是是硬實時RTOS,這個時候RT進程應該能搶入),以後IRQ1後又執行了IRQ2,到T4時刻IRQ1和IRQ2都結束了,紅色RT進程仍然不能執行(由於綠色進程還在spin_lock裏面),直到T5時刻,普通進程釋放spin_lock後,紅色RT進程才搶入。從T3到T5要多久, 鬼都不知道,這樣就沒法知足硬實時系統的「 可預期」延遲性,所以Linux不是硬實時操做系統。

Linux的preempt-rt補丁試圖把中斷、軟中斷線程化,變成能夠被搶佔的區間,而把會關本核調度器的spin_lock替換爲能夠調度的mutex,它實現了在T3時刻喚醒RT進程的時刻,RT進程能夠當即搶佔調度進入的目標,避免了T3-T5之間延遲的非肯定性。

CPU消耗型 vs. I/O消耗型

在Linux運行的進程,分爲2類,一類是CPU消耗型(狂算),一類是I/O消耗型(狂睡,等I/O),前者CPU利用率高,後者CPU利用率低。通常而言,I/O消耗型任務對延遲比較敏感,應該被優先調度。好比,你正在瘋狂編譯安卓,而等鼠標行爲的用戶界面老不工做(正在狂睡),可是鼠標一點,咱們應該優先打斷正在編譯的進程,而去響應鼠標這個I/O,這樣電腦的用戶體驗才符合人性。

Linux的進程,對於RT進程而言,按照SCHED_FIFO和SCHED_RR的策略,優先級高先執行;優先級高的睡眠了後優先級的執行;同等優先級的SCHED_FIFO先ready的跑到睡,後ready的接着跑;而同等優先級的RR則進行時間片輪轉。好比Linux存在以下4個進程,T1~T4(內核裏面優先級數字越低,優先級越高):

那麼它們在Linux的跑法就是:

 
RT的進程調度有一點「惡霸」色彩,我高優先級的沒睡,低優先級的你就靠邊站。可是Linux的絕大多數進程都不是RT的進程,而是採用SCHED_NORMAL策略(這符合蜘蛛俠法則)。NORMAL的人比較善良,咱們通常用nice來形容它們的優先級,nice越高,優先級越低(你越nice,就越喜歡在地鐵讓座,固然越坐不到座位)。普通進程的跑法,並非nice低的必定堵着nice高的(要否則還說什麼「善良」),它是按照以下公式進行:
vruntime =  pruntime * NICE_0_LOAD/ weight

其中NICE_0_LOAD是1024,也就是NICE是0的進程的weight。vruntime是進程的虛擬運行時間,pruntime是物理運行時間,weight是權重,權重徹底由nice決定,以下表:

 
在RT進程都睡過去以後(有一個特例就是RT沒睡也會跑普通進程,那就是RT加起來跑地實在過久過久,普通進程必須喝點湯了),Linux開始跑NORMAL的,它傾向於調度vruntime(虛擬運行時間)最小的普通進程,根據咱們小學數學知識,vruntime要小,要麼分子小(喜歡睡,I/O型進程,pruntime不容易長大),要麼分母大(nice值低,優先級高,權重大)。這樣一個簡單的公式,就同時照顧了普通進程的優先級和CPU/IO消耗狀況。
好比有4個普通進程,以下表,目前顯然T1的vruntime最小(這是它喜歡睡的結果),而後T1被調度到。
 

pruntime

Weight

vruntime

T1

8

1024(nice=0)

8*1024/1024=8

T2

10

526(nice=3)

10*1024/526 =19

T3

20

1024(nice=0)

20*1024/1024=20

T4

20

820(nice=1)

20*1024/820=24

而後,咱們假設T1被調度再執行12個pruntime,它的vruntime將增大delta*1024/weight(這裏delta是12,weight是1024),因而T1的vruntime成爲20,那麼這個時候vruntime最小的反而是T2(爲19),此後,Linux將傾向於調度T2(儘管T2的nice值大於T1,優先級低於T1,可是它的vruntime如今只有19)。
因此,普通進程的調度,是一個綜合考慮你喜歡幹活仍是喜歡睡和你的nice值是多少的結果。鑑於此,咱們去問一個普通進程的調度延遲究竟有多大,這個問題,自己意義就不是特別大,它徹底取決於當前的系統裏面還有誰在跑,取決於你喚醒的進程的nice和它前面喜歡不喜歡睡覺。
明白了這一點,你就不會在Linux裏面問一些讓回答的人吐血的問題。好比,一個普通進程多久被調度到?明確地說,不知道!裝逼的說法,就是「depend on …」,依賴的東西太多。再裝逼的說法,就是「一言難盡」,但這也是大實話。

分配vs. 佔據

Linux做爲一個把應用程序員當傻逼的操做系統,它必須容許應用程序犯錯。因此這類問題就不要問了:進程malloc()了內存,尚未free()就掛了,那麼我前面分配的內存沒有釋放,是否是就泄漏掉了?明確的說,這是不可能的,Linux內核若是這麼傻,它是沒法應付亂七八糟的各類開源有漏洞軟件的,因此進程死的時候,確定是資源皆被內核釋放的,這類傻問題,你明白Linux的出發點,就不會再去問了。

一樣的,你在應用程序裏面malloc()成功的一刻,也不要覺得真的拿到了內存,這個時候你的vss(虛擬地址空間,Virtual Set Size)會增大,可是你的rss(駐留在內存條上的內存,Resident SetSize)內存會隨着寫到每一頁而緩慢增大。因此,分配成功的一刻,頂多只是被忽悠了,和你實際佔有仍是不佔有,暫時沒有半毛錢關係。

以下圖,最初的堆是8KB,這8KB也寫過了,因此堆的vss和rss都是8KB。此後咱們調用brk()把堆變大到16KB,可是實際上它佔據的內存rss仍是8KB,由於第3頁尚未寫,根本沒有真正從內存條上拿到內存。直到寫第3頁,堆的rss才變爲12KB。這就是Linux針對app的lazy分配機制,它的出發點,固然也是防止應用程序傻逼了。

代碼段的內存、堆的內存、棧的內存都是這樣懶惰地拿到,demanding page。

咱們有一臺1GB內存的32位Linux系統,咱們關閉swap,同時透過修改overcommit_memory爲1來容許申請不超過進程虛擬地址空間的內存:

$ sudo swapoff -a

$ sudo sh -c 'echo 1 >/proc/sys/vm/overcommit_memory'

此後,咱們的應用能夠申請一個超級大的內存(比實際內存還大):

上述程序在1GB的電腦上面運行,申請2GB內存能夠申請成功,可是在寫到必定程度後,系統出現out-of-memory,上述程序對應的進程做爲oom_score最大(最該死的)的進程被系統殺死。

隔離vs. 共享

Linux進程究竟耗費了多少內存,是一個很是複雜的概念,除了上面的vss, rss外,還有pss和uss,這些都是Linux不一樣於RTOS的顯著特色之一。Linux各個進程既要作到隔離,可是隔離中又要實現共享,好比1000個進程都用libc,libc的代碼段顯然在內存只應該有一份。

下面的一幅圖上有3個進程,pid爲1044的 bash、pid爲1045的 bash和pid爲1054的 cat。每一個進程透過本身的頁表,把虛擬地址空間指向內存條上面的物理地址,每次切換一個進程,即切換一份獨特的頁表。

 

僅今後圖而言,進程1044的vss和rss分別是:

vss= 1+2+3

rss= 4+5+6

可是是否是「4+5+6」就是1044這個進程耗費的內存呢?這顯然也是不許確的,由於4明顯被3個進程指向,5明顯被2個進程指向,壞事是你們一塊兒乾的,不能1044一我的背黑鍋。這個時候,就衍生出了一個pss(按比例計算的駐留內存, Proportional Set Size )的概念,僅從這一幅圖而言,進程1044的pss爲:

rss= 4/3 +5/2 +6

最後,還有進程1044獨佔且駐留的內存uss(Unique Set Size ),僅今後圖而言,

Uss = 6。

因此,分析Linux,咱們不能模棱兩可地停留於表面,或者想固然地說:「Linux的進程耗費了多少內存?」由於這個問題,又是一個要靠裝逼來回答的問題,「dependon…」。坦白講,每次當我問到老外問題,老外第一句話就是「depend on…」的時候,我就想上去抽他了,可是我又抑制了這個衝動,由於,不少問題,不是簡單的0和1問題,正反問題,黑白問題,它確實是一個「depend on …」的問題。

有時候,小白問大拿一個問題,大拿實在是沒法正面回答,因而就支支吾吾一番。這個時候小白會很生氣,以爲大拿態度很差,或者在裝逼。你實際上,明白不少問題不是簡單的0與1問題以後,你就會理解,他真的不是在裝逼。這個時候,咱們要反過來反省本身,是否是咱們本身問的問題太LOW逼了?

思考大於接受

咱們前面提出了30個問題,而本文也僅僅只是回答了其中極少的一部分。此文的目的在於創建思惟,導入方向,而不是洋洋灑灑地把全部問題回答掉,由於哥確實沒有時間寫個幾百頁的文檔來一一回答這些問題。不少事情,用口頭描述,比直接寫冗長地文檔要更加容易也輕鬆。

最後,我仍然想要強調的一個觀點是,咱們在思惟Linux的時候,更多地能夠把本身想象成Linus Torvalds,若是你是Linus Torvalds,你要設計Linux,你碰到某個訴求,好比調度器和內存方面的訴求,你應該如何解決。咱們不是被動地接受「是什麼」,更多地要思考「爲何」,「怎麼辦」。

若是你是Linus Torvalds,有個傻逼應用程序員要申請1GB內存,你是直接給他,仍是僞裝給他,可是實際沒有給他,直到它寫的時候再給他?

若是你是Linus Torvalds,有個傢伙打開了串口,而後進程就作個1/0運算或者訪問空指針掛了,你要不要在這個進程掛的時候給它關閉串口?

若是你是Linus Torvalds,你是要讓nice值低(優先級高)的普通進程在睡眠前一直堵着nice值高的進程,仍是雖然它優先級高,可是因爲跑的時間比較長後,也要讓給優先級低(nice值高)的進程?若是你認爲nice值低的應該一直跑,那麼如何照顧喜歡睡覺的I/O消耗型進程?萬一nice值低的進程有bug,進入死循環,那麼nice高的進程豈不是絲毫機會都沒有?這樣的設計,是否是反人類?

當你帶着這些思考,武裝這些concept,再去看Linux的時候,你就從被動的「接受」,變成了主動地「思考」,這正好是任何一個優秀程序員都具有的品質,也是打通進程調度和內存管理任督二脈的關鍵。

 

linux進程調度淺析

     操做系統要實現多進程,進程調度必不可少。
      進程調度是對TASK_RUNNING狀態的進程進行調度(參見《linux進程狀態淺析》)。若是進程不可執行(正在睡眠或其餘),那麼它跟進程調度沒多大關係。
      因此,若是你的系統負載很是低,盼星星盼月亮纔出現一個可執行狀態的進程。那麼進程調度也就不會過重要。哪一個進程可執行,就讓它執行去,沒有什麼須要多考慮的。
      反之,若是系統負載很是高,時時刻刻都有N多個進程處於可執行狀態,等待被調度運行。那麼進程調度程序爲了協調這N個進程的執行,一定得作不少工做。協調得很差,系統的性能就會大打折扣。這個時候,進程調度就是很是重要的。
      儘管咱們日常接觸的不少計算機(如桌面系統、網絡服務器、等)負載都比較低,可是Linux做爲一個通用操做系統,不能假設系統負載低,必須爲應付高負載下的進程調度作精心的設計。
      固然,這些設計對於低負載(且沒有什麼實時性要求)的環境,沒多大用。極端狀況下,若是CPU的負載始終保持0或1(永遠都只有一個進程或沒有進程須要在CPU上運行),那麼這些設計基本上都是徒勞的。


優先級
      如今的操做系統爲了協調多個進程的「同時」運行,最基本的手段就是給進程定義優先級。定義了進程的優先級,若是有多個進程同時處於可執行狀態,那麼誰優先級高誰就去執行,沒有什麼好糾結的了。
      那麼,進程的優先級該如何肯定呢?有兩種方式:由用戶程序指定、由內核的調度程序動態調整。(下面會說到)
       linux內核將進程分紅兩個級別:普通進程和實時進程。實時進程的優先級都高於普通進程,除此以外,它們的調度策略也有所不一樣。

實時進程的調度
      實時,本來的涵義是「給定的操做必定要在肯定的時間內完成」。重點並不在於操做必定要處理得多快,而是時間要可控(在最壞狀況下也不能突破給定的時間)。
      這樣的「實時」稱爲「硬實時」,多用於很精密的系統之中(好比什麼火箭、導彈之類的)。通常來講,硬實時的系統是相對比較專用的。
      像linux這樣的通用操做系統顯然無法知足這樣的要求,中斷處理、虛擬內存、等機制的存在給處理時間帶來了很大的不肯定性。硬件的cache、磁盤尋道、總線爭用、也會帶來不肯定性。
      好比考慮「i++;」這麼一句C代碼。絕大多數狀況下,它執行得很快。可是極端狀況下仍是有這樣的可能:
  一、i的內存空間未分配,CPU觸發缺頁異常。而linux在缺頁異常的處理代碼中試圖分配內存時,又可能因爲系統內存緊缺而分配失敗,致使進程進入睡眠;
  二、代碼執行過程當中硬件產生中斷,linux進入中斷處理程序而擱置當前進程。而中斷處理程序的處理過程當中又可能發生新的硬件中斷,中斷永遠嵌套不止……;等等……
      而像linux這樣號稱實現了「實時」的通用操做系統,其實只是實現了「軟實時」,即儘量地知足進程的實時需求。
      若是一個進程有實時需求(它是一個實時進程),則只要它是可執行狀態的,內核就一直讓它執行,以儘量地知足它對CPU的須要,直到它完成所須要作的事情,而後睡眠或退出(變爲非可執行狀態)。
      而若是有多個實時進程都處於可執行狀態,則內核會先知足優先級最高的實時進程對CPU的須要,直到它變爲非可執行狀態。因而,只要高優先級的實時進程一直處於可執行狀態,低優先級的實時進程就一直不能獲得CPU;只要一直有實時進程處於可執行狀態,普通進程就一直不能獲得CPU。
後來,內核添加了/proc/sys/kernel/sched_rt_runtime_us和/proc/sys/kernel/sched_rt_period_us兩個參數,限定了在以sched_rt_period_us爲週期的時間內,實時進程最多隻能運行sched_rt_runtime_us這麼多時間。這樣就在一直有實時進程處於可執行狀態的狀況下,給普通進程留了一點點可以獲得執行的機會。參閱《linux組調度淺析》。)

       那麼,若是多個相同優先級的實時進程都處於可執行狀態呢?這時就有兩種調度策略可供選擇:
  一、SCHED_FIFO:先進先出。直到先被執行的進程變爲非可執行狀態,後來的進程才被調度執行。在這種策略下,先來的進程能夠行sched_yield系統調用,自願放棄CPU,以讓權給後來的進程;
  二、SCHED_RR:輪轉調度。內核爲實時進程分配時間片,在時間片用完時,讓下一個進程使用CPU;
      強調一下,這兩種調度策略僅僅針對於相同優先級的多個實時進程同時處於可執行狀態的狀況。

      在linux下,用戶程序能夠經過sched_setscheduler系統調用來設置進程的調度策略以及相關調度參數sched_setparam系統調用則只用於設置調度參數這兩個系統調用要求用戶進程具備設置進程優先級的能力(CAP_SYS_NICE,通常來講須要root權限)(參閱capability相關的文章)。
經過將進程的策略設爲SCHED_FIFO或SCHED_RR,使得進程變爲實時進程。而進程的優先級則是經過以上兩個系統調用在設置調度參數時指定的。

      對於實時進程,內核不會試圖調整其優先級。由於進程實時與否?有多實時?這些問題都是跟用戶程序的應用場景相關,只有用戶可以回答,內核不能臆斷。

      綜上所述,實時進程的調度是很是簡單的。進程的優先級和調度策略都由用戶定死了,內核只須要老是選擇優先級最高的實時進程來調度執行便可。惟一稍微麻煩一點的只是在選擇具備相同優先級的實時進程時,要考慮兩種調度策略。

普通進程的調度
      實時進程調度的中心思想是,讓處於可執行狀態的最高優先級的實時進程儘量地佔有CPU,由於它有實時需求;而普通進程則被認爲是沒有實時需求的進程,因而調度程序力圖讓各個處於可執行狀態的普通進程和平共處地分享CPU,從而讓用戶以爲這些進程是同時運行的。
與實時進程相比,普通進程的調度要複雜得多。內核須要考慮兩件麻煩事:

1、動態調整進程的優先級
      按進程的行爲特徵,能夠將進程分爲「交互式進程」和「批處理進程」:
      交互式進程(如桌面程序、服務器、等)主要的任務是與外界交互。這樣的進程應該具備較高的優先級,它們老是睡眠等待外界的輸入。而在輸入到來,內核將其喚醒時,它們又應該很快被調度執行,以作出響應。好比一個桌面程序,若是鼠標點擊後半秒種還沒反應,用戶就會感受系統「卡」了;
批處理進程(如編譯程序)主要的任務是作持續的運算,於是它們會持續處於可執行狀態。這樣的進程通常不須要高優先級,好比編譯程序多運行了幾秒種,用戶多半不會太在乎;

      若是用戶可以明確知道進程應該有怎樣的優先級,能夠經過nicesetpriority(非實時進程優先級的設置)系統調用來對優先級進行設置。(若是要提升進程的優先級,要求用戶進程具備CAP_SYS_NICE能力。)
      然而應用程序未必就像桌面程序、編譯程序這樣典型。程序的行爲可能五花八門,可能一下子像交互式進程,一下子又像批處理進程。以至於用戶難以給它設置一個合適的優先級。再者,即便用戶明確知道一個進程是交互式仍是批處理,也多半礙於權限或由於偷懶而不去設置進程的優先級。(你又是否爲某個程序設置過優先級呢?)
      因而,最終,區分交互式進程和批處理進程的重任就落到了內核的調度程序上。

      調度程序關注進程近一段時間內的表現(主要是檢查其睡眠時間和運行時間),根據一些經驗性的公式,判斷它如今是交互式的仍是批處理的?程度如何?最後決定給它的優先級作必定的調整。
進程的優先級被動態調整後,就出現了兩個優先級:
  一、用戶程序設置的優先級(若是未設置,則使用默認值),稱爲靜態優先級。這是進程優先級的基準,在進程執行的過程當中每每是不改變的;
  二、優先級動態調整後,實際生效的優先級。這個值是可能時時刻刻都在變化的;

2、調度的公平性
      在支持多進程的系統中,理想狀況下,各個進程應該是根據其優先級公平地佔有CPU。而不會出現「誰運氣好誰佔得多」這樣的不可控的狀況。
      linux實現公平調度基本上是兩種思路:
  一、給處於可執行狀態的進程分配時間片(按照優先級),用完時間片的進程被放到「過時隊列」中。等可執行狀態的進程都過時了,再從新分配時間片;
  二、動態調整進程的優先級。隨着進程在CPU上運行,其優先級被不斷調低,以便其餘優先級較低的進程獲得運行機會;
      後一種方式有更小的調度粒度,而且將「公平性」與「動態調整優先級」兩件事情合而爲一,大大簡化了內核調度程序的代碼。所以,這種方式也成爲內核調度程序的新寵。

      強調一下,以上兩點都是僅針對普通進程的。而對於實時進程,內核既不能自做多情地去動態調整優先級,也沒有什麼公平性可言。

      普通進程具體的調度算法很是複雜,而且隨linux內核版本的演變也在不斷更替(不只僅是簡單的調整),因此本文就不繼續深刻了。有興趣的朋友能夠參考下面的連接:《Linux 調度器發展簡述

調度程序的效率
      「優先級」明確了哪一個進程應該被調度執行,而調度程序還必需要關心效率問題。調度程序跟內核中的不少過程同樣會頻繁被執行,若是效率不濟就會浪費不少CPU時間,致使系統性能降低。
      在linux 2.4時,可執行狀態的進程被掛在一個鏈表中。每次調度,調度程序須要掃描整個鏈表,以找出最優的那個進程來運行。複雜度爲O(n);
      在linux 2.6早期,可執行狀態的進程被掛在N(N=140)個鏈表中,每個鏈表表明一個優先級,系統中支持多少個優先級就有多少個鏈表。每次調度,調度程序只須要從第一個不爲空的鏈表中取出位於鏈表頭的進程便可。這樣就大大提升了調度程序的效率,複雜度爲O(1);
      在linux 2.6近期的版本中,可執行狀態的進程按照優先級順序被掛在一個紅黑樹(能夠想象成平衡二叉樹)中。每次調度,調度程序須要從樹中找出優先級最高的進程。複雜度爲O(logN)。

      那麼,爲何從linux 2.6早期到近期linux 2.6版本,調度程序選擇進程時的複雜度反而增長了呢?
      這是由於,與此同時,調度程序對公平性的實現從上面提到的第一種思路改變爲第二種思路(經過動態調整優先級實現)。而O(1)的算法是基於一組數目不大的鏈表來實現的,按個人理解,這使得優先級的取值範圍很小(區分度很低),不能知足公平性的需求。而使用紅黑樹則對優先級的取值沒有限制(能夠用32位、64位、或更多位來表示優先級的值),而且O(logN)的複雜度也仍是很高效的。

調度觸發的時機
      調度的觸發主要有以下幾種狀況:
  一、當前進程(正在CPU上運行的進程)狀態變爲非可執行狀態。
進程執行系統調用主動變爲非可執行狀態。好比執行nanosleep進入睡眠、執行exit退出、等等;
進程請求的資源得不到知足而被迫進入睡眠狀態。好比執行read系統調用時,磁盤高速緩存裏沒有所須要的數據,從而睡眠等待磁盤IO;
進程響應信號而變爲非可執行狀態。好比響應SIGSTOP進入暫停狀態、響應SIGKILL退出、等等;

  二、搶佔。進程運行時,非預期地被剝奪CPU的使用權。這又分兩種狀況:進程用完了時間片、或出現了優先級更高的進程。
優先級更高的進程受正在CPU上運行的進程的影響而被喚醒。如發送信號主動喚醒,或由於釋放互斥對象(如釋放鎖)而被喚醒;
內核在響應時鐘中斷的過程當中,發現當前進程的時間片用完;
內核在響應中斷的過程當中,發現優先級更高的進程所等待的外部資源的變爲可用,從而將其喚醒。好比CPU收到網卡中斷,內核處理該中斷,發現某個socket可讀,因而喚醒正在等待讀這個socket的進程;再好比內核在處理時鐘中斷的過程當中,觸發了定時器,從而喚醒對應的正在nanosleep系統調用中睡眠的進程;

其餘問題
一、內核搶佔
      理想狀況下,只要知足「出現了優先級更高的進程」這個條件,當前進程就應該被馬上搶佔。可是,就像多線程程序須要用鎖來保護臨界區資源同樣,內核中也存在不少這樣的臨界區,不大可能隨時隨地都能接收搶佔。
      linux 2.4時的設計就很是簡單,內核不支持搶佔。進程運行在內核態時(好比正在執行系統調用、正處於異常處理函數中),是不容許搶佔的。必須等到返回用戶態時纔會觸發調度(確切的說,是在返回用戶態以前,內核會專門檢查一下是否須要調度);
      linux 2.6則實現了內核搶佔,可是在不少地方仍是爲了保護臨界區資源而須要臨時性的禁用內核搶佔。

      也有一些地方是出於效率考慮而禁用搶佔,比較典型的是spin_lock。spin_lock是這樣一種鎖,若是請求加鎖得不到知足(鎖已被別的進程佔有),則當前進程在一個死循環中不斷檢測鎖的狀態,直到鎖被釋放。
      爲何要這樣忙等待呢?由於臨界區很小,好比只保護「i+=j++;」這麼一句。若是由於加鎖失敗而造成「睡眠-喚醒」這麼個過程,就有些得不償失了。那麼既然當前進程忙等待(不睡眠),誰又來釋放鎖呢?其實已獲得鎖的進程是運行在另外一個CPU上的,而且是禁用了內核搶佔的。這個進程不會被其餘進程搶佔,因此等待鎖的進程只有可能運行在別的CPU上。(若是隻有一個CPU呢?那麼就不可能存在等待鎖的進程了。)
而若是不由用內核搶佔呢?那麼獲得鎖的進程將可能被搶佔,因而可能好久都不會釋放鎖。因而,等待鎖的進程可能就不知何年何月得償所望了。

      對於一些實時性要求更高的系統,則不能容忍spin_lock這樣的東西。寧肯改用更費勁的「睡眠-喚醒」過程,也不能由於禁用搶佔而讓更高優先級的進程等待。好比,嵌入式實時linux montavista就是這麼幹的。
      因而可知,實時並不表明高效。不少時候爲了實現「實時」,仍是須要對性能作必定讓步的。

二、多處理器下的負載均衡
      前面咱們並無專門討論多處理器對調度程序的影響,其實也沒有什麼特別的,就是在同一時刻能有多個進程並行地運行而已。那麼,爲何會有「多處理器負載均衡」這個事情呢?
      若是系統中只有一個可執行隊列,哪一個CPU空閒了就去隊列中找一個最合適的進程來執行。這樣不是很好很均衡嗎?
      的確如此,可是多處理器共用一個可執行隊列會有一些問題。顯然,每一個CPU在執行調度程序時都須要把隊列鎖起來,這會使得調度程序難以並行,可能致使系統性能降低。而若是每一個CPU對應一個可執行隊列則不存在這樣的問題。
      另外,多個可執行隊列還有一個好處。這使得一個進程在一段時間內老是在同一個CPU上執行,那麼極可能這個CPU的各級cache中都緩存着這個進程的數據,頗有利於系統性能的提高。
      因此,在linux下,每一個CPU都有着對應的可執行隊列,而一個可執行狀態的進程在同一時刻只能處於一個可執行隊列中。

      因而,「多處理器負載均衡」這個麻煩事情就來了。內核須要關注各個CPU可執行隊列中的進程數目,在數目不均衡時作出適當調整。何時須要調整,以多大力度進程調整,這些都是內核須要關心的。固然,儘可能不要調整最好,畢竟調整起來又要耗CPU、又要鎖可執行隊列,代價仍是不小的。
另外,內核還得關心各個CPU的關係。兩個CPU之間,多是相互獨立的、多是共享cache的、甚至多是由同一個物理CPU經過超線程技術虛擬出來的……CPU之間的關係也是實現負載均衡的重要依據。關係越緊密,進程在它們之間遷移的代價就越小。參見《linux內核SMP負載均衡淺析》。

三、優先級繼承
      因爲互斥,一個進程(設爲A)可能由於等待進入臨界區而睡眠。直到正在佔有相應資源的進程(設爲B)退出臨界區,進程A才被喚醒。
      可能存在這樣的狀況:A的優先級很是高,B的優先級很是低。B進入了臨界區,可是卻被其餘優先級較高的進程(設爲C)搶佔了,而得不到運行,也就沒法退出臨界區。因而A也就沒法被喚醒。
      A有着很高的優先級,可是如今卻淪落到跟B一塊兒,被優先級並不過高的C搶佔,致使執行被推遲。這種現象就叫作優先級反轉。

      出現這種現象是很不合理的。較好的應對措施是:當A開始等待B退出臨界區時,B臨時獲得A的優先級(仍是假設A的優先級高於B),以便順利完成處理過程,退出臨界區。以後B的優先級恢復。這就是優先級繼承的方法。
      爲了實現優先級繼承,內核又得作不少事情。更細節的東西能夠參考一下關於「優先級反轉」或「優先級繼承」的文章。

四、中斷處理線程化
      在linux下,中斷處理程序運行於一個不可調度的上下文中。從CPU響應硬件中斷自動跳轉到內核設定的中斷處理程序去執行,到中斷處理程序退出,整個過程是不能被搶佔的。
      一個進程若是被搶佔了,能夠經過保存在它的進程控制塊(task_struct)中的信息,在以後的某個時間恢復它的運行。而中斷上下文則沒有task_struct,被搶佔了就無法恢復了。
      中斷處理程序不能被搶佔,也就意味着中斷處理程序的「優先級」比任何進程都高(必須等中斷處理程序完成了,進程才能被執行)。可是在實際的應用場景中,可能某些實時進程應該獲得比中斷處理程序更高的優先級。
      因而,一些實時性要求更高的系統就給中斷處理程序賦予了task_struct以及優先級,使得它們在必要的時候可以被高優先級的進程搶佔。可是顯然,作這些工做是會給系統形成必定開銷的,這也是爲了實現「實時」而對性能作出的一種讓步。

 

 

6. 程序運行時內存佈局

   咱們在寫程序時,既有程序的邏輯代碼,也有在程序中定義的變量等數據,那麼當咱們的程序進行時,咱們的代碼和數據到底是存放在哪裏的呢?下面就來總結一下。

 

1、程序運行時的內存空間狀況

 
   其實在程序運行時,因爲內存的管理方式是以頁爲單位的,並且程序使用的地址都是虛擬地址,當程序要使用內存時,操做系統再把虛擬地址映射到真實的物理內存的地址上。因此在程序中,以虛擬地址來看,數據或代碼是一塊塊地存在於內存中的,一般咱們稱其爲一個段。並且代碼和數據是分開存放的,即不儲存於同於一個段中,並且各類數據也是分開存放在不一樣的段中的。
 
下面以一個簡單的程序來看一下在Linux下的程序運行空間狀況,代碼文件名爲space.c
  1. #include <unistd.h>
  2. #include <stdio.h>
  3.  
  4. int main()
  5. {
  6. printf("%d\n", getpid());
  7. while(1);
  8. return 0;
  9. }
這個程序很是簡單,輸出當前進程的進程號,而後進入一個死循環,這個死循環的目的只是讓程序不退出。而在Linux下有一個目錄/proc/$(pid),這個目錄保存了進程號爲pid的進程運行時的全部信息,其中有一個文件maps,它記錄了程序執行過程當中的內存空間的狀況。編譯運行上面的代碼,其運行結果如圖1所示:

在linux 64位操做系統中

從上面的圖中,咱們能夠看到這樣一個簡單的程序,在執行時,須要哪些庫和哪些空間。上面的圖的各列的意思,不一一詳述,只對重要的進行說明。
第一列的是一個段的起始地址和結束地址,第二列這個段的權限,第三列段的段內相對偏移量,第六列是這個段所存放的內容所對應的文件。從上圖能夠看到咱們的程序進行首先要加載系統的兩個共享庫,而後再加載咱們寫的程序的代碼(在linux 64位操做系統中嘗試是先加載咱們寫的程序代碼後加載兩個共享庫,最後是棧)。
 
對於第二列的權限,r:表示可讀,w:表示可寫,x:表示可執行,p:表示受保護(即只對本進程有效,不共享),與之相對的是s,意是就是共享。
 

從上圖咱們能夠很是形象地看到一個程序進行時的內存分佈狀況。下面咱們將會結合上圖,進行更加深刻的對內存中的數據段的解說。


2、程序運行時內存的各類數據段
 
1.bss段

該段用來存放沒有被初始化或初始化爲0的全局變量,由於是全局變量,因此在程序運行的整個生命週期內都存在於內存中。有趣的是這個段中的變量只佔用程序運行時的內存空間,而不佔用程序文件的儲存空間。能夠用如下程序來講明這點經過符號表能夠看到未初始化的全局變量沒有被存放在任何段,只是一個未定義的「COMMON符號」,這實際上是跟不一樣的語言與不一樣的編譯器實現有關,有些編譯器會將全局未初始化變量存放在.bss段,有些則不放,只是預留一個未定義的全局變量符號,等到最終鏈接成可執行文件的時候再在.bss段分配空間。)
文件名爲bss.c

  1. #include <stdio.h>
  2.  
  3. int bss_data[1024 * 1024];
  4.  
  5. int main()
  6. {
  7. return 0;
  8. }

 

這個程序很是簡單,定義一個4M的全局變量,而後返回。編譯成可執行文件bss,並查看可執行文件的文件屬性如圖2所示:

 
從可執行文件的大小4774B能夠看出,bss數據段(4M)並不佔用程序文件的儲存空間,在下面的data段中,咱們能夠看到data段的數據是佔用可執行文件的儲存空間的。
 
在圖1中,有文件名且屬性爲rw-p的內存區間,就是bss段。
 
2.data段
初始化過的全局變量數據段,該段用來保存初始化了的非0的全局變量,若是全局變量初始化爲0,則編譯有時會出於優化的考慮,將其放在bss段中。由於也是全局變量,因此在程序運行的整個生命週期內都存在於內存中。與bss段不一樣的是,data段中的變量既佔程序運行時的內存空間,也佔程序文件的儲存空間。能夠用下面的程序來講明,文件名爲data.c:
  1. #include <stdio.h>
  2.  
  3. int data_data[1024 * 1024] = {1};
  4.  
  5. int main()
  6. {
  7. return 0;
  8. }

 

這個程序與上面的bss惟一的不一樣就是全局變量int型數組data_data,其中第0個元素的值初始化爲1,其餘元素的值初始化成默認的0,而由於數組的地址是連續的,因此只要有一個元素在data段中,則其餘的元素也必然在data段中。編譯鏈接成可執行文件data,並查看可執行文件的文件屬性如圖3所示:

 
從可執行文件的大小來看,data段數據(data_data數組的大小,4M)佔用程序文件的儲存空間。
 
在圖1中,有文件名且屬性爲rw-p的內存區間,就是data段,它與bss段在內存中是共用一段內存的,不一樣的是,bss段數據不佔用文件,而data段數據佔用文件儲存空間。
 
3.rodata段
該段是常量數據段,用於存放常量數據,ro就是Read Only之意。可是注意並非全部的常量都是放在常量數據段的,其特殊狀況以下:
1)有些當即數與指令編譯在一塊兒直接放在代碼段(text段,下面會講到)中。
2)對於字符串常量,編譯器會去掉重複的常量,讓程序的每一個字符串常量只有一份。
3)有些系統中rodata段是多個進程共享的,目的是爲了提升空間的利用率。
 
在圖1中,有文件名的屬性爲r--p的內存區間就是rodata段。可見他是受保護的,只能被讀取,從而提升程序的穩定性。
 
4.text段
text段就是代碼段,用來存放程序的代碼(如函數)和部分整數常量。它與rodata段的主要不一樣是,text段是能夠執行的,並且不被不一樣的進程共享。
 
在圖1中,有文件名且屬性爲r-xp的內存區間就是text段。就如咱們所知道的那樣,代碼段是不能被寫的。
 
5.stack段
該段就是棧段,用來保存臨時變量和函數參數。程序中的函數調用就是以棧的方式來實現的,一般棧是向下(即向低地址)增加的,當向棧中push一個元素,棧頂指針就會向低地址移動,當從棧中pop一個元素,棧頂指針就會向高地址移動。棧中的數據只在當前函數或下一層函數中有效,當函數返回時,這些數據自動被釋放,若是繼續對這些數據進行訪問,將發生未知的錯誤。一般咱們在程序中定義的不是用malloc系統函數或new出來的變量,都是存放在棧中的。例如,以下函數:

  1. void func()
  2. {
  3. int a = 0;
  4. int *n_ptr = malloc(sizeof(int));
  5. char *c_ptr = new char;
  6. }
整型變量a,整型指針變量n_ptr和char型指針變量c_ptr,都存放在棧段中,而n_ptr和c_ptr指向的變量,因爲是malloc或new出來的,因此存放在堆中。當函數func返回時,a、n_ptr、c_ptr都會被釋放,可是n_ptr和c_ptr指向的內存卻不會釋放。由於它們是存在於堆中的數據。
 
在圖1中,文件名爲stack的內存區間即爲棧段。
 
6.heap段
heap(堆)是最自由的一種內存,它徹底由程序來負責內存的管理,包括何時申請,何時釋放,並且對它的使用也沒有什麼大小的限制。在C/C++中,用alloc系統函數和new申請的內存都存在於heap段中。
 
以上面的程序爲例,它向堆申請了一個int和一個char的內存,由於沒有調用free或delete,因此當函數返回時,堆中的int和char變量並無釋放,形成了內存泄漏。
 
因爲在圖1所對應的代碼中沒有使用alloc系統函數或new來申請內存,因此heap段並無在圖1中顯示出來,因此如下面的程序來講明heap段的位置,代碼文件爲heap.c,代碼以下:

  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4.  
  5. int main()
  6. {
  7. int *n_ptr = malloc(sizeof(int));
  8. printf("%d\n", getpid());
  9. while(1);
  10. free(n_ptr);
  11. return 0;
  12. }


查看其運行時內存空間分佈以下:

 
能夠看到文件名爲heap的內存區間就是heap段。從上圖,也能夠看出,雖然咱們只申請4個字節(sizeof(int))的空間,可是在操做系統中,內存是以頁的方式進行管理的,因此在分配heap內存時,仍是一次分配就爲咱們分配了一個頁的內存。注:不管是圖1,仍是上圖,都有一些沒有文件名的內存區間,其實沒用文件名的內存區間表示使用mmap映射的匿名空間。

 

 

C語言內存模型及運行時內存佈局

咱們知道,C程序開發並編譯完成後,要載入內存(主存或內存條)才能運行(請查看:載入內存,讓程序運行起來),變量名、函數名都會對應內存中的一塊區域。

內存中運行着不少程序,咱們的程序只佔用一部分空間,這部分空間又能夠細分爲如下的區域:

內存分區 說明
程序代碼區(code area) 存放函數體的二進制代碼
靜態數據區(data area) 也稱全局數據區,包含的數據類型比較多,如全局變量、靜態變量、通常常量、字符串常量。其中:
  • 全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。
  • 常量數據(通常常量、字符串常量)存放在另外一個區域。

注意:靜態數據區的內存在程序結束後由操做系統釋放。
堆區(heap area) 通常由程序員分配和釋放,若程序員不釋放,程序運行結束時由操做系統回收。malloc()calloc()free() 等函數操做的就是這塊內存,這也是本章要講解的重點。

注意:這裏所說的堆區與數據結構中的堆不是一個概念,堆區的分配方式卻是相似於鏈表。
棧區(stack area) 由系統自動分配釋放,存放函數的參數值、局部變量的值等。其操做方式相似於數據結構中的棧。
命令行參數區 存放命令行參數和環境變量的值,如經過main()函數傳遞的值。

 

C語言內存模型示意圖
圖1:C語言內存模型示意圖


提示:關於局部的字符串常量是存放在全局的常量區仍是棧區,不一樣的編譯器有不一樣的實現,VC 將局部常量像局部變量同樣對待,存儲於棧(⑥區)中,TC則存儲在靜態數據區的常量區(②區)。

注意:未初始化的全局變量的默認值是 0,而未初始化的局部變量的值倒是垃圾值(任意值)。請看下面的代碼:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. int global;
  4. int main()
  5. {
  6. int local;
  7. printf("global = %d\n", global);
  8. printf("local = %d\n", local);
  9. getch();
  10. return 0;
  11. }


運行結果:
global = 0
local = 1912227604

爲了更好的理解內存模型,請你們看下面一段代碼:

    1. #include<stdio.h>
    2. #include<stdlib.h>
    3. #include<string.h>
    4. int a = 0; // 全局初始化區(④區)
    5. char *p1; // 全局未初始化區(③區)
    6. int main()
    7. {
    8. int b; // 棧區
    9. char s[] = "abc"; // 棧區
    10. char *p2; // 棧區
    11. char *p3 = "123456"; // 123456\0 在常量區(②),p3在棧上,體會與 char s[]="abc"; 的不一樣
    12. static int c = 0; // 全局初始化區
    13. p1 = ( char *)malloc(10), // 堆區
    14. p2 = ( char *)malloc(20); // 堆區
    15. // 123456\0 放在常量區,但編譯器可能會將它與p3所指向的"123456"優化成一個地方
    16. strcpy(p1, "123456");
    17. }

 

 

 

1 內存模型

在C語言中,內存可分用五個部分:

1. BSS段(Block Started by Symbol): 用來存放程序中未初始化的全局變量的內存區域。

2. 數據段(data segment): 用來存放程序中已初始化的全局變量的內存區域。

3. 代碼段(text segment): 用來存放程序執行代碼的內存區域。

4. 堆(heap):用來存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc分配內存時,新分配的內存就被動態添加到堆上,當進程調用free釋放內存時,會從堆中剔除。

5. 棧(stack):存放程序中的局部變量(但不包括static聲明的變量,static變量放在數據段中)。同時,在函數被調用時,棧用來傳遞參數和返回值。因爲棧先進先出特色。因此棧特別方便用來保存/恢復調用現場。

 

APUE中的一個典型C內存空間分佈圖

 

以下往上,分別是text段,data段,BSS段,堆,棧

Linux下32位環境的用戶空間內存分佈狀況

 

 

由上圖可知:

0x0000 0000:保留區域, 最底層

代碼區:用來存放程序代碼和常量,只讀(運行期會一直存在)

常量區:通常常量,字符常量,只讀(運行期會一直存在)

全局數據區:全局變量和靜態變量,可讀寫(運行期會一直存在)

堆段:malloc/free的內存,malloc時分配,free時釋放(向上增加)

未分配堆內存

0x4000 0000:動態連接庫

未分配棧內存

棧段:局部變量,函數調用參數返回值(向上增加)

0xc000 0000 ~ 0xffff ffff:內核空間(1G)

 

2棧詳解

棧(stack): 是由系統自動分配和釋放,存放函數的參數值,返回值,局部變量等。其操做方式相似於數據結構中的棧。

 

2.1棧的申請

1. 當在函數或塊內部聲明一個局部變量時,如:int  nTmp; 系統會判斷申請的空間是否足夠,足夠,在棧中開闢空間,提供內存;不夠空間,報異常提示棧溢出。

2. 當調用一個函數時,系統會自動爲參數當局部變量,壓進棧中,當函數調用結束時,會自動提高堆棧。(可查看彙編中的函數調用機制)

 

2.2棧的大小

棧是有必定大小的,一般狀況下,棧只有2M,不一樣系統棧的大小可能不一樣。

在linux中,查看進程/線程棧大小,命令:  ulimit  -s

$  ulimit  -s

$  8192

個人系統中棧大小爲 8192, 有些系統爲 10240, 具體查看自已係統棧大小

設置棧大小:

1. 臨時改變棧大小:ulimit  -s  10240

2. 開機設置棧大小:在/etc/rc.local中加入 ulimit  -s  10240

3. 改變棧大小: 在/etc/security/limits.conf中加入

* soft stack 10240

 

因此,在聲明局部變量時,新手要特別注意棧的大小:

1. 對於局部變量,儘可能不定義大的變量,如大數組(大於2*1024*1024字節)

char  buf[2*1024*1024]; // 可能會致使棧溢出

2. 對於內存較大或不知大小的變量,用堆分配,局部變量用指針,注意要釋放

char*  pBuf = (char*)malloc(2*1024*1024); // char* 爲局部變量  malloc的內存在堆

free(pBuf);

3. 或定義在全局區中,static變量 或常量區中

static  char  buf[2*1024*1024];

 

2.3棧的生長方向

棧的生長方向和存放數據的方向相反,自頂向下

 

2.4 棧分配例子

int  function( int  var1 ,int  var2)

{

int  var3;

int  var4;

}

 

var1,var2,var3在棧中的圖以下:

0xc000 0000

var1

0xc000 0000 - 4

var2

0xc000 0000 - 8

var3

0xc000 0000 - 12

var4

 

3 堆詳解

堆(heap:是用來存放動態申請或釋放的區域。須要程序員分配和釋放,系統不會自動管理,若是用完不釋放,將會形成內存泄露,直到進程結速後,系統自動回收。

 

3.1 堆的目的

爲何在堆呢?緣由很簡單,在棧中,大小是有限制的,能常大小爲2M,若是須要更大的空間,那麼就要用到堆了,堆的目的就是爲了分配使用更大的空間。

3.2申請和釋放

int  function()

{

char *pTmp = (char*) malloc(1024);   // malloc在堆中分配1024字節空間

  //pTmp 爲局部變量,只佔四字節

free(pTmp); // free爲手動釋放堆中空間

pTmp = NULL; // 防止pTmp變野指針誤用

}

 

3.3堆的大小

堆是能夠申請大塊內存的區域,但堆的大小到底有多大,下面分析下,以32位系統爲例。

 

在linux中,堆區的內存申請,在32位系統中,理論上:2^32=4G,但如上面的內存分佈圖可知:內核佔用1G空間。

0xFFFF FFFF

1G內核空間

0xC000 0000

0XBFFF FFF

3G用戶空間(text段,data段,BSS段,堆,棧)

0x0000 0000


如上所知,理論上,使用malloc最大可以申請空間大約3G。但這是理論值,由於實際中,還會包含代碼區,全局變量區和棧區。

char  *buf = (char*) malloc(3GB);   // 理論上

 

3.4 堆的生長方向

   如上面的圖可知,堆是由低地址向高地址生長的

 

3.5 堆的注意事項

堆雖然能夠分配較大的空間,但有一些要注意的地方,不然會出現問題。

 

1. 釋放問題:分配了堆內存,必定要記得手動釋放,不然將會致使內存泄露

void*  alloc(int size)

{

char*  ptr = (char*)malloc(size);

return  ptr;

}

上面函數若是外部調用,沒有釋放,將內存不會釋放形成泄露

2. 碎片問題:若是頻繁地調用內存分配和釋放,將會使堆內存形成不少內存碎片,從而形成空間浪費和效率低下。

a) 對於比較固定,或可預測大小的,能夠程序啓動時,即分配好空間,如:某個對象不會超過500個,那個可先生成,object *ptr = (object*)malloc(object_size*500);

b) 結構對齊,儘可能使結構不浪費內存

3. 超堆大小問題:若是申請內存超過堆大小,會出現虛擬內存不足等問題

a) 儘可能不要申請很大的內存,如直須要,可採用內存數據庫等

4. 分配是否成功問題:申請內存後,都在判斷內存是否分配成功,分配成功後才能使用,不然會出現段錯誤

char *  pTmp = (char*)malloc(102400);

if(pTmp == 0)   // 必定在記得判斷

{

return false;

}

5. 釋放後野指針問題:釋放指針後,必定要記得把指針的值設置成NULL,防止指針被釋放後誤用

free(pTmp);

pTmp = NULL; // 防止變野指針

6. 屢次釋放問題:若是第5並沒置NULL,屢次釋放將會出現問題。

 

 

8. Linux的IPC都有哪些

爲何要進行進程間的通信(IPC (Inter-process communication))

數據傳輸:一個進程須要將它的數據發送給另外一個進程,發送的數據量在一個字節到幾M字節之間
共享數據:多個進程想要操做共享數據,一個進程對共享數據的修改,別的進程應該馬上看到。
通知事件:一個進程須要向另外一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
資源共享:多個進程之間共享一樣的資源。爲了做到這一點,須要內核提供鎖和同步機制。
進程控制:有些進程但願徹底控制另外一個進程的執行(如Debug進程),此時控制進程但願可以攔截另外一個進程的全部陷入和異常,並可以及時知道它的狀態改變。

linux經常使用的進程間的通信方式

(1)、管道(pipe):管道可用於具備親緣關係的進程間的通訊,是一種半雙工的方式,數據只能單向流動,容許一個進程和另外一個與它有共同祖先的進程之間進行通訊。

(2)、命名管道(named pipe):命名管道克服了管道沒有名字的限制,同時除了具備管道的功能外(也是半雙工),它還容許無親緣關係進程間的通訊。命名管道在文件系統中有對應的文件名。命名管道經過命令mkfifo或系統調用mkfifo來建立。

(3)、信號(signal):信號是比較複雜的通訊方式,用於通知接收進程有某種事件發生了,除了進程間通訊外,進程還能夠發送信號給進程自己;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又可以統一對外接口,用sigaction函數從新實現了signal函數)。

(4)、消息隊列:消息隊列是消息的連接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程能夠向隊列中添加消息,被賦予讀權限的進程則能夠讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺

(5)、共享內存:使得多個進程能夠訪問同一塊內存空間,是最快的可用IPC形式。是針對其餘通訊機制運行效率較低而設計的。每每與其它通訊機制,如信號量結合使用,來達到進程間的同步及互斥。

(6)、內存映射:內存映射容許任何多個進程間通訊,每個使用該機制的進程經過把一個共享的文件映射到本身的進程地址空間來實現它。

(7)、信號量(semaphore):主要做爲進程間以及同一進程不一樣線程之間的同步手段。

(8)、套接字(Socket):更爲通常的進程間通訊機制,可用於不一樣機器之間的進程間通訊。起初是由Unix系統的BSD分支開發出來的,但如今通常能夠移植到其它類Unix系統上:Linux和System V的變種都支持套接字。

 

 

linux進程間通訊(IPC)有幾種方式

       一。管道(pipe)

  管道是Linux支持的最初IPC方式,管道可分爲無名管道,有名管道等。

  (一)無名管道,它具備幾個特色:

  1) 管道是半雙工的,只能支持數據的單向流動;兩進程間須要通訊時須要創建起兩個管道;
  2) 無名管道使用pipe()函數建立,只能用於父子進程或者兄弟進程之間;
  3) 管道對於通訊的兩端進程而言,實質上是一種獨立的文件,只存在於內存中;
  4) 數據的讀寫操做:一個進程向管道中寫數據,所寫的數據添加在管道緩衝區的尾部;
                                    另外一個進程在管道中緩衝區的頭部讀數據。

  (二)有名管道

  有名管道也是半雙工的,不過它容許沒有親緣關係的進程間進行通訊。具體點說就是,有名管道提供了一個路徑名與之進行關聯,以FIFO(先進先出)的形式存在於文件系統中。這樣即便是不相干的進程也能夠經過FIFO相互通訊,只要他們能訪問已經提供的路徑。

  值得注意的是,只有在管道有讀端時,往管道中寫數據纔有意義。不然,向管道寫數據的進程會接收到內核發出來的SIGPIPE信號;應用程序能夠自定義該信號處理函數,或者直接忽略該信號。

       管道是*nix系統進程間通訊的最古老形式,全部*nix都提供這種通訊方式。管道是一種半雙工的通訊機制,也就是說,它只能一端用來讀,另一端用來寫;另外,管道只能用來在具備公共祖先的兩個進程之間通訊。管道通訊遵循先進先出的原理,而且數據只能被讀取一次,當此段數據被讀取後,立刻會從數據中消失,這一點很重要。

        Linux上,建立管道使用pipe函數,當它執行後,會產生兩個文件描述符,分別爲讀端和寫端。單個進程中的管道幾乎沒有任何做用,一般會先調用pipe,而後調用fork,從而建立從父進程到子進程的IPC通道。
------------------------------
waitpid()會暫時中止目前進程的執行,直到有信號來到或子進程結束。
#include<sys/types.h>
#include<sys/wait.h>
定義函數 pid_t waitpid(pid_t pid, int * status, int options);
---------------------------------------------------------------------------------------------------------------------------


  二。信號量(semophore)


  信號量是一種計數器,能夠控制進程間多個線程或者多個進程對資源的同步訪問,它常實現爲一種鎖機制。實質上,信號量是一個被保護的變量,而且只能經過初始化和兩個標準的原子操做(P/V)來訪問。(P,V操做也常稱爲wait(s),signal(s))

  三。信號(Signal)

  信號是Unix系統中使用的最古老的進程間通訊的方法之一。操做系統經過信號來通知某一進程發生了某一種預約好的事件;接收到信號的進程能夠選擇不一樣的方式處理該信號,一是能夠採用默認處理機制-進程中斷或退出,一是忽略該信號,還有就是自定義該信號的處理函數,執行相應的動做。

  內核爲進程生產信號,來響應不一樣的事件,這些事件就是信號源。信號源能夠是:異常,其餘進程,終端的中斷(Ctrl-C,Ctrl+\等),做業的控制(前臺,後臺進程的管理等),分配額問題(cpu超時或文件過大等),內核通知(例如I/O就緒等),報警(計時器)。

  四。消息隊列(Message Queue)

  消息隊列就是消息的一個鏈表,它容許一個或者多個進程向它寫消息,一個或多個進程向它讀消息。Linux維護了一個消息隊列向量表:msgque,來表示系統中全部的消息隊列。

  消息隊列克服了信號傳遞信息少,管道只能支持無格式字節流和緩衝區受限的缺點。

  五。共享內存(shared memory)

  共享內存映射爲一段能夠被其餘進程訪問的內存。該共享內存由一個進程所建立,而後其餘進程能夠掛載到該共享內存中。共享內存是最快的IPC機制,但因爲linux自己不能實現對其同步控制,須要用戶程序進行併發訪問控制,所以它通常結合了其餘通訊機制實現了進程間的通訊,例如信號量。

  五。套接字(socket)

  socket也是一種進程間的通訊機制,不過它與其餘通訊方式主要的區別是:它能夠實現不一樣主機間的進程通訊。一個套接口能夠看作是進程間通訊的端點(endpoint),每一個套接口的名字是惟一的;其餘進程能夠訪問,鏈接和進行數據通訊

 

 

9.共享內存的本質

進程間通訊---共享內存

   ------->雙向通訊

   ------->僅是一塊內存,能夠隨意寫入數據

   ------->無同步互斥

   ------->生命週期隨內核

   -----共享內存是最快的IPC形式.共享內存的本質是物理內存,一旦這樣的內存映射到共享它的進程的地址空間,這些空間不涉及內核.

進程是一個獨立的資源管理單元,不一樣進程間的資源是獨立的,不能在一個進程中訪問另外一個進程的用戶空間和內存空間。可是,進程不是孤立的,不一樣進程之間須要信息的交互和狀態的傳遞,所以須要進程間數據的傳遞、同步和異步的機制。   

固然,這些機制不能由哪個進程進行直接管理,只能由操做系統來完成其管理和維護,Linux提供了大量的進程間通訊機制,包括同一個主機下的不一樣進程和網絡主機間的進程通訊,以下圖所示:

共享內存是進程間通訊中最簡單的方式之中的一個。

共享內存是系統出於多個進程之間通信的考慮,而預留的的一塊內存區。

共享內存贊成兩個或不少其餘進程訪問同一塊內存,就如同 malloc() 函數向不一樣進程返回了指向同一個物理內存區域的指針。

當一個進程改變了這塊地址中的內容的時候,其餘進程都會察覺到這個更改

用ftok()函數得到一個ID號


應用說明,在IPC中,咱們經常用用key_t的值來建立或者打開信號量,共享內存和消息隊列。

key_t ftok(const char *pathname, int proj_id);
參數 描寫敘述
pathname 必定要在系統中存在並且進程可以訪問的
proj_id 一個1-255之間的一個整數值,典型的值是一個ASCII值。

當成功運行的時候,一個key_t值將會被返回。不然-1被返回。咱們可以使用strerror(errno)來肯定詳細的錯誤信息。

考慮到應用系統可能在不一樣的主機上應用,可以直接定義一個key,而不用ftok得到:

#define IPCKEY 0x344378

建立共享內存


進程經過調用shmget(Shared Memory GET,獲取共享內存)來分配一個共享內存塊。

int shmget(key_t key ,int size,int shmflg)
參數 描寫敘述
key 一個用來標識共享內存塊的鍵值
size 指定了所申請的內存塊的大小
shmflg 操做共享內存的標識

返回值:假設成功,返回共享內存表示符,假設失敗,返回-1。

該函數的第一個參數key是一個用來標識共享內存塊的鍵值。

該函數的第二個參數size指定了所申請的內存塊的大小

第三個參數shmflg是一組標誌。經過特定常量的按位或操做來shmget

映射共享內存


shmat()是用來贊成本進程訪問一塊共享內存的函數。將這個內存區映射到本進程的虛擬地址空間。

int shmat(int shmid,char *shmaddr,int flag)
參數 描寫敘述
shmid 那塊共享內存的ID。是shmget函數返回的共享存儲標識符
shmaddr 是共享內存的起始地址,假設shmaddr爲0,內核會把共享內存映像到調用進程的地址空間中選定位置。假設shmaddr不爲0,內核會把共享內存映像到shmaddr指定的位置。因此通常把shmaddr設爲0。
shmflag 是本進程對該內存的操做模式。假設是SHM_RDONLY的話,就是僅僅讀模式。

其餘的是讀寫模式

成功時,這個函數返回共享內存的起始地址。失敗時返回-1。

共享內存解除映射


當一個進程再也不需要共享內存時,需要把它從進程地址空間中多裏。

int shmdt(char *shmaddr)
參數 描寫敘述
shmaddr 那塊共享內存的起始地址

成功時返回0。失敗時返回-1。

應經過調用 shmdt(Shared Memory Detach。脫離共享內存塊)函數與該共享內存塊脫離。

將由 shmat 函數返回的地址傳遞給這個函數。假設當釋放這個內存塊的進程是最後一個使用該內存塊的進程,則這個內存塊將被刪除。

控制釋放


shmctl控制對這塊共享內存的使用

函數原型

int shmctl( int shmid , int cmd , struct shmid_ds *buf );
參數 描寫敘述
shmid 是共享內存的ID。
cmd 控制命令
buf 一個結構體指針。

IPC_STAT的時候,取得的狀態放在這個結構體中。假設要改變共享內存的狀態,用這個結構體指定。

當中cmd的取值例如如下

cmd 描寫敘述
IPC_STAT 獲得共享內存的狀態
IPC_SET 改變共享內存的狀態
IPC_RMID 刪除共享內存

返回值: 成功:0 失敗:-1

 

能夠看到內存映射中須要的一個參數是int fd(文件的標識符),可見函數是經過fd將文件內容映射到一個內存空間,
我須要建立另外一個映射來獲得文件內容並統計或修改,這時我建立這另外一個映射用的還是mmap函數,
它仍須要用到fd這個文件標識,那我不等於又從新打開文件讀取文件裏的數據
1.既然這樣那同對文件的直接操做有什麼區別呢? 2.映射到內存後經過映射的指針addr來修改內容的話是修改共享內存裏的內容仍是文件的內容呢? 3.解決上面2個問題,我仍是想確切知道共享內存有什麼用??? 一種回答|一、訪問共享內存的執行速度比直接訪問文件的快N倍(N》10),這對於要求快速輸入輸出的場合很是有效。 2、經過addr修改的內容是修改的是共享內容中的內容。至因而否修改了文件中的內容,要看文件的類型。 對於顯示設備等文件來講,修改的也是文件的內容,由於他直接寫到了顯存中。對於普通文件, 在close文件時,kernel會將數據更新到硬盤等存儲設備中。 3、共享內存主要是爲了提升程序的執行速度,方便多個進程進行快速的大數據量的交換。 第二種回答: 對因而修改文件內容的內存映射: 1、你的這個說法不確切。舉個例子來講:對顯示設備文件(顯卡)進行內存的映射,並不會在內存中新分配一塊內存, 而是直接將顯存地址經過addr參數傳給應用程序。這樣應用程序經過內存映射修改文件時, 其實就是直接修改顯存中的內容(也就是改變顯示內容)。 二、感受你把內存映射和共享內存搞混了。內存映射是用來加快對文件/設備的訪問。 (若是是大文件,並且還想提升讀寫速度的話,建議使用內存映射。) 共享內存是用來在多個進程間進行快速的大數據量的交換。 3、fd是文件描述符。它和內存映射沒有直接的關係。只有作過內存映射後,它和映射到的內存才存在對應關係。 對於不修改文件內容的內存映射 1、不必定,能夠在程序中指定要將文件內容映射到哪塊內存。對於多個進程打開同一個文件, 不一樣的內存映射能夠開闢多塊內存區域。更新文件內容的順序依照關閉文件的進程的順序執行,所以,存在髒讀的問題。 二、:-),必定要記住,內存映射是爲了加快對文件/設備的訪問速度,不是用來進行數據通訊的。 轉載自:http://bbs.csdn.net/topics/340203684 我對內存映射的理解就是經過操做內存來實現對文件的操做,這樣能夠加快執行速度,由於操做內存比操做文件的速度快多了! 共享內存,顧名思義,就是預留出的內存區域,它容許一組進程對其訪問。 共享內存是system vIPC中三種通訊機制最快的一種,也是最簡單的一種。對於進程來講, 得到共享內存後,他對內存的使用和其餘的內存是同樣的。由一個進程對共享內存所進行的 操做對其餘進程來講都是當即可見的,由於每一個進程只須要經過一個指向共享內存空間的指針就能夠來讀取 共享內存中的內容(說白了就比如申請了一塊內存,每一個須要的進程都有一個指針指向這個內存) 就能夠輕鬆得到結果。使用共享內存要注意的問題:共享內存不能確保對內存操做的互斥性。 一個進程能夠向共享內存中的給定地址寫入,而同時另外一個進程從相同的地址讀出,這將會致使不一致的數據。 所以使用共享內存的進程必須本身保證讀操做和寫操做的的嚴格互斥。 可以使用鎖和原子操做解決這一問題。也可以使用信號量保證互斥訪問共享內存區域。 共享內存在一些狀況下能夠代替消息隊列,並且共享內存的讀/寫比使用消息隊列要快!




共享內存能夠說是最有用的進程間通訊方式,也是最快的IPC形式。兩個不一樣進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A能夠即時看到進程B對共享內存中數據的更新,反之亦然。因爲多個進程共享同一塊內存區域,必然須要某種同步機制,互斥鎖和信號量均可以。

採用共享內存通訊的一個顯而易見的好處是效率高,由於進程能夠直接讀寫內存,而不須要任何數據的拷貝。對於像管道和消息隊列等通訊方式,則須要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據[1]:一次從輸入文件到共享內存區,另外一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不老是讀寫少許數據後就解除映射,有新的通訊時,再從新創建共享內存區域。而是保持共享區域,直到通訊完畢爲止,這樣,數據內容一直保存在共享內存中,並無寫回文件。共享內存中的內容每每是在解除映射時才寫回文件的。所以,採用共享內存的通訊方式效率是很是高的。

Linux的2.2.x內核支持多種共享內存方式,如mmap()系統調用,Posix共享內存,以及系統V共享內存。linux發行版本如Redhat 8.0支持mmap()系統調用及系統V共享內存,但還沒實現Posix共享內存,本文將主要介紹mmap()系統調用及系統V共享內存API的原理及應用。

1、內核怎樣保證各個進程尋址到同一個共享內存區域的內存頁面

一、page cache及swap cache中頁面的區分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的全部信息由struct page來描述。struct page中有一個域爲指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的全部頁面就是根據address_space結構以及一個偏移量來區分的。

二、文件與address_space結構的對應:一個具體的文件在打開後,內核會在內存中爲之創建一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個文件就對應一個address_space結構,一個address_space與一個偏移量可以肯定一個page cache 或swap cache中的一個頁面。所以,當要尋址某個數據時,很容易根據給定的文件及數據在文件內的偏移量而找到相應的頁面。

三、進程調用mmap()時,只是在進程空間內新增了一塊相應大小的緩衝區,並設置了相應的訪問標識,但並無創建進程空間到物理頁面的映射。所以,第一次訪問該空間時,會引起一個缺頁異常。

四、對於共享內存映射狀況,缺頁異常處理程序首先在swap cache中尋找目標頁(符合address_space以及偏移量的物理頁),若是找到,則直接返回地址;若是沒有找到,則判斷該頁是否在交換區(swap area),若是在,則執行一個換入操做;若是上述兩種狀況都不知足,處理程序將分配新的物理頁面,並把它插入到page cache中。進程最終將更新進程頁表。
注:對於映射普通文件狀況(非共享映射),缺頁異常處理程序首先會在page cache中根據address_space以及數據偏移量尋找相應的頁面。若是沒有找到,則說明文件數據尚未讀入內存,處理程序會從磁盤讀入相應的頁面,並返回相應地址,同時,進程頁表也會更新。

五、全部進程在映射同一個共享內存區域時,狀況都同樣,在創建線性地址與物理地址之間的映射以後,不論進程各自的返回地址如何,實際訪問的必然是同一個共享內存區域對應的物理頁面。
注:一個共享內存區域能夠看做是特殊文件系統shm中的一個文件,shm的安裝點在交換區上。

上面涉及到了一些數據結構,圍繞數據結構理解問題會容易一些。

2、mmap()及其相關係統調用

mmap()系統調用使得進程之間經過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程能夠向訪問普通內存同樣對文件進行訪問,沒必要再調用read(),write()等操做。

注:實際上,mmap()系統調用並非徹底爲了用於共享內存而設計的。它自己提供了不一樣於通常對普通文件的訪問方式,進程能夠像讀寫內存同樣對普通文件的操做。而Posix或系統V的共享內存IPC則純粹用於共享目的,固然mmap()實現共享內存也是其主要應用之一。

一、mmap()系統調用形式以下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
參數fd爲即將映射到進程空間的文件描述字,通常由open()返回,同時,fd能夠指定爲-1,此時須指定flags參數中的MAP_ANON,代表進行的是匿名映射(不涉及具體的文件名,避免了文件的建立及打開,很顯然只能用於具備親緣關係的進程間通訊)。len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。prot 參數指定共享內存的訪問權限。可取以下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。flags由如下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。offset參數通常設爲0,表示從文件頭開始映射。參數addr指定文件應被映射到進程空間的起始地址,通常被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值爲最後文件映射到進程空間的地址,進程可直接操做起始地址爲該值的有效地址。這裏再也不詳細介紹mmap()的參數,讀者可參考mmap()手冊頁得到進一步的信息。

二、系統調用mmap()用於共享內存的兩種方式:

(1)使用普通文件提供的內存映射:適用於任何進程之間;此時,須要打開或建立一個文件,而後再調用mmap();典型調用代碼以下:

 fd=open(name, flag, mode);
if(fd<0)
 ...
 

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 經過mmap()實現共享內存的通訊方式有許多特色和要注意的地方,咱們將在範例中進行具體說明。

 

(2)使用特殊文件提供匿名內存映射:適用於具備親緣關係的進程之間;因爲父子進程特殊的親緣關係,在父進程中先調用mmap(),而後調用fork()。那麼在調用fork()以後,子進程繼承父進程匿名映射後的地址空間,一樣也繼承mmap()返回的地址,這樣,父子進程就能夠經過映射區域進行通訊了。注意,這裏不是通常的繼承關係。通常來講,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。
對於具備親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,沒必要指定具體的文件,只要設置相應的標誌便可,參見範例2。

三、系統調用munmap()

int munmap( void * addr, size_t len )
該調用在進程地址空間中解除一個映射關係,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關係解除後,對原來映射地址的訪問將致使段錯誤發生。

四、系統調用msync()

int msync ( void * addr , size_t len, int flags)
通常說來,進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,每每在調用munmap()後才執行該操做。能夠經過調用msync()實現磁盤上文件內容與共享內存區的內容一致

 

3、mmap()範例

下面將給出使用mmap()的兩個範例:範例1給出兩個進程經過映射普通文件實現共享內存通訊;範例2給出父子進程經過匿名映射實現共享內存。系統調用mmap()有許多有趣的地方,下面是經過mmap()映射普通文件實現進程間的通訊的範例,咱們經過該範例來講明mmap()實現共享內存的特色及注意事項。

範例1:兩個進程經過映射普通文件實現共享內存通訊

範例1包含兩個子程序:map_normalfile1.c及map_normalfile2.c。編譯兩個程序,可執行文件分別爲map_normalfile1及map_normalfile2。兩個程序經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。map_normalfile2試圖打開命令行參數指定的一個普通文件,把該文件映射到進程的地址空間,並對映射後的地址空間進行寫操做。map_normalfile1把命令行參數指定的文件映射到進程地址空間,而後對映射後的地址空間執行讀操做。這樣,兩個進程經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。

 

從程序的運行結果中能夠得出的結論

一、 最終被映射文件的內容的長度不會超過文件自己的初始大小,即映射不能改變文件的大小;

二、 能夠用於進程通訊的有效地址空間大小大致上受限於被映射文件的大小,但不徹底受限於文件大小。打開文件被截短爲5個people結構大小,而在map_normalfile1中初始化了10個people數據結構,在恰當時候(map_normalfile1輸出initialize over 以後,輸出umap ok以前)調用map_normalfile2會發現map_normalfile2將輸出所有10個people結構的值,後面將給出詳細討論。
注:在linux中,內存的保護是以頁爲基本單位的,即便被映射文件只有一個字節大小,內核也會爲映射分配一個頁面大小的內存。當被映射文件小於一個頁面大小時,進程能夠對從mmap()返回地址開始的一個頁面大小進行訪問,而不會出錯;可是,若是對一個頁面之外的地址空間進行訪問,則致使錯誤發生,後面將進一步描述。所以,可用於進程間通訊的有效地址空間大小不會超過文件大小及一個頁面大小的和。

三、 文件一旦被映射後,調用mmap()的進程對返回地址的訪問是對某一內存區域的訪問,暫時脫離了磁盤上文件的影響。全部對mmap()返回地址空間的操做只在內存中有意義,只有在調用了munmap()後或者msync()時,才把內存中的相應內容寫回磁盤文件,所寫內容仍然不能超過文件的大小。

 

4、對mmap()返回地址的訪問

前面對範例運行結構的討論中已經提到,linux採用的是頁式管理機制。對於用mmap()映射普通文件來講,進程會在本身的地址空間新增一塊空間,空間大小由mmap()的len參數指定,注意,進程並不必定可以對所有新增空間都能進行有效訪問。進程可以訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,可以容納文件被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,可以有效訪問的地址空間大小。超過這個空間大小,內核會根據超過的嚴重程度返回發送不一樣的信號給進程。可用以下圖示說明:

 

注意:文件被映射部分而不是整個文件決定了進程可以訪問的空間大小,另外,若是指定文件的偏移部分,必定要注意爲頁面大小的整數倍。

 

 

 

9.  socket是怎麼回事,演繹從應用層到最底層的通訊

在說socket以前。咱們先了解下相關的網絡知識;

端口

 在Internet上有不少這樣的主機,這些主機通常運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不一樣的端口對應於不一樣的服務(應用程序)。

例如:http 使用80端口 ftp使用21端口 smtp使用 25端口

端口用來標識計算機裏的某個程序   1)公認端口:從0到1023   2)註冊端口:從1024到49151   3)動態或私有端口:從49152到65535

 

Socket相關概念

socket的英文原義是「孔」或「插座」。做爲進程通訊機制,取後一種意思。一般也稱做「套接字」,用於描述IP地址和端口,是一個通訊鏈的句柄。(其實就是兩個程序通訊用的。)

socket很是相似於電話插座。以一個電話網爲例。電話的通話雙方至關於相互通訊的2個程序,電話號碼就是IP地址。任何用戶在通話以前,

首先要佔有一部電話機,至關於申請一個socket;同時要知道對方的號碼,至關於對方有一個固定的socket。而後向對方撥號呼叫,

至關於發出鏈接請求。對方假如在場並空閒,拿起電話話筒,雙方就能夠正式通話,至關於鏈接成功。雙方通話的過程,

是一方向電話機發出信號和對方從電話機接收信號的過程,至關於向socket發送數據和從socket接收數據。通話結束後,一方掛起電話機至關於關閉socket,撤消鏈接。

 

Socket有兩種類型

流式Socket(STREAM): 是一種面向鏈接的Socket,針對於面向鏈接的TCP服務應用,安全,可是效率低;

數據報式Socket(DATAGRAM): 是一種無鏈接的Socket,對應於無鏈接的UDP服務應用.不安全(丟失,順序混亂,在接收端要分析重排及要求重發),但效率高.

 

TCP/IP協議

TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標準的協議集,它是爲廣域網(WANs)設計的。

UDP協議

UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。

應用層 (Application):應用層是個很普遍的概念,有一些基本相同的系統級 TCP/IP 應用以及應用協議,也有許多的企業商業應用和互聯網應用。 解釋:咱們的應用程序

傳輸層 (Transport):傳輸層包括 UDP 和 TCP,UDP 幾乎不對報文進行檢查,而 TCP 提供傳輸保證。 解釋;保證傳輸數據的正確性

網絡層 (Network):網絡層協議由一系列協議組成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。 解釋:保證找到目標對象,由於裏面用的IP協議,ip包含一個ip地址

鏈路層 (Link):又稱爲物理數據網絡接口層,負責報文傳輸。 解釋:在物理層面上怎麼去傳遞數據

 

你能夠cmd打開命令窗口。輸入

netstat -a

查看當前電腦監聽的端口,和協議。有TCP和UDP

 

 

TCP/IP與UDP有什麼區別呢?該怎麼選擇?

  UDP能夠用廣播的方式。發送給每一個鏈接的用戶   而TCP是作不到的

  TCP須要3次握手,每次都會發送數據包(但不是咱們想要發送的數據),因此效率低   但數據是安全的。由於TCP會有一個校驗和。就是在發送的時候。會把數據包和校驗和一塊兒   發送過去。當校驗和和數據包不匹配則說明不安全(這個安全不是指數據會不會   別竊聽,而是指數據的完整性)

  UDP不須要3次握手。能夠不發送校驗和

  web服務器用的是TCP協議

那何時用UDP協議。何時用TCP協議呢?   視頻聊天用UDP。由於要保證速度?反之相反

   

下圖顯示了數據報文的格式

 

 

Socket通常應用模式(服務器端和客戶端)

 

 

服務端跟客戶端發送信息的時候,是經過一個應用程序 應用層發送給傳輸層,傳輸層加頭部 在發送給網絡層。在加頭 在發送給鏈路層。在加幀

 

而後在鏈路層轉爲信號,經過ip找到電腦 鏈路層接收。去掉頭(由於發送的時候加頭了。去頭是爲了找到裏面的數據) 網絡層接收,去頭 傳輸層接收。去頭 在到應用程序,解析協議。把數據顯示出來

 

TCP3次握手

在TCP/IP協議中,TCP協議提供可靠的鏈接服務,採用三次握手創建一個鏈接。   第一次握手:創建鏈接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;SYN:同步序列編號(Synchronize SequenceNumbers)。   第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時本身也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;   第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。

 

 

看一個Socket簡單的通訊圖解

相關文章
相關標籤/搜索