自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我的服務(wù)程序被 SIGPIPE 信號(hào)給搞崩了!

開(kāi)發(fā) 前端
Golang 為了避免網(wǎng)絡(luò)斷連把程序搞崩在語(yǔ)言運(yùn)行時(shí)中,設(shè)置了對(duì)非 0、1 文件句柄的默認(rèn)處理行為為忽略。但是對(duì)于我使用的 Go 在進(jìn)程內(nèi)通過(guò) cgo 訪問(wèn) Rust 代碼的情況并沒(méi)有很好地處理。

就在前幾天,我們?cè)诨叶壬暇€時(shí)遇到了一個(gè)服務(wù)程序閃退的問(wèn)題。最后排查的結(jié)果是因?yàn)橐粋€(gè)小小的網(wǎng)絡(luò) SIGPIPE 信號(hào)導(dǎo)致的這個(gè)嚴(yán)重問(wèn)題。

今天,我就用一篇文章來(lái)介紹下 SIGPIPE 信號(hào)是如何發(fā)生的、為啥該信號(hào)會(huì)導(dǎo)致進(jìn)程的閃退、遇到這種問(wèn)題該如何解決。

讓我們開(kāi)啟今天的內(nèi)核原理學(xué)習(xí)之旅!

故障背景

我們對(duì)某個(gè)核心 Go 服務(wù)進(jìn)行了 Rust 重構(gòu)。由于源碼太多,全部重構(gòu)又不太現(xiàn)實(shí)。所以我們采用的方案是將部分代碼用 Rust 重構(gòu)掉。在服務(wù)進(jìn)程中,Go 和 Rust 通過(guò) cgo 進(jìn)行通信。

但該新服務(wù)在線上遇到了崩潰的問(wèn)題。而且崩潰還不是因?yàn)樗约?,而是它依?lài)的另一個(gè)業(yè)務(wù)進(jìn)程熱升級(jí)的時(shí)候出現(xiàn)的。只要對(duì)該依賴(lài)熱升級(jí),就會(huì)導(dǎo)致該新服務(wù)崩潰退出,進(jìn)而導(dǎo)致線上 SLA 出現(xiàn)較為嚴(yán)重的下降。

好在是灰度階段,影響不大。當(dāng)時(shí)臨時(shí)禁止熱升級(jí)后規(guī)避了這個(gè)問(wèn)題。但服務(wù)進(jìn)程有概率崩潰終究可不是小事,萬(wàn)一哪天誰(shuí)不知道,一把線上梭哈升級(jí)那可就完?duì)僮恿恕S谑俏伊⒓赐O铝怂惺诸^的工作,幫大伙兒開(kāi)始排查這個(gè)問(wèn)題。

遇到這種問(wèn)題,大家第一反應(yīng)是看日志。但不幸的是在業(yè)務(wù)日志中沒(méi)有找到任何線索。然后我的思路是找 coredump 文件單步調(diào)試一下,看看崩潰發(fā)生在代碼的哪一行,結(jié)果發(fā)現(xiàn)這次崩潰連 core 文件都沒(méi)有留下,悄無(wú)聲息的就消失了。

經(jīng)過(guò)七七四十九小時(shí)的激情排查后,最終的發(fā)現(xiàn)竟然是因?yàn)橐粋€(gè)小小的網(wǎng)絡(luò) SIGPIPE 信號(hào)導(dǎo)致的。接下來(lái)修改代碼,通過(guò)設(shè)置進(jìn)程對(duì) SIGPIPE 信號(hào)處理方式為 SIGIGN(忽略) 后徹底根治了該問(wèn)題。

問(wèn)題是解決了。但我還不滿足,想正好借此機(jī)會(huì)深入地給大家介紹一下內(nèi)核中信號(hào)的工作原理。抽了周末整整兩天,寫(xiě)出了本篇文章。

接下來(lái)的文章我分三大部分給大家講解:

  • SIGPIPE 信號(hào)是如何發(fā)生的,帶大家看看為什么連接異常會(huì)導(dǎo)致 SIGPIPE 的發(fā)生
  • 內(nèi)核 SIGPIPE 信號(hào)處理流程,帶大家看看為什么內(nèi)核默認(rèn)遇到 SIGPIPE 時(shí)會(huì)將應(yīng)用給殺死
  • 應(yīng)用層該如何應(yīng)對(duì) SIGPIPE,帶大家看語(yǔ)言運(yùn)行時(shí)以及我們自己的程序如何規(guī)避該問(wèn)題

一、SIGPIPE 信號(hào)如何發(fā)生

但內(nèi)核對(duì)象是不允許我們隨便訪問(wèn)的。我們平時(shí)在用戶態(tài)程序中看到的 socket 其實(shí)只是一個(gè)句柄而已,并不是真正的 socket 對(duì)象。

假如由于網(wǎng)絡(luò)、對(duì)端重啟等問(wèn)題這條 TCP 連接斷開(kāi)了。此時(shí)我們的用戶態(tài)程序根本是不知情的。很有可能還會(huì)調(diào)用 send、write 等系統(tǒng)調(diào)用往 socket 里面發(fā)送數(shù)據(jù)。

圖片圖片

當(dāng)數(shù)據(jù)包發(fā)送過(guò)程走到內(nèi)核中的時(shí)候,內(nèi)核是知道這個(gè) socket 已經(jīng)斷開(kāi)了的。就會(huì)給當(dāng)前進(jìn)程發(fā)送一個(gè) SIGPIPE 信號(hào)。

我們來(lái)看下具體的源碼。內(nèi)核的發(fā)送會(huì)走到 do_tcp_sendpages 函數(shù),在這里內(nèi)核如果發(fā)現(xiàn)該 socket 已經(jīng) 在這種情況下,會(huì)調(diào)用 sk_stream_error 函數(shù)。

//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 函數(shù)主要工作就是給正在 current(發(fā)送數(shù)據(jù)的進(jìn)程)發(fā)送一個(gè) SIGPIPE 信號(hào)。

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

二、內(nèi)核 SIGPIPE 信號(hào)處理流程

上一節(jié)我們看到如果遇到網(wǎng)絡(luò)連接異常斷開(kāi),內(nèi)核會(huì)給當(dāng)前進(jìn)程發(fā)送一個(gè) SIGPIPE 信號(hào)。那么為啥這個(gè)信號(hào)就能把服務(wù)程序給搞崩而且沒(méi)留下 coredump 文件呢?

簡(jiǎn)單來(lái)說(shuō),這是 Linux 內(nèi)核對(duì) SIGPIPE 信號(hào)處理的默認(rèn)行為。飛哥喝口水,接著給你說(shuō)。

目標(biāo)進(jìn)程每當(dāng)從內(nèi)核態(tài)返回用戶態(tài)的過(guò)程中,會(huì)檢測(cè)是否有掛起的信號(hào)。如果有信號(hào)存在,就會(huì)進(jìn)入到信號(hào)的處理過(guò)程中,會(huì)執(zhí)行到 do_notify_resume,然后再進(jìn)到核心函數(shù) do_signal。我們直接把 do_signal 的源碼翻出來(lái)。

//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 兩個(gè)操作。

內(nèi)核在 get_signal 中是獲取一個(gè)信號(hào)。值得注意的是,內(nèi)核獲取到信號(hào)后,還會(huì)判斷信號(hào)的關(guān)聯(lián)行為。如果發(fā)現(xiàn)這個(gè)信號(hào)內(nèi)核可以處理,內(nèi)核直接就操作了。

如果內(nèi)核發(fā)現(xiàn)獲得到的信號(hào)內(nèi)核需要交接給用戶態(tài)程序處理,才會(huì)在 get_signal 函數(shù)中返回。接著再把信號(hào)交給 handle_signal 函數(shù),由該函數(shù)來(lái)為用戶空間準(zhǔn)備好處理信號(hào)的環(huán)境,進(jìn)行后面的處理。

服務(wù)程序在收到 SIGPIPE 會(huì)導(dǎo)致進(jìn)程崩潰的關(guān)鍵就藏在這個(gè) get_signal 函數(shù)里。

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

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

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

  // 3.接下來(lái)就是內(nèi)核的默認(rèn)行為了
  ......
 }
out:
 ksig->sig = signr; 
 return ksig->sig > 0;
}

在 get_signal 函數(shù)里主要做了三件事。

  • 一是通過(guò) dequeue_xxx 函數(shù)來(lái)獲取一個(gè)信號(hào)
  • 二是判斷下用戶進(jìn)程是否為信號(hào)配置了 handler。如果用戶配置的是 SIG_IGN 直接跳過(guò)就行了,如果配置了處理函數(shù),get_signal 就會(huì)將信號(hào)返回交給后面的流程交給用戶態(tài)程序執(zhí)行。
  • 三是如果用戶沒(méi)配置 handler,則會(huì)進(jìn)入到內(nèi)核默認(rèn)行為中。

由于我們的服務(wù)程序沒(méi)對(duì) SIG_PIPE 信號(hào)配過(guò)任何處理邏輯,所以 get_signal 在遇到 SIG_PIPE 時(shí)會(huì)進(jìn)入到第三步 -- 內(nèi)核默認(rèn)行為處理。

我們來(lái)繼續(xù)看看,內(nèi)核的默認(rèn)行為究竟是啥樣的。

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

  // 2.判斷信號(hào)是否配置了 handler
  ......

  // 3.接下來(lái)就是內(nèi)核的默認(rèn)行為了
  // 3.1 如果是可以忽略的信號(hào),直接跳過(guò)
  if (sig_kernel_ignore(signr)) /* Default is nothing. */
   continue;

  // 3.2 判斷是否是暫停執(zhí)行信號(hào),是則暫停其運(yùn)行
  if (sig_kernel_stop(signr)) {
   do_signal_stop(ksig->info.si_signo)
  }

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

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

內(nèi)核默認(rèn)行為大概是分成四種。

第一種是默認(rèn)要忽略的信號(hào)。從內(nèi)核源碼里可以看到 SIGCONT、SIGCHLD、SIGWINCH 和 SIGURG,這幾個(gè)信號(hào)內(nèi)核都是默認(rèn)忽略的。

//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)    )

第二種是暫停信號(hào)。內(nèi)核對(duì) SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 這幾個(gè)信號(hào)的默認(rèn)行為是暫停進(jìn)程運(yùn)行。

是的,你沒(méi)猜錯(cuò)。各個(gè) IDE 中集成的代碼斷點(diǎn)調(diào)試器就是使用 SIGSTOP 信號(hào)來(lái)工作的。調(diào)試器給被調(diào)試進(jìn)程發(fā)送 SIGSTOP 信號(hào),讓其進(jìn)入停止?fàn)顟B(tài)。等到需要繼續(xù)運(yùn)行的時(shí)候,再發(fā)送 SIGCONT 信號(hào)讓被調(diào)試進(jìn)程繼續(xù)運(yùn)行。

理解了 SIGSTOP 你也就理解調(diào)試器的底層工作原理了。調(diào)試器通過(guò) SIGSTOP 和 SIGCONT 等信號(hào)將被調(diào)試進(jìn)程玩弄于股掌之間!

//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)   )

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

我們以 SIGSEGV 為例,當(dāng)應(yīng)用程序試圖訪問(wèn)空指針、數(shù)組越界訪問(wèn)等無(wú)效的內(nèi)存操作時(shí),內(nèi)核會(huì)給當(dāng)前進(jìn)程發(fā)送 SIGSEGV 信號(hào)。

內(nèi)核對(duì)于這些信號(hào)的默認(rèn)行為就是會(huì)調(diào)用 do_coredump 內(nèi)核函數(shù)。這個(gè)函數(shù)會(huì)殺死目標(biāo)程序所有線程的運(yùn)行,并生成 coredump 文件。

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

//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           )

但是看了這么多信號(hào)名了,還是找不到我們開(kāi)篇提到的 SIGPIPE,好氣?。?!

最后仔細(xì)看完代碼以后,發(fā)現(xiàn)對(duì)于非上面提到的信號(hào)外,對(duì)于其它的所有信號(hào)包括 SIGPIPE 的默認(rèn)行為都是調(diào)用 do_group_exit。這個(gè)內(nèi)核函數(shù)的行為也是殺死進(jìn)程下的所有線程,但不生成 coredump 文件!??!

三、應(yīng)用層如何應(yīng)對(duì) SIGPIPE

看完前面兩節(jié),我們徹底弄明白了為什么我們的應(yīng)用程序會(huì)崩潰了。

事故大體邏輯是這樣的:

  • 1.服務(wù)依賴(lài)的程序熱升級(jí)的時(shí)候有連接異常斷開(kāi)
  • 2.服務(wù)并不知道連接異常,還是正常向連接里發(fā)送數(shù)據(jù)
  • 3.內(nèi)核在處理數(shù)據(jù)發(fā)送時(shí)發(fā)現(xiàn),該連接已經(jīng)異常中斷了,直接給應(yīng)用程序發(fā)送一個(gè) SIGPIPE 信號(hào)
  • 4.服務(wù)程序會(huì)進(jìn)入到信號(hào)處理流程中
  • 5.由于應(yīng)用程序未對(duì) SIGPIPE 定義處理邏輯,所以走的是內(nèi)核默認(rèn)行為
  • 6.內(nèi)核對(duì)于 SIGPIPE 的默認(rèn)行為是終止程序運(yùn)行,但不生成 coredump 文件

弄懂了崩潰發(fā)生的原因,解決方法自然就明朗了。只需要在應(yīng)用程序中定義對(duì) SIGPIPE 的處理邏輯就行了。我在項(xiàng)目中增加了以下簡(jiǎn)單的幾行代碼。

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

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

這樣就不會(huì)走到內(nèi)核在處理 SIGPIPE 信號(hào)時(shí),在 get_signal 函數(shù)中發(fā)現(xiàn)用戶進(jìn)程設(shè)置了 SIGPIPE 信號(hào)的行為是 SIG_IGN,則就直接跳過(guò),再也不會(huì)把進(jìn)程殺死了。

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

不少同學(xué)可能會(huì)好奇,為啥我的進(jìn)程中從來(lái)沒(méi)處理過(guò) SIGPIPE 信號(hào),咋就沒(méi)遇到過(guò)這種詭異的崩潰問(wèn)題呢?

原因是 Golang 等語(yǔ)言運(yùn)行時(shí)會(huì)替我們做好這個(gè)設(shè)置。但我的開(kāi)發(fā)場(chǎng)景是使用 Golang 作為宿主,又通過(guò) cgo 調(diào)用了 Rust 的動(dòng)態(tài)鏈接庫(kù)。而 Golang 并沒(méi)有針對(duì)這種場(chǎng)景做好處理。

Golang 語(yǔ)言運(yùn)行時(shí)的處理行為解釋參見(jiàn) 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.

這段注釋清晰地說(shuō)了 Go 語(yǔ)言運(yùn)行時(shí)對(duì)于 SIGPIPE 信號(hào)處理

  • 如果 fd 是 stdout、stderr,那么程序收到 SIGPIPE 信號(hào),默認(rèn)行為是程序會(huì)退出;
  • 如果是其他 fd(比如 TCP 連接),程序收到SIGPIPE信號(hào),不采取任何動(dòng)作,返回一個(gè)EPIPE錯(cuò)誤即可

對(duì)于 cgo 場(chǎng)景,Go 的源碼注釋中講了很多,我把其中最關(guān)鍵的一句摘出來(lái)

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 線程上執(zhí)行,那么就取決于另一個(gè)語(yǔ)言運(yùn)行時(shí)有沒(méi)有設(shè)置 handler 了。如果沒(méi)有設(shè)置,就會(huì)走到內(nèi)核的默認(rèn)行為中,導(dǎo)致程序終止。

顯然我遇到的問(wèn)題就讓注釋中這句話給說(shuō)完了。

總結(jié)

好了,最后我們?cè)倏偨Y(jié)一下。我們的應(yīng)用程序會(huì)崩潰的原因是這樣的:

  • 服務(wù)依賴(lài)的程序熱升級(jí)的時(shí)候有連接異常斷開(kāi)
  • 服務(wù)并不知道連接異常,還是正常向連接里發(fā)送數(shù)據(jù)
  • 內(nèi)核在處理數(shù)據(jù)發(fā)送時(shí)發(fā)現(xiàn),該連接已經(jīng)異常中斷了,直接給應(yīng)用程序發(fā)送一個(gè) SIGPIPE 信號(hào)
  • 服務(wù)程序會(huì)進(jìn)入到信號(hào)處理流程中
  • 由于應(yīng)用程序未對(duì) SIGPIPE 定義處理邏輯,所以走的是內(nèi)核默認(rèn)行為
  • 內(nèi)核對(duì)于 SIGPIPE 的默認(rèn)行為是終止程序運(yùn)行,但不生成 coredump 文件

Golang 為了避免網(wǎng)絡(luò)斷連把程序搞崩在語(yǔ)言運(yùn)行時(shí)中,設(shè)置了對(duì)非 0、1 文件句柄的默認(rèn)處理行為為忽略。但是對(duì)于我使用的 Go 在進(jìn)程內(nèi)通過(guò) cgo 訪問(wèn) Rust 代碼的情況并沒(méi)有很好地處理。

最終導(dǎo)致在 SIGPIPE 信號(hào)發(fā)生時(shí),進(jìn)入到了內(nèi)核的默認(rèn)處理行為中,服務(wù)程序退出且不留 coredump。

線上問(wèn)題最難的地方在于定位根因,一但根因定位出來(lái)了,處理起來(lái)就簡(jiǎn)單多了。最后我在 Rust 代碼中配置了對(duì) SIGPIPE 的處理行為為 SIGIGN 后問(wèn)題就徹底搞定了??!

責(zé)任編輯:武曉燕 來(lái)源: 開(kāi)發(fā)內(nèi)功修煉
相關(guān)推薦

2022-03-01 20:33:50

服務(wù)web項(xiàng)目

2021-03-01 08:05:09

慢查詢(xún)SQL

2022-10-25 17:53:09

Java線程池

2023-03-06 08:59:18

AMD顯卡驅(qū)動(dòng)

2022-08-21 21:39:06

程序員建議

2021-04-29 23:45:07

函數(shù)式接口可用性

2021-12-06 07:47:36

Linux 驅(qū)動(dòng)程序Linux 系統(tǒng)

2020-10-14 10:29:58

人工智能

2024-08-27 09:02:21

2016-03-21 09:05:06

2020-03-12 07:55:50

訪問(wèn)量飆升DDoS

2010-07-15 13:54:25

最“搞”服務(wù)器

2024-04-02 08:30:40

RustUnix信號(hào)服務(wù)器

2021-03-11 16:45:29

TCP程序C語(yǔ)言

2025-03-24 08:00:00

數(shù)據(jù)庫(kù)開(kāi)發(fā)代碼

2024-11-11 14:57:56

JWTSession微服務(wù)

2013-06-20 11:11:00

程序員經(jīng)理

2024-11-19 08:36:16

2025-03-19 08:00:08

2020-05-02 15:10:53

AI 王者榮耀人工智能
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)