文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

今天我想谈谈我在使用Go语言的UDP套接字时遇到的一个问题,以及我是怎样通过使用Mac OS X自带的两个工具——dtruss和DTrace,来更多的理解我的程序的。

我几乎在Mac上完成所有的编程工作,并且最近在研究一个使用Go语言和UDP套接字的项目。我尝试向我的代码添加一些单元测试,并决定其中一个测试时关于重连的。在我的测试里,我创建了两个在localhost上绑定不同端口的套接字,读取/写入这些套接字,关闭它们,并重复这一过程几百次。最终我在创建套接字时收到了一条错误消息说“bind: address already in use”。我好奇的是,我已经关闭了套接字为什么还会发生这件事?

第 1 段(可获 1.71 积分)

理解Go语言的UDP

在Go语言中,我们使用net.ListenPacket方法来在一个给出的网络地址上监听UDP包。这个函数返回一个net.PacketConn接口。

根据Go语言关于PacketConn的文档,Close方法要么会关闭一个连接,要么会返回一个错误:

type PacketConn interface {
...
    // Close关闭连接。
    // 任何阻塞的ReadFrom或者WriteTo操作会被取消阻塞状态并返回错误。
    Close() error
...
}

如果这是对的,那么为什么我会得到一个EADDRINUSE的错误值?是调查的时候了,不过我们先要简要的回顾一下系统调用。

 

第 2 段(可获 0.84 积分)

系统调用

系统调用是你的程序和你的操作系统交互的方式。在基于Unix的X86系统中,编译后的的程序会包含一个软中断,该中断会向内核传递一个数值,这个数值对应一个内核功能。在我们的例子里,我们调查两个系统调用:bind和close。bind将一个文件描述符分配到一个网络地址上。close则是释放这个文件描述符。

我们如何查看我们的程序正在进行bind和close之类的系统调用?

 

dtruss

dtruss是一个Mac OS X自带的工具。它是Brendan Gregg编写的。它和Linux上的strace工具类似。你能通过它来查看一个程序向内核发起的系统调用。这意味着当我们想要关闭套接字之时我们能够看到程序是否真的这样做了,还是Go标准库在欺骗我们。运行dtruss之后能看到以下输出:

$ sudo dtruss -a -t bind go test -run TestReconnect
PID/THRD         RELATIVE  ELAPSD    CPU SYSCALL(args)               = return

82830/0x978a1c:   1357371      99     95 close(0x5)      = 0 0
82830/0x978a20:   1378137      20     16 socket(0x2, 0x2, 0x0)       = 5 0
82830/0x978a20:   1378139       4      0 fcntl(0x5, 0x2, 0x1)        = 0 0
82830/0x978a20:   1378142       2      0 fcntl(0x5, 0x3, 0x0)        = 2 0
82830/0x978a20:   1378144       4      1 fcntl(0x5, 0x4, 0x6)        = 0 0
82830/0x978a20:   1378148       6      2 setsockopt(0x5, 0xFFFF, 0x20)       = 0 0
82830/0x978a20:   1378161       8      4 bind(0x5, 0xC821AA576C, 0x10)       = -1 Err#48

 

第 3 段(可获 1.95 积分)

这里我们能看出一些有趣的信息。

  • 哪个线程在进行哪些系统调用
  • 这些系统调用花了多长时间
  • 系统调用的传入参数
  • 返回值

有趣的是我们在最后一行看见了Err#48. 这就是我们遇到的“bind: address already in use”错误。在这次跟踪中,我们能看见第一行我们似乎成功的关闭了套接字0x5,但是当我们尝试bind操作时,在最后一行却写着0x5套接字正在被使用。令人困惑的是,0x5仅仅指代了文件描述符,而非具体的网路地址。网络地址被储存在上面的第二个参数0xC821AA576C里。这是一个指向sockaddr结构体的指针,该结构体会被传给内核,并且其解引用后如下所示:

第 4 段(可获 1.69 积分)
struct sockaddr {
   sa_family_t sa_family;
   char        sa_data[14]; // 14 bytes of protocol address
}

我们感兴趣的部分是sa_data成员。

所以我们需要想法子解引用这个指针。怎么做?用DTrace!

DTrace

Dtrace 是一个强大的调试工具,最早是Sun公司为Solaris系统编写的。这个工具在Mac OS X上也能找到。Dtrace令人称奇的地方在于它拥有一个被称作D的脚本语言。利用这些脚本,您能够写一段跟踪程序,这段程序会在某个系统调用进入或者返回时被调用。利用这个,我们可以在bind系统调用前后写一些跟踪逻辑,用来解引用sockaddr结构体,最终发现哪个端口正在被绑定。

第 5 段(可获 1.28 积分)
dtrace -c "go test -run TestReconnect" -n 'bind:entry { trace(arg0); trace(copyinstr(arg1)); }' -f bind

上面的命令是说,“执行我的Go语言程序,一旦遇到bind系统调用,执行这个D脚本”。在脚本定义里,我跟踪了头两个参数。对于第二个参数,我跟踪了copyinstr函数的结果。在D语言中,copyinstr函数会解引用一个用户态的指针,并以字符串的形式输出。

用这些参数运行DTrace会有如下输出:

 0    358                       bind:entry                 5
             0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  0123456789abcdef
         0: 10 02 0b b9 7f 00 00 00 00 00 00 00 00 00 00 00  ................
        10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        ...
第 6 段(可获 0.9 积分)

在第一行的结尾我看到和之前一样的5号文件描述符。在下一行我们看到copyinstr(arg1)的输出。这是对指向sockaddr结构体指针的解引用的结果。看上去这段内存大部分没被使用。若我们看看第三个和第四个字节,我们可以看到该套接字的端口号是0x0bb9(3001)。现在我们知道文件描述符0x5被帮顶到了端口3001上。唷!通过这个工具我能看到当bind被调用绑定某断口时,这个端口依然被另一个已存在的文件描述符绑定着。

第 7 段(可获 1.29 积分)

结论

在研究Go语言的标准库后,我最终发现了答案。Go语言文档的确提到了任何被阻塞的ReadFrom和WriteTo会被唤醒并返回错误。但文档没能提到的是,在它们被唤醒前,Go不会关闭这个套接字。这意味着即使我们在net.PacketConn上调用了Close(),也不能保证该套接字真正被关闭了。这是由于在Go语言中底层文件描述符被一个互斥锁(mutex)保护着。我这个例子中的解决方案是使用一个信号量,以保证所有的ReadFrom和WriteTo调用在我们打开一个同样端口的套接字之前都返回了。

第 8 段(可获 1.34 积分)

这件事对我最重要的教训是,要确切地知道我的程序在做什么以及我的操作系统在做什么。这意味着我能准确的知道问题出现在整个系统栈的这两者之间。

调试快乐!

第 9 段(可获 0.63 积分)

文章评论