日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux内核分析 - 网络[十六]:TCP三次握手

發布時間:2025/3/21 linux 21 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux内核分析 - 网络[十六]:TCP三次握手 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
? 內核:2.6.34
????? TCP是應用最廣泛的傳輸層協議,其提供了面向連接的、可靠的字節流服務,但也正是因為這些特性,使得TCP較之UDP異常復雜,還是分兩部分[創建與使用]來進行分析。這篇主要包括TCP的創建及三次握手的過程。

????? 編程時一般用如下語句創建TCP Socket:

[cpp] view plain copy
  • socket(AF_INET,?SOCK_DGRAM,?IPPROTO_TCP)??
  • ????? 由此開始分析,調用接口[net/socket.c]: SYSCALL_DEFINE3(socket)
    ????? 其中執行兩步關鍵操作:sock_create()與sock_map_fd()

    [cpp] view plain copy
  • retval?=?sock_create(family,?type,?protocol,?&sock);??
  • if?(retval?<?0)??
  • ?goto?out;??
  • retval?=?sock_map_fd(sock,?flags?&?(O_CLOEXEC?|?O_NONBLOCK));??
  • if?(retval?<?0)??
  • ?goto?out_release;??
  • ????? sock_create()用于創建socket,sock_map_fd()將之映射到文件描述符,使socket能通過fd進行訪問,著重分析sock_create()的創建過程。
    ????? sock_create() -> __sock_create()
    ????? 從__sock_create()代碼看到創建包含兩步:sock_alloc()和pf->create()。sock_alloc()分配了sock內存空間并初始化inode;pf->create()初始化了sk。

    [cpp] view plain copy
  • sock?=?sock_alloc();??
  • sock->type?=?type;??
  • ……??
  • pf?=?rcu_dereference(net_families[family]);??
  • ……??
  • pf->create(net,?sock,?protocol,?kern);??
  • sock_alloc()
    ????? 分配空間,通過new_inode()分配了節點(包括socket),然后通過SOCKET_I宏獲得sock,實際上inode和sock是在new_inode()中一起分配的,結構體叫作sock_alloc。

    [cpp] view plain copy
  • inode?=?new_inode(sock_mnt->mnt_sb);??
  • sock?=?SOCKET_I(inode);??
  • ????? 設置inode的參數,并返回sock。

    [cpp] view plain copy
  • inode->i_mode?=?S_IFSOCK?|?S_IRWXUGO;??
  • inode->i_uid?=?current_fsuid();??
  • inode->i_gid?=?current_fsgid();??
  • return?sock;??
  • ????? 繼續往下看具體的創建過程:new_inode(),在分配后,會設置i_ino和i_state的值。

    [cpp] view plain copy
  • struct?inode?*new_inode(struct?super_block?*sb)??
  • {??
  • ?……??
  • ?inode?=?alloc_inode(sb);??
  • ?if?(inode)?{??
  • ??spin_lock(&inode_lock);??
  • ??__inode_add_to_lists(sb,?NULL,?inode);??
  • ??inode->i_ino?=?++last_ino;??
  • ??inode->i_state?=?0;??
  • ??spin_unlock(&inode_lock);??
  • ?}??
  • ?return?inode;??
  • }??
  • ????? 其中的alloc_inode() -> sb->s_op->alloc_inode(),sb是sock_mnt->mnt_sb,所以alloc_inode()指向的是sockfs的操作函數sock_alloc_inode。

    [cpp] view plain copy
  • static?const?struct?super_operations?sockfs_ops?=?{??
  • ?.alloc_inode?=?sock_alloc_inode,??
  • ?.destroy_inode?=sock_destroy_inode,??
  • ?.statfs?=?simple_statfs,??
  • };??
  • ????? sock_alloc_inode()中通過kmem_cache_alloc()分配了struct socket_alloc結構體大小的空間,而struct socket_alloc結構體定義如下,但只返回了inode,實際上socket和inode都已經分配了空間,在之后就可以通過container_of取到socket。

    [cpp] view plain copy
  • static?struct?inode?*sock_alloc_inode(struct?super_block?*sb)??
  • {??
  • ?struct?socket_alloc?*ei;??
  • ?ei?=?kmem_cache_alloc(sock_inode_cachep,?GFP_KERNEL);??
  • ?…..??
  • ?return?&ei->vfs_inode;??
  • }??
  • struct?socket_alloc?{??
  • ?struct?socket?socket;??
  • ?struct?inode?vfs_inode;??
  • };??
  • ??
  • net_families[AF_INET]:??
  • static?const?struct?net_proto_family?inet_family_ops?=?{??
  • ?.family?=?PF_INET,??
  • ?.create?=?inet_create,??
  • ?.owner?=?THIS_MODULE,??
  • };??

  • err = pf->create(net, sock, protocol, kern); ==> inet_create()
    ????? 這段代碼就是從inetsw[]中取到適合的協議類型answer,sock->type就是傳入socket()函數的type參數SOCK_DGRAM,最終取得結果answer->ops==inet_stream_ops,從上面這段代碼還可以看出以下問題:
    ? ? ? socket(AF_INET, SOCK_RAW, IPPROTO_IP)這樣是不合法的,因為SOCK_RAW沒有默認的協議類型;同樣socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)與socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)是一樣的,因為TCP的默認協議類型是IPPTOTO_TCP;SOCK_STREAM與IPPROTO_UDP同上。

    [cpp] view plain copy
  • sock->state?=?SS_UNCONNECTED;??
  • list_for_each_entry_rcu(answer,?&inetsw[sock->type],?list)?{??
  • ?err?=?0;??
  • ?/*?Check?the?non-wild?match.?*/??
  • ?if?(protocol?==?answer->protocol)?{??
  • ??if?(protocol?!=?IPPROTO_IP)??
  • ???break;??
  • ?}?else?{??
  • ??/*?Check?for?the?two?wild?cases.?*/??
  • ??if?(IPPROTO_IP?==?protocol)?{??
  • ???protocol?=?answer->protocol;??
  • ???break;??
  • ??}??
  • ??if?(IPPROTO_IP?==?answer->protocol)??
  • ???break;??
  • ?}??
  • ?err?=?-EPROTONOSUPPORT;??
  • }??
  • ????? sock->ops指向inet_stream_ops,然后創建sk,sk->proto指向tcp_prot,注意這里分配的大小是struct tcp_sock,而不僅僅是struct sock大小

    [cpp] view plain copy
  • sock->ops?=?answer->ops;??
  • answer_prot?=?answer->prot;??
  • ……??
  • sk?=?sk_alloc(net,?PF_INET,?GFP_KERNEL,?answer_prot);??
  • ????? 然后設置inet的一些參數,這里直接將sk類型轉換為inet,因為在sk_alloc()中分配的是struct tcp_sock結構大小,返回的是struct sock,利用了第一個成員的特性,三者之間的關系如下圖:

    [cpp] view plain copy
  • inet?=?inet_sk(sk);??
  • ……??
  • inet->inet_id?=?0;??
  • sock_init_data(sock,?sk);??
  • ????? 其中有些設置是比較重要的,如

    [cpp] view plain copy
  • sk->sk_state?=?TCP_CLOSE;??
  • sk_set_socket(sk,?sock);??
  • sk->sk_protocol?=?protocol;??
  • sk->sk_backlog_rcv?=?sk->sk_prot->backlog_rcv;??
  • ?
    ????? 創建socket后,接下來的流程會因為客戶端或服務器的不同而有所差異,下面著重于分析建立連接的三次握手過程。典型的客戶端流程:
    connect() -> send() -> recv()
    ????? 典型的服務器流程:

    bind() -> listen() -> accept() -> recv() -> send()

    ?

    客戶端流程
    *發送SYN報文,向服務器發起tcp連接
    ????? connect(fd, servaddr, addrlen);
    ?????? -> SYSCALL_DEFINE3()?
    ???????-> sock->ops->connect() == inet_stream_connect (sock->ops即inet_stream_ops)
    ?????? -> tcp_v4_connect()
    ????? 查找到達[daddr, dport]的路由項,路由項的查找與更新與”路由表”章節所述一樣。要注意的是由于是作為客戶端調用,創建socket后調用connect,因而saddr, sport都是0,同樣在未查找路由前,要走的出接口oif也是不知道的,因此也是0。在查找完路由表后(注意不是路由緩存),可以得知出接口,但并未存儲到sk中。因此插入的路由緩存是特別要注意的:它的鍵值與實際值是不相同的,這個不同點就在于oif與saddr,鍵值是[saddr=0, sport=0, daddr, dport, oif=0],而緩存項值是[saddr, sport=0, daddr, dport, oif]。

    [cpp] view plain copy
  • tmp?=?ip_route_connect(&rt,?nexthop,?inet->inet_saddr,??
  • ??????RT_CONN_FLAGS(sk),?sk->sk_bound_dev_if,??
  • ??????IPPROTO_TCP,??
  • ??????inet->inet_sport,?usin->sin_port,?sk,?1);??
  • if?(tmp?<?0)?{??
  • ?if?(tmp?==?-ENETUNREACH)??
  • ??IP_INC_STATS_BH(sock_net(sk),?IPSTATS_MIB_OUTNOROUTES);??
  • ?return?tmp;??
  • }??
  • ????? 通過查找到的路由項,對inet進行賦值,可以看到,除了sport,都賦予了值,sport的選擇復雜點,因為它要隨機從未使用的本地端口中選擇一個。

    [cpp] view plain copy
  • if?(!inet->inet_saddr)??
  • ?inet->inet_saddr?=?rt_rt_src;???
  • inet->inet_rcv_addr?=?inet->inet_saddr;??
  • ……??
  • inet->inet_dport?=?usin->sin_port;??
  • inet->inet_daddr?=?daddr;??
  • ????? 狀態從CLOSING轉到TCP_SYN_SENT,也就是我們熟知的TCP的狀態轉移圖。

    [cpp] view plain copy
  • tcp_set_state(sk,?TCP_SYN_SENT);??
  • ????? 插入到bind鏈表中

    [cpp] view plain copy
  • err?=?inet_hash_connect(&tcp_death_row,?sk);?//==?>?__inet_hash_connect()??
  • ????? 當snum==0時,表明此時源端口沒有指定,此時會隨機選擇一個空閑端口作為此次連接的源端口。low和high分別表示可用端口的下限和上限,remaining表示可用端口的數,注意這里的可用只是指端口可以用作源端口,其中部分端口可能已經作為其它socket的端口號在使用了,所以要循環1~remaining,直到查找到空閑的源端口。

    [cpp] view plain copy
  • if?(!snum)?{??
  • ?inet_get_local_port_range(&low,?&high);??
  • ?remaining?=?(high?-?low)?+?1;??
  • ?……??
  • ?for?(i?=?1;?i?<=?remaining;?i++)?{??
  • ……//?choose?a?valid?port??
  • }??
  • }??
  • ????? 下面來看下對每個端口的檢查,即//choose a valid port部分的代碼。這里要先了解下tcp的內核表組成,udp的表內核表udptable只是一張hash表,tcp的表則稍復雜,它的名字是tcp_hashinfo,在tcp_init()中被初始化,這個數據結構定義如下(省略了不相關的數據):

    [cpp] view plain copy
  • struct?inet_hashinfo?{??
  • ?struct?inet_ehash_bucket?*ehash;??
  • ?……??
  • ?struct?inet_bind_hashbucket?*bhash;??
  • ?……??
  • ?struct?inet_listen_hashbucket??listening_hash[INET_LHTABLE_SIZE]??
  • ?????____cacheline_aligned_in_smp;??
  • };??
  • ????? 從定義可以看出,tcp表又分成了三張表ehash, bhash, listening_hash,其中ehash, listening_hash對應于socket處在TCP的ESTABLISHED, LISTEN狀態,bhash對應于socket已綁定了本地地址。三者間并不互斥,如一個socket可同時在bhash和ehash中,由于TIME_WAIT是一個比較特殊的狀態,所以ehash又分成了chain和twchain,為TIME_WAIT的socket單獨形成一張表。
    回到剛才的代碼,現在還只是建立socket連接,使用的就應該是tcp表中的bhash。首先取得內核tcp表的bind表 – bhash,查看是否已有socket占用:
    ????? 如果沒有,則調用inet_bind_bucket_create()創建一個bind表項tb,并插入到bind表中,跳轉至goto ok代碼段;
    如果有,則跳轉至goto ok代碼段。
    ????? 進入ok代碼段表明已找到合適的bind表項(無論是創建的還是查找到的),調用inet_bind_hash()賦值源端口inet_num。

    [cpp] view plain copy
  • for?(i?=?1;?i?<=?remaining;?i++)?{??
  • ?port?=?low?+?(i?+?offset)?%?remaining;??
  • ?head?=?&hinfo->bhash[inet_bhashfn(net,?port,?hinfo->bhash_size)];??
  • ?……??
  • ?inet_bind_bucket_for_each(tb,?node,?&head->chain)?{??
  • ??if?(net_eq(ib_net(tb),?net)?&&?tb->port?==?port)?{??
  • ???if?(tb->fastreuse?>=?0)??
  • ????goto?next_port;??
  • ???WARN_ON(hlist_empty(&tb->owners));??
  • ???if?(!check_established(death_row,?sk,?port,?&tw))??
  • ????goto?ok;??
  • ???goto?next_port;??
  • ??}??
  • ?}??
  • ??
  • ?tb?=?inet_bind_bucket_create(hinfo->bind_bucket_cachep,?net,?head,?port);??
  • ?……??
  • ?next_port:??
  • ??spin_unlock(&head->lock);??
  • }??
  • ??
  • ok:??
  • ?……??
  • inet_bind_hash(sk,?tb,?port);??
  • ?……??
  • ?goto?out;??
  • ????? 在獲取到合適的源端口號后,會重建路由項來進行更新:

    [cpp] view plain copy
  • err?=?ip_route_newports(&rt,?IPPROTO_TCP,?inet->inet_sport,?inet->inet_dport,?sk);??
  • ??????函數比較簡單,在獲取sport前已經查找過一次路由表,并插入了key=[saddr=0, sport=0, daddr, dport, oif=0]的路由緩存項;現在獲取到了sport,調用ip_route_output_flow()再次更新路由緩存表,它會添加key=[saddr=0, sport, daddr, dport, oif=0]的路由緩存項。這里可以看出一個策略選擇,查詢路由表->獲取sport->查詢路由表,為什么不是獲取sport->查詢路由表的原因可能是效率的問題。

    [cpp] view plain copy
  • if?(sport?!=?(*rp)->fl.fl_ip_sport?||??
  • ????dport?!=?(*rp)->fl.fl_ip_dport)?{??
  • ?struct?flowi?fl;??
  • ??
  • ?memcpy(&fl,?&(*rp)->fl,?sizeof(fl));??
  • ?fl.fl_ip_sport?=?sport;??
  • ?fl.fl_ip_dport?=?dport;??
  • ?fl.proto?=?protocol;??
  • ?ip_rt_put(*rp);??
  • ?*rp?=?NULL;??
  • ?security_sk_classify_flow(sk,?&fl);??
  • ?return?ip_route_output_flow(sock_net(sk),?rp,?&fl,?sk,?0);??
  • }??
  • ????? write_seq相當于第一次發送TCP報文的ISN,如果為0,則通過計算獲取初始值,否則延用上次的值。在獲取完源端口號,并查詢過路由表后,TCP正式發送SYN報文,注意在這之前TCP狀態已經更新成了TCP_SYN_SENT,而在函數最后才調用tcp_connect(sk)發送SYN報文,這中間是有時差的。

    [cpp] view plain copy
  • if?(!tp->write_seq)??
  • ?tp->write_seq?=?secure_tcp_sequence_number(inet->inet_saddr,??
  • ?????????inet->inet_daddr,??
  • ?????????inet->inet_sport,??
  • ?????????usin->sin_port);??
  • inet->inet_id?=?tp->write_seq?^?jiffies;??
  • err?=?tcp_connect(sk);??
  • tcp_connect() 發送SYN報文
    ????? 幾步重要的代碼如下,tcp_connect_init()中設置了tp->rcv_nxt=0,tcp_transmit_skb()負責發送報文,其中seq=tcb->seq=tp->write_seq,ack_seq=tp->rcv_nxt。

    [cpp] view plain copy
  • tcp_connect_init(sk);??
  • tp->snd_nxt?=?tp->write_seq;??
  • ……??
  • tcp_transmit_skb(sk,?buff,?1,?sk->sk_allocation);??

  • ?
    *收到服務端的SYN+ACK,發送ACK
    tcp_rcv_synsent_state_process()
    ????? 此時已接收到對方的ACK,狀態變遷到TCP_ESTABLISHED。最后發送對方SYN的ACK報文。

    [cpp] view plain copy
  • tcp_set_state(sk,?TCP_ESTABLISHED);??
  • tcp_send_ack(sk);??

  • ?
    服務端流程
    *bind() -> inet_bind()
    ????? bind操作的主要作用是將創建的socket與給定的地址相綁定,這樣創建的服務才能公開的讓外部調用。當然對于socket服務器的創建來說,這一步不是必須的,在listen()時如果沒有綁定地址,系統會選擇一個隨機可用地址作為服務器地址。
    一個socket地址分為ip和port,inet->inet_saddr賦值了傳入的ip,snum是傳入的port,對于端口,要檢查它是否已被占用,這是由sk->sk_prot->get_port()完成的(這個函數前面已經分析過,在傳入port時它檢查是否被占用;傳入port=0時它選擇未用的端口)。如果沒有被占用,inet->inet_sport被賦值port,因為是服務監聽端,不需要遠端地址,inet_daddr和inet_dport都置0。
    注意bind操作不會改變socket的狀態,仍為創建時的TCP_CLOSE。

    [cpp] view plain copy
  • snum?=?ntohs(addr->sin_port);??
  • ……??
  • inet->inet_rcv_saddr?=?inet->inet_saddr?=?addr->sin_addr.s_addr;??
  • if?(sk->sk_prot->get_port(sk,?snum))?{??
  • ?inet->inet_saddr?=?inet->inet_rcv_saddr?=?0;??
  • ?err?=?-EADDRINUSE;??
  • ?goto?out_release_sock;??
  • }??
  • ……??
  • inet->inet_sport?=?htons(inet->inet_num);??
  • inet->inet_daddr?=?0;??
  • inet->inet_dport?=?0;??
  • ?
    listen() -> inet_listen()
    ????? listen操作開始服務器的監聽,此時服務就可以接受到外部連接了。在開始監聽前,要檢查狀態是否正確,sock->state==SS_UNCONNECTED確保仍是未連接的socket,sock->type==SOCK_STREAM確保是TCP協議,old_state確保此時狀態是TCP_CLOSE或TCP_LISTEN,在其它狀態下進行listen都是錯誤的。

    [cpp] view plain copy
  • if?(sock->state?!=?SS_UNCONNECTED?||?sock->type?!=?SOCK_STREAM)??
  • ?goto?out;??
  • old_state?=?sk->sk_state;??
  • if?(!((1?<<?old_state)?&?(TCPF_CLOSE?|?TCPF_LISTEN)))??
  • ?goto?out;??
  • ????? 如果已是TCP_LISTEN態,則直接跳過,不用再執行listen了,而只是重新設置listen隊列長度sk_max_ack_backlog,改變listen隊列長也是多次執行listen的作用。如果還沒有執行listen,則還要調用inet_csk_listen_start()開始監聽。
    ? ? ? inet_csk_listen_start()變遷狀態至TCP_LISTEN,分配監聽隊列,如果之前沒有調用bind()綁定地址,則這里會分配一個隨機地址。

    [cpp] view plain copy
  • if?(old_state?!=?TCP_LISTEN)?{??
  • ?err?=?inet_csk_listen_start(sk,?backlog);??
  • ?if?(err)??
  • ??goto?out;??
  • }??
  • sk->sk_max_ack_backlog?=?backlog;??
  • ?
    accept()
    accept() -> sys_accept4() -> inet_accept() -> inet_csk_accept()
    ????? accept()實際要做的事件并不多,它的作用是返回一個已經建立連接的socket(即經過了三次握手),這個過程是異步的,accept()并不親自去處理三次握手過程,而只是監聽icsk_accept_queue隊列,當有socket經過了三次握手,它就會被加到icsk_accept_queue中,所以accept要做的就是等待隊列中插入socket,然后被喚醒并返回這個socket。而三次握手的過程完全是協議棧本身去完成的。換句話說,協議棧相當于寫者,將socket寫入隊列,accept()相當于讀者,將socket從隊列讀出。這個過程從listen就已開始,所以即使不調用accept(),客戶仍可以和服務器建立連接,但由于沒有處理,隊列很快會被占滿。

    [cpp] view plain copy
  • if?(reqsk_queue_empty(&icsk->icsk_accept_queue))?{??
  • ?long?timeo?=?sock_rcvtimeo(sk,?flags?&?O_NONBLOCK);??
  • ?……??
  • ?error?=?inet_csk_wait_for_connect(sk,?timeo);??
  • ?……??
  • }??
  • ??
  • newsk?=?reqsk_queue_get_child(&icsk->icsk_accept_queue,?sk);??
  • ????? 協議棧向隊列中加入socket的過程就是完成三次握手的過程,客戶端通過向已知的listen fd發起連接請求,對于到來的每個連接,都會創建一個新的sock,當它經歷了TCP_SYN_RCV -> TCP_ESTABLISHED后,就會被添加到icsk_accept_queue中,而監聽的socket狀態始終為TCP_LISTEN,保證連接的建立不會影響socket的接收。

    *接收客戶端發來的SYN,發送SYN+ACK
    tcp_v4_do_rcv()
    ????? tcp_v4_do_rcv()是TCP模塊接收的入口函數,客戶端發起請求的對象是listen fd,所以sk->sk_state == TCP_LISTEN,調用tcp_v4_hnd_req()來檢查是否處于半連接,只要三次握手沒有完成,這樣的連接就稱為半連接,具體而言就是收到了SYN,但還沒有收到ACK的連接,所以對于這個查找函數,如果是SYN報文,則會返回listen的socket(連接尚未創建);如果是ACK報文,則會返回SYN報文處理中插入的半連接socket。其中存儲這些半連接的數據結構是syn_table,它在listen()調用時被創建,大小由sys_ctl_max_syn_backlog和listen()傳入的隊列長度決定。
    此時是收到SYN報文,tcp_v4_hnd_req()返回的仍是sk,調用tcp_rcv_state_process()來接收SYN報文,并發送SYN+ACK報文,同時向syn_table中插入一項表明此次連接的sk。

    [cpp] view plain copy
  • if?(sk->sk_state?==?TCP_LISTEN)?{??
  • ?struct?sock?*nsk?=?tcp_v4_hnd_req(sk,?skb);??
  • ?if?(!nsk)??
  • ??goto?discard;??
  • ?if?(nsk?!=?sk)?{??
  • ??if?(tcp_child_process(sk,?nsk,?skb))?{??
  • ???rsk?=?nsk;??
  • ???goto?reset;??
  • ??}??
  • ??return?0;??
  • ?}??
  • }??
  • TCP_CHECK_TIMER(sk);??
  • if?(tcp_rcv_state_process(sk,?skb,?tcp_hdr(skb),?skb->len))?{??
  • ?rsk?=?sk;??
  • ?goto?reset;??
  • }??
  • ????? tcp_rcv_state_process()處理各個狀態上socket的情況。下面是處于TCP_LISTEN的代碼段,處于TCP_LISTEN的socket不會再向其它狀態變遷,它負責監聽,并在連接建立時創建新的socket。實際上,當收到第一個SYN報文時,會執行這段代碼,conn_request() => tcp_v4_conn_request。

    [cpp] view plain copy
  • case?TCP_LISTEN:??
  • ……??
  • ?if?(th->syn)?{??
  • ??if?(icsk->icsk_af_ops->conn_request(sk,?skb)?<?0)??
  • ???return?1;??
  • ??kfree_skb(skb);??
  • ??return?0;??
  • ?}??
  • ????? tcp_v4_conn_request()中注意兩個函數就可以了:tcp_v4_send_synack()向客戶端發送了SYN+ACK報文,inet_csk_reqsk_queue_hash_add()將sk添加到了syn_table中,填充了該客戶端相關的信息。這樣,再次收到客戶端的ACK報文時,就可以在syn_table中找到相應項了。

    [cpp] view plain copy
  • if?(tcp_v4_send_synack(sk,?dst,?req,?(struct?request_values?*)&tmp_ext)?||?want_cookie)??
  • ?goto?drop_and_free;??
  • inet_csk_reqsk_queue_hash_add(sk,?req,?TCP_TIMEOUT_INIT);??
  • ?

    *接收客戶端發來的ACK
    tcp_v4_do_rcv()
    ????? 過程與收到SYN報文相同,不同點在于syn_table中已經插入了有關該連接的條目,tcp_v4_hnd_req()會返回一個新的sock: nsk,然后會調用tcp_child_process()來進行處理。在tcp_v4_hnd_req()中會創建新的sock,下面詳細看下這個函數。

    [cpp] view plain copy
  • if?(sk->sk_state?==?TCP_LISTEN)?{??
  • ?struct?sock?*nsk?=?tcp_v4_hnd_req(sk,?skb);??
  • ?if?(!nsk)??
  • ??goto?discard;??
  • ?if?(nsk?!=?sk)?{??
  • ??if?(tcp_child_process(sk,?nsk,?skb))?{??
  • ???rsk?=?nsk;??
  • ???goto?reset;??
  • ??}??
  • ??return?0;??
  • ?}??
  • }??
  • tcp_v4_hnd_req()
    ????? 之前已經分析過,inet_csk_search_req()會在syn_table中找到req,此時進入tcp_check_req()

    [cpp] view plain copy
  • struct?request_sock?*req?=?inet_csk_search_req(sk,?&prev,?th->source,?iph->saddr,?iph->daddr);??
  • if?(req)??
  • ?return?tcp_check_req(sk,?skb,?req,?prev);??
  • tcp_check_req()
    ????? syn_recv_sock() -> tcp_v4_syn_recv_sock()會創建一個新的sock并返回,創建的sock狀態被直接設置為TCP_SYN_RECV,然后因為此時socket已經建立,將它添加到icsk_accept_queue中。
    ????? 狀態TCP_SYN_RECV的設置可能比較奇怪,按照TCP的狀態轉移圖,在服務端收到SYN報文后變遷為TCP_SYN_RECV,但看到在實現中收到ACK后才有了狀態TCP_SYN_RECV,并且馬上會變為TCP_ESTABLISHED,所以這個狀態變得無足輕重。這樣做的原因是listen和accept返回的socket是不同的,而只有真正連接建立時才會創建這個新的socket,在收到SYN報文時新的socket還沒有建立,就無從談狀態變遷了。這里同樣是一個平衡的存在,你也可以在收到SYN時創建一個新的socket,代價就是無用的socket大大增加了。

    [cpp] view plain copy
  • child?=?inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk,?skb,?req,?NULL);??
  • if?(child?==?NULL)??
  • ?goto?listen_overflow;??
  • inet_csk_reqsk_queue_unlink(sk,?req,?prev);??
  • inet_csk_reqsk_queue_removed(sk,?req);??
  • inet_csk_reqsk_queue_add(sk,?req,?child);??
  • tcp_child_process()
    ????? 如果此時sock: child被用戶進程鎖住了,那么就先添加到backlog中__sk_add_backlog(),待解鎖時再處理backlog上的sock;如果此時沒有被鎖住,則先調用tcp_rcv_state_process()進行處理,處理完后,如果child狀態到達TCP_ESTABLISHED,則表明其已就緒,調用sk_data_ready()喚醒等待在isck_accept_queue上的函數accept()。

    [cpp] view plain copy
  • if?(!sock_owned_by_user(child))?{??
  • ?ret?=?tcp_rcv_state_process(child,?skb,?tcp_hdr(skb),?skb->len);??
  • ?if?(state?==?TCP_SYN_RECV?&&?child->sk_state?!=?state)??
  • ??parent->sk_data_ready(parent,?0);??
  • }?else?{??
  • ?__sk_add_backlog(child,?skb);??
  • }??
  • ????? tcp_rcv_state_process()處理各個狀態上socket的情況。下面是處于TCP_SYN_RECV的代碼段,注意此時傳入函數的sk已經是新創建的sock了(在tcp_v4_hnd_req()中),并且狀態是TCP_SYN_RECV,而不再是listen socket,在收到ACK后,sk狀態變遷為TCP_ESTABLISHED,而在tcp_v4_hnd_req()中也已將sk插入到了icsk_accept_queue上,此時它就已經完全就緒了,回到tcp_child_process()便可執行sk_data_ready()。

    [cpp] view plain copy
  • case?TCP_SYN_RECV:??
  • ?if?(acceptable)?{??
  • ??……??
  • ??tcp_set_state(sk,?TCP_ESTABLISHED);??
  • ??sk->sk_state_change(sk);??
  • ??……??
  • ??tp->snd_una?=?TCP_SKB_CB(skb)->ack_seq;??
  • ??tp->snd_wnd?=?ntohs(th->window)?<<?tp->rx_opt.snd_wscale;??
  • ??tcp_init_wl(tp,?TCP_SKB_CB(skb)->seq);???
  • ??……??
  • }??
  • ????? 最后總結三次握手的過程


    • 本文已收錄于以下專欄:
    • Linux內核協議棧
    《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

    總結

    以上是生活随笔為你收集整理的Linux内核分析 - 网络[十六]:TCP三次握手的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。