上篇文章 一個有關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
結合上篇的文章咱們能夠看到,咱們經過一個小問題,引伸出了這麼多問題,在咱們一一搞清楚這些問題以後,咱們纔算是對最開始的問題有了一個完美的解釋。
因此說,作技術的沒有小問題,每個小問題背後都須要咱們有不少的知識儲備才能完全搞清楚。
這同時也告訴咱們,工做中遇到的任何問題都不能忽視,它極可能是你進步的重要因素。
完。
更多原創文章,請關注我微信公衆號: