核心執行緒信號

關於

本文件說明執行緒信號,這個 Zircon 核心機制可用於實作執行緒暫停和終止作業。執行緒訊號與物件訊號無關。

目標對像是核心開發人員,也想瞭解暫停和終止作業在核心中的運作方式。

停權與終止要求

「暫停」和「終止」是可以對執行緒執行的作業。這兩項作業都是非同步的,呼叫端必須等待作業完成。在核心中,這些作業會以執行緒結構上的執行個體方法實作:

Thread::Suspend - 要求執行緒暫停執行作業,直到透過 Thread::Resume 繼續作業為止。「暫停」用於實作偵錯工具。暫停後,系統會先讀取/寫入執行緒的註冊狀態,再重新啟用執行緒。這項作業會透過 zx_task_suspend() 對使用者模式公開。

Thread::Kill - 要求執行緒自行終止。這項作業不會直接暴露在使用者模式。也就是說,嘗試zx_task_kill() 執行緒是錯誤。不過,這項作業會透過程序刪除,間接暴露於自願性和非自願行為。

請注意,這兩項作業都會以要求的形式說明。呼叫端要求目標暫停執行,或者在終止執行時終止其執行。呼叫端無法強制暫停或終止目標。雖然目標無法拒絕要求,但也可以將動作延遲到適當的時間和地點。這是設計的重要元素。

如要瞭解這些作業為何屬於要求,請考慮改用強制終止或暫停執行緒的替代方案。如果執行緒在保留資源 (例如互斥鎖) 時遭到強制終止,就無法在刪除前有機會釋出資源。您最終可能會發生記憶體流失、永久鎖定、資料結構毀損等問題。

透過模擬只能由目標執行緒執行的要求終止和暫停,我們讓目標在暫時 (暫停) 或永久 (終止) 停止執行前釋出資源並執行所有必要的清理作業。

安全的要點

在探討終止和暫停要求的發出方式之前,讓我們先談談執行緒終止的安全性。

有一個空間在從核心返回使用者模式之前,隨時確保執行緒可以安全暫停或終止執行作業 (核心的「邊緣」)。返回使用者模式之前,執行緒會解開呼叫堆疊,進而執行任何 RAII 物件的解構函式。在達到邊緣並即將返回使用者模式時,核心堆疊中不會有任何東西。可以看到執行緒可以安全暫停或終止執行作業。

具體而言,執行緒可在兩個安全位置暫停或終止。只會從系統呼叫返回使用者模式,也會先從例外狀況/錯誤/中斷處理常式 (簡稱例外狀況處理常式) 返回使用者模式。

請注意,在使用者模式中執行時,不只會叫用例外狀況處理常式。您也可以在核心模式中執行時叫用這些函式。返回核心模式時,由於外部核心模式內容可能仍在持有資源,因此暫停或終止不會安全。換句話說,例外狀況處理常式是從使用者模式內容觸發時,才是安全的點。

傳送訊號

因此,我們瞭解終止和暫停只是要求,而且要由目標執行緒決定何時完成要求以及執行方式。此外,我們也知道在返回使用者模式之前,執行緒本身最安全的位置是暫停或終止的執行緒本身。執行緒信號如何滿足上述所有需求?

執行緒信號是要求暫停和終止的機制。每個 Thread 物件都有包含一組斷言信號的欄位。THREAD_SIGNAL_SUSPENDTHREAD_SIGNAL_KILL 需要暫停操作。

只要在目標 Thread 物件上設定適當的位元,即可要求暫停或終止執行緒,然後根據目標的狀態以某種方式啟用,確保執行緒能及時到達安全點。具體的輪輻類型取決於目標執行緒的狀態:休眠/已封鎖、已暫停或執行中。請注意,睡眠/封鎖、中斷且不中斷的兩種變種版本。我們會著重於不受干擾 並且忽略不中斷的注意事項

睡眠或遭阻擋

如果目標執行緒處於休眠狀態或遭到封鎖,根據定義,其並未執行,但位於核心內。由於只有執行中的執行緒可以檢查其信號,因此我們必須喚醒或解除封鎖該執行緒。當執行緒解除封鎖或喚醒時,系統會提供 zx_status_t。通常值為 ZX_OKZX_ERR_TIMED_OUT。不過,如果以這種方式提早喚醒執行緒,我們會使用特殊的 zx_status_t 值,如果是終止作業,則會使用 ZX_ERR_INTERNAL_INTR_KILLED,並在暫停作業時使用 ZX_ERR_INTERNAL_INTR_RETRY

如果執行緒處於喚醒/未封鎖狀態,系統會顯示 zx_status_t 結果並開始從核心內背出核心,並展開其堆疊。一般而言,凡是傳回兩個特殊值其中之一的核心函式,都會導致呼叫端立即傳回並傳播該值。

最終,當堆疊有不復原的情形,執行緒就會位於邊緣,也就是安全點。這就是在返回使用者模式之前,執行緒會再次檢查信號,並透過呼叫 arch_iframe_process_pending_signals()x86_syscall_process_pending_signals() 採取行動。

暫停使用

就像睡眠/遭封鎖的案例一樣,執行緒必須繼續執行作業才能終止。如果終止,系統會透過 ZX_ERR_INTERNAL_INTR_KILLED 解除封鎖執行緒並解開,直到返回使用者模式對信號採取行動為止。

跑步

目標執行緒可能是執行使用者程式碼或核心程式碼。如果它執行的是使用者程式碼,我們會需要強製程式碼進入核心來檢查其 Thread 結構的信號欄位。如果執行核心程式碼,則必須信任它會在合理的時間範圍內檢查待處理的信號。

傳送者無法得知目標是處於核心模式還是使用者模式,因此無論在哪種情況下運作方式都相同。傳送方將跨處理器中斷 (IPI) 傳送至目標目前執行的 CPU。中斷處理常式工作的一部分是檢查及選擇性處理待處理的信號。

如果在使用者情境中叫用處理常式 (也就是 CPU 會在中斷時處於使用者模式),則可以放心暫停/終止,而處理常式會呼叫 arch_iframe_process_pending_signals()

但是,如果已在核心情境中叫用處理常式,則處理常式不會執行任何動作,因為它不知道執行緒中斷時的狀態。請勿在此停權/終止。反之,處理常式會返回叫用的核心結構定義,並依賴此外部內容來最終發現信號並到達安全點。

您可能會好奇,IPI 是否確實必要。有兩種情況非常重要第一種是目標執行緒在使用者模式中執行,且並非單獨進入核心時。在沒有其他中斷流量的精簡版系統中,執行緒可能無法長時間進入核心,或遇到無限迴圈時,才能進入核心。在這種情況下,我們需要 IPI,以確保目標執行緒及時觀察並處理任何待處理的信號。第二種是目標執行緒在核心中執行長時間執行作業,但未檢查待處理的信號。這些情況相當罕見,但確實存在。最佳做法是透過 zx_vcpu_enter() 執行訪客 OS。中斷會導致 VMEXIT 返回主機核心,並在該核心上檢查待處理的信號及解開。

要點總整理

以下將舉例說明運作方式。假設執行緒 A 因 B 執行 zx_port_wait() 而暫停執行執行緒 B。視實際執行的時間而定,我們最終可能會遇到數種不同的情境。我們會簡單說明每種情境。

情境 1:在系統呼叫之前暫停 (在使用者模式下執行)

執行緒 A 會在執行緒 B 開始 zx_port_wait() 系統呼叫之前發出暫停動作。執行緒 B 仍處於使用者模式且正在執行中。執行緒 A 會設定執行緒 B 的 THREAD_SIGNAL_SUSPEND 位元,並發出 IPI 至執行緒 B 目前的 CPU。執行緒 B 的 CPU 會接收中斷,並呼叫中斷處理常式。返回使用者模式之前,執行緒 B 會檢查待處理的信號。當 THREAD_SIGNAL_SUSPEND 已設定時,其本身會暫停。以下是執行緒 B 呼叫堆疊的草圖:

suspend_self()
interrupt_handler()
---- interrupt ----
user code

重新啟用後,執行緒 B 會返回使用者模式,就像未發生一樣。

情境 2:在系統呼叫期間先暫停、再封鎖

執行緒 A 會在執行緒 B 進入核心執行 zx_port_wait() 系統呼叫後暫停暫停。執行緒 B 正在執行核心程式碼且尚未封鎖。和情境 1 一樣,執行緒 A 會發出 IPI,導致執行緒 B 檢查待處理的信號:

interrupt_handler()
---- interrupt ----
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

不過,這次中斷處理常式看到的是在核心環境 (而非使用者情境) 中叫用,因此不會自行暫停。而是返回叫用時的核心結構定義。執行緒 B 到達 zx_port_wait() 作業的核心,如果沒有可用封包,會封鎖執行緒。執行緒 B 發現沒有可用的封包,並準備進行封鎖:

WaitQueue::BlockEtcPreamble()
WaitQueue::BlockEtc()
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

在封鎖前,系統會檢查待處理的信號,並發現系統要求將其停權。我們不會封鎖,而是會傳回 ZX_ERR_INTERNAL_INTR_RETRY,且呼叫堆疊會在返回使用者模式之前解開邊緣:

WaitQueue::BlockEtcPreabmle()   ZX_ERR_INTERNAL_INTR_RETRY
WaitQueue::BlockEtc()                       |
PortDispatcher::Dequeue()                   |
sys_port_wait()                             |
syscall_dispatch()                          V
---- syscall ----
vdso
zx_port_wait()
user code

這裡的執行緒會檢查待處理的信號,並自行暫停。重新啟用後,執行緒會返回使用者模式 (至 vDSO),並顯示狀態結果 ZX_ERR_INTERNAL_INTR_RETRY。vDSO 具有特殊邏輯來處理傳回 ZX_ERR_INTERNAL_INTR_RETRY 的系統呼叫,只會使用原始引數重新發出系統呼叫:

suspend_self()                  ZX_ERR_INTERNAL_INTR_RETRY
syscall_dispatch()                                   |
---- syscall ----                                    |      A
vdso                                                 |______|
zx_port_wait()
user code

情境 3:在核心中遭到封鎖時暫停

執行緒 A 會在執行緒 B 進入核心並遭到封鎖後發出暫停,正在等待通訊埠封包。執行緒 A 發現執行緒 B 已遭封鎖,因此會解除封鎖含有 ZX_ERR_INTERNAL_INTR_RETRY 值的執行緒 B。從這裡開始,行為與情境 2 的行為相符。呼叫會返回使用者模式,並讓 vDSO 重試:

blocked                           ZX_ERR_INTERNAL_INTR_RETRY
WaitQueue::BlockEtcPostamble()                         |
WaitQueue::BlockEtc()                                  |
PortDispatcher::Dequeue()                              |
sys_port_wait()                                        |
syscall_dispatch()                                     |
---- syscall ----                                      |      A
vdso                                                   |______|
zx_port_wait()
user code

情境 4:解除封鎖後暫停,再從核心傳回

當執行緒 B 遭到封鎖時,在等待通訊埠封包上收到封包,並使封包解除封鎖 (使用 ZX_OK):

blocked                            ZX_OK
WaitQueue::BlockEtcPostamble()       |
WaitQueue::BlockEtc()                |
PortDispatcher::Dequeue()            V
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

執行緒 B 現已發出暫停,並朝使用者模式展開。執行緒 A 會設定位元,您會看到執行緒 B 標示為執行中,以便傳送 IPI。與「暫停之前在 syscall 之前暫停」情況類似,中斷處理常式會執行:

interrupt_handler()
---- interrupt ----
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

不過,由於處理常式中斷核心內容,而非使用者情境,因此目前系統不會檢查待處理信號。處理常式完成,而執行緒 B 會繼續解開。最後,執行緒 B 到達邊緣,即將從系統呼叫返回使用者模式。這裡會檢查待處理的信號,查看 THREAD_SIGNAL_SUSPEND 並自行暫停:

suspend_self()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

重新啟用後,系統會返回使用者模式,並顯示解除封鎖狀態結果 (ZX_OK):

syscall_dispatch()    ZX_OK
---- syscall ----       |
vdso                    V
zx_port_wait()
user code

回顧

請將重點放在:

  1. 您無法強制暫停或終止執行緒。您只能要求它自行暫停或終止。

  2. 執行緒信號是要求執行緒暫停或終止的機制。

  3. 執行緒應該只在核心內的特定點暫停或終止執行。請特別注意,只有在執行緒不含任何資源 (例如鎖定),且即將從核心模式返回使用者模式時,才能暫停或終止執行作業。

  4. 為維持回應速度,長時間執行的核心作業必須定期檢查待處理的信號,如果有已設定的信號則傳回。