過去這麼多講,咱們的程序都是自動運行且正常運行的。自動運行的意思是說,咱們的程序和指令都是一條條順序執行,你不須要經過鍵盤或者網絡給這個程序任何輸入。
正常運行是說,咱們的程序都是可以正常執行下去的,沒有遇到計算溢出之類的程序錯誤。前端
不過,現實的軟件世界可沒有這麼簡單。一方面,程序不只是簡單的執行指令,更多的還須要和外部的輸入輸出打交道。另外一方面,程序在執行過程當中,還會遇到各類異常狀況,
好比除以0、溢出,甚至咱們本身也可讓程序拋出異常。java
那這一講,我就帶你來看看,若是遇到這些狀況,計算機是怎麼運轉的,也就是說,計算機到底是如何處理異常的。程序員
一提到計算機當中的 異常(Exception),可能你的第一反應就是C++或者Java中的Exception。不過咱們今天講的,並非這些軟件開發過程當中遇到的「軟件異常」,而是和硬件、系統相關的「硬件異常」。固然,「軟件異常」和「硬件異常」並非實際業界使用的專有名詞,只是我爲了方便給你說明,和C++、Java中軟件拋出的Exception進行的人爲區分,你明白這個意思就好。後端
儘管,這裏我把這些硬件和系統相關的異常,叫做「硬件異常」。可是,實際上,這些異常,既有來自硬件的,也有來自軟件層面的。網絡
好比,咱們在硬件層面,當加法器進行兩個數相加的時候,會遇到算術溢出;或者,你在玩遊戲的時候,按下鍵盤發送了一個信號給到CPU,CPU要去執行一個現有流程以外的指令,
這也是一個「異常」。一樣,來自軟件層面的,好比咱們的程序進行系統調用,發起一個讀文件的請求。這樣應用程序向系統調用發起請求的狀況,同樣是經過「異常」來實現的。前後端分離
關於異常,最有意思的一點就是,它實際上是一個硬件和軟件組合到一塊兒的處理過程。異常的前半生,也就是異常的發生和捕捉,是在硬件層面完成的。可是異常的後半生,
也就是說,異常的處理,實際上是由軟件來完成的。異步
計算機會爲每一種可能會發生的異常,分配一個異常代碼(Exception Number)。有些教科書會把異常代碼叫做中斷向量(Interrupt Vector)。異常發生的時候,
一般是CPU檢測到了一個特殊的信號。好比,你按下鍵盤上的按鍵,輸入設備就會給CPU發一個信號。或者,正在執行的指令發生了加法溢出,一樣,咱們能夠有一個進位溢出的信號。
這些信號呢,在組成原理裏面,咱們通常叫做發生了一個事件(Event)。CPU在檢測到事件的時候,其實也就拿到了對應的異常代碼。ide
這些異常代碼裏,I/O發出的信號的異常代碼,是由操做系統來分配的,也就是由軟件來設定的。而像加法溢出這樣的異常代碼,則是由CPU預先分配好的,
也就是由硬件來分配的。這又是另外一個軟件和硬件共同組合來處理異常的過程。函數
拿到異常代碼以後,CPU就會觸發異常處理的流程。計算機在內存裏,會保留一個異常表(ExceptionTable)。也有地方,把這個表叫做中斷向量表(Interrupt Vector Table),性能
好和上面的中斷向量對應起來。這個異常表有點兒像咱們在第10講裏講的GOT表,存放的是不一樣的異常代碼對應的異常處理程序(Exception Handler)所在的地址。
咱們的CPU在拿到了異常碼以後,會先把當前的程序執行的現場,保存到程序棧裏面,而後根據異常碼查詢,找到對應的異常處理程序,最後把後續指令執行的指揮權,交給這個異常處理程序。
這樣「檢測異常,拿到異常碼,再根據異常碼進行查表處理」的模式,在平常開發的過程當中是很常見的。
好比說,如今咱們平常進行的Web或者App開發,一般都是先後端分離的。前端的應用,會向後端發起HTTP的請求。當後端遇到了異常,一般會給到前端一個對應的錯誤代碼。
前端的應用根據這個錯誤代碼,在應用層面去進行錯誤處理。在不能處理的時候,它會根據錯誤代碼向用戶顯示錯誤信息。
public class LastChanceHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { // do something here - log to file and upload to server/close resources/delete files... } } Thread.setDefaultUncaughtExceptionHandler(new LastChanceHandler());
再好比說,Java裏面,咱們使用一個線程池去運行調度任務的時候,能夠指定一個異常處理程序。對於各個線程在執行任務出現的異常狀況,咱們是經過異常處理程序進行處理,
而不是在實際的任務代碼裏處理。這樣,咱們就把業務處理代碼就和異常處理代碼的流程分開了。
我在前面說了,異常能夠由硬件觸發,也能夠由軟件觸發。那咱們平時會碰到哪些異常呢?下面咱們就一塊兒來看看。
顧名思義,天然就是程序在執行到一半的時候,被打斷了。這個打斷執行的信號,來自於CPU外部的I/O設備。你在鍵盤上按下一個按鍵,就會對應觸發一個相應的信號到達CPU
裏面。CPU裏面某個開關的值發生了變化,也就觸發了一箇中斷類型的異常。
實際上是咱們程序員「故意「主動觸發的異常。就好像你在程序裏面打了一個斷點,這個斷點就是設下的一個"陷阱"。當程序的指令執行到這個位置的時候,就掉到了這個陷阱當
中。而後,對應的異常處理程序就會來處理這個"陷阱"當中的獵物。
最多見的一類陷阱發生在咱們的應用程序調用系統調用的時候,也就是從程序的用戶態切換到內核態的時候。咱們在第3講講CPU性能的時候說過,能夠用Linux下的time指令,去查看一個程序運行實際花費的時間,裏面有在用戶態花費的時間(user time),也有在內核態發生的時間(system time)。
咱們的應用程序經過系統調用去讀取文件、建立進程,其實也是經過觸發一次陷阱來進行的。這是由於,咱們用戶態的應用程序沒有權限來作這些事情,
須要把對應的流程轉交給有權限的異常處理程序來進行。
它和陷阱的區別在於,陷阱是咱們開發程序的時候刻意觸發的異常,而故障一般不是。好比,咱們在程序執行的過程當中,進行加法計算髮生了溢出,其實就是故障類型的異常。這個
異常不是咱們在開發的時候計劃內的,也同樣須要有對應的異常處理程序去處理。
故障和陷阱、中斷的一個重要區別是,故障在異常程序處理完成以後,仍然回來處理當前的指令,而不是去執行程序中的下一條指令。由於當前的指令由於故障的緣由並無成功執行完成。
最後一種異常叫 停止(Abort)。與其說這是一種異常類型,不如說這是故障的一種特殊狀況。當CPU遇到了故障,可是恢復不過來的時候,程序就不得不停止了。
在這四種異常裏,中斷異常的信號來自系統外部,而不是在程序本身執行的過程當中,因此咱們稱之爲「異步」類型的異常。而陷阱、故障以及停止類型的異常,
是在程序執行的過程當中發生的,因此咱們稱之爲「同步「類型的異常。
在處理異常的過程中,不管是異步的中斷,仍是同步的陷阱和故障,咱們都是採用同一套處理流程,也就是上面所說的,「保存現場、異常代碼查詢、異常處理程序調用「。而停止類型的異常,
實際上是在故障類型異常的一種特殊狀況。當故障發生,可是咱們發現沒有異常處理程序可以處理這種異常的狀況下,程序就不得不進入停止狀態,也就是最終會退出當前的程序執行。
在實際的異常處理程序執行以前,CPU須要去作一次「保存現場」的操做。這個保存現場的操做,和我在第7講裏講解函數調用的過程很是類似。
由於切換到異常處理程序的時候,其實就好像是去調用一個異常處理函數。指令的控制權被切換到了另一個"函數"裏面,因此咱們天然要把當前正在執行的指令去壓棧。
這樣,咱們才能在異常處理程序執行完成以後,從新回到當前的指令繼續往下執行。
不過,切換到異常處理程序,比起函數調用,仍是要更復雜一些。緣由有下面幾點。
由於異常狀況每每發生在程序正常執行的預期以外,好比中斷、故障發生的時候。因此,除了原本程序壓棧要作的事情以外,咱們還須要把CPU內當前運行程序用到的全部寄存器,
都放到棧裏面。最典型的就是條件碼寄存器裏面的內容。
像陷阱這樣的異常,涉及程序指令在用戶態和內核態之間的切換。對應壓棧的時候,對應的數據是壓到內核棧裏,而不是程序棧裏。
像故障這樣的異常,在異常處理程序執行完成以後。從棧裏返回出來,繼續執行的不是順序的下一條指令,而是故障發生的當前指令。由於當前指令由於故障沒有正常執行成功,
必須從新去執行一次。因此,對於異常這樣的處理流程,不像是順序執行的指令間的函數調用關係。而是更像兩個不一樣的獨立進程之間在CPU層面的切換,
因此這個過程咱們稱之爲 上下文切換(Context Switch)。
這一講,我給你講了計算機裏的「異常」處理流程。這裏的異常能夠分紅中斷、陷阱、故障、停止這樣四種狀況。這四種異常,分別對應着I/O設備的輸入、
程序主動觸發的狀態切換、異常狀況下的程序出錯以及出錯以後無可挽回的退出程序。
當CPU遭遇了異常的時候,計算機就須要有相應的應對措施。CPU會經過「查表法」來解決這個問題。在硬件層面和操做系統層面,各自定義了全部CPU可能會遇到的異常代碼,
而且經過這個異常代碼,在異常表裏面查詢相應的異常處理程序。在捕捉異常的時候,咱們的硬件CPU在進行相應的操做,而在處理異常層面,則是由做爲軟件的異常處理程序進行相應的操做。
而在實際處理異常以前,計算機須要先去作一個「保留現場」的操做。有了這個操做,咱們才能在異常處理完成以後,從新回到以前執行的指令序列裏面來。
這個保留現場的操做,和咱們以前講解指令的函數調用很像。可是,由於「異常」和函數調用有一個很大的不一樣,那就是它的發生時間。函數調用的壓棧操做咱們在寫程序的時候徹底可以知道,而「異常」發生的時間卻很不肯定。
因此,「異常」發生的時候,咱們稱之爲發生了一次「上下文切換」(Context Switch)。這個時候,除了普通須要壓棧的數據外,計算機還須要把全部寄存器信息都存儲到棧裏面去。