<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從linux源碼看socket的close

          共 10595字,需瀏覽 22分鐘

           ·

          2021-07-17 05:07

          從linux源碼看socket的close

          筆者一直覺得如果能知道從應(yīng)用到框架再到操作系統(tǒng)的每一處代碼,是一件Exciting的事情。上篇博客講了socket的阻塞和非阻塞,這篇就開始談一談socket的close(以tcp為例且基于linux-2.6.24內(nèi)核版本)

          TCP關(guān)閉狀態(tài)轉(zhuǎn)移圖:

          眾所周知,TCP的close過程是四次揮手,狀態(tài)機(jī)的變遷也逃不出TCP狀態(tài)轉(zhuǎn)移圖,如下圖所示:

          tcp的關(guān)閉主要分主動關(guān)閉、被動關(guān)閉以及同時(shí)關(guān)閉(特殊情況,不做描述)

          主動關(guān)閉

          close(fd)的過程

          以C語言為例,在我們關(guān)閉socket的時(shí)候,會使用close(fd)函數(shù):

          int    socket_fd;
          socket_fd = socket(AF_INET, SOCK_STREAM, 0);
          ...
          // 此處通過文件描述符關(guān)閉對應(yīng)的socket
          close(socket_fd)

          而close(int fd)又是通過系統(tǒng)調(diào)用sys_close來執(zhí)行的:

          asmlinkage long sys_close(unsigned int fd)
          {
          // 清除(close_on_exec即退出進(jìn)程時(shí))的位圖標(biāo)記
          FD_CLR(fd, fdt->close_on_exec);
          // 釋放文件描述符
          // 將fdt->open_fds即打開的fd位圖中對應(yīng)的位清除
          // 再將fd掛入下一個(gè)可使用的fd以便復(fù)用
          __put_unused_fd(files, fd);
          // 調(diào)用file_pointer的close方法真正清除
          retval = filp_close(filp, files);
          }

          我們看到最終是調(diào)用的filp_close方法:

          int filp_close(struct file *filp, fl_owner_t id)
          {
          // 如果存在flush方法則flush
          if (filp->f_op && filp->f_op->flush)
          filp->f_op->flush(filp, id);
          // 調(diào)用fput
          fput(filp);
          ......
          }

          緊接著我們進(jìn)入fput:

          void fastcall fput(struct file *file)
          {
          // 對應(yīng)file->count--,同時(shí)檢查是否還有關(guān)于此file的引用
          // 如果沒有,則調(diào)用_fput進(jìn)行釋放
          if (atomic_dec_and_test(&file->f_count))
          __fput(file);
          }

          同一個(gè)file(socket)有多個(gè)引用的情況很常見,例如下面的例子:

          所以在多進(jìn)程的socket服務(wù)器編寫過程中,父進(jìn)程也需要close(fd)一次,以免socket無法最終關(guān)閉

          然后就是_fput函數(shù)了:

          void fastcall __fput(struct file *file)
          {
          // 從eventpoll中釋放file
          eventpoll_release(file);
          // 如果是release方法,則調(diào)用release
          if (file->f_op && file->f_op->release)
          file->f_op->release(inode, file);
          }

          由于我們討論的是socket的close,所以,我們現(xiàn)在探查下file->f_op->release在socket情況下的實(shí)現(xiàn):

          f_op->release的賦值

          我們跟蹤創(chuàng)建socket的代碼,即

          socket(AF_INET, SOCK_STREAM, 0);
          |-sock_create // 創(chuàng)建sock
          |-sock_map_fd // 將sock和fd關(guān)聯(lián)
          |-sock_attach_fd
          |-init_file(file,...,&socket_file_ops);
          |-file->f_op = fop; //fop賦值為socket_file_ops

          socket_file_ops的實(shí)現(xiàn)為:

          static const struct file_operations socket_file_ops = {
          .owner = THIS_MODULE,
          ......
          // 我們在這里只考慮sock_close
          .release = sock_close,
          ......
          };

          繼續(xù)跟蹤:

          sock_close
          |-sock_release
          |-sock->ops->release(sock);

          在上一篇博客中,我們知道sock->ops為下圖所示:

          即(在這里我們僅考慮tcp,即sk_prot=tcp_prot):

          inet_stream_ops->release
          |-inet_release
          |-sk->sk_prot->close(sk, timeout);
          |-tcp_prot->close(sk, timeout);
          |->tcp_prot.tcp_close

          關(guān)于fd與socket的關(guān)系如下圖所示:

          上圖中紅色線標(biāo)注的是close(fd)的調(diào)用鏈

          tcp_close

          void tcp_close(struct sock *sk, long timeout)
          {
          if (sk->sk_state == TCP_LISTEN) {
          // 如果是listen狀態(tài),則直接設(shè)為close狀態(tài)
          tcp_set_state(sk, TCP_CLOSE);
          }
          // 清空掉recv.buffer
          ......
          // SOCK_LINGER選項(xiàng)的處理
          ......
          else if (tcp_close_state(sk)){
          // tcp_close_state會將sk從established狀態(tài)變?yōu)閒in_wait1
          // 發(fā)送fin包
          tcp_send_fin(sk);
          }
          ......
          }

          四次揮手

          現(xiàn)在就是我們的四次揮手環(huán)節(jié)了,其中上半段的兩次揮手下圖所示:

          首先,在tcp_close_state(sk)中已經(jīng)將狀態(tài)設(shè)置為fin_wait1,并調(diào)用tcp_send_fin

          void tcp_send_fin(struct sock *sk)
          {
          ......
          // 這邊設(shè)置flags為ack和fin
          TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN);
          ......
          // 發(fā)送fin包,同時(shí)關(guān)閉nagle
          __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_OFF);
          }

          如上圖Step1所示。
          接著,主動關(guān)閉的這一端等待對端的ACK,如果ACK回來了,就設(shè)置TCP狀態(tài)為FIN_WAIT2,如上圖Step2所示,具體代碼如下:

          tcp_v4_do_rcv
          |-tcp_rcv_state_process
          int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len)
          {
          ......
          /* step 5: check the ACK field */
          if (th->ack) {
          ...
          case TCP_FIN_WAIT1:
          // 這處判斷是確認(rèn)此ack是發(fā)送Fin包對應(yīng)的那個(gè)ack
          if (tp->snd_una == tp->write_seq) {
          // 設(shè)置為FIN_WAIT2狀態(tài)
          tcp_set_state(sk, TCP_FIN_WAIT2);
          ......
          // 設(shè)定TCP_FIN_WAIT2定時(shí)器,將在tmo時(shí)間到期后將狀態(tài)變遷為TIME_WAIT
          // 不過是這時(shí)候改的已經(jīng)是inet_timewait_sock了
          tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
          ......
          }
          }
          /* step 7: process the segment text */
          switch(sk->sk_state) {
          case TCP_FIN_WAIT1:
          case TCP_FIN_WAIT2:
          ......
          case TCP_ESTABLISHED:
          tcp_data_queue(sk, skb);
          queued = 1;
          break;
          }
          .....
          }

          值的注意的是,從TCP_FIN_WAIT1變遷到TCP_FIN_WAIT2之后,還調(diào)用tcp_time_wait設(shè)置一個(gè)TCP_FIN_WAIT2定時(shí)器,在tmo+(2MSL或者基于RTO計(jì)算超時(shí))超時(shí)后會直接變遷到closed狀態(tài)(不過此時(shí)已經(jīng)是inet_timewait_sock了)。這個(gè)超時(shí)時(shí)間可以配置,如果是ipv4的話,則可以按照下列配置:

          net.ipv4.tcp_fin_timeout
          /sbin/sysctl -w net.ipv4.tcp_fin_timeout=30

          如下圖所示:

          有這樣一步的原因是防止對端由于種種原因始終沒有發(fā)送fin,防止一直處于FIN_WAIT2狀態(tài)。

          接著在FIN_WAIT2狀態(tài)等待對端的FIN,完成后面兩次揮手:

          由Step1和Step2將狀態(tài)置為了FIN_WAIT_2,然后接收到對端發(fā)送的FIN之后,將會將狀態(tài)設(shè)置為time_wait,如下代碼所示:

          tcp_v4_do_rcv
          |-tcp_rcv_state_process
          |-tcp_data_queue
          |-tcp_fin
          static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
          {
          switch (sk->sk_state) {
          ......
          case TCP_FIN_WAIT1:
          // 這邊是處理同時(shí)關(guān)閉的情況
          tcp_send_ack(sk);
          tcp_set_state(sk, TCP_CLOSING);
          break;
          case TCP_FIN_WAIT2:
          /* Received a FIN -- send ACK and enter TIME_WAIT. */
          // 收到FIN之后,發(fā)送ACK同時(shí)將狀態(tài)進(jìn)入TIME_WAIT
          tcp_send_ack(sk);
          tcp_time_wait(sk, TCP_TIME_WAIT, 0);
          }
          }

          time_wait狀態(tài)時(shí),原socket會被destroy,然后新創(chuàng)建一個(gè)inet_timewait_sock,這樣就能及時(shí)的將原socket使用的資源回收。而inet_timewait_sock被掛入一個(gè)bucket中,由
          inet_twdr_twcal_tick定時(shí)從bucket中將超過(2MSL或者基于RTO計(jì)算的時(shí)間)的time_wait的實(shí)例刪除。
          我們來看下tcp_time_wait函數(shù)

          void tcp_time_wait(struct sock *sk, int state, int timeo)
          {
          // 建立inet_timewait_sock
          tw = inet_twsk_alloc(sk, state);
          // 放到bucket的具體位置等待定時(shí)器刪除
          inet_twsk_schedule(tw, &tcp_death_row, time,TCP_TIMEWAIT_LEN);
          // 設(shè)置sk狀態(tài)為TCP_CLOSE,然后回收sk資源
          tcp_done(sk);
          }

          具體的定時(shí)器操作函數(shù)為inet_twdr_twcal_tick,這邊就不做描述了

          被動關(guān)閉

          close_wait

          在tcp的socket時(shí)候,如果是established狀態(tài),接收到了對端的FIN,則是被動關(guān)閉狀態(tài),會進(jìn)入close_wait狀態(tài),如下圖Step1所示:

          具體代碼如下所示:

          tcp_rcv_state_process
          |-tcp_data_queue
          static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
          {
          ...
          if (th->fin)
          tcp_fin(skb, sk, th);
          ...
          }

          我們再看下tcp_fin

          static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
          {
          ......
          // 這一句表明當(dāng)前socket有ack需要發(fā)送
          inet_csk_schedule_ack(sk);
          ......
          switch (sk->sk_state) {
          case TCP_SYN_RECV:
          case TCP_ESTABLISHED:
          /* Move to CLOSE_WAIT */
          // 狀態(tài)設(shè)置程close_wait狀態(tài)
          tcp_set_state(sk, TCP_CLOSE_WAIT);
          // 這一句表明,當(dāng)前fin可以延遲發(fā)送
          // 即和后面的數(shù)據(jù)一起發(fā)送或者定時(shí)器到時(shí)后發(fā)送
          inet_csk(sk)->icsk_ack.pingpong = 1;
          break;
          }
          ......
          }

          這邊有意思的點(diǎn)是,收到對端的fin之后并不會立即發(fā)送ack告知對端收到了,而是等有數(shù)據(jù)攜帶一塊發(fā)送,或者等攜帶重傳定時(shí)器到期后發(fā)送ack。

          如果對端關(guān)閉了,應(yīng)用端在read的時(shí)候得到的返回值是0,此時(shí)就應(yīng)該手動調(diào)用close去關(guān)閉連接

          if(recv(sockfd, buf, MAXLINE,0) == 0){
          close(sockfd)
          }

          我們看下recv是怎么處理fin包,從而返回0的,上一篇博客可知,recv最后調(diào)用tcp_rcvmsg,由于比較復(fù)雜,我們分兩段來看:
          tcp_recvmsg第一段

                  ......
          // 從接收隊(duì)列里面獲取一個(gè)sk_buffer
          skb = skb_peek(&sk->sk_receive_queue);
          do {
          // 如果已經(jīng)沒有數(shù)據(jù),直接跳出讀取循環(huán),返回0
          if (!skb)
          break;
          ......
          // *seq表示已經(jīng)讀到多少seq
          // TCP_SKB_CB(skb)->seq表示當(dāng)前sk_buffer的起始seq
          // offset即是在當(dāng)前sk_buffer中已經(jīng)讀取的長度
          offset = *seq - TCP_SKB_CB(skb)->seq;
          // syn處理
          if (tcp_hdr(skb)->syn)
          offset--;
          // 此處判斷表示,當(dāng)前skb還有數(shù)據(jù)可讀,跳轉(zhuǎn)found_ok_skb
          if (offset < skb->len)
          goto found_ok_skb;
          // 處理fin包的情況
          // offset == skb->len,跳轉(zhuǎn)到found_fin_ok然后跳出外面的大循環(huán)
          // 并返回0
          if (tcp_hdr(skb)->fin)
          goto found_fin_ok;
          BUG_TRAP(flags & MSG_PEEK);
          skb = skb->next;
          } while (skb != (struct sk_buff *)&sk->sk_receive_queue);
          ......

          上面代碼的處理過程如下圖所示:

          我們看下tcp_recmsg的第二段:

          found_ok_skb:
          // tcp已讀seq更新
          *seq += used;
          // 這次讀取的數(shù)量更新
          copied += used;
          // 如果還沒有讀到當(dāng)前sk_buffer的盡頭,則不檢測fin標(biāo)識
          if (used + offset < skb->len)
          continue;
          // 如果發(fā)現(xiàn)當(dāng)前skb有fin標(biāo)識,去found_fin_ok
          if (tcp_hdr(skb)->fin)
          goto found_fin_ok;
          ......
          found_fin_ok:
          /* Process the FIN. */
          // tcp已讀seq++
          ++*seq;
          ...
          break;
          } while(len > 0);

          由上面代碼可知,一旦當(dāng)前skb讀完了而且攜帶有fin標(biāo)識,則不管有沒有讀到用戶期望的字節(jié)數(shù)量都會返回已讀到的字節(jié)數(shù)。下一次再讀取的時(shí)候則在剛才描述的tcp_rcvmsg上半段直接不讀取任何數(shù)據(jù)再跳轉(zhuǎn)到found_fin_ok并返回0。這樣應(yīng)用就能感知到對端已經(jīng)關(guān)閉了。
          如下圖所示:

          last_ack

          應(yīng)用層在發(fā)現(xiàn)對端關(guān)閉之后已經(jīng)是close_wait狀態(tài),這時(shí)候再調(diào)用close的話,會將狀態(tài)改為last_ack狀態(tài),并發(fā)送本端的fin,如下代碼所示:

          void tcp_close(struct sock *sk, long timeout)
          {
          ......
          else if (tcp_close_state(sk)){
          // tcp_close_state會將sk從close_wait狀態(tài)變?yōu)閘ast_ack
          // 發(fā)送fin包
          tcp_send_fin(sk);
          }
          }

          在接收到主動關(guān)閉端的last_ack之后,則調(diào)用tcp_done(sk)設(shè)置sk為tcp_closed狀態(tài),并回收sk的資源,如下代碼所示:

          tcp_v4_do_rcv
          |-tcp_rcv_state_process
          int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len)
          {
          ......
          /* step 5: check the ACK field */
          if (th->ack) {
          ...
          case TCP_LAST_ACK:
          // 這處判斷是確認(rèn)此ack是發(fā)送Fin包對應(yīng)的那個(gè)ack
          if (tp->snd_una == tp->write_seq) {
          tcp_update_metrics(sk);
          // 設(shè)置socket為closed,并回收socket的資源
          tcp_done(sk);
          goto discard;
          }
          ...
          }
          }

          上述代碼就是被動關(guān)閉端的后兩次揮手了,如下圖所示:

          出現(xiàn)大量close_wait的情況

          linux中出現(xiàn)大量close_wait的情況一般是應(yīng)用在檢測到對端fin時(shí)沒有及時(shí)close當(dāng)前連接。有一種可能如下圖所示:

          當(dāng)出現(xiàn)這種情況,通常是minIdle之類參數(shù)的配置不對(如果連接池有定時(shí)收縮連接功能的話)。給連接池加上心跳也可以解決這種問題。
          如果應(yīng)用close的時(shí)間過晚,對端已經(jīng)將連接給銷毀。則應(yīng)用發(fā)送給fin給對端,對端會由于找不到對應(yīng)的連接而發(fā)送一個(gè)RST(Reset)報(bào)文。

          操作系統(tǒng)何時(shí)回收close_wait

          如果應(yīng)用遲遲沒有調(diào)用close_wait,那么操作系統(tǒng)有沒有一個(gè)回收機(jī)制呢,答案是有的。
          tcp本身有一個(gè)包活(keep alive)定時(shí)器,在(keep alive)定時(shí)器超時(shí)之后,會強(qiáng)行將此連接關(guān)閉??梢栽O(shè)置tcp keep alive的時(shí)間

          /etc/sysctl.conf
          net.ipv4.tcp_keepalive_intvl = 75
          net.ipv4.tcp_keepalive_probes = 9
          net.ipv4.tcp_keepalive_time = 7200

          默認(rèn)值如上面所示,設(shè)置的很大,7200s后超時(shí),如果想快速回收close_wait可以設(shè)置小一點(diǎn)。但最終解決方案還是得從應(yīng)用程序著手。
          關(guān)于tcp keepalive包活定時(shí)器可見筆者另一篇博客:
          https://my.oschina.net/alchemystar/blog/833981

          進(jìn)程關(guān)閉時(shí)清理socket資源

          進(jìn)程在退出時(shí)候(無論kill,kill -9 或是正常退出)都會關(guān)閉當(dāng)前進(jìn)程中所有的fd(文件描述符)

          do_exit
          |-exit_files
          |-__exit_files
          |-close_files
          |-filp_close

          這樣我們又回到了博客伊始的filp_close函數(shù),對每一個(gè)是socket的fd發(fā)送send_fin

          Java GC時(shí)清理socket資源

          Java的socket最終關(guān)聯(lián)到AbstractPlainSocketImpl,且其重寫了object的finalize方法

          abstract class AbstractPlainSocketImpl extends SocketImpl
          {
          ......
          /**
          * Cleans up if the user forgets to close it.
          */
          protected void finalize() throws IOException {
          close()
          }
          ......
          }

          所以Java會在GC時(shí)刻會關(guān)閉沒有被引用的socket,但是切記不要寄希望于Java的GC,因?yàn)镚C時(shí)刻并不是以未引用的socket數(shù)量來判斷的,所以有可能泄露了一堆socket,但仍舊沒有觸發(fā)GC。

          總結(jié)

          linux內(nèi)核源代碼博大精深,閱讀其代碼很費(fèi)周折。之前讀\<\>的時(shí)候由于有先輩引導(dǎo)和梳理,所以看書中所使用的BSD源碼并不覺得十分費(fèi)勁。直到現(xiàn)在自己帶著問題獨(dú)立看linux源碼的時(shí)候,盡管有之前的基礎(chǔ),仍舊被其中的各種細(xì)節(jié)所迷惑。希望筆者這篇文章能幫助到閱讀linux網(wǎng)絡(luò)協(xié)議棧代碼的人。

          原文鏈接

          https://my.oschina.net/alchemystar/blog/1821680



          瀏覽 69
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.亚洲黄色 | 国产白浆一区二区三区 | 99热这里 | 137无码XXXX肉体裸交摄影XXX | av毛片网站 |