從 Go 語言一個文件描述符錯誤講起
先來看一個 demo:
1 package main
2
3 import (
4 "fmt"
5 "net"
6 "os"
7 "runtime"
8 )
9
10 var rawFileList []*os.File
11
12 func main() {
13 l, err := net.Listen("tcp", ":12345")
14 if err != nil {
15 fmt.Println(err)
16 return
17 }
18
19 var connList []net.Conn
20 for {
21 conn, err := l.Accept()
22 connList = append(connList, conn)
23 if err != nil {
24 fmt.Println(err)
25 return
26 }
27
28 go func() {
29 f, err := conn.(*net.TCPConn).File()
30 if err != nil {
31 fmt.Println(err)
32 return
33 }
34
35 rawFile := os.NewFile(f.Fd(), "")
36 rawFileList = append(rawFileList, rawFile)
37 _ = rawFile
38 for {
39 var buf = make([]byte, 1024)
40 conn.Read(buf)
41 conn.Write([]byte(`HTTP/1.1 200 OK
42 Connection: Keep-Alive
43 Content-Length: 0
44 Content-Type: text/html
45 Server: Apache
46
47 `))
48 runtime.GC()
49 }
50 }()
51 }
52 }
可以認為是一個簡單 read request,write response 的 http server,用 wrk 壓的話,也能正常運行:
~ ??? wrk http://localhost:12345
Running 10s test @ http://localhost:12345
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 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
這個結果還是有點令人意外的,我們又沒有主動關閉連接,為什么會出現(xiàn) bad file descriptor?
在代碼中,我們使用連接的 rawFile 的 fd 新建了一個文件:
29 f, err := conn.(*net.TCPConn).File()
30 if err != nil {
31 fmt.Println(err)
32 return
33 }
34
35 rawFile := os.NewFile(f.Fd(), "") // 這里
36 rawFileList = append(rawFileList, rawFile)
37 _ = rawFile
注釋掉 36 和沒注釋有什么區(qū)別呢?是誰把我們的連接給關了?
答案比較簡單,rawFileList 是在堆上分配的全局對象,我們把 rawFile 追加進該數(shù)組后,GC 時便不會回收 rawFile。在 Go 語言中,文件類型在 GC 回收時會執(zhí)行其 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 創(chuàng)建出來的,而 raw fd 本身是 uintptr 類型。我們知道,帶 GC 的語言,對象之間主要是通過指針引用的,當我們用 uintptr 來創(chuàng)建新文件時,其實已經(jīng)把這個引用關系破壞掉了:

右邊的 NewFile 如果被 GC 先回收了,那么左邊還在用這個文件就會報 bad file descriptor:

這時候可能有讀者會覺得奇怪了,按說 net.Conn 是有 File 方法的,為什么我們直接用 File 這個方法生成出來的文件就沒有問題?
那是因為 File 的實現(xiàn)中,將原有的 fd 復制了一份:
func (c *conn) File() (f *os.File, err error) {
f, err = c.fd.dup() // 復制 fd
if err != nil {
err = &OpError{Op: "file", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return
}
dup 操作會在 fd 上增加一個引用計數(shù),當引用計數(shù)減為 0 時,才會執(zhí)行 finalizer。
綜上,看起來是個很簡單的問題,生產環(huán)境查起來還是要費一些時間。因為類似的問題并不常見,祝你好運。
歡迎大家關注Xargin的公眾號:
推薦閱讀
