大多數開發者並不對進程有過多細緻瞭解,至少在不少層面上,普通開發者不必理會這些細節,操做系統存在的意義就是消除開發者在這方面遭遇的恐慌,使得可以快速編寫出能夠執行的代碼。那麼理解進程的意義正是但願在操做系統層面有新的認識,在處理多進程和併發時能從根本上判斷問題。php
本文將從最簡單的操做系統模型去分析進程的存在,在操做系統中,任何代碼的執行都必須依附於某個進程,能夠說,進程就是代碼在操做系統執行的基本單元。所以,下面將操做系統各個核心模塊作個描繪,而後瞭解進程在整個系統中存在的形式。java
不管是何種語言,高級的如php、java、python仍是低級的c最終都會轉化成統一規範的二進制流,在操做系統的控制下二進制流流通於各個硬件,經由CPU實現計算處理。python
這裏,咱們以c語言爲例,講述代碼轉化的過程。當你建立以下代碼的c文件code.c:linux
int sum=0;
int sum(int x,inty){
int t=x+y;
sum+=t;
return t;
}複製代碼
操做系統接下來經過編譯器轉化成以下形式的彙編代碼code.s:程序員
pushl %ebp
movl %esp,%ebp
movl 12(%ebp),%eax
addl 8(%ebp),%eax
addl %eax,sum
popl %ebp
ret複製代碼
彙編代碼無限接近代碼文件在操做系統最終存在的形式,若是不理解上面每一行的意義並沒關係,咱們如今只須要知道每一行表明着一個指令,而指令是處理器(CPU)執行的基本單位。儘管彙編代碼已經無限接近機器硬件了,但計算機硬件只能識別二進制,因此彙編代碼還會通過一次轉換,經過彙編器將彙編代碼轉化成二進制形式(下文左邊):golang
55 pushl %ebp
89 e5 movl %esp,%eb
8b 45 0c movl 12(%ebp),%eax
03 45 08 addl 8(%ebp),%eax
01 05 00 00 00 00 addl %eax,sum
5d popl %ebp
c3 ret複製代碼
爲了便於文本編寫,上文左邊二進制代碼咱們經過16進制來表示,每行的右邊是對應的原彙編代碼說明,經過上面的轉化,咱們編寫的c代碼最終變成了二進制流!編程
接下來CPU將加載上面每一行二進制流,按順序執行每一個指令,完成整個過程。bash
計算機從上個世紀發展至今,性能實現了巨大的飛躍,但計算機的處理模型一直沒有改變,就是咱們熟知的"馮諾依曼結構計算機"。這個結構的計算機要求:計算機的數制採用二進制,計算機應該按照程序順序執行。服務器
時至至今,咱們所用的計算機依然是馮諾依曼計算機(額...你或許據說過量子計算機,但你應該沒體驗過)。CPU保持了高速的運算能力,這裏的運算能力體如今對指令的執行頻次上,一個完整邏輯的代碼最終都將轉化成按順序排放的指令序列,CPU時時刻刻地對待處理指令按序執行。架構
一個單核的CPU在每一個時鐘週期內完成一次運算,咱們常說的主頻就是指 CPU 內核 工做的 時鐘頻率,即每秒鐘能產生多少次時鐘中斷。咱們須要瞭解的是,時鐘中斷是計算機硬件運做的一種方式,每次時鐘的中斷至關於一次對硬件的觸發(你或許能夠想象爲對函數的一次調用了)。一個主頻爲1GHz的CPU意味着每一秒鐘CPU可實現1000000000次觸發,若是每條指令均可以在一次觸發中完成執行,那麼CPU的運算能力咱們能夠簡單理解爲每秒可執行1000000000條指令。
那麼,CPU在運算過程當中和指令的關係是怎樣的?
經過上文可知,代碼編譯後的最終形式是二進制流,而且會保存在存儲器中供加載。在指令運行過程當中,CPU須要知道每次執行的下一條指令地址,而且執行後的相關狀態須要實時保存,在CPU執行頻率如此快的狀況下,要求必須有足夠快的速度實現數據存儲,而寄存器就是這麼一個離CPU最近的存儲硬件,它速度足夠快,知足CPU的高速存儲需求,接下來咱們結合上文示例的彙編代碼來說述寄存器和CPU之間的關係。
寄存器表明的是CPU可直接訪問的高速存儲器,在Y86處理器中,有8個寄存器(在彙編程序中以%符號開頭來表示,如%eax,%ecx....表明不一樣寄存器),每一個寄存器可存儲四個字節
上一節在講述代碼編譯的過程當中,咱們以一個sum函數作了示例,該函數實現了兩個參數x、y的相加,其中咱們看到以下一條指令:
movl 12(%ebp),%eax複製代碼
該指令在原c代碼中至關於獲取參數x的值,具體功能爲:將寄存器%ebp保存的值加上12,而後將獲得的值做爲內存地址去內存中獲取對應值,並保存到%eax寄存器中。能夠理解爲,%ebp保存着內存某個地址,而參數x的值被放置在該內存地址偏移12(這裏的單位是字節)的地方,找到該值後放置到%eax寄存器中,存儲狀態以下圖所示:
隨後開始執行指令:
addl 8(%ebp),%eax複製代碼
其中addl指令會命令CPU執行加的運算操做,具體功能爲:將%ebp保存的值加上8,而後獲得的結果做爲內存地址從內存空間取出對應值,將該值和%eax保存的數據進行相加操做,最後將結果保存到%eax寄存器中。能夠理解爲,參數y一開始放置在%ebp所保存內存地址偏移量爲8的地方,addl命令首先從該地址獲取y值,接下來從%eax獲取x值,最後執行相加操做,由此實現了sum函數中對參數x和y的計算。存儲狀態以下圖所示:
經過上面兩條指令,咱們大體瞭解了sum函數的核心計算過程,CPU在這個過程當中扮演了主要角色,寄存器則配合CPU完成了一些存儲狀態的相關操做(放置參數變量的地址,保存相關結果值...)。
值得注意的是,這個過程還有一個專門寄存器用於存放待執行指令的地址,每執行一條指令時都會更新PC寄存器,用於指向下一條指令的地址,操做系統經過維護好PC寄存器保存的內容很好地控制了CPU的運算過程。
從CPU的執行過程來看,在單核的條件下,CPU在每一個時鐘週期最多隻能處理一條指令,若是CPU一直在不間斷處理某個代碼邏輯,那麼其它的應用程序將沒法獲得執行的機會。所以,必須引入一種機制合理分配計算資源,讓不一樣的代碼邏輯可以及時獲得處理,調度的概念在這裏出現了,操做系統經過調度的機制,在每秒鐘處理頻次如此高的狀況下,每一秒鐘的單位時間CPU能夠處理不少不一樣代碼邏輯的指令,實現了「併發」的操做。
那麼,調度的對象是什麼,這個過程如何去理解呢?
上文中的示例代碼,完成了兩個數值的相加操做,假設如今還有另外一個代碼邏輯也在運行過程當中,完成的是兩個數值的相減操做,咱們但願這兩個過程互不相關互不干擾。
另外一個代碼邏輯以下:
int sub(int x,inty){
int t=x-y;
return t;
}複製代碼
編譯後的形式爲:
55 pushl %ebp
89 e5 movl %esp,%ebp
8b 45 0c movl 20(%ebp),%eax
61 45 08 subl 16(%ebp),%eax
01 05 00 00 00 00 addl %eax,sum
5d popl %ebp
c3 ret複製代碼
從彙編代碼能夠知道,該函數的x、y參數在執行過程當中放置在%ebp寄存器所保存地址對應偏移字節的位置處,最終的執行結果也保存在%eax寄存器,只不過%ebp在兩個執行過程當中會存放不一樣的值用於區分不一樣代碼空間的內存地址。寄存器是被全部執行過程共享的,按照咱們的設想,若是CPU在運算過程當中直接來回調用兩個邏輯的指令,那麼期間寄存器保存的值將會受到另外一個程序的干擾,那麼將沒法獲得正確的結果!
咱們來模擬CPU運算過程可能遭遇的一種狀態,當PC寄存器定位到sum函數以下指令時:
movl 12(%ebp),%eax複製代碼
完成了x值在%eax寄存器的保存操做,此時操做系統經過調度將PC指定到sub函數的以下指令:
movl 20(%ebp),%eax複製代碼
若是在此過程不對%ebp進行更新操做,那麼sub函數的x值將會按照sum函數執行的存儲狀態來繼續處理,這顯然是不對的,會形成取值的錯誤。
一樣的狀況,當CPU執行完sum函數的下一條指令時:
addl 8(%ebp),%eax複製代碼
此時,%eax保存着x、y相加的結果,接下來操做系統經過調度將PC指定到sub函數的以下指令:
movl 20(%ebp),%eax複製代碼
能夠預見的是,%eax值被覆蓋了,那麼當CPU從新調度到sum函數的後續指令時,將發生各類可能的錯誤。
因此,在調度過程當中,必須設計一套模型,使得各個代碼邏輯在執行過程當中,關於存儲空間的利用不會互相干擾,而且CPU能完整地執行完各個邏輯。
在前面示例的案例中,若是要讓兩個函數同時運行,經過CPU高頻的調度能夠「無感知」地分配計算資源進行處理,但在兩個不一樣代碼邏輯中來回計算,須要考慮一些存儲空間的衝突問題,避免數據受到彼此干擾,這裏須要說起一個重要概念:上下文環境
我的剛開始接觸編程時,偶爾會在書本或相關文檔中見到「上下文」的字眼,當時並無太在乎這個概念,也沒真正理解過。何謂上下文?百科是這樣歸納的:
上下文,即語境、語意,是語言學科(語言學、社會語言學、篇章分析、語用學、符號學等)的概念。
但在剛剛分析的問題中,我我的能夠用蹩腳的話語來講明上下文環境:某個執行中代碼邏輯的先後關係和存儲狀態,先後關係說明了在這個環境下需保證邏輯的正確性,存儲狀態說明了相關數據內容在先後執行過程當中需保持一致的狀態,不可異常變更。
假設CPU在執行sum函數過程當中,還未執行指令movl 12(%ebp),%eax的狀況下直接執行addl 8(%ebp),%eax,那麼這個先後關係就被破壞了。若是在執行sum函數的指令addl 8(%ebp),%eax後開始調度sub函數的movl 20(%ebp),%eax指令,那麼%eax數值被幹擾,當CPU從新調度執行sum函數後續指令時,存儲狀態的一致性被破壞了。
所以,維護好上下文環境就是將每一個獨立的代碼邏輯當作一個完整而封閉的執行單元來區別處理,進程的概念就是在這樣的需求下被設計了出來,能夠說,進程做爲不一樣程序執行的基本單元,維護了相應的上下文環境,在CPU高速調度過程當中保證了不一樣程序的正確運行!
關於進程的概念,linux操做系統中是這樣理解的:
程序是一個可執行文件,而進程是一個執行中的程序實例。利用分時技術,在Linux操做系統上能夠同時運行多個進程。分時技術的基本原理是把CPU的運行時間劃分紅一個個規定長度的時間片,讓每一個進程在一個時間內運行。當進程的時間片用完時系統就利用調度程序切換到另外一個進程去運行。所以實際上對於具備單個CPU的機器來講某一時刻只能運行一個進程。但因爲每一個進程運行的時間片很短,因此表面看起來好像全部進程都在同時運行着。
當前包括Linux等操做系統通過數十年發展,進程在操做系統內部的表示已經變得很是複雜,包括線程、協程等概念也被創造出來,但全部的程序最終都依附於進程。本文意在對進程完成初步印象,所以咱們將用最簡單的方式來結合前面案例構建一個進程結構。
在上面的調度案例中,咱們遭遇了上下文環境不一致的問題,一個是代碼邏輯方面、一個是存儲狀態方面,咱們分別從這兩個方面進行分析。
代碼邏輯
CPU在調度過程當中,從sum函數切換到sub函數執行時,首先須要知道接下來該執行sub函數哪一個位置的指令,從而切換到sub函數執行前能夠更新PC寄存器的值,讓CPU沿着上一次執行的位置按序處理後續指令。這裏,須要有一個存儲空間用於保存各個代碼邏輯待需執行的指令位置,每當CPU調度到該程序時,從該空間提取出指令位置信息,恢復到PC寄存器,整個過程能夠實現完整的處理。
存儲狀態
當執行完sum函數的 movl 12(%ebp),%eax 指令後,%eax保存着sum函數的x變量值,用於後續指令的調用,但當CPU接下來調度到sub函數的 movl 20(%ebp),%eax 指令執行後,%eax的值將受到干擾,所以每次調度都必須對當前上下文環境的寄存器值進行保存,即在準備調度到sub函數前將%eax的值保存到sum函數專有的內存空間,在後續從新執行sum函數時,再從該內存空間恢復%eax值,這樣保證了sum函數後續的指令 addl 8(%ebp),%eax 正常處理!這些過程由操做系統的進程機制自動處理,對程序員而言都是透明的。
因此,每一個進程必需要求有一個獨立的內存空間,這個內存空間的第一做用就是維護當前代碼邏輯的上下文環境信息!
linux操做系統是怎麼實現進程管理的?咱們來窺探下linux本人在30年前開發linux內核第一版時的思路:linux內核在內存空間爲每一個進程開闢一個獨立而固定的內存空間來存放進程結構,進程結構保存了不一樣進程當前的上下文環境信息。結合前文的分析,咱們瞭解到至少在該空間保存了進程待執行指令的地址(用於恢復PC寄存器),當調度發生準備切換到另外一進程時,操做系統會將各個寄存器值保存到進程空間對應位置中,當調度從新切換到該進程時再從進程空間恢復到各個寄存器,進程切換就是在這麼一個過程當中反反覆覆。
若是你理解了這個基本過程,那麼應該明白進程的調度實際上是有成本的,操做系統每次對進程的調度,都須要對不少存儲狀態進行處理,目前操做系統不少進程狀態都存儲在內存中,這就要求每次調度都會對內存進行了必定的IO操做,這對於每秒鐘億萬次運算的CPU來講不得不等待IO的過程,可能會形成必定的延遲。
有個技術編寫一個從遠端服務器拉取數據的腳本,在四核CPU的服務器上操做。當時一會兒開啓了幾十個進程同時往遠端拉取,發現進程開的越多,整個服務器從遠端拉取數據的速度反而越慢,最終尚未開4個進程的塊,這是爲何呢?
爲何說線程比較輕量?這個輕量的輕如何從底層去理解?
golang語言做爲21世紀的C語言,以高性能、高併發著稱,其中golang有一個叫「協程」的概念,經過協程一個應用程序能夠在消耗極少內存和其它計算資源的條件下實現大量併發處理,這種技術究竟是怎麼實現的?
在本人的理解中,進程存在的意義就是爲了管理不一樣的程序,維護着不一樣程序的上下文環境,這些都是在調度的場景中被設計出來的。不管操做系統如何龐大複雜,進程調度的核心概念都離不開此。本文在不少關鍵細節上並無作很是細緻而嚴謹的說明,突出的是進程在調度中扮演的角色,這個角色若是深究還有很是多的特性和細節,能夠自行查找相關資料瞭解。
參考:《深刻理解計算機系統》、《linux內核徹底註釋》、《linux內核架構》