前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个 bad file descriptor 的问题

一个 bad file descriptor 的问题

作者头像
梦醒人间
发布2021-02-26 15:29:59
4K0
发布2021-02-26 15:29:59
举报
文章被收录于专栏:码农桃花源码农桃花源

先来看一个 demo:

代码语言:javascript
复制
     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 压的话,也能正常运行:

代码语言:javascript
复制
~ ❯❯❯ 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 压测,这回结果就不一样了:

代码语言:javascript
复制
file tcp [::1]:12345->[::1]:58949: fcntl: bad file descriptor

这个结果还是有点令人意外的,我们又没有主动关闭连接,为什么会出现 bad file descriptor?

在代码中,我们使用连接的 rawFile 的 fd 新建了一个文件:

代码语言:javascript
复制
    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 和没注释有什么区别呢?是谁把我们的连接给关了?

答案比较简单,rawFileList 是在堆上分配的全局对象,我们把 rawFile 追加进该数组后,GC 时便不会回收 rawFile。在 Go 语言中,文件类型在 GC 回收时会执行其 close 动作,这是通过 newFile 时的 SetFinalizer 完成的:

代码语言:javascript
复制
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 复制了一份:

代码语言:javascript
复制
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 上增加一个引用计数,当引用计数减为 0 时,才会执行 finalizer。

综上,看起来是个很简单的问题,生产环境查起来还是要费一些时间。因为类似的问题并不常见,祝你好运。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农桃花源 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档