一个 bad file descriptor 的问题
先來看一個 demo:
?????1?package?main2?3?import?(4??"fmt"5??"net"6??"os"7??"runtime"8?)9?10?var?rawFileList?[]*os.File11?12?func?main()?{13??l,?err?:=?net.Listen("tcp",?":12345")14??if?err?!=?nil?{15???fmt.Println(err)16???return17??}18?19??var?connList?[]net.Conn20??for?{21???conn,?err?:=?l.Accept()22???connList?=?append(connList,?conn)23???if?err?!=?nil?{24????fmt.Println(err)25????return26???}27?28???go?func()?{29????f,?err?:=?conn.(*net.TCPConn).File()30????if?err?!=?nil?{31?????fmt.Println(err)32?????return33????}34?35????rawFile?:=?os.NewFile(f.Fd(),?"")36????rawFileList?=?append(rawFileList,?rawFile)37????_?=?rawFile38????for?{39?????var?buf?=?make([]byte,?1024)40?????conn.Read(buf)41?????conn.Write([]byte(`HTTP/1.1?200?OK42?Connection:?Keep-Alive43?Content-Length:?044?Content-Type:?text/html45?Server:?Apache46?47?`))48?????runtime.GC()49????}50???}()51??}52?}可以認為是一個簡單 read request,write response 的 http server,用 wrk 壓的話,也能正常運行:
~?????wrk?http://localhost:12345 Running?10s?test?@?http://localhost:123452?threads?and?10?connectionsThread?Stats???Avg??????Stdev?????Max???+/-?StdevLatency???589.84us????0.86ms??27.30ms???98.98%Req/Sec?????9.19k?????1.00k???10.91k????68.50%183093?requests?in?10.02s,?16.94MB?read Requests/sec:??18278.93 Transfer/sec:??????1.69MB進程也沒有什么錯誤日志,把上面的代碼注釋掉第 36 行再用 wrk 壓測,這回結果就不一樣了:
file?tcp?[::1]:12345->[::1]:58949:?fcntl:?bad?file?descriptor這個結果還是有點令人意外的,我們又沒有主動關閉連接,為什么會出現 bad file descriptor?
在代碼中,我們使用連接的 rawFile 的 fd 新建了一個文件:
????29????f,?err?:=?conn.(*net.TCPConn).File()30????if?err?!=?nil?{31?????fmt.Println(err)32?????return33????}34?35????rawFile?:=?os.NewFile(f.Fd(),?"")?//?這里36????rawFileList?=?append(rawFileList,?rawFile)37????_?=?rawFile注釋掉 36 和沒注釋有什么區別呢?是誰把我們的連接給關了?
答案比較簡單,rawFileList 是在堆上分配的全局對象,我們把 rawFile 追加進該數組后,GC 時便不會回收 rawFile。在 Go 語言中,文件類型在 GC 回收時會執行其 close 動作,這是通過 newFile 時的 SetFinalizer 完成的:
func?newFile(fd?uintptr,?name?string,?kind?newFileKind)?*File?{...?省略runtime.SetFinalizer(f.file,?(*file).close)return?f }也就是說所有文件類型都會在 GC 時被 close,在本文開頭的 demo 中,這個被 close 的文件是我們用 raw fd 創建出來的,而 raw fd 本身是 uintptr 類型。我們知道,帶 GC 的語言,對象之間主要是通過指針引用的,當我們用 uintptr 來創建新文件時,其實已經把這個引用關系破壞掉了:
右邊的 NewFile 如果被 GC 先回收了,那么左邊還在用這個文件就會報 bad file descriptor:
這時候可能有讀者會覺得奇怪了,按說 net.Conn 是有 File 方法的,為什么我們直接用 File 這個方法生成出來的文件就沒有問題?
那是因為 File 的實現中,將原有的 fd 復制了一份:
func?(c?*conn)?File()?(f?*os.File,?err?error)?{f,?err?=?c.fd.dup()?//?復制?fdif?err?!=?nil?{err?=?&OpError{Op:?"file",?Net:?c.fd.net,?Source:?c.fd.laddr,?Addr:?c.fd.raddr,?Err:?err}}return }dup 操作會在 fd 上增加一個引用計數,當引用計數減為 0 時,才會執行 finalizer。
綜上,看起來是個很簡單的問題,生產環境查起來還是要費一些時間。因為類似的問題并不常見,祝你好運。
總結
以上是生活随笔為你收集整理的一个 bad file descriptor 的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 那些年我们一起追过的大佬
- 下一篇: 代码防腐