阿里架構師剖析程序運行原理,程序是如何運行又是如何崩潰的?

本文已收錄GitHub,更有互聯(lián)網大廠面試真題,面試攻略,高效學習資料等

軟件的核心載體是程序代碼,軟件開發(fā)的主要工作產出也是代碼,但是代碼被存儲在磁盤上本身沒有任何價值,軟件要想實現(xiàn)價值,代碼就必須運行起來。那么代碼是如何運行的?在運行中可能會出現(xiàn)什么問題呢?

一、程序是如何運行起來的

軟件被開發(fā)出來,是文本格式的代碼,這些代碼通常不能直接運行,需要使用編譯器編譯成操作系統(tǒng)或者虛擬機可以運行的代碼,即可執(zhí)行代碼,它們都被存儲在文件系統(tǒng)中。不管是文本格式的代碼還是可執(zhí)行的代碼,都被稱為程序,程序是靜態(tài)的,安靜地呆在磁盤上,什么也干不了。要想讓程序處理數(shù)據,完成計算任務,必須把程序從外部設備加載到內存中,并在操作系統(tǒng)的管理調度下交給 CPU 去執(zhí)行,去運行起來,才能真正發(fā)揮軟件的作用,程序運行起來以后,被稱作進程

進程除了包含可執(zhí)行的程序代碼,還包括進程在運行期使用的內存堆空間、??臻g、供操作系統(tǒng)管理用的數(shù)據結構。如下圖所示:

操作系統(tǒng)把可執(zhí)行代碼加載到內存中,生成相應的數(shù)據結構和內存空間后,就從可執(zhí)行代碼的起始位置讀取指令交給 CPU 順序執(zhí)行。指令執(zhí)行過程中,可能會遇到一條跳轉指令,即CPU 要執(zhí)行的下一條指令不是內存中可執(zhí)行代碼順序的下一條指令。編程中使用的循環(huán)for…,while…和 if…else…最后都被編譯成跳轉指令。

程序運行時如果需要創(chuàng)建數(shù)組等數(shù)據結構,操作系統(tǒng)就會在進程的堆空間申請一塊相應的內存空間,并把這塊內存的首地址信息記錄在進程的棧中。堆是一塊無序的內存空間,任何時候進程需要申請內存,都會從堆空間中分配,分配到的內存地址則記錄在棧中。

棧是嚴格的一個后進先出的數(shù)據結構,同樣由操作系統(tǒng)維護,主要用來記錄函數(shù)內部的局部變量、堆空間分配的內存空間地址等。

我們以如下代碼示例,描述函數(shù)調用過程中,棧的操作過程:

voidf(){
    intx = g(1);
    x++;
    //g函數(shù)返回,當前堆棧頂部為f函數(shù)棧幀,在當前棧幀繼續(xù)執(zhí)行f函數(shù)的代碼。
}
intg(int x){
    returnx +1;
}

每次函數(shù)調用,操作系統(tǒng)都會在棧中創(chuàng)建一個棧幀(stack frame)。正在執(zhí)行的函數(shù)參數(shù)、局部變量、申請的內存地址等都在當前棧幀中,也就是堆棧的頂部棧幀中。如下圖所示:

當 f 函數(shù)執(zhí)行的時候,f 函數(shù)就在棧頂,棧幀中存儲著 f 函數(shù)的局部變量,輸入參數(shù)等等。當 f 函數(shù)調用 g 函數(shù),當前執(zhí)行函數(shù)就變成 g 函數(shù),操作系統(tǒng)會為 g 函數(shù)創(chuàng)建一個棧幀并放置在棧頂。當函數(shù) g() 調用結束,程序返回 f 函數(shù),g 函數(shù)對應的棧幀出棧,頂部棧幀變又為 f 函數(shù),繼續(xù)執(zhí)行 f 函數(shù)的代碼,也就是說,真正執(zhí)行的函數(shù)永遠都在棧頂。而且因為棧幀是隔離的,所以不同函數(shù)可以定義相同的變量而不會發(fā)生混亂。

二、一臺計算機如何同時處理數(shù)以百計的任務

我們自己日常使用的 PC 計算機通常只是一核或者兩核的 CPU,我們部署應用程序的服務器雖然有更多的 CPU 核心,通常也不過幾核或者幾十核。但是我們的 PC 計算機可以同時編程、聽音樂,而且還能執(zhí)行下載任務,而服務器則可以同時處理數(shù)以百計甚至數(shù)以千計的并發(fā)用戶請求。

那么為什么一臺計算機服務器可以同時處理數(shù)以百計,以千計的計算任務呢?這里主要依靠的是操作系統(tǒng)的 CPU 分時共享技術。如果同時有很多個進程在執(zhí)行,操作系統(tǒng)會將 CPU的執(zhí)行時間分成很多份,進程按照某種策略輪流在 CPU 上運行。由于現(xiàn)代 CPU 的計算能力非常強大,雖然每個進程都只被執(zhí)行了很短一個時間,但是在外部看來卻好像是所有的進程都在同時執(zhí)行,每個進程似乎都獨占一個 CPU 執(zhí)行。

所以雖然從外部看起來,多個進程在同時運行,但是在實際物理上,進程并不總是在 CPU上運行的,一方面進程共享 CPU,所以需要等待 CPU 運行,另一方面,進程在執(zhí)行 I/O操作的時候,也不需要 CPU 運行。進程在生命周期中,主要有三種狀態(tài),運行、就緒、阻塞。

  • 運行:當一個進程在 CPU 上運行時,則稱該進程處于運行狀態(tài)。處于運行狀態(tài)的進程的數(shù)目小于等于 CPU 的數(shù)目。
  • 就緒:當一個進程獲得了除 CPU 以外的一切所需資源,只要得到 CPU 即可運行,則稱此進程處于就緒狀態(tài),就緒狀態(tài)有時候也被稱為等待運行狀態(tài)。
  • 阻塞:也稱為等待或睡眠狀態(tài),當一個進程正在等待某一事件發(fā)生(例如等待 I/O 完成,等待鎖……)而暫時停止運行,這時即使把 CPU 分配給進程也無法運行,故稱該進程處于阻塞狀態(tài)。

不同進程輪流在 CPU 上執(zhí)行,每次都要進行進程間 CPU 切換,代價是非常大的,實際上,每個用戶請求對應的不是一個進程,而是一個線程。線程可以理解為輕量級的進程,在進程內創(chuàng)建,擁有自己的線程棧,在 CPU 上進行線程切換的代價也更小。線程在運行時,和進程一樣,也有三種主要狀態(tài),從邏輯上看,進程的主要概念都可以套用到線程上。我們在進行服務器應用開發(fā)的時候,通常都是多線程開發(fā),理解線程對我們設計、開發(fā)軟件更有價值。

三、系統(tǒng)為什么會變慢,為什么會崩潰

現(xiàn)在的服務器軟件系統(tǒng)主要使用多線程技術實現(xiàn)多任務處理,完成對很多用戶的并發(fā)請求處理。也就是我們開發(fā)的應用程序通常以一個進程的方式在操作系統(tǒng)中啟動,然后在進程中創(chuàng)建很多線程,每個線程處理一個用戶請求。

以 Java 的 web 開發(fā)為例,似乎我們編程的時候通常并不需要自己創(chuàng)建和啟動線程,那么我們的程序是如何被多線程并發(fā)執(zhí)行,同時處理多個用戶請求的呢?實際中,啟動多線程,為每個用戶請求分配一個處理線程的工作是在 web 容器中完成的,比如常用的 Tomcat 容器。

如下圖所示:

Tomcat 啟動多個線程,為每個用戶請求分配一個線程,調用和請求 URL 路徑相對應的Servlet(或者 Controller)代碼,完成用戶請求處理。而 Tomcat 則在 JVM 虛擬機進程中,JVM 虛擬機則被操作系統(tǒng)當做一個獨立進程管理。真正完成最終計算的,是 CPU、內存等服務器硬件,操作系統(tǒng)將這些硬件進行分時(CPU)、分片(內存)管理,虛擬化成一個獨享資源讓 JVM 進程在其上運行。

以上就是一個 Java web 應用運行時的主要架構,有時也被稱作架構過程視圖。需要注意的是,這里有個很多 web 開發(fā)者容易忽略的事情,那就是不管你是否有意識,你開發(fā)的 web程序都是被多線程執(zhí)行的,web 開發(fā)天然就是多線程開發(fā)

CPU 以線程為單位進行分時共享執(zhí)行,可以想象代碼被加載到內存空間后,有多個線程在這些代碼上執(zhí)行,這些線程從邏輯上看,是同時在運行的,每個線程有自己的線程棧,所有的線程棧都是完全隔離的,也就是每個方法的參數(shù)和方法內的局部變量都是隔離的,一個線程無法訪問到其他線程的棧內數(shù)據。

但是當某些代碼修改內存堆里的數(shù)據的時候,如果有多個線程在同時執(zhí)行,就可能會出現(xiàn)同時修改數(shù)據的情況,比如,兩個線程同時對一個堆中的數(shù)據執(zhí)行 +1 操作,最終這個數(shù)據只會被加一次,這就是人們常說的線程安全問題,實際上線程的結果應該是依次加一,即最終的結果應該是 +2。

多個線程訪問共享資源的這段代碼被稱為臨界區(qū),解決線程安全問題的主要方法是使用鎖,將臨界區(qū)的代碼加鎖,只有獲得鎖的線程才能執(zhí)行臨界區(qū)代碼,如下:

如果當前線程執(zhí)行到第一行,獲得鎖的代碼的時候,鎖已經被其他線程獲取并沒有釋放,那么這個線程就會進入阻塞狀態(tài),等待前面釋放鎖的線程將自己喚醒重新獲得鎖。

鎖會引起線程阻塞,如果有很多線程同時在運行,那么就會出現(xiàn)線程排隊等待鎖的情況,線程無法并行執(zhí)行,系統(tǒng)響應速度就會變慢。此外 I/O 操作也會引起阻塞,對數(shù)據庫連接的獲取也可能會引起阻塞。目前典型的 web 應用都是基于 RDBMS 關系數(shù)據庫的,web 應用要想訪問數(shù)據庫,必須獲得數(shù)據庫連接,而受數(shù)據庫資源限制,每個 web 應用能建立的數(shù)據庫的連接是有限的,如果并發(fā)線程數(shù)超過了連接數(shù),那么就會有部分線程無法獲得連接而進入阻塞,等待其他線程釋放連接后才能訪問數(shù)據庫,并發(fā)的線程數(shù)越多,等待連接的時間也越多,從 web 請求者角度看,響應時間變長,系統(tǒng)變慢。

lock.lock();
//線程獲得鎖
i++;
//臨界區(qū)代碼,i位于堆中
lock.unlock();
//線程釋放鎖

被阻塞的線程越多,占據的系統(tǒng)資源也越多,這些被阻塞的線程既不能繼續(xù)執(zhí)行,也不能釋放當前已經占據的資源,在系統(tǒng)中一邊等待一邊消耗資源,如果阻塞的線程數(shù)超過了某個系統(tǒng)資源的極限,就會導致系統(tǒng)宕機,應用崩潰

解決系統(tǒng)因高并發(fā)而導致的響應變慢、應用崩潰的主要手段是使用分布式系統(tǒng)架構,用更多的服務器構成一個集群,以便共同處理用戶的并發(fā)請求,保證每臺服務器的并發(fā)負載不會太高。此外必要時還需要在請求入口處進行限流,減小系統(tǒng)的并發(fā)請求數(shù);在應用內進行業(yè)務降級,減小線程的資源消耗。

四、總結

事實上,現(xiàn)代 CPU 和操作系統(tǒng)的設計遠比這篇文章講的要復雜得多,但是基礎原理大致就是如此。為了讓程序能很好地被執(zhí)行,軟件開發(fā)的時候要考慮很多情況,為了讓軟件能更好地發(fā)揮效能,需要在部署上進行規(guī)劃和架構。軟件是如何運行的,應該是軟件工程師和架構師的常識,在設計開發(fā)軟件的時候,應該時刻以常識去審視自己的工作,保證軟件開發(fā)在正確的方向上前進。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容