NETDEV 協議 九

陸由表作爲三層協議的核心數據結構,理解它是至關重要的。前面已經分析過路由表,有興趣的可以參考:
      第一篇:路由表
http://blog.csdn.net/qy532846454/article/details/6423496
                分析了路由表的基本數據結構和基本操作
      第二篇:路由表使用
http://blog.csdn.net/qy532846454/article/details/6726171
                分析了路由表的基本使用

      這次將以更實際的例子來分析過程中路由表的使用情況,注意下文都是對路由緩存表的描述,因爲路由表在配置完網卡地址後就不會再改變了(除非人爲的去改動),測試環境如下圖:

      兩臺主機Host1與Host2,分別配置了IP地址192.168.1.1與192.168.1.2,兩臺主機間用網線直連。在兩臺主機上分別執行如下操作:
      1. 在Host1上ping主機Host2
      2. 在Host2上ping主機Host1
      很簡單常的兩臺主機互ping的例子,下面來分析這過程中路由表的變化,準備說是路由緩存的變化。首先,路由緩存會存在幾個條目?答案不是2條而是3條,這點很關鍵,具體可以通過/proc/net/rt_cache來查看路由緩存表,下圖是執行上述操作後得到的結果:

      brcm0.1是Host主機上的網卡設備,等同於常用的eth0,lo是環路設備。對結果稍加分析,可以發現,條目1和條目2是完全一樣的,除了計數的Use稍有差別,存在這種情況的原因是緩存表是以Hash表的形式存儲的,儘管兩者內容相同,在實際插入時使用的鍵值是不同的,下面以Host2主機的路由緩存表爲視角,針對互ping的過程進行逐一分析。

假設brcm0.1設備的index = 2
步驟0:初始時陸由緩存爲空

步驟1:主機Host1 ping 主機Host2
      Host2收到來自Host1的echo報文(dst = 192.168.1.2, src = 192.168.1.1)
      在報文進入IP層後會查詢路由表,以確定報文的接收方式,相應調用流程:
        ip_route_input() -> ip_route_input_slow()
      在ip_route_input()中查詢路由緩存,使用的鍵值是[192.168.1.2, 192.168.1.1, 2, id],由於緩存表爲空,查詢失敗,繼續走ip_route_input_slow()來創建並插入新的緩存項。

hash = rt_hash(daddr, saddr, iif, rt_genid(net));

      在ip_route_input_slow()中查詢路由表,因爲發往本機,在會LOCAL表中匹配192.168.1.2條目,查詢結果res.type==RTN_LOCAL。

if ((err = fib_lookup(net, &fl, &res)) != 0) {
 if (!IN_DEV_FORWARD(in_dev))
  goto e_hostunreach;
 goto no_route;
}

      然後根據res.type跳轉到local_input代碼段,創建新的路由緩存項,並插入陸由緩存。

rth = dst_alloc(&ipv4_dst_ops);
……
rth->u.dst.dev = net->loopback_dev;
rth->rt_dst = daddr;
rth->rt_src = saddr;
rth->rt_gateway = daddr;
rth->rt_spec_dst = spec_dst; (spec_dst=daddr)
……
hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net));
err = rt_intern_hash(hash, rth, NULL, skb, fl.iif);

      因此插入的第一條緩存信息如下:
        Key = [dst = 192.168.1.2  src = 192.168.1.1 idx = 2 id = id]
        Value = [Iface = lo dst = 192.168.1.2 src = 192.168.1.1 idx = 2 id = id ……]

步驟2:主機Host2 發送echo reply報文給主機 Host1 (dst = 192.168.1.1 src = 192.168.1.2)
      步驟2是緊接着步驟1的,Host2在收到echo報文後會立即回覆echo reply報文,相應調用流程:
      icmp_reply() -> ip_route_output_key() -> ip_route_output_flow() -> __ip_route_output_key() -> ip_route_output_slow() -> ip_mkroute_output() -> __mkroute_output()
      在icmp_reply()中生成稍後路由查找中的關鍵數據flowi,可以看作查找的鍵值,由於是回覆已收到的報文,因此目的與源IP地址者是已知的,下面結構中daddr=192.168.1.1,saddr=192.168.1.2。

struct flowi fl = { .nl_u = { .ip4_u =
  { .daddr = daddr,
  .saddr = rt->rt_spec_dst,
  .tos = RT_TOS(ip_hdr(skb)->tos) } },
  .proto = IPPROTO_ICMP };

      在__ip_route_output_key()時會查詢路由緩存表,查詢的鍵值是[192.168.1.1, 192.168.1.2, 0, id],由於此時路由緩存中只有一條剛剛插入的從192.168.1.1->192.168.1.2的緩存項,因而查詢失敗,繼續走ip_route_output_slow()來創建並插入新的緩存項。

hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp->oif, rt_genid(net));

      在ip_route_input_slow()中查詢路由表,因爲在同一網段,在會MAIN表中匹配192.168.1.0/24條目,查詢結果res.type==RTN_UNICAST。

if (fib_lookup(net, &fl, &res)) {
…..
}

      然後調用__mkroute_output()來生成新的路由緩存,信息如下:

rth->u.dst.dev = dev_out;
rth->rt_dst = fl->fl4_dst;
rth->rt_src = fl->fl4_src;
rth->rt_gateway = fl->fl4_dst;
rth->rt_spec_dst= fl->fl4_src;
rth->fl.oif = oldflp->oif; (oldflp->oif爲0)

      插入路由緩存表時使用的鍵值是:

hash = rt_hash(oldflp->fl4_dst, oldflp->fl4_src, oldflp->oif, rt_genid(dev_net(dev_out)));

      這條語句很關鍵,緩存的存儲形式是hash表,除了生成緩存信息外,還要有相應的鍵值,這句的hash就是產生的鍵值,可以看到,它是由(dst, src, oif, id)四元組生成的,dst和src很好理解,id對於net來說是定值,oif則是關鍵,注意這裏用的是oldflp->oif(它的值爲0),儘管路由緩存對應的出接口設備是dev_out。所以,第二條緩存信息的如下:
        Key = [dst = 192.168.1.1  src = 192.168.1.2 idx = 0 id = id]
        Value = [Iface = brcm0.1  dst = 192.168.1.1 src = 192.168.1.2 idx = 2 id = id ……]

步驟3:主機Host2 ping 主機Host1
      Host2向Host1發送echo報文(dst = 192.168.1.1, src = 192.168.1.2)
      Host2主動發送echo報文,使用SOCK_RAW與IPPROTO_ICMP組合的套接字,相應調用流程:
      raw_sendmsg() -> ip_route_output_flow() -> __ip_route_output_key() -> ip_route_output_slow() -> ip_mkroute_output() -> __mkroute_output()
在raw_sendmsg()中生成稍後路由查找中的關鍵數據flowi,可以看作查找的鍵值,由於是主動發送的報文,源IP地址者還是未知的,因爲主機可能是多接口的,在查詢完路由表後才能得到要走的設備接口和相應的源IP地址。下面結構中daddr=192.168.1.1,saddr=0。

struct flowi fl = { .oif = ipc.oif,
  .mark = sk->sk_mark,
  .nl_u = { .ip4_u =
    { .daddr = daddr,
   .saddr = saddr,
   .tos = tos } },
  .proto = inet->hdrincl ? IPPROTO_RAW :
        sk->sk_protocol,
 };

      在__ip_route_output_key()時會查詢路由緩存表,查詢的鍵值是[192.168.1.1, 0, 0, id],儘管此時路由緩存中剛剛插入了192.168.1.2->192.168.1.1的條目,但由於兩者的鍵值不同,因而查詢依舊失敗,繼續走ip_route_output_slow()來創建並插入新的緩存項。

hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp->oif, rt_genid(net));

         與Host2回覆Host1的echo報文相比,除了進入函數不同(前者爲icmp_reply,後者爲raw_sendmsg),後續調用流程是完全相同的,導致最終路由緩存不同(準確說是鍵值)是因爲初始時flowi不同。
      此處,raw_sendmsg()中,flowi的初始值:dst = 192.168.1.1, src = 0, oif = 0
      對比icmp_reply()中,flowi的初始值:dst = 192.168.1.1, src = 192.168.1.2, oif = 0
      在上述調用流程中,在__ip_route_output_key()中查找路由緩存,儘管此時路由緩存有從192.168.1.2到192.168.1.1的緩存項,但它的鍵值與此次查找的鍵值[192.168.1.1, 192.168.1.2, 0],從下表可以明顯看出:

      由於查找失敗,生成新的路由緩存項並插入路由緩存表,注意在ip_route_output_slow()中查找完路由表後,設置了緩存的src。

if (!fl.fl4_src)
 fl.fl4_src = FIB_RES_PREFSRC(res);

      因此插入的第三條緩存信息如下,它與第二條緩存完成相同,區別在於鍵值不同:
        Key = [dst = 192.168.1.1  src = 0 idx = 0 id = id]
        Value = [Iface = brcm0.1  dst = 192.168.1.1 src = 192.168.1.2 idx = 2 id = id ……]

      最終,路由緩存表如下:

      第三條緩存條目鍵值使用src=0, idx=0的原因是當主機要發送報文給192.168.1.1的主機時,直到IP層路由查詢前,它都無法知道該使用的接口地址(如果沒有綁定的話),而路由緩存的查找發生在路由查詢之前,所以src=0,idx=0才能保證後續報文使用該條目。

 

  TCP是應用最廣泛的傳輸層協議,其提供了面向連接的、可靠的字節流服務,但也正是因爲這些特性,使得TCP較之UDP異常複雜,還是分兩部分[創建與使用]來進行分析。這篇主要包括TCP的創建及三次握手的過程。

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

socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)

      由此開始分析,調用接口[net/socket.c]: SYSCALL_DEFINE3(socket)
      其中執行兩步關鍵操作:sock_create()與sock_map_fd()

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。

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。

inode = new_inode(sock_mnt->mnt_sb);
sock = SOCKET_I(inode);

      設置inode的參數,並返回sock。

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的值。

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。

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。

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同上。

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大小

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,利用了第一個成員的特性,三者之間的關係如下圖:

inet = inet_sk(sk);
……
inet->inet_id = 0;
sock_init_data(sock, sk);

      其中有些設置是比較重要的,如

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]。

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的選擇複雜點,因爲它要隨機從未使用的本地端口中選擇一個。

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的狀態轉移圖。

tcp_set_state(sk, TCP_SYN_SENT);

      插入到bind鏈表中

err = inet_hash_connect(&tcp_death_row, sk); //== > __inet_hash_connect()

      當snum==0時,表明此時源端口沒有指定,此時會隨機選擇一個空閒端口作爲此次連接的源端口。low和high分別表示可用端口的下限和上限,remaining表示可用端口的數,注意這裏的可用只是指端口可以用作源端口,其中部分端口可能已經作爲其它socket的端口號在使用了,所以要循環1~remaining,直到查找到空閒的源端口。

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()中被初始化,這個數據結構定義如下(省略了不相關的數據):

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。

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;

      在獲取到合適的源端口號後,會重建路由項來進行更新:

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->查詢路由表的原因可能是效率的問題。

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報文,這中間是有時差的。

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。

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報文。

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。

 
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都是錯誤的。

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()綁定地址,則這裏會分配一個隨機地址。

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(),客戶仍可以和服務器建立連接,但由於沒有處理,隊列很快會被佔滿。

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。

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。

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中找到相應項了。

 
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,下面詳細看下這個函數。

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()

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大大增加了。

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()。

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()。

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); 
  ……
}

      最後總結三次握手的過程


發佈了0 篇原創文章 · 獲贊 7 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章