TL;DR
筆者最美好的記憶來自於早年在6502 cpu的cc800上寫彙編的年代, 那個時代的計算機甚至沒有作業系統,也沒有真實模式等保護機制。在6502上寫彙編應用其實非常簡單,系統會把bin檔案載入到一個固定的記憶體地址中,cpu會固定地從一個特定的位置開始執行。然後cpu就按照你提供的機器指令開始一條一條的執行。在高階語言中的“函式呼叫”的概念,在彙編裡主要體現為兩個暫存器。暫存器是cpu內部臨時儲存資料的區域,相當於高階語言裡的變數。但是有一個暫存器是特殊的,它存放了cpu當前正在執行的指令的記憶體地址(Instruction Register)。一個高階語言中的函式一般會被編譯成指令存放在一段連續的記憶體空間中(data segment)。那麼所謂函式執行到了第幾行這樣的資訊其實就是儲存在這個Instruction Register中的。另外一個很特殊的暫存器是Stack Register,它其中存放的記憶體地址指向的記憶體區域用於函式之間傳遞引數和返回值,以及存放一個函式內的區域性變數。如果不考慮現代計算機cpu中各種各樣其他存放中間結果的暫存器,理論上儲存了Instruction Register(執行到哪兒了)和Stack Register(堆疊上的變數)就儲存了一個函式的當前執行狀態,分別是函式當前執行到了哪,以及這個函式區域性變數所代表的當前state。
事實上,作業系統的幾個關鍵切換也是這麼來完成的。作業系統提供了兩個執行態,一個是使用者態,一般我們的程式碼都是執行在使用者態的。另外一個是核心態,像驅動程式之類的程式碼會用各種方式被載入到作業系統內部執行在核心之中。核心態裡的程式碼可以完全控制CPU的I/O中斷,從而可以和外部裝置互動。使用者態的程式碼屬於受限程式碼,必須把I/O請求通過syscall交由執行在核心態的作業系統來完成。當一個cpu的核在執行使用者態程式碼時,其暫存器裡存放的狀態是你的應用的程式碼的狀態,但是應用要進行I/O操作的時候,cpu要被切換到核心的程式碼裡去執行核心態的程式碼。這裡就需要進行一次context switch,所謂context switch其實原理不會比把暫存器的值存到記憶體的一個地方,等回來的時候再把記憶體中臨時儲存的值載入回暫存器複雜多少。
作業系統還有一個需要進行context switch的地方,那就是在協程與協程之間。作業系統在執行一個ELF或者PE的可執行檔案的時候,對於這個可執行檔案內的彙編程式碼來說,整個記憶體定址空間是獨立的。也就是1.exe的執行狀態完全無法感知到2.exe的執行狀態的記憶體。也就是現代作業系統的虛擬記憶體空間。有cpu在兩個程式之間切換狀態的時候,需要把記憶體的對映關係調整過來,否則虛擬記憶體的地址是無法對應到正確的實體地址的。一個程式內的兩個線成切換的時候,要稍微簡單一些,只需要把當前線成正在執行的位置和棧做切換就可以了。
無論是作業系統做user/kernel的switch,還是process/process,thread/thread的switch,其實現方式都是大同小異的。通過把“當前執行狀態”這樣的一個抽象概念落實為一個具體的資料結構儲存起來,然後指揮cpu在不同的場合載入不同的資料恢復不同的“當前執行狀態”。
在高階語言中,一個函式正在執行的位置以及其狀態,內部都可以有一個抽象的表達方式。有的高階語言直接被編譯成原生的機器碼,那麼其執行狀態的表述就和作業系統的context switch的context非常類似。有的高階語言自身執行在一個虛擬機器之上,那麼其context的表述可能是虛擬機器的instruction register和stack register,而不是80x86這樣原生的機器的物理暫存器。但是原理是非常類似的。
取決於語言設計者的覺悟,有的語言會把這種表達執行狀態的能力直接提供出來,讓一個函式在執行過程中可以把當前狀態儲存,然後把執行權交給另外一個函式執行,等那個函式放棄執行權回來的時候再把儲存的狀態恢復。這也就是所謂的協程(co-routine)。協程與執行緒的區別在於,協程的context switch是在完全在使用者態,由語言的runtime或者是庫來完成的。而執行緒的context switch則是作業系統來完成的。