socket关闭: close()和shutdown()的差异

来源:互联网 发布:职业生涯规划 知乎 编辑:程序博客网 时间:2024/05/08 23:50

转:http://drmingdrmer.github.io/tech/programming/network/2015/07/28/close-shutdown.html

对于一个tcp连接,在c语言里一般有2种方法可以将其关闭:

close(sock_fd);

或者

shutdown(sock_fd, ...);

多数情况下这2个方法的效果没有区别,可以互换使用。除了:

  • close() 是针对file的操作
  • shutdown() 是针对socket的操作

nix系统里socket是1个文件,但文件不1定是1个socket;

所以在进入系统调用后和达到协议层前(发出FIN包这一段),close()和shutdown()的行为会有1点差异。

到达协议层以后,close()和shutdown()没有区别。

举几个栗子示范下close()和shutdown()的差异

下面通过几个例子演示下close()和shutdown()在多线程并发时的行为差异,我们假设场景是:

  • sock_fd 是一个blocking mode的socket。
  • thread-1 正在对sock_fd进行阻塞的recv(),还没有返回。
  • thread-2 直接对sock_fd调用close() 或 shutdown()。
  • 不考虑linger。

栗子1: socket阻塞在recv()上, 调用close()

// Close a waiting recv()Time | |  thread-1                  | thread-2           | tcpdump |                            |                    | |  recv(sock_fd              |                    | |      <unfinished ...>      |                    |1|                            | close(sock_fd) = 0 | |                            |                    | // Some data arrived |                            |                    | // after close()2|                            |                    | < seq 1:36 ... length 35 |                            |                    | > ack 36 ... |  // Data was received.     |                    |3|  <... recv resumed>) = 35  |                    |4|                            |                    | > FIN sent |                            |                    | < ack of FIN received |                            |                    | ... |  // Can't be used any more |                    |5v  recv(sock_fd) = -1        |                    |

在上面的例子里:

  • (1) thread-2 调用close()立即成功返回,这时recv()还在使用sock_fd。

    这里因为有另外1个线程thread-1正在使用sock_fd,所以只是标记这个sock_fd为要关闭的。socket并没有真正关闭。

    这时recv()还继续处于阻塞读取状态。

  • (2) close()之后,有些数据到了,recv可以读取并返回了。

  • (3) recv()收到数据, 正确退出。

  • (4) rece()结束调用,释放socket的引用,这时底层开始关闭socket的流程。

  • (5) 再次调用recv()就会得到错误。

可以看到,close()没有立即关闭socket的连接,也没有打断等待的recv()。

栗子2: socket阻塞在recv()上, 调用shutdown()

// Shutdown a waiting recv()Time | |  thread-1                  | thread-2              | tcpdump |                            |                       | |  recv(sock_fd              |                       | |      <unfinished ...>      |                       |1|                            | shutdown(sock_fd) = 0 | > FIN sent |                            |                       | < ack of FIN received |                            |                       | ... |  // Woken up by shutdown() |                       | |  // no errno set           |                       |2|  <... recv resumed>) = 0   |                       | v                            |                       |

在上面的例子里:

  • (1) thread-1还在等待sock_fd, thread-2调用shutdown(),立即开始关闭socket的流程,发FIN 包等。

    然后, 内核中tcp_shutdown中会调用sock_def_wakeup唤醒阻塞在recv()上的thread-1。

  • (2) 这时recv()阻塞的线程被唤醒等待并立即返回。返回码是0,表示socket已经关了。

可以看到,shutdown()和close()不同,会立即关闭socket的连接,并唤醒等待的recv()。

以上2个例子的代码

close-or-shutdown-recv

栗子3: socket阻塞在accept()上, 调用shutdown()

类似的,对阻塞在accept()上的socket调用shutdown(),accept也会被唤醒:

// Shutdown a waiting accept()Time | |  thread-1                      | thread-2 |                                | |  accept(sock_fd                | |      <unfinished ...>          |1|                                | shutdown(sock_fd) = 0 |                                | |  // Woken up by shutdown()     | |  // errno set to EINVA         |2|  <... accept resumed>) = -1    | |                                | v                                |
  • (1) thread-1还在等待sock_fd, thread-2调用shutdown(),立即开始关闭socket的流程,发FIN 包等。

    然后, 内核中tcp_shutdown中会调用sock_def_wakeup唤醒阻塞在accept()上的thread-1。

  • (2) 这时在accept()上阻塞的线程被唤醒, 并立即返回。

    返回码是-1,errno设置为EINVA。

  • 这里如果thread-2调用的是close(),accept不会被唤醒,如果后面有请求connect进来,还能正确接受并返回。

结论

  • shutdown() 立即关闭socket;

    并可以用来唤醒等待线程;

  • close() 不一定立即关闭socket(如果有人引用, 要等到引用解除);

    不会唤醒等待线程。

现在大部分网络应用都使用nonblocking socket和事件模型如epoll的时候,因为nonblocking所以没有线程阻塞,上面提到的行为差别不会体现出来 。

当时注意到这个问题是在做1个go的server,因为在go的实现中,一个tcp的accept的底层实现里,对accept()的系统调用还是阻塞的。当另1个goroutine想要退出整个进程的时候,需要通知accept的goroutine先退出。最初我使用`func (*TCPListener) Close`来关闭监听的socket,但发现TCPListener:Close实际调用了系统调用close(),无法唤醒当前正在accept()的goroutine,必须等到有下一个连接进来才能唤醒accept(),进而退出整个进程。所以后来改成使用shutdown()来关闭sock_fd,以达到唤醒accept()的goroutine的目的。
更新 2015 Aug 05 - go中不能唤醒的问题和重现方法

(开始写的时候没有记清楚重现步骤,感谢 foxmailed 提醒。)

上面的描述不准确,更新一下,实际上是2个问题在1起引起的TCPListener.Close无法唤醒Accept的goroutine:

  • go里的socket本来应该都是nonblocking的。

    go内部accept的系统调用在没有连接时返回-1,然后进入事件的等待(epoll_wait等)。

    执行TCPListener.Accept的goroutine如果没有收到connect请求,就把自己挂起来, 等待网络事件到来.

  • TCPListener.Close 本身是有的唤醒机制的。

    但和系统调用shutdown()的唤醒不一样,shutdown是线程调度层面的,TCPListener.Close是网络事件层和goroutine层面。

    TCPListener.Close实际上是把TCPListener.Accept的goroutine唤醒。所以正常的阻塞的TCPListener.Accept的goroutine在TCPListener.Close调用时会被唤醒.

    如果监听的TCPListener内部的fd时blocking模式的,它在调用系统调用accept()时, accept()不会返回-1, 而是阻塞住, 这时线程被挂起(不是goroutine挂起了).要唤醒就需要先把它从系统调用中唤醒(例如用shutdown,TCPListener.Close 没有这个步骤)。

    所以TCPListener.Close的唤醒机制前提是nonblocking。一旦进入blocking模式并调用了accept, TCPListener.Close就没能力把它唤醒了.

  • 但go里面有1个问题,就是它的dup()实现时,每次dup之后还会顺手把fd设置为blocking模式:

    net/fd_unix.go里的实现, 看注释里地描述:

    func (fd *netFD) dup() (f *os.File, err error) {        ns, err := dupCloseOnExec(fd.sysfd)        if err != nil {                syscall.ForkLock.RUnlock()                return nil, &OpError{"dup", fd.net, fd.laddr, err}        }        // We want blocking mode for the new fd, hence the double negative.        // This also puts the old fd into blocking mode, meaning that        // I/O will block the thread instead of letting us use the epoll server.        // Everything will still work, just with more threads.        if err = syscall.SetNonblock(ns, false); err != nil {                return nil, &OpError{"setnonblock", fd.net, fd.laddr, err}        }        return os.NewFile(uintptr(ns), fd.name()), nil}

    简单说就是dup的副作用是把fd变成阻塞的,但go开发者不是很屌这件事情,觉得阻塞就阻塞,无非多用几个线程而已。

    可是TCPListener.Close的唤醒机制是必须基于nonblocking的。。。。。

  • 所以只要dup()被调用了1下,TCPListener.Close就无法唤醒等待的TCPListener.Accept了。

    哪些场合dup会被调用呢?最简单地就是从Listener里取1下File对象就好了:

    l.(*net.TCPListener).File()

    go里File方法实现:

    net/tcpsock_posix.go:func (l *TCPListener) File() (f *os.File, err error) { return l.fd.dup() }

.File()在我们的代码里用在进程重启过程中的监听fd的继承.

为了解决这个问题, 我们在代码里每次调用.File()后,都加上了1句修正:

syscall.SetNonblock( int(f.Fd()), true )

下面这段代码可以重现go中Close不唤醒的问题:

close-does-not-wake-up-accept.go

package mainimport ("log""net""runtime""time")func main() {runtime.GOMAXPROCS(2)l, err := net.Listen("tcp", ":2000")if err != nil {log.Fatal(err)}show_bug := trueif show_bug {// TCPListener.File() calls dup() that switches the fd to blocking// model.(*net.TCPListener).File()}go func() {log.Println("listening... expect an 'closed **' error in 1 second")_, e := l.Accept()log.Println(e)}()time.Sleep(time.Second * 1)l.Close()time.Sleep(time.Second * 1)}

更新 2015 Aug 05 结束

0 0
原创粉丝点击