成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

我的服務程序被 SIGPIPE 信號給搞崩了!

開發 前端
Golang 為了避免網絡斷連把程序搞崩在語言運行時中,設置了對非 0、1 文件句柄的默認處理行為為忽略。但是對于我使用的 Go 在進程內通過 cgo 訪問 Rust 代碼的情況并沒有很好地處理。

就在前幾天,我們在灰度上線時遇到了一個服務程序閃退的問題。最后排查的結果是因為一個小小的網絡 SIGPIPE 信號導致的這個嚴重問題。

今天,我就用一篇文章來介紹下 SIGPIPE 信號是如何發生的、為啥該信號會導致進程的閃退、遇到這種問題該如何解決。

讓我們開啟今天的內核原理學習之旅!

故障背景

我們對某個核心 Go 服務進行了 Rust 重構。由于源碼太多,全部重構又不太現實。所以我們采用的方案是將部分代碼用 Rust 重構掉。在服務進程中,Go 和 Rust 通過 cgo 進行通信。

但該新服務在線上遇到了崩潰的問題。而且崩潰還不是因為它自己,而是它依賴的另一個業務進程熱升級的時候出現的。只要對該依賴熱升級,就會導致該新服務崩潰退出,進而導致線上 SLA 出現較為嚴重的下降。

好在是灰度階段,影響不大。當時臨時禁止熱升級后規避了這個問題。但服務進程有概率崩潰終究可不是小事,萬一哪天誰不知道,一把線上梭哈升級那可就完犢子了。于是我立即停下了所有手頭的工作,幫大伙兒開始排查這個問題。

遇到這種問題,大家第一反應是看日志。但不幸的是在業務日志中沒有找到任何線索。然后我的思路是找 coredump 文件單步調試一下,看看崩潰發生在代碼的哪一行,結果發現這次崩潰連 core 文件都沒有留下,悄無聲息的就消失了。

經過七七四十九小時的激情排查后,最終的發現竟然是因為一個小小的網絡 SIGPIPE 信號導致的。接下來修改代碼,通過設置進程對 SIGPIPE 信號處理方式為 SIGIGN(忽略) 后徹底根治了該問題。

問題是解決了。但我還不滿足,想正好借此機會深入地給大家介紹一下內核中信號的工作原理。抽了周末整整兩天,寫出了本篇文章。

接下來的文章我分三大部分給大家講解:

  • SIGPIPE 信號是如何發生的,帶大家看看為什么連接異常會導致 SIGPIPE 的發生
  • 內核 SIGPIPE 信號處理流程,帶大家看看為什么內核默認遇到 SIGPIPE 時會將應用給殺死
  • 應用層該如何應對 SIGPIPE,帶大家看語言運行時以及我們自己的程序如何規避該問題

一、SIGPIPE 信號如何發生

但內核對象是不允許我們隨便訪問的。我們平時在用戶態程序中看到的 socket 其實只是一個句柄而已,并不是真正的 socket 對象。

假如由于網絡、對端重啟等問題這條 TCP 連接斷開了。此時我們的用戶態程序根本是不知情的。很有可能還會調用 send、write 等系統調用往 socket 里面發送數據。

圖片圖片

當數據包發送過程走到內核中的時候,內核是知道這個 socket 已經斷開了的。就會給當前進程發送一個 SIGPIPE 信號。

我們來看下具體的源碼。內核的發送會走到 do_tcp_sendpages 函數,在這里內核如果發現該 socket 已經 在這種情況下,會調用 sk_stream_error 函數。

//file:net/core/stream.c
ssize_t do_tcp_sendpages(struct sock *sk, struct page *page, int offset,
    size_t size, int flags)
{
 ......
 err = -EPIPE;
 if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
  goto out_err;
out_err:
 return sk_stream_error(sk, flags, err);
}

sk_stream_error 函數主要工作就是給正在 current(發送數據的進程)發送一個 SIGPIPE 信號。

int sk_stream_error(struct sock *sk, int flags, int err)
{
 ......
 if (err == -EPIPE && !(flags & MSG_NOSIGNAL))
  send_sig(SIGPIPE, current, 0);
 return err;
}

二、內核 SIGPIPE 信號處理流程

上一節我們看到如果遇到網絡連接異常斷開,內核會給當前進程發送一個 SIGPIPE 信號。那么為啥這個信號就能把服務程序給搞崩而且沒留下 coredump 文件呢?

簡單來說,這是 Linux 內核對 SIGPIPE 信號處理的默認行為。飛哥喝口水,接著給你說。

目標進程每當從內核態返回用戶態的過程中,會檢測是否有掛起的信號。如果有信號存在,就會進入到信號的處理過程中,會執行到 do_notify_resume,然后再進到核心函數 do_signal。我們直接把 do_signal 的源碼翻出來。

//file:arch/x86/kernel/signal.c
static void do_signal(struct pt_regs *regs)
{
 struct ksignal ksig;
 ...
 if (get_signal(&ksig)) {
  /* Whee!  Actually deliver the signal.  */
  handle_signal(&ksig, regs);
  return;
 }
 ...
}

在 do_signal 主要包含 get_signal 和 handle_signal 兩個操作。

內核在 get_signal 中是獲取一個信號。值得注意的是,內核獲取到信號后,還會判斷信號的關聯行為。如果發現這個信號內核可以處理,內核直接就操作了。

如果內核發現獲得到的信號內核需要交接給用戶態程序處理,才會在 get_signal 函數中返回。接著再把信號交給 handle_signal 函數,由該函數來為用戶空間準備好處理信號的環境,進行后面的處理。

服務程序在收到 SIGPIPE 會導致進程崩潰的關鍵就藏在這個 get_signal 函數里。

//file:kernel/signal.c
bool get_signal(struct ksignal *ksig)
{
 ...
 for (;;) {
  // 1.取出信號
  signr = dequeue_synchronous_signal(&ksig->info);
  if (!signr)
   signr = dequeue_signal(current, ¤t->blocked,
           &ksig->info, &type);

  // 2.判斷用戶進程是否為信號配置了 handler
  // 2.1 如果是 SIG_IGN(ignore的縮寫),就跳過
  if (ka->sa.sa_handler == SIG_IGN) 
   continue;

  // 2.3 判斷如果不是 SIG_DFL(default的縮寫),
  //     則證明用戶定義了處理函數,break 退出循環后返回信號對象
  if (ka->sa.sa_handler != SIG_DFL) {
   ksig->ka = *ka;
   ...
   break; 
  }

  // 3.接下來就是內核的默認行為了
  ......
 }
out:
 ksig->sig = signr; 
 return ksig->sig > 0;
}

在 get_signal 函數里主要做了三件事。

  • 一是通過 dequeue_xxx 函數來獲取一個信號
  • 二是判斷下用戶進程是否為信號配置了 handler。如果用戶配置的是 SIG_IGN 直接跳過就行了,如果配置了處理函數,get_signal 就會將信號返回交給后面的流程交給用戶態程序執行。
  • 三是如果用戶沒配置 handler,則會進入到內核默認行為中。

由于我們的服務程序沒對 SIG_PIPE 信號配過任何處理邏輯,所以 get_signal 在遇到 SIG_PIPE 時會進入到第三步 -- 內核默認行為處理。

我們來繼續看看,內核的默認行為究竟是啥樣的。

//file:kernel/signal.c
bool get_signal(struct ksignal *ksig)
{
 ...
 for (;;) {
  // 1.取出信號
  ......

  // 2.判斷信號是否配置了 handler
  ......

  // 3.接下來就是內核的默認行為了
  // 3.1 如果是可以忽略的信號,直接跳過
  if (sig_kernel_ignore(signr)) /* Default is nothing. */
   continue;

  // 3.2 判斷是否是暫停執行信號,是則暫停其運行
  if (sig_kernel_stop(signr)) {
   do_signal_stop(ksig->info.si_signo)
  }

  fatal:
  // 3.3 判斷是否需要 coredump
  //     coredump 會殺死進程下的所有線程,并生成 coredump 文件
  if (sig_kernel_coredump(signr)) {
   do_coredump(&ksig->info);
  }

  // 3.4 對于非以上情形的信號
  //     直接讓進程下所有線程退出,并且不生成coredump
  do_group_exit(ksig->info.si_signo);
 }
 ......
}

內核默認行為大概是分成四種。

第一種是默認要忽略的信號。從內核源碼里可以看到 SIGCONT、SIGCHLD、SIGWINCH 和 SIGURG,這幾個信號內核都是默認忽略的。

//file: include/linux/signal.h
#define sig_kernel_ignore(sig)  siginmask(sig, SIG_KERNEL_IGNORE_MASK)
#define SIG_KERNEL_IGNORE_MASK (\
        rt_sigmask(SIGCONT)   |  rt_sigmask(SIGCHLD)   | \
 rt_sigmask(SIGWINCH)  |  rt_sigmask(SIGURG)    )

第二種是暫停信號。內核對 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 這幾個信號的默認行為是暫停進程運行。

是的,你沒猜錯。各個 IDE 中集成的代碼斷點調試器就是使用 SIGSTOP 信號來工作的。調試器給被調試進程發送 SIGSTOP 信號,讓其進入停止狀態。等到需要繼續運行的時候,再發送 SIGCONT 信號讓被調試進程繼續運行。

理解了 SIGSTOP 你也就理解調試器的底層工作原理了。調試器通過 SIGSTOP 和 SIGCONT 等信號將被調試進程玩弄于股掌之間!

//file: include/linux/signal.h
#define sig_kernel_stop(sig)  siginmask(sig, SIG_KERNEL_STOP_MASK)
#define SIG_KERNEL_STOP_MASK (\
 rt_sigmask(SIGSTOP)   |  rt_sigmask(SIGTSTP)   | \
 rt_sigmask(SIGTTIN)   |  rt_sigmask(SIGTTOU)   )

第三種是需要終止程序運行,并生成 coredump 文件的信號。通過源碼我們可以看到 SIGQUIT、SIGILL、SIGTRAP、SIGABRT、SIGABRT、SIGFPE、SIGSEGV、SIGBUS、SIGSYS、SIGXCPU、SIGXFSZ 這些信號的默認行為走這個邏輯。

我們以 SIGSEGV 為例,當應用程序試圖訪問空指針、數組越界訪問等無效的內存操作時,內核會給當前進程發送 SIGSEGV 信號。

內核對于這些信號的默認行為就是會調用 do_coredump 內核函數。這個函數會殺死目標程序所有線程的運行,并生成 coredump 文件。

我們線上遇到的絕大部分程序崩潰都是這一類。

//file: include/linux/signal.h
#define sig_kernel_coredump(sig) siginmask(sig, SIG_KERNEL_COREDUMP_MASK)
#define SIG_KERNEL_COREDUMP_MASK (\
        rt_sigmask(SIGQUIT)   |  rt_sigmask(SIGILL)    | \
 rt_sigmask(SIGTRAP)   |  rt_sigmask(SIGABRT)   | \
        rt_sigmask(SIGFPE)    |  rt_sigmask(SIGSEGV)   | \
 rt_sigmask(SIGBUS)    |  rt_sigmask(SIGSYS)    | \
        rt_sigmask(SIGXCPU)   |  rt_sigmask(SIGXFSZ)   | \
 SIGEMT_MASK           )

但是看了這么多信號名了,還是找不到我們開篇提到的 SIGPIPE,好氣!!!

最后仔細看完代碼以后,發現對于非上面提到的信號外,對于其它的所有信號包括 SIGPIPE 的默認行為都是調用 do_group_exit。這個內核函數的行為也是殺死進程下的所有線程,但不生成 coredump 文件!!!

三、應用層如何應對 SIGPIPE

看完前面兩節,我們徹底弄明白了為什么我們的應用程序會崩潰了。

事故大體邏輯是這樣的:

  • 1.服務依賴的程序熱升級的時候有連接異常斷開
  • 2.服務并不知道連接異常,還是正常向連接里發送數據
  • 3.內核在處理數據發送時發現,該連接已經異常中斷了,直接給應用程序發送一個 SIGPIPE 信號
  • 4.服務程序會進入到信號處理流程中
  • 5.由于應用程序未對 SIGPIPE 定義處理邏輯,所以走的是內核默認行為
  • 6.內核對于 SIGPIPE 的默認行為是終止程序運行,但不生成 coredump 文件

弄懂了崩潰發生的原因,解決方法自然就明朗了。只需要在應用程序中定義對 SIGPIPE 的處理邏輯就行了。我在項目中增加了以下簡單的幾行代碼。

// 設置 SIGPIPE 的信號處理器為忽略
let ignore_action = SigAction::new(
 SigHandler::SigIgn, // SigIgn表示忽略信號
 signal::SaFlags::empty(),
  SigSet::empty(),
);

// 注冊信號處理器
unsafe {
 signal::sigaction(Signal::SIGPIPE, &ignore_action)
  .expect("Failed to set SIGPIPE handler to ignore");
}

這樣就不會走到內核在處理 SIGPIPE 信號時,在 get_signal 函數中發現用戶進程設置了 SIGPIPE 信號的行為是 SIG_IGN,則就直接跳過,再也不會把進程殺死了。

//file:kernel/signal.c
bool get_signal(struct ksignal *ksig)
{
 ...
 for (;;) {
  // 1.取出信號
  ...
  // 2.判斷用戶進程是否為信號配置了 handler
  // 2.1 如果是 SIG_IGN(ignore的縮寫),就跳過
  if (ka->sa.sa_handler == SIG_IGN) 
   continue;
  // 3.接下來就是內核的默認行為了
  ...
 }
 ...
}

不少同學可能會好奇,為啥我的進程中從來沒處理過 SIGPIPE 信號,咋就沒遇到過這種詭異的崩潰問題呢?

原因是 Golang 等語言運行時會替我們做好這個設置。但我的開發場景是使用 Golang 作為宿主,又通過 cgo 調用了 Rust 的動態鏈接庫。而 Golang 并沒有針對這種場景做好處理。

Golang 語言運行時的處理行為解釋參見 Go 源碼的 os/signal/doc.go 文件中的注釋。

If the program has not called Notify to receive SIGPIPE signals, then
the behavior depends on the file descriptor number. A write to a
broken pipe on file descriptors 1 or 2 (standard output or standard
error) will cause the program to exit with a SIGPIPE signal. A write
to a broken pipe on some other file descriptor will take no action on
the SIGPIPE signal, and the write will fail with an EPIPE error.

這段注釋清晰地說了 Go 語言運行時對于 SIGPIPE 信號處理

  • 如果 fd 是 stdout、stderr,那么程序收到 SIGPIPE 信號,默認行為是程序會退出;
  • 如果是其他 fd(比如 TCP 連接),程序收到SIGPIPE信號,不采取任何動作,返回一個EPIPE錯誤即可

對于 cgo 場景,Go 的源碼注釋中講了很多,我把其中最關鍵的一句摘出來

If the SIGPIPE is received on a non-Go thread the signal will
be forwarded to the non-Go handler, if any; if there is none the
default system handler will cause the program to terminate.

如果 SIGPIPE 是在非 go 線程上執行,那么就取決于另一個語言運行時有沒有設置 handler 了。如果沒有設置,就會走到內核的默認行為中,導致程序終止。

顯然我遇到的問題就讓注釋中這句話給說完了。

總結

好了,最后我們再總結一下。我們的應用程序會崩潰的原因是這樣的:

  • 服務依賴的程序熱升級的時候有連接異常斷開
  • 服務并不知道連接異常,還是正常向連接里發送數據
  • 內核在處理數據發送時發現,該連接已經異常中斷了,直接給應用程序發送一個 SIGPIPE 信號
  • 服務程序會進入到信號處理流程中
  • 由于應用程序未對 SIGPIPE 定義處理邏輯,所以走的是內核默認行為
  • 內核對于 SIGPIPE 的默認行為是終止程序運行,但不生成 coredump 文件

Golang 為了避免網絡斷連把程序搞崩在語言運行時中,設置了對非 0、1 文件句柄的默認處理行為為忽略。但是對于我使用的 Go 在進程內通過 cgo 訪問 Rust 代碼的情況并沒有很好地處理。

最終導致在 SIGPIPE 信號發生時,進入到了內核的默認處理行為中,服務程序退出且不留 coredump。

線上問題最難的地方在于定位根因,一但根因定位出來了,處理起來就簡單多了。最后我在 Rust 代碼中配置了對 SIGPIPE 的處理行為為 SIGIGN 后問題就徹底搞定了!!

責任編輯:武曉燕 來源: 開發內功修煉
相關推薦

2022-03-01 20:33:50

服務web項目

2021-03-01 08:05:09

慢查詢SQL

2022-10-25 17:53:09

Java線程池

2023-03-06 08:59:18

AMD顯卡驅動

2022-08-21 21:39:06

程序員建議

2021-04-29 23:45:07

函數式接口可用性

2021-12-06 07:47:36

Linux 驅動程序Linux 系統

2016-03-21 09:05:06

2024-08-27 09:02:21

2020-10-14 10:29:58

人工智能

2020-03-12 07:55:50

訪問量飆升DDoS

2010-07-15 13:54:25

最“搞”服務器

2024-04-02 08:30:40

RustUnix信號服務器

2024-11-11 14:57:56

JWTSession微服務

2025-03-24 08:00:00

數據庫開發代碼

2013-06-20 11:11:00

程序員經理

2021-03-11 16:45:29

TCP程序C語言

2020-05-02 15:10:53

AI 王者榮耀人工智能

2025-03-19 08:00:08

2022-11-18 07:40:57

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 午夜伊人| 久久久久国产一区二区三区不卡 | 亚洲最大看片网站 | 一区二区三区视频在线观看 | 亚洲午夜av久久乱码 | 国产日韩欧美在线一区 | 一级毛片视频 | 国产精品久久久久久久久久不蜜臀 | 无码一区二区三区视频 | 蜜月va乱码一区二区三区 | 成人av高清在线观看 | 亚洲最大的成人网 | 中文字幕av亚洲精品一部二部 | 福利网址| 亚洲最大av | 欧美日韩在线精品 | 亚洲三区在线 | 精精国产xxxx视频在线播放 | 欧美日韩视频在线第一区 | 国产成人免费一区二区60岁 | 久久机热 | 九色.com| 久久丝袜 | 精品欧美一区二区三区久久久 | 久久久人| 午夜寂寞影院在线观看 | 久久91精品久久久久久9鸭 | 黄色a三级 | h片在线观看网站 | 日韩精品视频一区二区三区 | 91精品国产91久久久久福利 | 中文字幕国产日韩 | 欧美激情一区二区三级高清视频 | 粉嫩av久久一区二区三区 | 免费精品视频在线观看 | 成人精品一区 | 欧美成人一区二免费视频软件 | 国产精品久久久久久久久久久新郎 | 亚洲欧美另类在线观看 | 免费一区在线 | 中文av电影 |