浅谈shutdown()和close()的区别

  shutdown()函数可以选择关闭全双工连接的读通道或者写通道,如果两个通道同时关闭,则这个连接不能再继续通信。close()函数会同时关闭全双工连接的读写通道,除了关闭连接外,还会释放套接字占用的文件描述符。而shutdown()只会关闭连接,但是不会释放占用的文件描述符。所以即使使用了SHUT_RDWR类型调用shutdown()关闭连接,也仍然要调用close()来释放连接占用的文件描述符。
1. close()
     close()函数对应的系统调用是sys_close(),在fs/open.c中定义。在sys_close()中,会首先根据文件描述符在进程的打开文件表中查找对应的file结构实例,然后调用filp_close()来关闭文件。关闭操作是在fput()(由filp_close()调用)中进行的,引用数减1后为零,才会调用__fput()来释放文件占用的内存。对套接字来说,__fput()中我们主要关心以下代码:
void __fput( struct file *file)
{
    ......
    if (file - >f_op && file - >f_op - >release)
        file - >f_op - >release(inode, file);
    .....
     dput(dentry);
     ......
}
  file->f_op指向的是文件操作实例,套接字的文件操作由socket_file_ops提供。socket_file_ops属于socket层,socket层是vfs和底层协议栈连接的桥梁,真正的操作还是由协议栈来提供。在这里,file->f_op->release指向sock_close()函数。在socket层下面,接着是协议族,在这个层,不同的传输层协议都会提供自己的操作接口。在协议族层,TCP和UDP协议提供的接口都是inet_release(),这个函数最终会调用到不同的传输层协议提供的close接口。TCP协议提供的是tcp_close()函数,UDP协议提供的是udp_lib_close()。
  tcp_close()中会首先将套接字的sk_shutdown标志设置为SHUTDOWN_MASK,表示双向关闭。然后检查接收缓冲区是否有数据未读(不包括FIN包),如果有数据未读,协议栈会发送RST包,而不是FIN包。如果套接字设置了SO_LINGER选项,并且lingertime设置为0,这种情况下也会发送RST包来终止连接。其他情况下,会检查套接字的状态,只有在套接字的状态是TCP_ESTABLISHED、TCP_SYN_RECV和TCP_CLOSE_WAIT的状态下,才会发送FIN包。在决定了是否发包以及发送什么类型的包之后,协议栈会进行套接字占用的资源的清理,包括sock结构、缓冲区和错误队列占用的内存等,并进行状态的变更。如果是发送FIN包进行正常关闭,后续会进行四次关闭操作,这个过程是在协议栈中完成的,和用户进程没有关系,用户进程也不能再操作这个套接字。
  udp_lib_close()中只是简单地调用了sk_common_release()函数,sk_common_release()中会调用udp_destroy_sock()来释放发送队列中占用的内存。如果UDP套接字已绑定本地端口,会添加到udp_table哈希表中,所以套接字如果已经被添加到哈希表中,udp_lib_unhash()中会将套接字从哈希表中移除。接下来会调用sock_orphan()解除进程和套接字的关系,然后释放sock结构占用的资源。
  socket结构实例占用的内存,是在dput()调用到的sock_destroy_inode()函数来释放的, sock_destroy_inode()中只是简单地调用kmem_cache_free()释放占用的内存。
2. shutdown()
  shutdown()函数对应的系统调用是sys_shutdown(),在net/socket.c中定义。由于close()不仅可以用于关闭套接字,也可以关闭普通文件、字符设备文件等类型,为了处理不同类型文件的关闭,操作比较复杂。而shutdown()只能用于套接字类型的文件,处理也比较简单。
  sys_shutdown()中首先调用sockfd_lookup_light()来查找描述符对应的socket结构,然后调用套接字对应的协议族层中提供的shutdown接口。UDP和TCP协议提供的接口都是inet_shutdown()函数,主要处理如下所示:
int inet_shutdown( struct socket *sock, int how)
{
    ......
    switch (sk - >sk_state) {
    case TCP_CLOSE :
        err = -ENOTCONN;
        /* Hack to wake up other listeners, who can poll for
           POLLHUP, even on eg. unconnected UDP sockets -- RR */

    default :
        sk - >sk_shutdown |= how;
        if (sk - >sk_prot - >shutdown)
            sk - >sk_prot - >shutdown(sk, how);
        break;

    /* Remaining two branches are temporary solution for missing
     * close() in multithreaded environment. It is _not_ a good idea,
     * but we have no choice until close() is repaired at VFS level.
     */

    case TCP_LISTEN :
        if ( !(how & RCV_SHUTDOWN))
            break;
        /* Fall through */
    case TCP_SYN_SENT :
        err = sk - >sk_prot - >disconnect(sk, O_NONBLOCK);
        sock - >state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
        break;
    }

    /* Wake up anyone sleeping in poll. */
    sk - >sk_state_change(sk);
    ......
}
  在说明代码的处理之前,先来了解一下UDP套接字的状态。UDP的传输是没有状态的,内核中在描述UDP套接字的状态时,借用了TCP的状态。UDP套接字只有两种状态,TCP_CLOSE和TCP_ESTABLISHED。在套接字刚创建时,不管是UDP还是TCP,状态都是TCP_CLOSE。UDP在调用connect()后,状态改变为TCP_ESTABLISHED。
  如果套接字的状态TCP_CLOSE,套接字要么是刚创建的,要么连接已经关闭,所以调用shutdown()是不合适的,此时要返回ENOTCONN错误。
   接下来的代码会处理TCP_LISTEN和TCP_SYN_SENT状态以外的情况。将用户设置的关闭选项设置到套接字的sk_shutdown标志,然后调用传输层协议提供的shutdown接口。TCP协议提供的是tcp_shutdown()函数,而UDP并没有提供任何函数。
  在tcp_shutdown()中,首先检查是否是否关闭了写通道,如果不是,则直接返回。如果关闭了写通道,并且状态是TCP_ESTABLISHED、TCP_SYN_SENT、TCP_SYN_RECV或TCP_CLOSE_WAIT,会调用tcp_close_state()来进行状态的变更。如果变更状态后需要发送FIN包,则调用tcp_send_fin()来发送。
  由于UDP没有TCP_LISTEN和TCP_SYN_SENT状态,所以 sk - > sk_prot - > disconnect只会调用调用tcp_disconnect()函数。如果是套接字状态是TCP_LISTEN状态,并且是关闭读通道,内核会停止套接字的监听状态,释放sock结构占用的资源。如果是TCP_SYN_SENT状态,会发送RST包来终止连接的创建过程,释放sock结构占用的资源。
  最后会调用套接字的sk_state_change接口(通常是 sock_def_wakeup()),通知用户进程状态已经发生改变。
3. 总结
  现在总结一下shutdown()和close()的主要区别:
     1)对应的系统调用不同
     2)shutdown()只能用于套接字文件,close()可以用于所有文件类型
     3)shutdown()只是关闭连接,并没有释放文件描述符,close()可以
     4)shutdown()不能用于TCP_CLOSE状态的套接字,否则会返回 ENOTCONN 错误
     5)shutdown()可以选择关闭读通道或写通道,close()不能。

你可能感兴趣的:(Linux内核)