對上一篇文章中tcp問題的進一步思考

上篇文章 一個有關tcp的很是有意思的問題 中咱們講到,在tcp創建鏈接後,若是一端關閉了鏈接,另外一端的第一次write仍是能夠寫成功的,文章中也分析了形成這種現象的具體緣由。微信

那若是在此種狀況下,read又會有什麼樣的結果呢?socket

其實具體結果已經在read的man文檔中有詳細介紹,不過咱們仍是從源碼角度來證明下:tcp

// net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
                int flags, int *addr_len)
{
        struct tcp_sock *tp = tcp_sk(sk);
        int copied = 0; // 總共拷貝給用戶的字節數,用於返回
        ...
        u32 *seq;
        ...
        seq = &tp->copied_seq; // 下一個拷貝給用戶的字節
        ...
        do {
                u32 offset;
                ...
                skb_queue_walk(&sk->sk_receive_queue, skb) {
                        ...
                        offset = *seq - TCP_SKB_CB(skb)->seq;
                        ...
                        if (offset < skb->len)
                                goto found_ok_skb;
                        if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
                                goto found_fin_ok;
                        ...
                }
                ...
found_ok_skb:
                /* Ok so how much can we use? */
                used = skb->len - offset; // 當前buf剩餘可拷貝給用戶的字節數
                ...
                *seq += used;
                copied += used;
                len -= used;
                ...
                continue;
found_fin_ok:
                ...
                break;
        } while (len > 0);
        ...
        return copied;
        ...
}
EXPORT_SYMBOL(tcp_recvmsg);

由上可見,當咱們發起read時,無論此時咱們的socket是否已經收到fin包,咱們都會先把socket中的未讀字節讀出來,並返回拷貝的字節數給用戶,表示這次read成功。源碼分析

若是咱們把socket中的數據都讀完了,而後檢測到了最後的fin包,此時直接跳出read循環,返回copied的值(此時是0)給用戶。spa

綜上可見,read方法用返回值表示該socket的當前狀況,若是返回值大於0,表示read成功,當前socket正常(即便此時socket已經處於CLOSE_WAIT狀態),若是返回值等於0,表示該socket的對應的socket已經關閉,而且咱們已經收到了fin包,進入了CLOSE_WAIT狀態,通常在這種狀況下,咱們都會在應用層調用close方法,關閉咱們本身的socket,進而完整的關閉整個tcp鏈接。線程

對應看下read的man文檔,咱們會發現,源碼和文檔中的描述是一致的。3d

至此,read相關的返回值咱們就分析完畢了。code

下面咱們再來分析下,在一樣的情景下,epoll相關操做會有什麼樣的反應呢?隊列

咱們先來看下收到fin包後,咱們socket的處理流程:事件

// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
        struct tcp_sock *tp = tcp_sk(sk);
        bool fragstolen;
        int eaten;
        ...
        if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
                ...
                // 將當前接受到的tcp包加入到接受隊列中
                eaten = tcp_queue_rcv(sk, skb, &fragstolen);
                ...
                if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
                        tcp_fin(sk);
                ...
                return;
        }
        ...
}

該方法在收到fin包後調用了tcp_fin方法:

// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
        ...
        sk->sk_shutdown |= RCV_SHUTDOWN;
        sock_set_flag(sk, SOCK_DONE);

        switch (sk->sk_state) {
        case TCP_SYN_RECV:
        case TCP_ESTABLISHED:
                /* Move to CLOSE_WAIT */
                tcp_set_state(sk, TCP_CLOSE_WAIT);
                ...
                break;
        ...
        }
        ...
        if (!sock_flag(sk, SOCK_DEAD)) {
                sk->sk_state_change(sk);
                ...
        }
}

由上可見,該方法在收到fin包後,設置該socket的shutdown狀況爲RCV_SHUTDOWN,而且設置其狀態爲TCP_CLOSE_WAIT。

以後調用了sk->sk_state_change方法,標識該socket有epoll事件發生,此時因調用epoll_wait而阻塞的線程也會從阻塞狀態中退出,epoll_wait線程進而會去檢測該socket準備好了哪些epoll事件,對應的檢測方法爲下面這個方法:

// net/ipv4/tcp.c
__poll_t tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
        __poll_t mask;
        struct sock *sk = sock->sk;
        ...
        if (sk->sk_shutdown & RCV_SHUTDOWN)
                mask |= EPOLLIN | EPOLLRDNORM | EPOLLRDHUP;
        ...
        return mask;
}
EXPORT_SYMBOL(tcp_poll);

由上可見,當咱們socket的shutdown處於RCV_SHUTDOWN狀態時,epoll_wait返回給用戶的事件爲 EPOLLIN | EPOLLRDNORM | EPOLLRDHUP。

也就是說,當咱們的socket收到fin包以後,監聽該socket的對應的epoll_wait方法會從阻塞狀態中退出,並調用上面的tcp_poll方法,該方法檢測到這個socket此時已經準備好的epoll事件爲 EPOLLIN | EPOLLRDNORM | EPOLLRDHUP,最後epoll_wait將這些事件返回給用戶。

此時,用戶的通常操做爲繼續對這個socket進行read,經過read返回0的形式,來表示對方socket已經關閉,咱們的socket也能夠關閉了。

至此,epoll相關的行爲也以已經分析完畢了。

整個過程仍是比較簡單的。

有關epoll相關的源碼分析系列文章,能夠看下我以前寫的這些:

Linux epoll 源碼分析 1
Linux epoll 源碼分析 2
Linux epoll 源碼分析 3

結合上篇的文章咱們能夠看到,咱們經過一個小問題,引伸出了這麼多問題,在咱們一一搞清楚這些問題以後,咱們纔算是對最開始的問題有了一個完美的解釋。

因此說,作技術的沒有小問題,每個小問題背後都須要咱們有不少的知識儲備才能完全搞清楚。

這同時也告訴咱們,工做中遇到的任何問題都不能忽視,它極可能是你進步的重要因素。

完。

更多原創文章,請關注我微信公衆號:

底層技術研究

相關文章
相關標籤/搜索