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

2024-08-20 20:58

本文主要是介绍socket关闭: close()和shutdown()的差异,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转: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 main
import ("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 结束

这篇关于socket关闭: close()和shutdown()的差异的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1091159

相关文章

C#关闭指定时间段的Excel进程的方法

private DateTime beforeTime;            //Excel启动之前时间          private DateTime afterTime;               //Excel启动之后时间          //举例          beforeTime = DateTime.Now;          Excel.Applicat

argodb自定义函数读取hdfs文件的注意点,避免FileSystem已关闭异常

一、问题描述 一位同学反馈,他写的argo存过中调用了一个自定义函数,函数会加载hdfs上的一个文件,但有些节点会报FileSystem closed异常,同时有时任务会成功,有时会失败。 二、问题分析 argodb的计算引擎是基于spark的定制化引擎,对于自定义函数的调用跟hive on spark的是一致的。udf要通过反射生成实例,然后迭代调用evaluate。通过代码分析,udf在

各个地区饮食结构的差异 第九篇

比如原来蛋自质吃太少了 消耗太多 亏空 太多 就会虚 所有的方案要有循证医学证据

一次生产环境大量CLOSE_WAIT导致服务无法访问的定位过程

1.症状 生产环境的一个服务突然无法访问,服务的交互过程如下所示: 所有的请求都是通过网关进入,之后分发到后端服务。 现在的情况是用户服务无法访问商旅服务,网关有大量java.net.SocketTimeoutException: Read timed out报错日志,商旅服务也不断有日志打印,大多是回调和定时任务日志,所以故障点在网关和商旅服务,大概率是商旅服务无法访问导致网关超时。 后

Java Socket服务器端与客户端的编程步骤总结

一,InetAddress类: InetAddress类没有构造方法,所以不能直接new出一个对象; 可以通过InetAddress类的静态方法获得InetAddress的对象; InetAddress.getLocalHost(); InetAddress.getByName(""); 类主要方法: String - address.getHostName(); String - addre

VC环境下window网络程序:UDP Socket程序

最近在学Windows网络编程,正好在做UDPsocket的程序,贴上来: 服务器框架函数:              socket();    bind();    recfrom();  sendto();  closesocket(); 客户机框架函数:            socket();      recfrom();  sendto();  closesocket();

【虚拟机/服务器】XAMPP错误: Apache shutdown unexpectedly解决办法

XAMPP安装好之后启动,但有些用户在启动apache的时候会出现: 11:41:38 [Apache] Status change detected: running11:41:38 [Apache] Status change detected: stopped11:41:38 [Apache] Error: Apache shutdown unexpectedly.11:41:38

linux定时监听ssh服务是否启动-------麒麟操作系统永久关闭swap

linux监听ssh服务是否启动 1、监听脚本2、定时任务3、麒麟操作系统,永久关闭swap 1、监听脚本 #在/usr/local/bin目录下新建脚本文件 cd /usr/local/bintouch check_sshd.sh#给可执行权限chmod +x /usr/local/bin/check_sshd.sh 脚本内容如下: #!/bin/bashs

socket()接口与内核协议栈的挂接

最近在看Brdige的代码,发现一个问题,同样的调用ioctl接口实现添加网桥、删除网桥、网桥增加网卡、网桥删除网卡等操作,一个应用层的接口,却通过两条路径实现,sock_ioctl和RTNETLINK(这本就不是一个级别的东西),而应用层的brctl-utils源码中并没有直接使用PF_NETLINK协议簇的情况,让我感到非常奇怪,因此想把glibc到系统调用,到协议簇注册,以及和VFS的关系再

使用Python控制Excel应用:打开与关闭工作簿的技术性探讨

目录 引言 一、安装必要的库 1. xlwings 2. openpyxl 二、使用xlwings打开和关闭Excel工作簿 2.1 启动和退出Excel 2.2 打开和关闭工作簿 2.3 创建新工作簿 三、使用openpyxl打开和关闭Excel工作簿 3.1 打开工作簿 3.2 保存和关闭工作簿 四、案例分析 4.1 读取Excel文件中的数据 4.2 写入数据到E