深入理解TCP協議及其源代碼——網絡程序設計課第五次作業


  本次實驗,我們以tcp的三次握手為例,跟蹤並分析tcp協議中相關內核處理函數從而加深對tcp協議三次握手這項機制的理解。

  環境:linux-5.0.1內核 ,32位系統的MenuOS

 

  首先,弄清楚三次握手的具體流程:

 

    1.client端發起主動連接,向服務器端發送一個SYN被置1的報文表示請求連接

  2.server端收到后向client發送ACK和SYN均置為1的數據包,表示收到請求並同意建立連接

  3.client收到后向server端發送ACK置為1的數據包,表示接收到了該數據包。自此,三次握手完畢,連接建立成功。

 

 一、自頂向下逐級追蹤

  通過上次的socket實驗,我們已經搞清楚在replyhi(server端)和hello(client端)建立通訊這一過程中(從socket的創建后到tcp連接的建立)分別按序涉及到以下的linux socket api的調用:

  server端:bind、listen、accept

  client端:connect

 

  將以上的api置於三次握手的時序圖中:

                                         

  按照先啟動server再啟動client的順序,在我們啟動server端並完成server socket的建立后,按序執行了bind和listen,他們分別完成了將socket綁定到某一端口和設置此socket能夠處理的請求排隊的最大長度的工作。

完成上述操作后執行accept,它會一致阻塞直到有客戶端的socket發起連接。

  啟動client后,同樣在先完成socket的建立后我們執行了connect這個阻塞函數,它發起和server的主動連接,連接成功后返回。

  也就是說,三次握手的具體過程發生在accept和connect之間。

 

  對於這一結論,我們可以研究bind、listen、accept和connect等api涉及到的內核處理函數的細節來驗證(即看看這些內核處理函數是否調用了數據包發送的相關的底層函數),也可以用相關的抓包軟件來驗證——

我們分別把斷點打在這些函數調用處,看看運行它們后是否引起了三次握手中相關數據包的發送就能達到我們的目的,本來計划用gdb直接調試內核,發現wireshark抓不到qemu產生的數據包,因此我們在用戶態下用gdb

調試tcp通信的程序(不影響對結論的驗證)。

  在用戶態下,為了能捕捉到斷點,我們自己再寫四個函數,break_bind,  break_listen,  break_accept和break_connect。這四個函數什么都不做,只是起到gdb能捕捉到這個斷點的作用。然后分別在對應的linux socket api

調用之前調用它們,如:

 

 

  其他三個函數也是在類似的對應位置調用。編譯完成后,用gdb在這四處打上斷點並跟蹤,同時運行wireshark:

   

  當我們運行server直到斷點break_accept處時(此時,bind和listen已經執行完畢,accept尚未執行),如下圖所示:

   

 

  並未抓取到任何三次握手的數據包

  繼續執行到accept,server端處於continue狀態,陷入阻塞。此時開始運行client:

  在connect運行前也是沒有產生三次握手的數據包:

  

 

  直到我們運行完成connect:

  

    

 

  三次握手的數據包已經產生。且此時server端顯示:

 

 

 

  accept client 127.0.0.1。完全與我們前面所說的三次握手的具體過程發生在accept和connect之間,且accept會在connect完成后返回這一描述一致。

 

  知道了這一點后,我們分析三次握手的流程就能聚焦到connect和accept這兩個函數之中。從上次的實驗我們已經知道accept和connect最終調用了__sys_accept4和__sys_connect這兩個內核處理函數,檢查其源碼我們發現,

這倆函數核心是通過sock->opt->connect和sock->opt->accept這兩個函數指針調用了某個函數。

  其中sock是struct socket類型的,為了追蹤下去我們還得找到struct socket這個結構體的定義。

  這里,順便一提,為了找linux內核代碼中的某個結構體的定義我們可以在源碼根目錄下的include目錄(所有的系統預定義的結構體都在內核源代碼的/include下有定義)中使用正則表達式搜尋,如:find -name "*.h" | xargs grep "struct socket {" -rn

  struct socket的定義在include/net.h中

struct socket { socket_state state; short type; unsigned long flags; struct socket_wq    *wq; struct file        *file; struct sock        *sk; const struct proto_ops *ops; }; 

  從上面的定義我們知道了,ops是struct proro_ops類型,接着找。

  同樣在include/net.h中:   

struct proto_ops { int family; struct module    *owner; int        (*release)   (struct socket *sock); int        (*bind)         (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int        (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags, bool kern); int        (*getname)   (struct socket *sock, struct sockaddr *addr, int peer); __poll_t (*poll)         (struct file *file, struct socket *sock, struct poll_table_struct *wait); int        (*ioctl)     (struct socket *sock, unsigned int cmd, unsigned long arg); #ifdef CONFIG_COMPAT int         (*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #endif
    int        (*listen)    (struct socket *sock, int len); int        (*shutdown)  (struct socket *sock, int flags); int        (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); int        (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); #ifdef CONFIG_COMPAT int        (*compat_setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); int        (*compat_getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); #endif
    int        (*sendmsg)   (struct socket *sock, struct msghdr *m, size_t total_len); int        (*recvmsg)   (struct socket *sock, struct msghdr *m, size_t total_len, int flags); int        (*mmap)         (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage)  (struct socket *sock, struct page *page, int offset, size_t size, int flags); ssize_t (*splice_read)(struct socket *sock,  loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags); int        (*set_peek_off)(struct sock *sk, int val); int        (*peek_len)(struct socket *sock); int        (*read_sock)(struct sock *sk, read_descriptor_t *desc, sk_read_actor_t recv_actor); int        (*sendpage_locked)(struct sock *sk, struct page *page, int offset, size_t size, int flags); int        (*sendmsg_locked)(struct sock *sk, struct msghdr *msg, size_t size); int        (*set_rcvlowat)(struct sock *sk, int val); };

  根據以上代碼:accept和connect都是函數指針,我們還得知道這個結構體是在哪里始化的,初始化的過程中給accept和connect指針分別綁定到了哪個函數上去。

原來struct proto tcp_prot的初始化設定了TCP協議棧的訪問接口函數,socket接口層里sock->opt->connect和sock->opt->accept對應的接口函數即是在這里制定:

struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect,  .connect = tcp_v4_connect, .disconnect = tcp_disconnect,  .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, .sendpage = tcp_sendpage, .backlog_rcv = tcp_v4_do_rcv, .release_cb = tcp_release_cb, .hash = inet_hash, .unhash = inet_unhash, .get_port = inet_csk_get_port, .enter_memory_pressure = tcp_enter_memory_pressure, .leave_memory_pressure = tcp_leave_memory_pressure, .stream_memory_free = tcp_stream_memory_free, .sockets_allocated = &tcp_sockets_allocated, .orphan_count = &tcp_orphan_count, .memory_allocated = &tcp_memory_allocated, .memory_pressure = &tcp_memory_pressure, .sysctl_mem = sysctl_tcp_mem, .sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem), .sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem), .max_header = MAX_TCP_HEADER, .obj_size = sizeof(struct tcp_sock), .slab_flags = SLAB_TYPESAFE_BY_RCU, .twsk_prot = &tcp_timewait_sock_ops, .rsk_prot = &tcp_request_sock_ops, .h.hashinfo = &tcp_hashinfo, .no_autobind = true, #ifdef CONFIG_COMPAT .compat_setsockopt = compat_tcp_setsockopt, .compat_getsockopt = compat_tcp_getsockopt, #endif .diag_destroy = tcp_abort, };

   代碼中標黃的部分已經告訴我們的sock->opt->connect指針綁定到了 tcp_v4_connect函數,sock->opt->accept指針綁定到了inet_csk_accept函數。

   OK,有了這個認識后,接下來我們就依次來查看tcp_v4_connect和inet_csk_accept。

  1.tcp_v4_connect:

  根據老師ppt所指出的,tcp_v4_connect函數的主要作用就是發起一個TCP連接,生一個包含SYN標志和一個32位的序號的連接請求包,並發送給服務器端,這是TCP三次握手的第一步。

tcp_set_state(sk, TCP_SYN_SENT);
    err = inet_hash_connect(tcp_death_row, sk);
    if (err)
        goto failure;

    sk_set_txhash(sk);

    rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                   inet->inet_sport, inet->inet_dport, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        goto failure;

  並通過:

  tcp_set_state(sk, TCP_SYN_SENT)把套接字的狀態從CLOSE切換到SYN_SENT,並進一步調用了 tcp_connect(sk)來實際構造SYN並發送出去。然后調用inet_hash_connect(&tcp_death_row, sk)和

ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk),為套接字綁定一個端口,並記錄在TCP的哈希表中。

  總的來說,整個過程的函數調用關系是這樣的:

  tcp_v4_connect -> tcp_connect_init -> tcp_transmit_skb -> icsk->icsk_af_ops->send_check (tcp_v4_send_check)-> icsk->icsk_af_ops->queue_xmit (ip_queue_xmit)-> inet_csk_reset_xmit_timer
                                           

  2.inet_csk_accept:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern) { struct inet_connection_sock *icsk = inet_csk(sk); struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct request_sock *req; struct sock *newsk; int error; lock_sock(sk); /* We need to make sure that this socket is listening, * and that it has something pending. */ error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; /* Find already established connection */
    if (reqsk_queue_empty(queue)) { long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); /* If this is a non blocking socket don't sleep */ error = -EAGAIN; if (!timeo) goto out_err; error = inet_csk_wait_for_connect(sk, timeo); if (error) goto out_err; } req = reqsk_queue_remove(queue, sk); newsk = req->sk; if (sk->sk_protocol == IPPROTO_TCP && tcp_rsk(req)->tfo_listener) { spin_lock_bh(&queue->fastopenq.lock); if (tcp_rsk(req)->tfo_listener) { /* We are still waiting for the final ACK from 3WHS * so can't free req now. Instead, we set req->sk to * NULL to signify that the child socket is taken * so reqsk_fastopen_remove() will free the req * when 3WHS finishes (or is aborted). */ req->sk = NULL; req = NULL; } spin_unlock_bh(&queue->fastopenq.lock); } out: release_sock(sk); if (req) reqsk_put(req); return newsk; out_err: newsk = NULL; req = NULL; *err = error; goto out; }

  在上面的代碼中,我們重點關注這一部分:

if (reqsk_queue_empty(queue)) { long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); /* If this is a non blocking socket don't sleep */ error = -EAGAIN; if (!timeo) goto out_err; error = inet_csk_wait_for_connect(sk, timeo); if (error) goto out_err; } req = reqsk_queue_remove(queue, sk); newsk = req->sk;

  一個分支語句:當reqsk_queue不空時我們調用reqsk_queue_remove函數從隊列中取出某個連接請求。而當reqsk_queue為空時,我們調用了inet_csk_wait_for_connect,來看看這個函數的實現(在net/ipv4/inet_connection_sock.c中):

inet_csk_wait_for_connect:

static int inet_csk_wait_for_connect(struct sock *sk, long timeo) { struct inet_connection_sock *icsk = inet_csk(sk); DEFINE_WAIT(wait); int err; for (;;) { prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); release_sock(sk); if (reqsk_queue_empty(&icsk->icsk_accept_queue)) timeo = schedule_timeout(timeo); sched_annotate_sleep(); lock_sock(sk); err = 0; if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) break; err = -EINVAL; if (sk->sk_state != TCP_LISTEN) break; err = sock_intr_errno(timeo); if (signal_pending(current)) break; err = -EAGAIN; if (!timeo) break; } finish_wait(sk_sleep(sk), &wait); return err; }

 

  原來核心是一個for(;;)的死循環,難怪我們說accept是阻塞的,原因在這里。只有當一個連接請求發過來時才會跳出循環。也就是說:

   

 

 

  整個過程大概是這個樣子:client通過conncet調用tcp協議中的tcp_v4_connect函數,server通過accept調用inet_csk_wait_for_connect函數來監聽連接請求的隊列,一旦有請求發來就跳出循環,否則一直阻塞。這其中,

tcp協議棧負責寫入隊列,而inet_csk_wait_for_connec負責監聽隊列並將請求從隊列中讀出。也就是說當協議棧往隊列中寫入請求,accept讀出請求的時候就是三次握手的完成之時。那么協議棧是怎么寫入的呢?

 

二、自底向上分析tcp的接收過程

  以上都是我們按照函數調用的一層層關系自頂向下了解到的三次握手的流程。反過來,自底向上的,底層所抓到或產生的數據是如何通知上層的?也就是上面所提出的問題協議棧是怎么寫入的呢?是tcp_v4_rcv,ip層在收到數據后就會通過函數指針回調tcp_v4_rcv函數。 

static struct net_protocol tcp_protocol = { .early_demux = tcp_v4_early_demux, .early_demux_handler = tcp_v4_early_demux, .handler = tcp_v4_rcv, .err_handler = tcp_v4_err, .no_policy =    1, .netns_ok =    1, .icmp_strict_tag_validation = 1, };

 

  上面初始化的過程中,函數指針handler綁定到了tcp_v4_rcv。tcp_v4_rcv函數為TCP的總入口,數據包從IP層傳遞上來,進入該函數(也就是說它是自底向上進入tcp的入口)。而入口函數tcp_v4_rcv又做了什么,查詢有關資料后,我找到了這張流程圖,

它大概地梳理了這一過程:

   

 

 

  具體的過程,下面這篇博客已經講的很完善了:

       https://blog.csdn.net/xiaoyu_750516366/article/details/85539495

   簡而言之,tcp從ip接收數據的過程總共維護了prequeue、receive以及backlog這三個隊列。由於協議棧對輸入數據包的處理實際上都是中斷中進行的,出於性能的考慮,我們總是期望中斷能夠快速的結束

當我們調用tcp_v4_rcv時首先會檢查傳輸控制塊TCB,如果它被用戶進程鎖定,就將數據包放入到backlog隊列中,這類數據包的真正處理是在用戶進程釋放TCB時進行的;如果TCB沒有被進程鎖定,那么首先嘗試將數據包放入prequeue隊列,這類數據包的處理是在用戶進程讀數據過程中處理的;如果沒有被進程鎖定,prequeue隊列也沒有接受該數據包(出於性能考慮,比如prequeue隊列不能無限制增大),那么必須在中斷中對數據包進行處理,處理完畢后將數據包加入到receive隊列中。放入receive隊列的數據包都是已經被TCP處理過的數據包,比如校驗、回ACK等動作都已經完成了,這些數據包等待用戶空間程序讀即可;相反,放入backlog隊列和prequeue隊列的數據包都還需要TCP處理,實際上,這些數據包也都是在合適的時機通過tcp_v4_do_rcv()處理的。

 

  明白了從ip層往上到tcp層的入口函數是tcp_v4_rcv之后,我們再來看看三次握手中每一次的tcp_v4_rcv之中的函數調用關系是怎樣的。

  先給出結論:

    1.客戶端發送SYN

     tcp_v4_connect() -> tcp_connect() -> tcp_transmit_skb() -> ip_queue_xmit()

        

    2.服務器端接收SYN,並返回SYN和ACK

     tcp_v4_rcv() -> tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_rcv_synsent_state_process() -> tcp_send_ack()


                               
              3.客戶端發送ACK

      tcp_send_ack() -> tcp_transmit_skb() -> ip_queue_xmit()
      
  涉及到的內容很多,鑽研代碼的細節的話很費功夫,因此我們只關注了個別函數,窺見其一斑 。

  1.tcp_rcv_state_process
 

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    
  ...
    switch (sk->sk_state) {
    case TCP_CLOSE:
      ...
goto discard; case TCP_LISTEN: ...goto discard; case TCP_SYN_SENT: ...return 0; }   ...switch (sk->sk_state) { case TCP_SYN_RECV: ... tcp_set_state(sk, TCP_ESTABLISHED); sk->sk_state_change(sk); break; case TCP_FIN_WAIT1: {
    ... }
case TCP_CLOSING:   ...break; case TCP_LAST_ACK: ... } break; } tcp_urg(sk, skb, th); switch (sk->sk_state) { case TCP_CLOSE_WAIT: case TCP_CLOSING: case TCP_LAST_ACK: if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) break; case TCP_FIN_WAIT1: case TCP_FIN_WAIT2: case TCP_ESTABLISHED: tcp_data_queue(sk, skb); queued = 1; break; } if (sk->sk_state != TCP_CLOSE) { tcp_data_snd_check(sk); tcp_ack_snd_check(sk); } if (!queued) { discard: tcp_drop(sk, skb); } return 0; }

 

  以上是tcp_rcv_state_process這個函數刪減了一部分后的內容,我們看到,這里頭通過幾個switch語句塊包含了所有的socket的狀態:

  

 

 

  也就是說這個函數實通過switch語句使得各個狀態的socket找到對應的分支從而進行不同的操作轉換到不同的狀態,也即它是實現tcp自動機的入口函數。

  2.tcp_transmit_skb

   tcp_transmit_skb的作用是復制或者拷貝skb,構造skb中的tcp首部,並將調用網絡層的發送函數發送skb;在發送前,首先需要克隆或者復制skb,因為在成功發送到網絡設備之后,skb會釋放,而tcp層不能真正的釋放,是需要等到對該數據段的ack才可以釋放;然后構造tcp首部和選項;最后調用網絡層提供的發送回調函數發送skb,ip層的回調函數為ip_queue_xmit;

  參考自:https://www.cnblogs.com/wanpengcoder/p/11755347.html

 

  總結:第一部分我們自頂向下地追蹤和通過抓包分析,明白了三次握手的過程發生在accept和connect之間。再通過第二部分自底向上研究了從ip層進入tcp,也即tcp接收處理的過程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM