例外狀況處理

簡介

當執行緒遇到錯誤狀況 (例如分段) 時,執行作業就會暫停,且執行緒會進入例外狀況處理。已註冊接收這些例外狀況的處理常式會收到通知,並讓他們有機會檢查或修正條件。

偵錯工具或當機記錄器經常使用這個功能,這樣在執行緒造成其他當機前,仍有機會與執行緒互動。如果應用程式只想追蹤任務的生命週期,而不必攔截當機,信號可能會是更好的選擇。

基本概念

系統會透過 zx_task_create_exception_channel() 系統呼叫,在工作 (執行緒、程序或工作) 上建立例外狀況管道,從使用者空間處理例外狀況。建立的控制代碼是標準 Zircon 管道,但會建立唯讀狀態,因此只能用於接收例外狀況訊息。

發生例外狀況時,執行緒會暫停,且系統會將包含 zx_exception_info_t 的訊息和例外狀況處理常式傳送至管道。例外狀況的生命週期繫結於這個例外狀況控制代碼的生命週期,因此接收器完成處理後,關閉這個例外狀況處理常式就會恢復例外狀況。這個例外狀況處理常式無法複製,也就是說,這個例外狀況在任何特定情況下只會有一個處理常式。

根據預設,關閉例外狀況控制代碼時,執行緒會保持暫停狀態,並將例外狀況傳送至下一個處理常式。如果接收器已修正例外狀況,並希望執行緒改為繼續執行,則可在關閉前,透過 zx_object_set_property() 將例外狀況狀態變更為 ZX_EXCEPTION_STATE_HANDLED

例外狀況控制代碼

例外狀況處理行為與暫停權杖的方式類似,也就是讓執行緒保持暫停,直到關閉該權杖為止。此外,例外狀況控點也提供函式,可協助接收器處理例外狀況:

從例外狀況擷取的工作處理序,會擁有的權限與原始傳遞至 zx_task_create_exception_channel() 的工作相同。

範例

這個簡易範例會建立例外狀況管道,並循環讀取例外狀況,直到工作關閉為止。

void ExceptionHandlerLoop(zx_handle_t task) {
  // Create the exception channel.
  uint32_t options = 0;
  zx_handle_t channel;
  zx_status_t status = zx_task_create_exception_channel(task, options,
                                                        &channel);
  // ... check status ...

  while (true) {
    // Wait until we get ZX_CHANNEL_READABLE (exception) or
    // ZX_CHANNEL_PEER_CLOSED (task terminated).
    zx_signals_t signals = 0;
    status = zx_object_wait_one(channel,
                                ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED,
                                ZX_TIME_INFINITE, &signals);
    // ... check status ...

    if (signals & ZX_CHANNEL_READABLE) {
      // Read the exception info and handle from the channel.
      zx_exception_info_t info;
      zx_handle_t exception;
      status = zx_channel_read(channel, 0, &info, &exception, sizeof(info), 1,
                               nullptr, nullptr);
      // ... check status ...

      // Send the exception out to some other function for processing, which
      // returns true if the exception has been handled and we can resume the
      // thread, or false to pass the exception to the next handler.
      bool handled = process_exception(info, exception);
      if (handled) {
        uint32_t state = ZX_EXCEPTION_STATE_HANDLED;
        status = zx_object_set_property(exception, ZX_PROP_EXCEPTION_STATE,
                                        &state, sizeof(state));
        // ... check status ...
      }

      // Close the exception to finish handling.
      zx_handle_close(exception);
    } else {
      // We got ZX_CHANNEL_PEER_CLOSED, the task has terminated.
      zx_handle_close(channel);
      return;
    }
  }
}

例外狀況類型

整體來說,例外狀況分為兩種類型:架構和綜合。架構例外狀況包括分段 (例如反參照 NULL 指標) 或執行未定義的指示。合成例外狀況是指執行緒開始/停止通知,或違反政策等情況。

架構和政策例外狀況都視為嚴重違規,若未處理,將導致程序終止。只對偵錯工具的例外狀況 (執行緒開始/停止和程序啟動) 屬於資訊性質,即使未明確重新啟用執行緒,仍會照常執行。這些例外狀況的目的是讓偵錯工具有機會正確回應這些生命週期事件,因為相應的執行緒會暫停,直到例外狀況重新啟用為止。

<zircon/syscalls/exception.h> 中定義例外狀況類型。

例外管道類型

例外狀況管道有不同的特性,取決於工作類型,以及 ZX_EXCEPTION_CHANNEL_DEBUGGER 旗標是否傳遞至 zx_task_create_exception_channel()。下表摘要列出各種管道類型的差異:

管道類型 get_thread get_process 架構與政策例外狀況 執行緒開始/停止例外狀況 程序開始例外狀況
執行緒 X X
程序 X X X
程序偵錯工具 X X X X
工作 X X X
工作偵錯工具 X X X

管道類型也會決定例外狀況管道有機會處理例外狀況的順序:

  1. 程序偵錯工具
  2. 執行緒
  3. 程序
  4. 程序偵錯工具 (如果例外狀況為 'second-chance',則選擇性使用)
  5. 工作 (父項工作 -> 祖父母工作 -> 等)

如果沒有剩餘的例外狀況管道可嘗試,核心會像呼叫 zx_task_kill() 一樣終止程序。因例外狀況終止的程序回傳代碼為 ZX_TASK_RETCODE_EXCEPTION_KILL,您可使用 ZX_INFO_PROCESS 透過 zx_object_get_info() 取得。

每項工作都僅支援每個類型一個例外狀況管道,因此舉例來說,如果執行附加偵錯例外狀況管道的程序,嘗試建立第二個偵錯例外狀況管道將失敗,但建立非偵錯管道將成功。

ZX_EXCP_PROCESS_STARTING 和工作偵錯工具管道

ZX_EXCP_PROCESS_STARTING 的行為與其他例外狀況不同。這個欄位只會傳送至工作偵錯工具例外狀況管道,而且一律會傳送給所有找到的處理常式,基本上系統會假設 ZX_EXCEPTION_STATE_TRY_NEXT 為何,無論實際的處理常式行為為何。這也是工作偵錯工具管道唯一會收到的核心定義例外狀況,使其成為偵測新程序的特殊案例處理常式。工作偵錯工具也可以收到 ZX_EXCP_USER 例外狀況,可以使用 zx_thread_raise_exception() Syscall 引發例外狀況。

系統會將工作偵錯工具管道視為「唯讀」,因此系統可能會在單一工作上建立多個工作偵錯工具管道 (最多 ZX_EXCEPTION_CHANNEL_JOB_DEBUGGER_MAX_COUNT 個)。在同一工作上建立多個偵錯管道時,系統會將 ZX_EXCP_PROCESS_STARTING 事件依序傳送至所有管道,而先前建立的管道則會在後續建立管道之前收到通知。

使用者定義的例外狀況

zx_thread_raise_exception() 系統呼叫可用來引發使用者定義的例外狀況。這些例外狀況具有 ZX_EXCP_USER 類型,並透過 zx_exception_context_tsynth_codesynth_data 欄位提供使用者定義的值。目前,使用者定義的例外狀況只能傳送至工作偵錯工具的例外狀況管道。

先處理 Debugger...,之後可能還會再處理

在 Zircon 中,系統會先嘗試處理偵錯工具例外狀況管道。這種做法至少有幾個原因:

  • 允許「修正並繼續」偵錯 (例如執行緒發生偏差時),偵錯工具使用者可在不讓非偵錯工具管道看到例外狀況的情況下,修正分段並繼續執行執行緒。
  • 確保偵錯工具中斷點會直接傳送至偵錯工具,不必明確傳遞其他處理常式。

如果已設定 ZX_EXCEPTION_STRATEGY_sec_CHANCE 例外狀況,並在嘗試程序例外狀況管道後仍未處理,則會收到第二次的程序偵錯工具例外狀況管道。這個做法的實用性是

與工作停權的互動

例外狀況和執行緒停權會分別處理。換句話說,執行緒可以同時屬於例外狀況和暫停使用。如果執行緒在等待例外狀況處理常式的回應時暫停,就可能發生這種情況。執行緒會保持暫停狀態,直到不論是例外狀況還是停權都重新啟用為止:

zx_handle_close(exception);
zx_handle_close(suspend_token);

順序沒有影響。

與任務終止互動

zx_task_kill() 會停止處理工作上的任何例外狀況。如果在執行緒發生例外狀況時,在執行緒 (或其父項程序/工作) 上呼叫該函式:

  • 執行緒會停止等待目前的例外狀況處理常式
  • 就不會再收到例外狀況處理常式
  • 待處理的例外狀況處理常式上的 zx_exception_get_thread()zx_exception_get_process() 會繼續提供有效的工作控制代碼
  • 用於設定例外狀況狀態的 zx_object_set_property() 仍會傳回 ZX_OK,但由於執行緒不會再封鎖處理常式,因此狀態不會有任何效果

此外,遭終止的執行緒仍會傳送 ZX_EXCP_THREAD_EXITING 例外狀況 (如果程序偵錯處理常式已註冊),但如上所述,不會等待處理常式的回應。

雖然 zx_task_kill() 通常是非同步的,也就是說,執行緒可能無法在系統呼叫傳回時結束終止作業,但會同步停止例外狀況處理作業,因此一旦傳回例外狀況,關閉例外狀況處理程序就不會繼續執行執行緒,也不會將例外狀況傳遞給其他處理常式。

訊號

「信號」是觀察核心物件狀態變更的核心 Zircon 機制 (可讀取的管道、程序終止、事件持續發出訊號等)。

與例外狀況不同,信號不需要使用例外狀況處理常式的回應。另一方面,信號會傳送至等待執行緒控制代碼的人,而不會傳送至可能繫結至執行緒程序的例外狀況管道。

Zircon 的常見模式是建立訊息迴圈,等待一或多個物件的信號傳入,並在傳送時處理這些信號。如要將例外狀況處理方式納入這個模式,請使用 zx_object_wait_async() 等待例外狀況管道上的 ZX_CHANNEL_READABLE (並視需要選擇 ZX_CHANNEL_PEER_CLOSED):

zx_handle_t port;
zx_status_t status = zx_port_create(0, &port);
// ... check status ...

// Start waiting on relevant signals on the exception channel.
status = zx_object_wait_async(exception_channel, port, kMyExceptionKey,
                              ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
// ... check status ...

// ... add other objects to |port| with wait_async() ...

while (1) {
  zx_port_packet_t packet;
  status = zx_port_wait(port, ZX_TIME_INFINITE, &packet);
  // ... check status ...

  if (packet.key == kMyExceptionKey) {
    if (packet.signal.observed & ZX_CHANNEL_READABLE) {
      // ... extract exception from |exception_channel| and process it ...

      // wait_async() is one-shot so we need to reload it to continue
      // receiving signals.
      status = zx_object_wait_async(
          exception_channel, port, kMyExceptionKey,
          ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
      // ... check status ...
    } else {
      // Got ZX_CHANNEL_PEER_CLOSED, task has terminated.
      zx_handle_close(exception_channel);
    }
  } else {
    // ... handle other objects added to |port| ...
  }
}

與 Posix (和 Linux) 比較

下表顯示 Zircon 與 Posix/Linux 之間的對等字詞、類型和函式呼叫,以找出例外狀況,並列出例外狀況處理常式通常執行的操作類型。

Zircon Posix/Linux
例外狀況/信號 信號
ZXEXCP* SIG*
zx_task_create_exception_channel() ptrace(ATTACH、DETACH)
zx_task_suspend() kill(SIGSTOP)、ptrace(KILL(SIGSTOP))
zx_handle_close(suspend_token) kill(SIGCONT)、ptrace(CONT)
zx_handle_close(exception) kill(SIGCONT)、ptrace(CONT)
zx_task_kill() 殺手(SIGKILL)
zx_thread_raise_exception() 殺手(SIGUSR1)
kill(allthing_else)
未定 信號()/sigaction()
zx_port_wait() wait*()
各種 sys/wait.h 的 W*() 巨集
zx_exception_info_t siginfo_t
zx_exception_context_t siginfo_t
zx_thread_read_state() ptrace(GETREGS,GETREGSET)
zx_thread_write_state() ptrace(SETREGS,SETREGSET)
zx_process_read_memory() ptrace(PEEKTEXT)
zx_process_write_memory() ptrace(POKETEXT)

Zircon 沒有非嚴重的非同步信號,例如 SIGINTSIGQUITSIGTERMSIGUSR1SIGUSR2 等。

與 Posix 的另一個重大差異是,Zircon 中的執行緒無法處理自身的例外狀況,因為 Zircon 例外狀況的處理方式是由使用者空間驅動的同步作業,而非由核心叫用的非同步回呼。

範例

您可以查看使用例外狀況的 Zircon 程式碼,以取得更多範例,包括:

  • src/bringup/bin/pwrbtn-monitor/crashsvc:系統層級的當機處理常式
  • system/utest/exception:例外狀況單元測試
  • system/utest/debugger:偵錯工具相關功能單元測試

另請參閱