全文整理自 golang issue#3250
2012年3月9日:發(fā)現(xiàn)
一名開(kāi)發(fā)者在使用 cgo 調(diào)用 gtk 時(shí),程序持續(xù)報(bào) segment fault,其程序非常簡(jiǎn)潔:
package main
/*
#include <gtk/gtk.h>
#cgo pkg-config: gtk+-3.0
*/
import "C"
func main() {
C.gtk_init(nil, nil)
C.gtk_file_chooser_button_new(nil, 0)
}
對(duì)應(yīng)程序的 C 版本無(wú)此問(wèn)題:
#include <gtk/gtk.h>
int main(int argc, char **argv)
{
gtk_init(0, 0);
gtk_file_chooser_button_new(0, 0);
}
在經(jīng)過(guò)一番簡(jiǎn)單的背景調(diào)查后,rsc(Russ Cox,Go 的核心開(kāi)發(fā)人員)很快就想到了觸發(fā)場(chǎng)景:通過(guò) cgo 調(diào)用的 C 函數(shù)創(chuàng)建了私有線程,這些線程繼承了 Go 的信號(hào)處理函數(shù),而 Go 的信號(hào)處理函數(shù)并不能與非 Go 線程良好合作。
2012年3月13日:增加診斷日志
rsc 提交了一個(gè)簡(jiǎn)單的 fix,在觸發(fā)此 bug 時(shí),打印出非 Go 線程接收到具體信號(hào),并認(rèn)為目前沒(méi)有能力在 Go 1.0 發(fā)布前 fix 此 bug。自此,該 bug 石沉大海。
2013年6月13日:重新提上日程
15個(gè)月后,一位名叫 joshrickmar 的開(kāi)發(fā)者遇到了相同問(wèn)題:使用 cgo 調(diào)用 gtk,程序意外退出。
在有了 rsc 的診斷日志后,該開(kāi)發(fā)者明確了能夠?qū)е鲁绦虮罎⒌男盘?hào)集,并提交了一個(gè) 適用于 OpenBSD 平臺(tái)的 fix,其修復(fù)方案為:直接忽略導(dǎo)致程序崩潰的信號(hào)集。
2013年7月12日:初步修復(fù)
minux(Go 的開(kāi)發(fā)人員)進(jìn)一步完善 joshrickmar 的修復(fù)方案,對(duì)于非 Go 線程中 Go 運(yùn)行時(shí)(os/signal.Notify)不關(guān)心的信號(hào),直接忽略。并將修復(fù)方案適配到了所有已支持平臺(tái),完成了對(duì)該 bug 的初步修復(fù)。
隱患
忽略信號(hào)確實(shí)解決了使用 cgo 調(diào)用 C 函數(shù)導(dǎo)致程序崩潰的問(wèn)題,但于此同時(shí),也帶來(lái)了隱患。舉個(gè)例子,程序在非 Go 線程里運(yùn)行的代碼存在 bug,比如寫(xiě)亂內(nèi)存,正常情況下,程序會(huì)因?yàn)槭盏?segment fault 而退出。而直接忽略 SIGSEGV 會(huì)導(dǎo)致僵尸線程,實(shí)際上,線程已經(jīng) crash,卻因?yàn)?SIGSEGV 被吞掉,不被外界所知。這種 bug 極其隱蔽,會(huì)嚴(yán)重推遲問(wèn)題的暴露時(shí)間,造成不可估量的影響,而且還給問(wèn)題的定位平添麻煩。
2015年7月22日:最終修復(fù)
2年后,ianlancetaylor(Go 的開(kāi)發(fā)人員)最終修復(fù)了該 bug,其修復(fù)方案簡(jiǎn)單直覺(jué):對(duì)于非 Go 線程中 Go 運(yùn)行時(shí)不關(guān)心的信號(hào),將信號(hào)處理權(quán)力歸還給用戶。
至此,完美地解決了該 bug。
Timeline
2012年3月09日 發(fā)現(xiàn)
2012年3月13日 增加診斷日志
2012年3月28日 Go 1.0 發(fā)布
2013年5月13日 Go 1.1 發(fā)布
2013年6月13日 重新提上日程
2013年7月12日 初步修復(fù)
2013年12月01日 Go 1.2 發(fā)布
2014年06月18日 Go 1.3 發(fā)布
2014年12月10日 Go 1.4 發(fā)布
2015年7月22日 最終修復(fù)
2015年8月19日 Go 1.5 發(fā)布
2016年2月17日 Go 1.6 發(fā)布
2016年8月15日 Go 1.7 發(fā)布
測(cè)試程序
我寫(xiě)了一份測(cè)試程序用以驗(yàn)證該 bug 在不同 Go 版本中的表現(xiàn),我測(cè)試了 Go 1.4 和 1.7 兩個(gè)版本,結(jié)論如下:
- 對(duì)于 Go thread 中觸發(fā)的 segfault,無(wú)論哪個(gè)版本都會(huì)導(dǎo)致程序 crash。
- 對(duì)于 C thread 中觸發(fā)的 segfault
在 1.4 中,segfault 信號(hào)會(huì)被悄無(wú)聲息的吞掉,出錯(cuò)線程僵死,進(jìn)程不退出
在 1.7 中,segfault 會(huì)導(dǎo)致進(jìn)程 crash
與此同時(shí),程序中也給出了在低版本 Go 中的修復(fù)方案:既然 Go 運(yùn)行時(shí)中的信號(hào)處理函數(shù)會(huì)吞掉其不關(guān)注的信號(hào),那么在 C 線程啟動(dòng)后,重置相應(yīng)信號(hào)的處理函數(shù)即可。