寫給android開發的Linux 訊號 - 下篇

語言: CN / TW / HK

前言

本篇是Linux訊號的下篇,上篇可見 寫給android開發的Linux 訊號 - 上篇,本篇主要介紹一些訊號阻塞行為與其他傳送訊號的系統呼叫,同時也會了解到,android這個執行在linux核心的“龐大應用”是如何處理訊號

訊號阻塞 - 掩碼

我們前文已經說了,訊號會由核心投遞給每個需要的程序,我們說的阻塞,其實就是Linux核心內部給每個程序維護一個訊號掩碼,其實就是一個訊號陣列,如果傳送給程序的訊號位於掩碼裡面,核心就暫時阻塞該訊號對程序的傳遞,直到程序通過呼叫通知核心移除為止,此時訊號會被重新投遞

使用sigprocmask呼叫,能夠通知核心在訊號掩碼中新增/刪除訊號 int sigprocmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);

  • 第一個引數how,有如下取之,SIG_BLOCK ,將__new_set引數裡面的訊號集新增到當前訊號集中,當前訊號掩碼集合結果就是new_set&old_set。SIG_UNBLOCK,移除當前訊號掩碼集合中new_set所設定的訊號,當前訊號掩碼集合結果就是new_set&~old_set。SIG_SETMASK,將new_set所指向的訊號集合設定為當前的訊號掩碼集合,當前訊號掩碼集合結果就是new_set
  • new_set 與old_set都是一個結構體為sigset_t的指標,我們常用以下方式新增一個訊號到set中 sigset_t blockset; sigemptyset(&blockset); sigaddset(&blockset,SIGBUS);

當然sigprocmask是屬於程序級別的阻塞,我們還可以用pthread_sigmask指定某個執行緒獨立去修改掩碼 int pthread_sigmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);

比如常見apm需要監聽ANR產生的SIGQUIT訊號時,需要解除當前執行緒中的SIGQUIT掩碼(可見android系統應用執行緒建立時,會把SIGQUIT加入到掩碼集合中,這部分之前文章講過,這裡就不詳述) sigemptyset(&mask); sigaddset(&mask, SIGQUIT); if (0 != pthread_sigmask(SIG_UNBLOCK, &mask, &old)) { // }

傳送訊號方式

我們有以下發送訊號的方式 int raise(int __signal); 呼叫者自身 int kill(pid_t __pid, int __signal); 指定程序/程序組 int killpg(int __pgrp, int __signal); 指定程序組 int tgkill(int __tgid, int __tid, int __signal);傳送訊號給執行緒/執行緒組 當然,還有我們用pthread_create 建立時,也可以直接指定pthread_t針對執行緒發起訊號 int pthread_kill(pthread_t __pthread, int __signal);

上面的呼叫其實就如同字面所示,不過值得注意的是,當程序呼叫raise的時候,訊號會在raise返回前就被髮出,因此需要注意,同時由於不需要程序id,所以我們常用raise去模擬發出訊號相關的動作

針對執行緒的訊號發出,使用場景也很多,比如在apm中,如果我們想要獲取anr之後的trace檔案,當捕獲完SIGQUIT後可以通過tgkill發出訊號給SignalCatcher執行緒

其他訊號補充

這一小節是對訊號的補充

同步訊號監聽

我們通過sigaction進行的訊號監聽,也被稱為非同步訊號監聽,那麼有沒有同步的訊號監聽呢?

有的 int sigwait(const sigset_t* __set, int* __signal);

我們可以通過sigwait等相關呼叫,去執行一個同步等待,比如SignalCatcher執行緒會一直通過sigwait去等待SIGQUIT訊號

image.png

abort()

我們這裡還特別介紹了abort()這個系統呼叫,因為它在android原始碼中頻繁用到,abort呼叫時會產生SIGABRT訊號。那麼這個呼叫有什麼特別之處嗎?還記得我們上文說的傳遞給程序的訊號是有可能被阻塞或者被忽略的,但是abort()呼叫卻不受影響,Linux遵從SUSV3的要求,abort呼叫必須終止程序(具體實現就是,在SIGABRT訊號處理器結束執行時,會把相應訊號處理還原成預設訊號處理器,如有),除非程序註冊了SIGABRT的訊號處理器且處理函式還未被返回(完成後也一定會終止程序),因此,我們能夠監聽到abort系統呼叫產生的SIGABRT訊號,但是如果訊號處理函式正常執行完,就會立即終止程序。但是!這裡有趣的是,非區域性跳轉(非本地跳轉 setjmp等)是可以抵消abort產生的影響的,非常強大!

android系統對於訊號的處理

我們都知道,android系統執行在Linux核心中,其實算是核心的一個“大應用”(可以認為androix虛擬機器是linux作業系統的一個程式),那麼我們在android系統上執行的我們自己的應用程式,定義的訊號處理器會被第一時間執行到嗎(地位等同於android系統嗎),非常有趣!答案是,我們部分訊號,注意是部分,其實是“二手”訊號,這個訊號其實是由核心 - android虛擬機器 - 應用,經過了這麼一層轉換關係的。

我們可以在FaultManager中看到,這是android虛擬機器訊號初始化的起點(值得注意的是,這裡以android13原始碼為分析,每個版本有些差異,但是大體流程相同)

``` void FaultManager::Init() { CHECK(!initialized_); sigset_t mask; sigfillset(&mask); sigdelset(&mask, SIGABRT); sigdelset(&mask, SIGBUS); sigdelset(&mask, SIGFPE); sigdelset(&mask, SIGILL); sigdelset(&mask, SIGSEGV);

SigchainAction sa = {
        .sc_sigaction = art_fault_handler,
        .sc_mask = mask,
        .sc_flags = 0UL,
};
// 新增訊號處理器
AddSpecialSignalHandlerFn(SIGSEGV, &sa);

// Notify the kernel that we intend to use a specific `membarrier()` command.
int result = art::membarrier(MembarrierCommand::kRegisterPrivateExpedited);
if (result != 0) {
    LOG(WARNING) << "FaultHandler: MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED failed: "
                 << errno << " " << strerror(errno);
}

{
    MutexLock lock(Thread::Current(), generated_code_ranges_lock_);
    for (size_t i = 0; i != kNumLocalGeneratedCodeRanges; ++i) {
        GeneratedCodeRange* next = (i + 1u != kNumLocalGeneratedCodeRanges)
                                   ? &generated_code_ranges_storage_[i + 1u]
                                   : nullptr;
        generated_code_ranges_storage_[i].next.store(next, std::memory_order_relaxed);
        generated_code_ranges_storage_[i].start = nullptr;
        generated_code_ranges_storage_[i].size = 0u;
    }
    free_generated_code_ranges_ = generated_code_ranges_storage_;
}

initialized_ = true;

} ```

```

extern "C" void AddSpecialSignalHandlerFn(int signal, SigchainAction* sa) { InitializeSignalChain();

if (signal <= 0 || signal >= _NSIG) {
    fatal("Invalid signal %d", signal);
}

// Set the managed_handler.
chains[signal].AddSpecialHandler(sa);
chains[signal].Claim(signal);

} ```

這裡一件事,就是找到sigaction與sigprocmask這兩個符號,並放入了相關的結構體 ```

attribute((constructor)) static void InitializeSignalChain() { static std::once_flag once; std::call_once(once, { lookup_libc_symbol(&linked_sigaction, sigaction, "sigaction"); lookup_libc_symbol(&linked_sigprocmask, sigprocmask, "sigprocmask");

if defined(BIONIC)

    lookup_libc_symbol(&linked_sigaction64, sigaction64, "sigaction64");
    lookup_libc_symbol(&linked_sigprocmask64, sigprocmask64, "sigprocmask64");

endif

});

} 這裡的目的其實就是,通過FaultManager,確保了虛擬機器能夠第一時間先收到感興趣的訊號,先讓虛擬機器進行處理,之後有需要再發給我們應用定義的訊號處理器,之所以需要找到sigaction與sigprocmask,其實就是採用了hook思想,android應用層呼叫的sigaction會被虛擬機器統一替換成自定義的“sigaction” 處理,注意這裡的區別,**這個不是Linux呼叫的sigaction** extern "C" int sigaction(int signal, const struct sigaction new_action, struct sigaction old_action) { InitializeSignalChain(); return __sigaction(signal, new_action, old_action, linked_sigaction); } ```

``` template static int __sigaction(int signal, const SigactionType new_action, SigactionType old_action, int (linked)(int, const SigactionType, SigactionType*)) { if (is_signal_hook_debuggable) { return 0; }

// If this signal has been claimed as a signal chain, record the user's // action but don't pass it on to the kernel. // Note that we check that the signal number is in range here. An out of range signal // number should behave exactly as the libc sigaction. if (signal <= 0 || signal >= _NSIG) { errno = EINVAL; return -1; }

if (chains[signal].IsClaimed()) { SigactionType saved_action = chains[signal].GetAction(); if (new_action != nullptr) { 真正對真的sigaction預處理 chains[signal].SetAction(new_action); } if (old_action != nullptr) { *old_action = saved_action; } return 0; }

// Will only get here if the signal chain has not been claimed. We want // to pass the sigaction on to the kernel via the real sigaction in libc. return linked(signal, new_action, old_action); 呼叫真正的Linux sigaction的函式 } void SetAction(const SigactionType new_action) { if constexpr (std::is_same_v) { action_ = new_action; } else { action_.sa_flags = new_action->sa_flags; action_.sa_handler = new_action->sa_handler;

if defined(SA_RESTORER)

    action_.sa_restorer = new_action->sa_restorer;

endif

    sigemptyset(&action_.sa_mask);
    memcpy(&action_.sa_mask, &new_action->sa_mask,
           std::min(sizeof(action_.sa_mask), sizeof(new_action->sa_mask)));
}
action_.sa_flags &= kernel_supported_flags_;

} ```

最後完成了核心 - android虛擬機器 - 應用 這麼一個訊號傳遞機制,當遇到需要虛擬機器先處理的訊號時,比如SIGSEGV,就會通過FaultManager::HandleFault去處理,通過預先註冊的各個Handler去處理

``` bool FaultManager::HandleFault(int sig, siginfo_t info, void context) { if (VLOG_IS_ON(signals)) { PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info); }

ifdef TEST_NESTED_SIGNAL

// Simulate a crash in a handler.

raise(SIGSEGV);

endif

if (IsInGeneratedCode(info, context)) {
    VLOG(signals) << "in generated code, looking for handler";
    for (const auto& handler : generated_code_handlers_) {
        VLOG(signals) << "invoking Action on handler " << handler;
        if (handler->Action(sig, info, context)) {
            // We have handled a signal so it's time to return from the
            // signal handler to the appropriate place.
            return true;
        }
    }
}

// We hit a signal we didn't handle.  This might be something for which
// we can give more information about so call all registered handlers to
// see if it is.
if (HandleFaultByOtherHandlers(sig, info, context)) {
    return true;
}

// Set a breakpoint in this function to catch unhandled signals.
art_sigsegv_fault();
return false;

} ```

這裡有一個應用場景就是,上虛擬機器如何針對java層的“異常”進行處理的native實現,所以說異常這個概念,真的在虛擬機器中做了很多嘗試。之前有講過相關的處理Android效能優化 - 捕獲java crash的那些事,因此就不繼續了。

最後

最後我們通過本文,應該能瞭解到相關的訊號知識了,同時也明白了android虛擬機器對訊號做的一個先捕獲處理,從而保證了訊號會先傳遞到虛擬機器中