在介紹JAVA如何一步步被執行起來之前,我們需要先弄明白為什么Java可以實現跨平臺運行,因為搞清楚了這個問題之后,對于我們理解Java程序如何被CPU執行起來非常有幫助。
無論是剛剛入門Java的新手還是已經工作了的老司機,恐怕都不容易把Java代碼如何一步步被CPU執行起來這個問題完全講清楚。但是對于一個Java程序員來說寫了那么久的代碼,我們總要搞清楚自己寫的Java代碼到底是怎么運行起來的。另外在求職面試的時候這個問題也常常會聊到,面試官主要想通過它考察求職同學對于Java以及計算機基礎技術體系的理解程度,看似簡單的問題實際上囊括了JVM運行原理、操作系統以及CPU運行原理等多方面的技術知識點。我們一起來看看Java代碼到底是怎么被運行起來的。通過這種雙親委派模型,可以保證同一個類在不同的類加載器中只會被加載一次,從而避免了類的重復加載,也保證了類的唯一性。同時,由于每個類加載器只會加載自己所負責的類,因此可以防止惡意代碼的注入和類的篡改,提高了Java程序的安全性。
Java如何實現跨平臺
在介紹Java如何一步步被執行起來之前,我們需要先弄明白為什么Java可以實現跨平臺運行,因為搞清楚了這個問題之后,對于我們理解Java程序如何被CPU執行起來非常有幫助。
為什么需要JVM
write once run anywhere曾經是Java響徹編程語言圈的slogan,也就是所謂的程序員開發完java應用程序后,可以在不需要做任何調整的情況下,無差別的在任何支持Java的平臺上運行,并獲得相同的運行結果從而實現跨平臺運行,那么Java到底是如何做到這一點的呢?
其實對于大多數的編程語言來說,都需要將程序轉換為機器語言才能最終被CPU執行起來。因為無論是如Java這種高級語言還是像匯編這種低級語言實際上都是給人看的,但是計算機無法直接進行識別運行。因此想要CPU執行程序就必須要進行語言轉換,將程序語言轉化為CPU可以識別的機器語言。
學過計算機組成原理的同學肯定都知道,CPU內部都是用大規模晶體管組合而成的,而晶體管只有高電位以及低點位兩種狀態,正好對應二進制的0和1,因此機器碼實際就是由0和1組成的二進制編碼集合,它可以被CPU直接識別和執行。
但是像X86架構或者ARM架構,不同類型的平臺對應的機器語言是不一樣的,這里的機器語言指的是用二進制表示的計算機可以直接識別和執行的指令集集合。不同平臺使用的CPU不同,那么對應的指令集也就有所差異,比如說X86使用的是CISC復雜指令集而ARM使用的是RISC精簡指令集。所以Java要想實現跨平臺運行就必須要屏蔽不同架構下的計算機底層細節差異。因此,如何解決不同平臺下機器語言的適配問題是Java實現一次編寫,到處運行的關鍵所在。
那么Java到底是如何解決這個問題的呢?怎么才能讓CPU可以看懂程序員寫的Java代碼呢?其實這就像在我們的日常生活中,如果雙方語言不通,要想進行交流的話就必須中間得有一個翻譯,這樣通過翻譯的語言轉換就可以實現雙方暢通無阻的交流了。打個比方,一個中國廚師要教法國廚師和阿拉伯廚師做菜,中國廚師不懂法語和阿拉伯語,法國廚師和阿拉伯廚師不懂中文,要想順利把菜做好就需要有翻譯來幫忙。中國廚師把做菜的菜譜告訴翻譯者,翻譯者將中文菜譜轉換為法文菜譜以及阿拉伯語菜譜,這樣法國廚師和阿拉伯廚師就知道怎么做菜了。
因此Java的設計者借助了這樣的思想,通過JVM(Java Virtual machine,Java虛擬機)這個中間翻譯來實現語言轉換。程序員編寫以.java為結尾的程序之后通過javac編譯器把.java為結尾的程序文件編譯成.class結尾的字節碼文件,這個字節碼文件需要JVM這個中間翻譯進行識別解析,它由一組如下圖這樣的16進制數組成。JVM將字節碼文件轉化為匯編語言后再由硬件解析為機器語言最終最終交給CPU執行。
所以說通過JVM實現了計算機底層細節的屏蔽,因此windows平臺有windows平臺的JVM,linux平臺有Linux平臺的JVM,這樣在不同平臺上存在對應的JVM充當中間翻譯的作用。因此只要編譯一次,不同平臺的JVM都可以將對應的字節碼文件進行解析后運行,從而實現在不同平臺下運行的效果。
那么問題又來了,JVM是怎么解析運行.class文件的呢?要想搞清楚這個問題,我們得先看看JVM的內存結構到底是怎樣的,了解JVM結構之后這個問題就迎刃而解了。
JVM結構
JVM(Java Virtual Machine)即Java虛擬機,它的核心作用主要有兩個,一個是運行Java應用程序,另一個是管理Java應用程序的內存。它主要由三部分組成,類加載器、運行時數據區以及字節碼執行引擎。
類加載器
類加載器負責將字節碼文件加載到內存中,主要經歷加載-》連接-》實例化三個階段完成類加載操作。
另外需要注意的是.class并不是一次性全部加載到內存中,而是在Java應用程序需要的時候才會加載。也就是說當JVM請求一個類進行加載的時候,類加載器就會嘗試查找定位這個類,當查找對應的類之后將他的完全限定類定義加載到運行時數據區中。
運行時數據區
JVM定義了在Java程序運行期間需要使用到的內存區域,簡單來說這塊內存區域存放了字節碼信息以及程序執行過程數據。運行時數據區主要劃分了堆、程序計數器虛擬機棧、本地方法棧以及元空間數據區。其中堆數據區域在JVM啟動后便會進行分配,而虛擬機棧、程序計數器本地方法棧都是在常見線程后進行分配。
不過需要說明的是在JDK 1.8及以后的版本中,方法區被移除了,取而代之的是元空間(Metaspace)。元空間與方法區的作用相似,都是存儲類的結構信息,包括類的定義、方法的定義、字段的定義以及字節碼指令。不同的是,元空間不再是JVM內存的一部分,而是通過本地內存(Native Memory)來實現的。在JVM啟動時,元空間的大小由MaxMetaspaceSize參數指定,JVM在運行時會自動調整元空間的大小,以適應不同的程序需求。
字節碼執行引擎
字節碼執行引擎最核心的作用就是將字節碼文件解釋為可執行程序,主要包含了解釋器、即使編譯以及垃圾回收器。字節碼執行引擎從元空間獲取字節碼指令進行執行。當Java程序調用一個方法時,JVM會根據方法的描述符和方法所在的類在元空間中查找對應的字節碼指令。字節碼執行引擎從元空間獲取字節碼指令,然后執行這些指令。
JVM如何運行Java程序
在搞清楚了JVM的結構之后,接下來我們一起來看看天天寫的Java代碼是如何被CPU飆起來的。一般公司的研發流程都是產品經理提需求然后程序員來實現。所以當產品經理把需求提過來之后,程序員就需要分析需求進行設計然后編碼實現,比如我們通過Idea來完成編碼工作,這個時候工程中就會有一堆的以.java結尾的Java代碼文件,實際上就是程序員將產品需求轉化為對應的Java程序。但是這個.java結尾的Java代碼文件是給程序員看的,計算機無法識別,所以需要進行轉換,轉換為計算機可以識別的機器語言。
通過上文我們知道,Java為了實現write once,run anywhere的宏偉目標設計了JVM來充當轉換翻譯的工作。因此我們編寫好的.java文件需要通過javac編譯成.class文件,這個class文件就是傳說中的字節碼文件,而字節碼文件就是JVM的輸入。
當我們有了.class文件也就是字節碼文件之后,就需要啟動一個JVM實例來進一步加載解析.class字節碼。實際上JVM本質其實就是操作系統中的一個進程,因此要想通過JVM加載解析.class文件,必須先啟動一個JVM進程。JVM進程啟動之后通過類加載器加載.class文件,將字節碼加載到JVM對應的內存空間。
當.class文件對應的字節碼信息被加載到中之后,操作系統會調度CPU資源來按照對應的指令執行java程序。
以上是CPU執行Java代碼的大致步驟,看到這里我相信很多同學都有疑問這個執行步驟也太大致了吧。哈哈,別著急,有了基本的解析流程之后我們再對其中的細節進行分析,首先我們就需要弄清楚JVM是如何加載編譯后的.class文件的。
字節碼文件結構
要想搞清楚JVM如何加載解析字節碼文件,我們就先得弄明白字節碼文件的格式,因為任何文件的解析都是根據該文件的格式來進行。就像CPU有自己的指令集一樣,JVM也有自己一套指令集也就是Java字節碼,從根上來說Java字節碼是機器語言的.class文件表現形式。字節碼文件結構是一組以 8 位為最小單元的十六進制數據流,具體的結構如下圖所示,主要包含了魔數、class文件版本、常量池、訪問標志、索引、字段表集合、方法表集合以及屬性表集合描述數據信息。
這里簡單說明下各個部分的作用,后面會有專門的文章再詳細進行闡述。
魔數與文件版本
魔數的作用就是告訴JVM自己是一個字節碼文件,你JVM快來加載我吧,對于Java字節碼文件來說,其魔數為0xCAFEBABE,現在知道為什么Java的標志是咖啡了吧。而緊隨魔數之后的兩個字節是文件版本號,Java的版本號通常是以52.0的形式表示,其中高16位表示主版本號,低16位表示次版本號。。
常量池
在常量池中說明常量個數以及具體的常量信息,常量池中主要存放了字面量以及符號引用這兩類常量數據,所謂字面量就是代碼中聲明為final的常量值,而符號引用主要為類和接口的完全限定名、字段的名稱和描述符以及方法的名稱以及描述符。這些信息在加載到JVM之后在運行期間將符號引用轉化為直接引用才能被真正使用。常量池的第一個元素是常量池大小,占據兩個字節。常量池表的索引從1開始,而不是從0開始,這是因為常量池的第0個位置是用于特殊用途的。
訪問標志
類或者接口的訪問標記,說明類是public還是abstract,用于描述該類的訪問級別和屬性。訪問標志的取值范圍是一個16位的二進制數。
索引
包含了類索引、父類索引、接口索引數據,主要說明類的繼承關系。
字段表集合
主要是類級變量而不是方法內部的局部變量。
方法表集合
主要用來描述類中有幾個方法,每個方法的具體信息,包含了方法訪問標識、方法名稱索引、方法描述符索引、屬性計數器、屬性表等信息,總之就是描述方法的基礎信息。
屬性表集合
方法表集合之后是屬性表集合,用于描述該類的所有屬性。屬性表集合包含了所有該類的屬性的描述信息,包括屬性名稱、屬性類型、屬性值等等。
解析字節碼文件
知道了字節碼文件的結構之后,JVM就需要對字節碼文件進行解析,將字節碼結構解析為JVM內部流轉的數據結構。大致的過程如下:
1、讀取字節碼文件
JVM首先需要讀取字節碼文件的二進制數據,這通常是通過文件輸入流來完成的。
2、解析字節碼
JVM解析字節碼的過程是將字節碼文件中的二進制數據解析為Java虛擬機中的數據結構。首先JVM首先會讀取字節碼文件的前四個字節,判斷魔數是否為0xCAFEBABE,以此來確認該文件是否是一個有效的Java字節碼文件。JVM接著會解析常量池表,將其中的常量轉換為Java虛擬機中的數據結構,例如將字符串常量轉換為Java字符串對象。解析類、接口、字段、方法等信息:JVM會依次解析類索引、父類索引、接口索引集合、字段表集合、方法表集合等信息,將這些信息轉換為Java虛擬機中的數據結構。最后,JVM將解析得到的數據結構組裝成一個Java類的結構,并將其放入元空間中。
在完成字節碼文件解析之后,接下來就需要類加載器閃亮登場了,類加載器會將類文件加載到JVM內存中,并為該類生成一個Class對象。
類加載
加載器啟動
我們都知道,Java應用的類都是通過類加載器加載到運行時數據區的,這里很多同學可能會有疑問,那么類加載器本身又是被誰加載的呢?這有點像先有雞還是先有蛋的靈魂拷問。實際上類加載器啟動大致會經歷如下幾個階段:
1、以linux系統為例,當我們通過"java"啟動一個Java應用的時候,其實就是啟動了一個JVM進程實例,此時操作系統會為這個JVM進程實例分配CPU、內存等系統資源;
2、"java"可執行文件此時就會解析相關的啟動參數,主要包括了查找jre路徑、各種包的路徑以及虛擬機參數等,進而獲取定位libjvm.so位置,通過libjvm.so來啟動JVM進程實例;
3、當JVM啟動后會創建引導類加載器Bootsrap ClassLoader,這個ClassLoader是C++語言實現的,它是最基礎的類加載器,沒有父類加載器。通過它加載Java應用運行時所需要的基礎類,主要包括JAVA_HOME/jre/lib下的rt.jar等基礎jar包;
4、而在rt.jar中包含了Launcher類,當Launcher類被加載之后,就會觸發創建Launcher靜態實例對象,而Launcher類的構造函數中,完成了對于ExtClassLoader及AppClassLoader的創建。Launcher類的部分代碼如下所示:
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
//類靜態實例
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
//Launcher構造器
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
...
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
雙親委派模型
為了保證Java程序的安全性和穩定性,JVM設計了雙親委派模型類加載機制。在雙親委派模型中,啟動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)以及應用程序類加載器(Application ClassLoader)按照一個父子關系形成了一個層次結構,其中啟動類加載器位于最頂層,應用程序類加載器位于最底層。當一個類加載器需要加載一個類時,它首先會委派給它的父類加載器去嘗試加載這個類。如果父類加載器能夠成功加載這個類,那么就直接返回這個類的Class對象,如果父類加載器無法加載這個類,那么就會交給子類加載器去嘗試加載這個類。這個過程會一直持續到頂層的啟動類加載器。
通過這種雙親委派模型,可以保證同一個類在不同的類加載器中只會被加載一次,從而避免了類的重復加載,也保證了類的唯一性。同時,由于每個類加載器只會加載自己所負責的類,因此可以防止惡意代碼的注入和類的篡改,提高了Java程序的安全性。
數據流轉過程
當類加載器完成字節碼數據加載任務之后,JVM劃分了專門的內存區域內承載這些字節碼數據以及運行時中間數據。其中程序計數器、虛擬機棧以及本地方法棧屬于線程私有的,堆以及元數據區屬于共享數據區,不同的線程共享這兩部分內存數據。我們還是以下面這段代碼來說明程序運行的時候,各部分數據在Runtime data area中是如何流轉的。
public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}
private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
以上代碼對應的字節碼指令如下所示:
如上代碼所示,JVM創建線程來承載代碼的執行過程,我們可以將線程理解為一個按照一定順序執行的控制流。當線程創建之后,同時創建該線程獨享的程序計數器(Program Counter Register)以及Java虛擬機棧(Java Virtual Machine Stack)。如果當前虛擬機中的線程執行的是Java方法,那么此時程序計數器中起初存儲的是方法的第一條指令,當方法開始執行之后,PC寄存器存儲的是下一個字節碼指令的地址。但是如果當前虛擬機中的線程執行的是naive方法,那么程序計數器中的值為undefined。
那么程序計數器中的值又是怎么被改變的呢?如果是正常進行代碼執行,那么當線程執行字節碼指令時,程序計數器會進行自動加1指向下一條字節碼指令地址。但是如果遇到判斷分支、循環以及異常等不同的控制轉移語句,程序計數器會被置為目標字節碼指令的地址。另外在多線程切換的時候,虛擬機會記錄當前線程的程序計數器,當線程切換回來的時候會根據此前記錄的值恢復到程序計數器中,來繼續執行線程的后續的字節碼指令。
除了程序計數器之外,字節碼指令的執行流轉還需要虛擬機棧的參與。我們先來看下虛擬機棧的大致結構,如下圖所示,棧大家肯定都知道,它是一個先入后出的數據結構,非常適合配合方法的執行過程。虛擬機棧操作的基本元素就是棧幀,棧幀的結構主要包含了局部變量、操作數棧、動態連接以及方法返回地址這幾個部分。
局部變量:主要存放了棧幀對應方法的參數以及方法中定義的局部變量,實際上它是一個以0為起始索引的數組結構,可以通過索引來訪問局部變量表中的元素,還包括了基本類型以及對象引用等。非靜態方法中,第0個槽位默認是用于存儲this指針,而其他參數和變量則會從第1個槽位開始存儲。在靜態方法中,第0個槽位可以用來存放方法的參數或者其他的數據。
操作數棧:和虛擬機棧一樣操作數棧也是一個棧數據結構,只不過兩者存儲的對象不一樣。操作數棧主要存儲了方法內部操作數的值以及計算結果,操作數棧會將運算的參與方以及計算結果都壓入操作數棧中,后續的指令操作就可以從操作數棧中使用這些值來進行計算。當方法有返回值的時候,返回值也會被壓入操作數棧中,這樣方法調用者可以獲取到返回值。
動態鏈接:一個類中的方法可能會被程序中的其他多個類所共享使用,因此在編譯期間實際無法確定方法的實際位置到底在哪里,因此需要在運行時動態鏈接來確定方法對應的地址。動態鏈接是通過在棧幀中維護一張方法調用的符號表來實現的。這張符號表中保存了當前方法中所有調用的方法的符號引用,包括方法名、參數類型和返回值類型等信息。當方法需要調用另一個方法時,它會在符號表中查找所需方法的符號引用,然后進行動態鏈接,確定方法的具體內存地址。這樣,就能夠正確地調用所需的方法。
方法返回地址:當一個方法執行完畢后,JVM會將記錄的方法返回地址數據置入程序計數器中,這樣字節碼執行引擎可以根據程序計數器中的地址繼續向后執行字節碼指令。同時JVM會將方法返回值壓入調用方的操作棧中以便于后續的指令計算,操作完成之后從虛擬機棧中獎棧幀進行彈出。
知道了虛擬機棧的結構之后,我們來看下方法執行的流轉過程是怎樣的。
1、JVM啟動完成.class文件加載之后,它會創建一個名為"main"的線程,并且該線程會自動調用定義在該類中的名為"main"的靜態方法,這也是Java程序的入口點。
2、當JVM在主線程中調用當方法的時候就會創建當前線程獨享的程序計數器以及虛擬機棧,在Test.class類中,開始執行mian方法 ,因此JVM會虛擬機棧中壓入main方法對應的幀棧幀。
3、在棧幀的操作數棧中存儲了操作的數據,JVM執行字節碼指令的時候從操作數棧中獲取數據,執行計算操作之后再將結果壓入操作數棧。
4、當進行calculate方法調用的時候,虛擬機棧繼續壓入calculate方法對應的棧幀,被調用方法的參數、局部變量和操作數棧等信息會存儲在新創建的棧幀中。其中該棧幀中的方法返回地址中存放了main方法執行的地址信息,方便在調用方法執行完成后繼續恢復調用前的代碼執行。
5、對于age + 3一條加法指令,在執行該指令之前,JVM會將操作數棧頂部的兩個元素彈出,并將它們相加,然后將結果推入操作數棧中。在這個例子中,指令的操作碼是“add”,它表示執行加法操作;操作數是0,它表示從操作數棧的頂部獲取第一個操作數;操作數是1,它表示從操作數棧的次頂部獲取第二個操作數。
6、程序計數器中存儲了下一條需要執行操作的字節碼指令的地址,因此Java線程執行業務邏輯的時候必須借助于程序計數器才能獲得下一步命令的地址。
7、當calculate方法執行完成之后,對應的棧幀將從虛擬機棧中彈出,其中方法執行的結果會被壓入main方法對應的棧幀中的操作數棧中,而方法返回地址被重置到main現場對應的程序計數器中,以便于后續字節碼執行引擎從程序計數器中獲取下一條命令的地址。如果方法沒有返回值,JVM仍然會將一個null值推送到調用該方法的棧幀的操作數棧中,作為占位符,以便恢復調用方的操作數棧狀態。
8、字節碼執行引擎中的解釋器會從程序計數器中獲取下一個字節碼指令的地址,也就是從元空間中獲取對應的字節碼指令,在獲取到指令之后,通過翻譯器翻譯為對應的匯編語言而再交給硬件解析為機器指令,最終由CPU進行執行,而后再將執行結果進行寫回。
CPU執行程序
通過上文我們知道無論什么編程語言最終都需要轉化為機器語言才能被CPU執行,但是CPU、內存這些硬件資源并不是直接可以和應用程序打交道,而是通過操作系統來進行統一管理的。對于CPU來說,操作系統通過調度器(Scheduler)來決定哪些進程可以被CPU執行,并為它們分配時間片。它會從就緒隊列中選擇一個進程并將其分配給CPU執行。當一個進程的時間片用完或者發生了I/O等事件時,CPU會被釋放,操作系統的調度器會重新選擇一個進程并將其分配給CPU執行。也就是說操作系統通過進程調度算法來管理CPU的分配以及調度,進程調度算法的目的就是為了最大化CPU使用率,避免出現任務分配不均空閑等待的情況。主要的進程調度算法包括了FCFS、SJF、RR、MLFQ等。
CPU如何執行指令?
前文中我們大致搞清楚了類是如何被加載的,各部分類字節碼數據在運行時數據區怎么流轉以及字節碼執行引擎翻譯字節碼。實際上在運行時數據區數據流轉的過程中,CPU已經參與其中了。程序的本質是為了根據輸入獲得相應的輸出,而CPU本質就是根據程序的指令一步步執行獲得結果的工具。對于CPU來說,它核心工作主要分為如下三個步驟;
1、獲取指令
CPU從PC寄存器中獲取對應的指令地址,此處的指令地址是將要執行指令的地址,根據指令地址獲取對應的操作指令到指令寄存中,此時如果是順存執行則PC寄存器地址會自動加1,但是如果程序涉及到條件、循環等分支執行邏輯,那么PC寄存器的地址就會被修改為下一條指令執行的地址。
2、指令譯碼
將獲取到的指令進行翻譯,搞清楚哪些是操作碼哪些是操作數。CPU首先讀取指令中的操作碼然后根據操作碼來確定該指令的類型以及需要進行的操作,CPU接著根據操作碼來確定指令所需的寄存器和內存地址,并將它們提取出來。
3、執行指令
經過指令譯碼之后,CPU根據獲取到的指令進行具體的執行操作,并將指令運算的結果存儲回內存或者寄存器中。
因此一旦CPU上電之后,它就像一個勤勞的小蜜蜂一樣,一直不斷重復著獲取指令-》指令譯碼-》執行指令的循環操作。
CPU如何響應中斷?
當操作系統需要執行某些操作時,它會發送一個中斷請求給CPU。CPU在接收到中斷請求后,會停止當前的任務,并轉而執行中斷處理程序,這個處理程序是由操作系統提供的。中斷處理程序會根據中斷類型,執行相應的操作,并返回到原來的任務繼續執行。
在執行完中斷處理程序后,CPU會將之前保存的程序現場信息恢復,然后繼續執行被中斷的程序。這個過程叫做中斷返回(Interrupt Return,IRET)。在中斷返回過程中,CPU會將處理完的結果保存在寄存器中,然后從棧中彈出被中斷的程序的現場信息,恢復之前的現場狀態,最后再次執行被中斷的程序,繼續執行之前被中斷的指令。
那么CPU又是如何響應中斷的呢?主要經歷了以下幾個步驟:
1、保存當前程序狀態
CPU會將當前程序的狀態(如程序計數器、寄存器、標志位等)保存到內存或棧中,以便在中斷處理程序執行完畢后恢復現場。
2、確定中斷類型
CPU會檢查中斷信號的類型,以確定需要執行哪個中斷處理程序。
3、轉移控制權
CPU會將程序的控制權轉移到中斷處理程序的入口地址,開始執行中斷處理程序。
4、執行中斷處理程序
中斷處理程序會根據中斷類型執行相應的操作,這些操作可能包括保存現場信息、讀取中斷事件的相關數據、執行特定的操作,以及返回到原來的程序繼續執行等。
5、恢復現場
中斷處理程序執行完畢后,CPU會從保存的現場信息中恢復原來程序的狀態,然后將控制權返回到原來的程序中,繼續執行被中斷的指令。
后記
很多時候看似理所當然的問題,當我們深究下去就會發現原來別有一番天地。正如阿里王堅博士說的那樣,要想看一個人對某個領域的知識掌握的情況,那就看他能就這個領域的知識能講多長時間。想想的確如此,如果我們能夠對某個知識點高度提煉同時又可以細節滿滿的進行展開闡述,那我們對于這個領域的理解程度就會鞭辟入里。這種檢驗自己知識學習深度的方式也推薦給大家。