淺談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()不能 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章