天天看點

Linux 核心網絡之 傳輸層接收消息(一)

作者:Linux碼農

在傳輸層發送消息時,TCP 發送出去消息後,會跟蹤這些資料包,直到得到對方的确認為止。對于接收方來講,當收到一個封包段時,其會根據情況把這些資料包添加到接收隊列或者 prequeue 隊列或者後備隊列中。

在TCP傳輸控制塊中存在三個隊列:接收隊列、prequeue隊列和後備隊列。

當系統啟動tcp_low_latency時,TCP 傳輸控制塊在軟中斷中接收并處理tcp段,然後将其插入到接收隊列中。等待使用者程序從接收隊列中擷取TCP段後複制到使用者空間,然後删除并釋放該段。

當不啟用 tcp_low_latency 時能夠提高TCP/IP協定棧的吞吐量以及反應速度(因為啟用時在軟中斷中處理tcp段會導緻軟中斷執行過長),TCP 傳輸控制塊在軟中斷中将 TCP 段添加到 prequeue 隊列中,然後立即處理 prequeue 中的段,若使用者正在讀取資料,則可以直接複制資料到使用者緩沖區中,否則添加到接收隊列中,然後從軟中斷中傳回。在多數情況下有機會處理 prequeue 隊列中的段,但隻有當使用者程序在進行 recv 類系統調用傳回前,才在軟中斷中複制資料到使用者空間。

當使用者程序因操作傳輸控制塊将其鎖定時,無論是否啟用 tcp_low_latency , 都會将未處理的 TCP 段添加到後備隊列中,一旦使用者解鎖後,就會立即處理後備隊列,将 TCP 段處理後添加到接收隊列中。

TCP 的接收隊列為 sock 結構中的 sk_receive_queue。通常情況下,接收的TCP 段都會在緩存在這裡,等待使用者程序主動讀取。

Linux 核心網絡之 傳輸層接收消息(一)

該連結清單用來儲存已接收待複制資料到使用者空間的 SKB。

在 TCP 傳輸控制塊中還有一個成員是用來描述使用者空間的,那就是 ucopy。在未啟用 tcp_low_latency時,若資料可以從核心空間直接到拷貝到使用者空間,則需從該字段擷取有關使用者空間的資訊。

當接收方将資料從核心空間複制到使用者空間後,需更新接收視窗,删除并釋放已讀取的段,以便有空間接收新資料到緩存中。

在 TCP 接收消息時,調用過程如下:

Linux 核心網絡之 傳輸層接收消息(一)

從圖中可以看到,TCP 接收時涉及的3個隊列。

從接收到的角度來看,TCP控制塊處于如下幾種狀态

1、使用者程序正在讀寫資料,傳輸控制塊被鎖定。

2、使用者程序正在讀寫資料,但由于沒有資料可以讀取而處于休眠狀态,此時傳輸控制塊不會被使用者程序鎖定。

3、使用者程序壓根沒有進行讀寫資料,傳輸控制塊也不會被使用者程序鎖定。

在協定棧中,對資料包的處理都是在軟中斷中進行處理了,出于性能的考慮,一般都希望軟中斷盡快結束。是以軟中斷會有如下處理:

  • 當傳輸控制塊被使用者程序鎖定時,軟中斷會把資料包加入到 backlog 後備隊列中,以便快速結束軟中斷。這類資料包的真正處理是在使用者程序釋放 TCB 時進行的;
  • 當傳輸控制塊沒有被使用者程序鎖定時,為了盡快結束軟中斷,首先會嘗試把資料包加入到 prequeue 隊列。這種資料包的處理是在使用者程序讀資料過程中處理的;
  • 當傳輸控制塊沒有被使用者程序鎖定時, prequeue 隊列也沒有接受該資料包(出于性能考慮,比如 prequeue 隊列不能無限制增大),那就隻能必須在軟中斷中對資料包進行處理,處理完畢後将資料包加入到 receive 隊列中。

在協定棧中,放入 receive 隊列的資料包都是已經被 TCP 處理過的資料包,比如校驗、回 ACK 等動作都已經完成了,這些資料包等待使用者空間程式讀即可;而放入 backlog 隊列和 prequeue 隊列的資料包都還需要 TCP 處理,實際上,這些資料包也都是在合适的時機通過 tcp_v4_do_rcv( )處理的;

int tcp_v4_rcv(struct sk_buff *skb)
{
struct tcphdr *th;
struct sock *sk;
int ret;
//若段不是發送到本地的,則直接丢棄
if (skb->pkt_type != PACKET_HOST)
goto discard_it;

/* Count it even if it's bad */
TCP_INC_STATS_BH(TCP_MIB_INSEGS);
/*若tcp段在傳輸過程中被分片了,則到達本地後會在ip層重新組裝,
組裝完成後,封包分片都存儲在分片連結清單中。在此需把存儲在分片的封包
複制到SKB的線性存儲區中。若發生異常,丢棄該封包*/
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;

th = skb->h.th;
/*若tcp首部中首部長度字段的值小于不帶首部的tpc首部長度,
說明tcp資料異常,統計相關資訊後丢棄
*/
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
// 檢測整個tcp段長度和tcp首部長度是否正常,若有異常,丢棄
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;

/*校驗tcp首部中的校驗和,若校驗和有誤,說明封包已損壞,統計相關資訊後丢棄*/ 
if ((skb->ip_summed != CHECKSUM_UNNECESSARY &&
tcp_v4_checksum_init(skb)))
goto bad_packet;

/*根據tcp首部中的資訊來設定tcp控制塊中的值,因為tcp首部中的值都是
網絡位元組序的,為了便于後續處理直接通路tcp首部字段,
在此将這些值轉換為本機序後存儲在tcp私有控制塊中*/
th = skb->h.th;
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->when = 0;
TCP_SKB_CB(skb)->flags = skb->nh.iph->tos;
TCP_SKB_CB(skb)->sacked = 0;

/*從ehash或bhash中根據位址和端口查找傳輸控制塊。
若在ehash中找到,表示3次握手後已建立起了連接配接,可以正常通信。
若在bhash中找到,表示已經綁定了端口,處于監聽狀态。
若都找不到,說明對應的傳輸控制塊還沒有建立,跳轉*/
sk = __inet_lookup(&tcp_hashinfo, skb->nh.iph->saddr, th->source,
skb->nh.iph->daddr, th->dest,
inet_iif(skb));

if (!sk)
goto no_tcp_socket;

process:
//若處于TCP_TIME_WAIT,跳轉
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
//查找IPSec政策庫
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;

//初始化SKB中與netfilter有關的成員
nf_reset(skb);

/*若在傳輸控制塊中安裝了過濾器,則是有符合過濾規則的
封包才能放行,不符合的丢棄*/
if (sk_filter(sk, skb))
goto discard_and_relse;

//接收到的包已到傳輸層,此時dev已不再有意義
skb->dev = NULL;
//在接收tcp段之前,需對傳輸控制塊加鎖,已同步對傳輸控制塊接收隊列的通路
bh_lock_sock_nested(sk);
ret = 0;

//若此時程序沒有通路傳輸控制塊,則可以調用tcp_v4_do_rcv進行接收
//沒有使用者程序在讀取套接字,sock_owned_by_user(sk)會傳回0
if (!sock_owned_by_user(sk)) { 
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb); 
} else
//否則将封包添加到sk_backlog隊列中,使用者程序解鎖傳輸控制塊後再處理
sk_add_backlog(sk, skb);


bh_unlock_sock(sk);

sock_put(sk);

return ret;

//處理沒有建立傳輸控制塊收到的封包情況,通常給對方發送RST段
no_tcp_socket:
//查找IPSec政策資料庫,查找失敗跳轉
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;

/* 檢測封包的長度和校驗和,若有異常說明封包已損壞,
統計後丢棄,否則給對端發送rst段後丢棄*/
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
TCP_INC_STATS_BH(TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb);
}

// 丢棄資料包
discard_it:
/* Discard frame. */
kfree_skb(skb);
return 0;

discard_and_relse:
sock_put(sk);
goto discard_it;

//處理傳輸層控制塊處于TIME_WAIT狀态的情況
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}

if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
TCP_INC_STATS_BH(TCP_MIB_INERRS);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(&tcp_hashinfo,
skb->nh.iph->daddr,
th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
goto no_tcp_socket;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
           

上述函數完成如下功能

  • 先完成對 TCP 段進行簡單點的校驗;
  • 根據源位址、源端口、目的位址和目的端口查找所屬的傳輸控制塊。
  • 判斷傳輸控制塊有沒有被使用者程序鎖定;
  • 若被使用者程序鎖定,則把資料包存放到後備隊列中;
  • 若未被程序鎖定,則判斷是否需要加入到 prequeue 隊列中;
  • 若加入到了 prequeue 隊列中,則結束軟中斷;
  • 否則在軟中斷種進行處理資料包,然後加入到接收隊列中。

判斷資料包是否需要加入到 prequeue 隊列中。

/*
在未啟用tcp_low_latency情況下,若使用者程序正在讀取資料,則将接收到的tcp段
直接加入到prequeue中,隻有當prequeue隊列消耗的記憶體大于接收緩沖區時,
才會立即處理prequeue中的段,才能在軟中斷中複制資料到使用者空間中
*/
static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
/*必須在未啟用tcp_low_latency,且使用者程序正在讀取資料的情況下,
才能将接收到的段添加到prequeue隊列中*/
if (!sysctl_tcp_low_latency && tp->ucopy.task) { //task 不為空表示有程序在等待資料的到來
//将段加入到prequeue隊列中,更新prequeue消耗的記憶體
__skb_queue_tail(&tp->ucopy.prequeue, skb);
tp->ucopy.memory += skb->truesize;

//若prequeue隊列消耗的記憶體超過接收緩存上限,立即處理隊列上的段
if (tp->ucopy.memory > sk->sk_rcvbuf) {
struct sk_buff *skb1;

BUG_ON(sock_owned_by_user(sk));
//立即處理prequeue隊列上的段
while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
sk->sk_backlog_rcv(sk, skb1); //tcp_v4_do_rcv()
NET_INC_STATS_BH(LINUX_MIB_TCPPREQUEUEDROPPED);
}

tp->ucopy.memory = 0;
//若未超限,則隊列中隻有一個skb,且不需要發送ack,則複位重新啟動延遲确認定時器
} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
wake_up_interruptible(sk->sk_sleep);
if (!inet_csk_ack_scheduled(sk))
////如果應用程式一直不處理,則在tcp_delack_timer中處理prequeue隊列
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
(3 * TCP_RTO_MIN) / 4,
TCP_RTO_MAX);
}
//傳回非0 表示已添加prequeue隊列中或已經被處理
return 1;
}

//傳回0表示段未處理,需繼續接收處理
return 0;
}

           

若未啟用 tcp_low_latency , TCP 段将首先緩存到此隊列,直到程序主動讀取時才真正的接收到接收隊列中,這裡為什麼要有prequeue 呢,直接放到receive_queue不就好了.因為receive_queue的處理比較繁瑣(看 tcp_rcv_established 的實作就知道了,分為 slow path 和 fast path ),而軟中斷每次隻能處理一個資料包 (在一個 cpu 上),是以為了軟中斷能盡快完成,我們就可以先将資料放到 prequeue 中( tcp_prequeue ),然後軟中斷就直接傳回. 而處理 prequeue 就放到程序上下文( tcp_recvmsg 調用中)去處理了。

若資料段未被加入到 prequeue 隊列中,則資料的接收在軟中斷中進行處理,是以繼續調用 tcp_v4_do_rcv 函數進行繼續處理。

/*TCP傳輸層接收到段之後,經過簡單的校驗,并确定接收處理該段的傳輸控制塊之後,
除非處于FIN_WAIT_2 或 TIME_WAIT狀态,否則都會調用tcp_v4_do_rcv做具體處理*/
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;

//若狀态為TCP_ESTABLISHED,調用tcp_rcv_established接收處理
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
TCP_CHECK_TIMER(sk);
if (tcp_rcv_established(sk, skb, skb->h.th, skb->len)) {
rsk = sk;
goto reset;
}
TCP_CHECK_TIMER(sk);
return 0;
}

//校驗封包的長度和校驗碼,若失敗則跳轉csum_err處統計後丢棄
if (skb->len < (skb->h.th->doff << 2) || tcp_checksum_complete(skb))
goto csum_err;

//伺服器收到第一步握手 SYN 或者第三步 ACK 都會走到這裡
if (sk->sk_state == TCP_LISTEN) {
//從半連接配接表syn_table中取出節點
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);

/*tcp_rcv_state_process 處理TCP_LISTEN,TCP_SYN_RECV、
TCP_SYN_SENT、TCP_FIN_WAIT1、TCP_FIN_WAIT2、TCP_LAST_ACK 、TCP_CLOSING
狀态的傳輸控制塊。若接收過程中出錯,則跳轉,給對端發送rst段後丢棄*/
if (tcp_rcv_state_process(sk, skb, skb->h.th, skb->len)) {
rsk = sk;
goto reset;
}
TCP_CHECK_TIMER(sk);
return 0;

//給對端發送rst段,丢棄異常封包,傳回
reset:
tcp_v4_send_reset(rsk, skb);

//丢棄異常封包,傳回
discard:
kfree_skb(skb);

return 0;

//進行TCP_MIB_INERRS統計後,丢棄異常封包,傳回
csum_err:
TCP_INC_STATS_BH(TCP_MIB_INERRS);
goto discard;
}
           

若是鍊路已建立成連接配接,則調用 tcp_rcv_established 接收處理。

/*
tcp_rcv_established 對tcp的處理提供了兩種路徑:
1、快速路徑: 用于處理預期、理想情況下的輸入段。在正常情況下,tcp連接配接最常見的情形應該
被盡可能檢測并最優化處理,而無需去檢測一些邊緣的情形。

2、慢速路徑: 用于所有和預期、理想不對應的且需要進行進一步處理的段。如接收到的段存在除
時間戳選項之外選項的段

*/ 
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
struct tcp_sock *tp = tcp_sk(sk);
tp->rx_opt.saw_tstamp = 0;

/*tcp_flag_word 首先擷取TCP首部中第4個32位字,然後和TCP_HP_BITS做與操作,
屏蔽保留的位和PSH,最後和預測标志比較,若通過則還需要進行首部預測的其他比較
,否則直接執行慢速路徑*/
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
/*TCP_SKB_CB(skb)->seq是本次接收到的tcp段的序号,
tp->rcv_nxt是等待接收的下一個段的序号。
當順利完成預測标志的比較之後,将這兩者做比較,
若相等則進行是首部預測的其他比較,否則執行慢速路徑*/
TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
int tcp_header_len = tp->tcp_header_len;

/* Check timestamp 
通過tcp首部長度來檢測tcp首部中是否僅存在時間戳選項
*/
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
__be32 *ptr = (__be32 *)(th + 1);

/* No? Slow path! 
還需要對類型及長度進行檢測,以保證是一個正常的時間戳選項,
若檢測不過,那就隻能執行慢速路徑
*/
if (*ptr != htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16)
| (TCPOPT_TIMESTAMP << 8) | TCPOLEN_TIMESTAMP))
goto slow_path;

/*從時間戳選項中擷取時間戳,然後将擷取到的值與下一個發送的tcp段的時間戳回顯值相比,
做PAW檢測,若前者比後者小,則說明接收到tcp段的序号雖然時預期的,
但時間戳過早,以發生了序号回卷,需執行慢速路徑處理*/
tp->rx_opt.saw_tstamp = 1;
++ptr; 
tp->rx_opt.rcv_tsval = ntohl(*ptr);
++ptr;
tp->rx_opt.rcv_tsecr = ntohl(*ptr);

/* If PAWS failed, check it more carefully in slow path 
ts_recent記錄上一次接收到對端發送的包的發送時間戳
rcv_tsval 本次包對端發送包時發送的時間戳
rcv_tsval - ts_recent 小于0,說明本次收到的包為發送方很久以前發送的舊包。
正常情況下,接收的新包發送時間戳時累計的,時間戳一定比ts_recent大
*/
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;

}

if (len <= tcp_header_len) {
/* Bulk data transfer: sender */
//說明沒有負荷
if (len == tcp_header_len) {
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/
/*若tcp首部中存在時間戳選項,且接收到的段都已确認,則儲存時間戳值,
用于發送下一個段的時間戳回顯*/
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
//更新時間戳,ts_recent 設定為最新的值rcv_tsval, 以便後續進行PAWS檢測
tcp_store_ts_recent(tp); 

/* We know that such packets are checksummed
* on entry.
對ack進行适當處理,如更新視窗,釋放已确認的段,處理完後釋放該ack段
*/
tcp_ack(sk, skb, 0);
__kfree_skb(skb); 

//檢測是否有資料、有必要發送,若都有則給對方發送資料,
//同時檢測是否有必要增加發送緩沖區大小
tcp_data_snd_check(sk, tp);
return 0;
} else { 
/* Header too small 
接收的tcp首部比預期的小,說明tcp段有異常,更新統計資訊後直接丢棄
*/
TCP_INC_STATS_BH(TCP_MIB_INERRS);
goto discard;
}
} else {
/*通過了首部預測,說明接收到的段正是預期的段,開始處理負荷部分的資料。
tcp接收資料的最終目的就是把資料複制給使用者程序,若可以會盡量把資料複制到使用者空間,
實在不行才把資料緩存下來,等待使用者程序的讀取。
*/

int eaten = 0;
int copied_early = 0;

/*
判斷正在接收的段是否有直接複制到使用者空間的條件:
條件1: 正在接收的段的序号是否與尚未從核心空間複制到
使用者空間的段最前面的序号相等,即接收隊列應該是空的
2: tcp段中的使用者資料長度小于使用者空間緩存的剩餘可使用量
以上2個條件,隻能構成了直接向使用者空間複制資料的必要條件,還需要一些其他的條件。 
*/
if (tp->copied_seq == tp->rcv_nxt &&
len - tcp_header_len <= tp->ucopy.len) {

/* 隻有當使用者程序正在調用recv等系統調用從核心空間讀取資料(使用者程序正在睡眠),
且傳輸層被使用者程序鎖定,啟用I/O 加速以網絡DMA方式複制資料失敗時,
才可以直接将資料複制到使用者空間*/
if (tp->ucopy.task == current && sock_owned_by_user(sk) && !copied_early) {
__set_current_state(TASK_RUNNING);

if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))
eaten = 1;
}
// 若複制資料到使用者空間成功,則跟新時間戳、往返時間及等待接收的下一個tcp段的序号
if (eaten) {
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/
if (tcp_header_len ==
(sizeof(struct tcphdr) +
TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);

tcp_rcv_rtt_measure_ts(sk, skb);

__skb_pull(skb, tcp_header_len);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
NET_INC_STATS_BH(LINUX_MIB_TCPHPHITSTOUSER);
}

/*老版本中的tcp_cleanup_rbuf主要用來清理接收緩存,而現在的功能與函數名稱已經不符合了。
--若有必要立即發送ACK段,則發送ack段。在此,若通過 I/O加速直接将資料複制到使用者空間,
則需确定是否發送ack段。*/
if (copied_early)
tcp_cleanup_rbuf(sk, skb->len);
}
/*若沒有将資料直接複制到使用者空間,或者資料複制到使用者空間時操作失敗,
則對資料進行校驗和檢測*/
if (!eaten) {
if (tcp_checksum_complete_user(sk, skb))
goto csum_error;


//接收到的段都已确認,更新時間戳
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);

tcp_rcv_rtt_measure_ts(sk, skb);

/*若整個skb緩沖區的總長度超出預配置設定的長度,則執行慢速路徑*/
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;

NET_INC_STATS_BH(LINUX_MIB_TCPHPHITS);

/* Bulk data transfer: receiver 
删除skb中tcp的首部,然後将資料包添加到接收隊列中緩存起來,等待程序主動讀取。
設定該skb的宿主,釋放回調函數,更新傳輸控制塊的已使用接收緩存總量及預配置設定
緩存長度,此時該套接口已屬于目前傳輸控制塊了
*/
__skb_pull(skb,tcp_header_len);
__skb_queue_tail(&sk->sk_receive_queue, skb);
sk_stream_set_owner_r(skb, sk);
//擷取預期下一個接收的tcp段的序号
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
}

//更新延時确認控制塊,同時根據條件進行快速确認或延遲确認操作
tcp_event_data_recv(sk, tp, skb);

/*若接收到段的ack序号不等于最早未确認段的序号,則調用tcp_ack處理ack,
然後輸出發送隊列中的段。*/
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk, tp);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}

//根據條件做快速确認或延遲确認操作
__tcp_ack_snd_check(sk, 0);
no_ack:

// 若資料已複制到使用者空間,則釋放該skb
if (eaten)
__kfree_skb(skb);
else //否則,說明資料已準備,喚醒等待隊列fasync_list上的程序,通知他們讀取資料
sk->sk_data_ready(sk, 0);
return 0;
}
}



/*若沒有滿足快速路徑條件,也即沒有通過tcp首部預測檢驗,
則資料包就會在慢速路徑上處理。
需要全面校驗,事實上在正常情況下隻有一小部分資料包才會執行慢速路徑*/
slow_path:
//檢查長度和校驗和
if (len < (th->doff<<2) || tcp_checksum_complete_user(sk, skb))
goto csum_error;

/*
* RFC1323: H1. Apply PAWS check first.
*/
/*解析tcp選項,并檢查時間戳選項。*/ 
if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&
tcp_paws_discard(sk, skb)) { //若存在時間戳且PAWS檢驗失敗
if (!th->rst) { //并且不存在 RST 标志則還需發送 DACK 給對端,說明接收到的TCP段已确認過,然後丢棄該資料包
NET_INC_STATS_BH(LINUX_MIB_PAWSESTABREJECTED);
tcp_send_dupack(sk, skb);
goto discard;
}

}

/*
* Standard slow path.
*/
/*檢測接收包序号是否在接收視窗内,若不是*/
if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {

/*若不是複位段,則還需發送dack給對端,說明接收的段不在接收視窗内*/ 
if (!th->rst)
tcp_send_dupack(sk, skb);

goto discard;
}
//封包在視窗内,接收的段是複位段,則處理複位後丢棄該段
if(th->rst) {
tcp_reset(sk);
goto discard;
}

//若tcp首部中存在時間戳選項且有效,則儲存該時間戳
tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq);
/*已建立連接配接的tcp收到syn段,說明對端發送了錯誤資訊,調用tcp_reset做複位處理*/
if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
TCP_INC_STATS_BH(TCP_MIB_INERRS);
NET_INC_STATS_BH(LINUX_MIB_TCPABORTONSYN);
tcp_reset(sk);
return 1;
}

step5:
/*首部中設定了ack标志,則調用tcp_ack進行處理*/
if(th->ack)
tcp_ack(sk, skb, FLAG_SLOWPATH);
//采樣、更新接收方的RTT
tcp_rcv_rtt_measure_ts(sk, skb);

/* Process urgent data. */
//若首部中設定了URG 标志,就會處理帶外資料
tcp_urg(sk, skb, th);

/* step 7: process the segment text 
tcp段中的負荷部分由tcp_data_queue處理,包括對接收緩沖區是否有足夠空間的檢查,
以及将SKB插入到接收隊列或亂序隊列中等
*/
tcp_data_queue(sk, skb);
//檢查是否有資料需要發送
tcp_data_snd_check(sk, tp);
//檢查是否有ACK需要發送(快速确認或延遲确認)
tcp_ack_snd_check(sk);
return 0;

csum_error:
TCP_INC_STATS_BH(TCP_MIB_INERRS);

discard:
__kfree_skb(skb);
return 0;
}
           

tcp_rcv_established 對 tcp 的處理提供了兩種路徑:

快速路徑: 用于處理預期、理想情況下的輸入段。是以可以直接緩存到接收隊列中或直接複制到使用者空間。

慢速路徑: 慢速路徑處理的段包括預期的(可以直接緩存到接收隊列中或直接複制到使用者空間的資料)、亂序的(序号在接收視窗内,但不是預期的)、接收視窗之外的資料。

從代碼中也可以看到對于已建立連接配接的 tcp 收到 syn 段,說明對端發送了錯誤資訊,調用 tcp_reset 做複位處理。

慢速路徑封包處理流程

對資料報進行合法校驗後,調用 tcp_data_queue 對資料包進行處理。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcphdr *th = skb->h.th;
struct tcp_sock *tp = tcp_sk(sk);
int eaten = -1;
// 若tcp段沒有負荷,則丢棄
if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq)
goto drop;

//丢棄tcp首部
__skb_pull(skb, th->doff*4);

//處理cwr标志
TCP_ECN_accept_cwr(tp, skb);
/*若dsack置位,說明上次發送的段中存在sack選項及D-SACK,是以現在把dsack複位*/
if (tp->rx_opt.dsack) {
tp->rx_opt.dsack = 0;
tp->rx_opt.eff_sacks = min_t(unsigned int, tp->rx_opt.num_sacks,
4 - tp->rx_opt.tstamp_ok);
}

/* Queue data for delivery to the user.
* Packets in sequence go to the receive queue.
* Out of sequence packets to the out_of_order_queue.
*/
// 開始處理資料

/*收到的段序号和期望的一樣,說明預期的段,可以緩存到接收隊列或直接複制到使用者空間*/
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
//接收視窗為0,目前不能接收資料,跳轉去給對方發送ack,讓對方知道接收方視窗大小為0,然後丢棄該段
if (tcp_receive_window(tp) == 0)
goto out_of_window;

/* Ok. In sequence. In window. 
判斷是否可以直接複制到使用者空間

*/
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
sock_owned_by_user(sk) && !tp->urg_data) {
//擷取可以複制到使用者空間的資料長度
int chunk = min_t(unsigned int, skb->len,
tp->ucopy.len);

//喚醒目前讀取套接口的程序,當系統完成本次中斷處理後,該程序又可以得到運作機會
__set_current_state(TASK_RUNNING);

/*将資料複制到使用者空間,若成功,則更新使用者空間剩餘的可用緩存長度、複制到使用者空間
的序号等*/
local_bh_enable();
if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) {
tp->ucopy.len -= chunk;
tp->copied_seq += chunk;
eaten = (chunk == skb->len && !th->fin);

//看是否需要更新接收緩存和接收視窗
tcp_rcv_space_adjust(sk);
}
local_bh_disable();
}

//若沒有複制到使用者空間,則需更新緩存到接收隊列中

if (eaten <= 0) {
queue_and_out:
//若接收緩存大小不夠,則丢棄
if (eaten < 0 &&
(atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
!sk_stream_rmem_schedule(sk, skb))) {
if (tcp_prune_queue(sk) < 0 ||
!sk_stream_rmem_schedule(sk, skb))
goto drop;
}
//設定skb的宿主,并添加到接收隊列尾
sk_stream_set_owner_r(skb, sk);
__skb_queue_tail(&sk->sk_receive_queue, skb);
}
//更新下一個預期接收段的序号
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
//若接收的段存在資料,則處理一些資料接收的相關操作。主要是有關延時ack以及ECN标志的處理
if(skb->len)
tcp_event_data_recv(sk, tp, skb);
//處理fin
if(th->fin)
tcp_fin(skb, sk, th);

/*檢測緩存亂序隊列中是否存在可以确認的段*/
if (!skb_queue_empty(&tp->out_of_order_queue)) {
//亂序隊列中有可以确認的段,則将其轉移到接收隊列中
tcp_ofo_queue(sk);

/* RFC2581. 4.2. SHOULD send immediate ACK, when
* gap in queue is filled.
*/
if (skb_queue_empty(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pingpong = 0;
}

if (tp->rx_opt.num_sacks)
tcp_sack_remove(tp);

tcp_fast_path_check(sk, tp);

if (eaten > 0)
__kfree_skb(skb);
else if (!sock_flag(sk, SOCK_DEAD))
sk->sk_data_ready(sk, 0);
return;
}

if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
/* A retransmit, 2nd most common case. Force an immediate ack. */
NET_INC_STATS_BH(LINUX_MIB_DELAYEDACKLOST);
tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);

out_of_window:
//立即給發送方發送ack,
tcp_enter_quickack_mode(sk);
inet_csk_schedule_ack(sk);
drop: //丢棄該段後傳回
__kfree_skb(skb);
return;
}

/* Out of window. F.e. zero window probe. 
若接收的段序号過大,在接收視窗之外,則處理方法和接收到過早的段一樣 
*/
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;

//進入快速确認模式
tcp_enter_quickack_mode(sk);

/*若接收到的段有一部分已經接收,則先處理SACK選項的D-SACK,在下個确認中發送*/
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
SOCK_DEBUG(sk, "partial packet: rcv_next %X seq %X - %X\n",
tp->rcv_nxt, TCP_SKB_CB(skb)->seq,
TCP_SKB_CB(skb)->end_seq);

tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);

/* If window is closed, drop tail of packet. But after
* remembering D-SACK for its head made in previous line.
*/
/*檢測接收視窗是否為0,若為0,則暫時不能接收,跳到out_of_window作丢棄處理,
若不為0,則跳到queue_and_out接收資料*/
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}


/*若收到亂序的段,則很有可能在傳輸過程中經曆了擁塞,是以需要檢測ECN标志,在經過路由器時,
若有路由器擁塞會設定該标志,說明路徑上存在擁塞,需要發送方和接收方進行擁塞控制。
若沒有經曆擁塞,則需盡快通知對方,讓對方盡可能的重傳丢失的段*/
TCP_ECN_check_ce(tp, skb);
//若接收緩存空間不夠,則丢棄
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
!sk_stream_rmem_schedule(sk, skb)) {
if (tcp_prune_queue(sk) < 0 ||
!sk_stream_rmem_schedule(sk, skb))
goto drop;
}

/* 由于接收到亂序的段,是以需把預測标志清零,亂序隊列不為空的情況下是不能執行快速路徑的 */
tp->pred_flags = 0;
//設定發送确認緊急程度,辨別有确認發送
inet_csk_schedule_ack(sk);

SOCK_DEBUG(sk, "out of order segment: rcv_next %X seq %X - %X\n",
tp->rcv_nxt, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);

//段在緩存到亂序隊列中之前,需設定該skb的宿主
sk_stream_set_owner_r(skb, sk);

/*若亂序隊列為空,則設定用于生成确認段的sack選項的屬性,并将其添加到亂序隊列中。
若亂序隊列不為空,則按序号将其插入到隊列中,然後重新設定用于生成确認段的sack選項的屬性*/
if (!skb_peek(&tp->out_of_order_queue)) {
/* Initial out of order segment, build 1 SACK. */
if (tp->rx_opt.sack_ok) {
tp->rx_opt.num_sacks = 1;
tp->rx_opt.dsack = 0;
tp->rx_opt.eff_sacks = 1;
tp->selective_acks[0].start_seq = TCP_SKB_CB(skb)->seq;
tp->selective_acks[0].end_seq =
TCP_SKB_CB(skb)->end_seq;
}
__skb_queue_head(&tp->out_of_order_queue,skb);
} else {
/*雖然接收的是亂序的段,但按順序接收的可能性還是最大的,是以檢測接收到的亂序段與亂序隊列中最後
一個段是不是連續的。*/
struct sk_buff *skb1 = tp->out_of_order_queue.prev;
u32 seq = TCP_SKB_CB(skb)->seq;
u32 end_seq = TCP_SKB_CB(skb)->end_seq;
//如是連續的,則直接将其添加到亂序隊列尾部
if (seq == TCP_SKB_CB(skb1)->end_seq) {
__skb_append(skb1, skb, &tp->out_of_order_queue);

/*若是連續的,下個确認的sack選項中的sack塊數不為0,且接收段序号與存儲用于回複對方的第一個sack
資訊塊end_seq相等,則直接修改用于回複對方的第一個sack資訊塊的end_seq.
否則跳到add_sack,修改回複對方的sack資訊塊。*/
if (!tp->rx_opt.num_sacks ||
tp->selective_acks[0].end_seq != seq)
goto add_sack;

/* Common case: data arrive in order after hole. */
tp->selective_acks[0].end_seq = end_seq;
return;
}

/* Find place to insert this segment. 
若接收到的亂序段與亂序隊列中最後一個段不是連續的,就從尾部起向前周遊亂序隊列,根據序号找出
插入的位置,及找到小于或等于亂序段序号的段,将亂序段插入到該段之後
*/
do {
if (!after(TCP_SKB_CB(skb1)->seq, seq))
break;
} while ((skb1 = skb1->prev) !=
(struct sk_buff*)&tp->out_of_order_queue);

/* Do skb overlap to previous one? */
//檢測與在插入的位置與前一個段是否有重疊,并插入到亂序隊列的相應位置

//檢測與前一個段是否有重疊(至少一部分)
if (skb1 != (struct sk_buff*)&tp->out_of_order_queue &&
before(seq, TCP_SKB_CB(skb1)->end_seq)) {
/*檢測是否包含在前一個段,若是,則丢棄,并設定有關D-SACK的屬性,然後跳到add_sack處增加sack塊*/
if (!after(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
/* All the bits are present. Drop. */
__kfree_skb(skb);
tcp_dsack_set(tp, seq, end_seq);
goto add_sack;
}
/*再次檢測是否與前一個段有部分重疊,若有,設定有關D-SACK的屬性,否則需要重新調整插入的位置*/
if (after(seq, TCP_SKB_CB(skb1)->seq)) {
/* Partial overlap. */
tcp_dsack_set(tp, seq, TCP_SKB_CB(skb1)->end_seq);
} else {
skb1 = skb1->prev;
}
}
//若不存在包含的情況,則将接收到的亂序段加入到合适位置
__skb_insert(skb, skb1, skb1->next, &tp->out_of_order_queue);

/* And clean segments covered by new one as whole. 
若有在插入的位置後的段中包含在目前待插入的段内,則将其删除并釋放
*/
//從目前位置向後周遊,查找與插入的段是否有重疊(包括被包含)的段
while ((skb1 = skb->next) !=
(struct sk_buff*)&tp->out_of_order_queue &&
after(end_seq, TCP_SKB_CB(skb1)->seq)) {
//确定與找到的段是否部分重疊,若是則設定有關d-sack屬性後,結束目前周遊查找
if (before(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
tcp_dsack_extend(tp, TCP_SKB_CB(skb1)->seq, end_seq);
break;
}
/*若找到的段被包含,則将其從亂序隊列上删除并釋放,同時設定相關的D-SACK屬性,
然後繼續周遊查找*/
__skb_unlink(skb1, &tp->out_of_order_queue);
tcp_dsack_extend(tp, TCP_SKB_CB(skb1)->seq, TCP_SKB_CB(skb1)->end_seq);
__kfree_skb(skb1);
}

/*若要進行sack塊的設定都會執行到add_sack處,這是接收亂序段的最後一步。若雙方都支援sack選項,則調用
tcp_sack_new_ofo_skb 設定sack塊 */
add_sack:
if (tp->rx_opt.sack_ok)
tcp_sack_new_ofo_skb(sk, seq, end_seq);
}
}
           

什麼是 SACK ?

在 TCP 通信時,如果發送隊列中的某個資料包丢失,TCP 會從最後确認的包開始進行重傳後續的包,那麼原先已經正确傳輸的包也可能會重複發送,這樣會急劇降低 TCP 性能。為了改善這種現象,産生了SACK(Selective Acknowledgment, 選擇性确認) 技術,使得 TCP 隻重新發送丢失的包,不用發送後續所有的包,并且提供相應的機制使接收方能告訴發送方哪些資料丢失,哪些資料重發了,哪些資料已經提前收到等。

SACK 資訊是通過 TCP 頭部選項部分提供的,資訊分為兩種,一種辨別是否支援 SACK, 在 TCP 握手時發送; 另一種是具體的 SACK 資訊。

SACK 選項中的參數告訴對方已經接收到并緩存的不連續的資料塊,注意都是已經接收到的,發送方根據這些資訊計算出哪些塊丢失了,進而重新發送這些資料塊。

SACK 通常都是由 TCP 接收方産生的,若在 TCP 握手時接收到對方 SACK 允許選項,且本端也支援 SACK 的話,接收異常時就可以發送 SACK 包通知對方。

TCP 接收方接收到非期望序号的資料時,若該塊的序号小于期待的序号,說明是網絡複制或重發的包,可以丢棄; 若接收到的資料塊号大于期待的序号,說明中間有包被丢棄或延遲,這是會發送 SACK 通知發送方出現了網絡丢包。

為了反映接收方接收緩存和網絡傳輸情況,SACK 中的第一個塊必須描述是哪個資料塊激發了 SACK 選項,接收方應該在 SACK 選項中填寫盡可能多的塊資訊,若空間有限不能全部寫入,則報告最近接收的不連續資料塊,讓發送方能了解網絡傳輸情況的最新資訊。

繼續閱讀