TCP数据接收及快速路径和慢速路径
概述
tcp握手完成后,收到数据包后,调用路径为tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established
在tcp_rcv_established中处理TCP_ESTABLISHED状态的包。 并分为快速路径和慢速路径。
快速路径只进行非常少量的处理。
快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的;
慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数据段的处理;
首部预测字段格式:首页预测字段,实际上是与TCP首部中的【头部长度+保留字段+标记字段+窗口值】这个32位值完全对应的;进行快速路径判断的时候,只需要将该预测值与TCP首部中的对应部分进行比对即可,具体见tcp_rcv_established;
快速路径(Fast Path)
内核使用tcp_sock中的pred_flags作为判断条件,0表示使用慢速路径,非0则表示快速的判断条件,值为tcp首部的第13-16字节,包含首部长度,标记位,窗口大小。
因此使用pred_flags来检查tcp头就能避免了头部的一些控制信息的处理。
static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd) {//tcphdr首部的第13-16字节,包含首部长度,标记位,窗口大小 tp->pred_flags = htonl((tp->tcp_header_len << 26) | ntohl(TCP_FLAG_ACK) | snd_wnd); } static inline void tcp_fast_path_on(struct tcp_sock *tp) {//snd_wnd已经缩放过,要还原tcp头信息这里缩放回去 __tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale); } static inline void tcp_fast_path_check(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); /* 1 没有乱序数据包 2 接收窗口不为0 3 还有接收缓存空间 4 没有紧急数据 */ if (RB_EMPTY_ROOT(&tp->out_of_order_queue) && tp->rcv_wnd && atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf && !tp->urg_data) tcp_fast_path_on(tp); }
__tcp_fast_path_on调用时机
在tcp_finish_connect中没有开启wscale的时候,会调用__tcp_fast_path_on来设置快速路径条件。
因为没有开启wscale,所以不需要调用tcp_fast_path_on。
为什么开启wscale-窗口因子 后就要关闭快速路径呢?
这时候只是客户端进入TCP_ESTABLISHED状态,服务端还在等待客户端最后一次ack才能发送数据。
因此不会收到服务端的数据,也就不用考虑快速路径了。
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); tcp_set_state(sk, TCP_ESTABLISHED); ... if (!tp->rx_opt.snd_wscale) //对方没有开启wscale,则开启快速路径 __tcp_fast_path_on(tp, tp->snd_wnd); else tp->pred_flags = 0; //目前不会收到服务端数据,不用开启快速路径 if (!sock_flag(sk, SOCK_DEAD)) { sk->sk_state_change(sk); //唤醒connect sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); } }
tcp_fast_path_on调用时机
跟tcp_finish_connect一样,服务端进入TCP_ESTABLISHED状态的时候,也要尝试开启快速路径,因此调用tcp_fast_path_on设定快速路径判断条件
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) { ... /* step 5: check the ACK field */ acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) > 0; switch (sk->sk_state) { case TCP_SYN_RECV: //握手完成时的新建连接的初始状态 if (!acceptable) return 1; ... tcp_set_state(sk, TCP_ESTABLISHED); sk->sk_state_change(sk); ... tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; //snd_wnd已经缩放过 tcp_init_wl(tp, TCP_SKB_CB(skb)->seq); if (tp->rx_opt.tstamp_ok) tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; ... tcp_fast_path_on(tp); break; } ... }
tcp_fast_path_check调用时机
相比起前两个进入TCP_ESTABLISHED就设置pred_flags,因为建立连接前没有其他数据包作为判定依据。
tcp_fast_path_check主要是在连接过程中,有其他数据包作为判定依据的条件下调用:
- 没有乱序的数据包
- 接收窗口不为0
- 接收缓存未用完
- 非紧急数据
完成紧急数据的读取
紧急数据是由慢速路径处理,需要保持在慢速路径模式直到收完紧急数据,读完后就能检测是否能够开启fast path
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len) { ... if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) { tp->urg_data = 0; tcp_fast_path_check(sk); } ... }
在慢速路径收到非乱序包的时候
tcp_data_queue是在慢速路径,对数据部分进行处理。
只有当前包是非乱序包,且接收窗口非0的时候,才能调用tcp_fast_path_check尝试开启快速路径
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); bool fragstolen = false; int eaten = -1; if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) { //没有数据部分,直接释放 __kfree_skb(skb); return; } ... if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) { //非乱序包 if (tcp_receive_window(tp) == 0) //接受窗口满了,不能接受 goto out_of_window; ... tcp_fast_path_check(sk); //当前是slow path, 尝试开启快速路径 ... } ... }
当收到新的通告窗口值时
因为pred_flags中包含了窗口值,显然收到新的通告窗口时,需要更新
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq) { struct tcp_sock *tp = tcp_sk(sk); int flag = 0; u32 nwin = ntohs(tcp_hdr(skb)->window); if (likely(!tcp_hdr(skb)->syn)) nwin <<= tp->rx_opt.snd_wscale; if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //更新滑动窗口,或者收到新的窗口通知 flag |= FLAG_WIN_UPDATE; tcp_update_wl(tp, ack_seq); //snd_wl1=ack_seq if (tp->snd_wnd != nwin) { //窗口更新 tp->snd_wnd = nwin; /* Note, it is the only place, where * fast path is recovered for sending TCP. */ tp->pred_flags = 0; tcp_fast_path_check(sk); //窗口更新了,要重新设置fast path检测条件 ... } } ... return flag; }
快速路径包处理
在tcp_rcv_established中,通过快速路径判断后,
/* * TCP receive function for the ESTABLISHED state. * * It is split into a fast path and a slow path. The fast path is * disabled when: * - A zero window was announced from us - zero window probing * is only handled properly in the slow path. * - Out of order segments arrived. * - Urgent data is expected. * - There is no buffer space left * - Unexpected TCP flags/window values/header lengths are received * (detected by checking the TCP header against pred_flags) * - Data is sent in both directions. Fast path only supports pure senders * or pure receivers (this means either the sequence number or the ack * value must stay constant) * - Unexpected TCP option. * * When these conditions are not satisfied it drops into a standard * receive procedure patterned after RFC793 to handle all cases. * The first three cases are guaranteed by proper pred_flags setting, * the rest is checked inline. Fast processing is turned on in * tcp_data_queue when everything is OK. */ void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len) { struct tcp_sock *tp = tcp_sk(sk); skb_mstamp_get(&tp->tcp_mstamp); if (unlikely(!sk->sk_rx_dst)) /* 路由为空,则重新设置路由 */ inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb); /* * Header prediction. * The code loosely follows the one in the famous * "30 instruction TCP receive" Van Jacobson mail. * * Van's trick is to deposit buffers into socket queue * on a device interrupt, to call tcp_recv function * on the receive process context and checksum and copy * the buffer to user space. smart... * * Our current scheme is not silly either but we take the * extra cost of the net_bh soft interrupt processing... * We do checksum and copy also but from device to kernel. */ tp->rx_opt.saw_tstamp = 0; /* pred_flags is 0xS?10 << 16 + snd_wnd * if header_prediction is to be made * 'S' will always be tp->tcp_header_len >> 2 * '?' will be 0 for the fast path, otherwise pred_flags is 0 to * turn it off (when there are holes in the receive * space for instance) * PSH flag is ignored. */ /* 快路检查&& 序号正确 && ack序号正确 */ if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags && TCP_SKB_CB(skb)->seq == tp->rcv_nxt && !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) { int tcp_header_len = tp->tcp_header_len; /* tcp头部长度 */ /* Timestamp header prediction: tcp_header_len * is automatically equal to th->doff*4 due to pred_flags * match. */ /* Check timestamp */ /* 有时间戳选项 */ if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) { /* No? Slow path! /* 解析时间戳选项失败,执行慢路 */ if (!tcp_parse_aligned_timestamp(tp, th)) goto slow_path; /* If PAWS failed, check it more carefully in slow path */ /* 序号回转,执行慢路 */ if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0) goto slow_path; /* DO NOT update ts_recent here, if checksum fails * and timestamp was corrupted part, it will result * in a hung connection since we will drop all * future packets due to the PAWS test. */ } 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: *//* 有时间戳选项 && 所有接收的数据段均确认完毕 保存时间戳 */ if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup) tcp_store_ts_recent(tp); /* We know that such packets are checksummed * on entry. */ /* 输入/快速路径ack处理 */ tcp_ack(sk, skb, 0); __kfree_skb(skb); /* 检查是否有数据要发送,并检查发送缓冲区大小 收到ack了,给数据包一次发送机会,tcp_push_pending_frames*/ tcp_data_snd_check(sk); return; } else { /* Header too small */ /* 数据多小,比头部都小,错包 */ TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS); goto discard; } } else { /* 有数据 */ int eaten = 0; bool fragstolen = false; /* 进程上下文 */ if (tp->ucopy.task == current && /* 期待读取的和期待接收的序号一致 */ tp->copied_seq == tp->rcv_nxt && len - tcp_header_len <= tp->ucopy.len && /* 数据<= 待读取长度 */ /* 控制块被用户空间锁定 */ sock_owned_by_user(sk)) { __set_current_state(TASK_RUNNING); /* 设置状态为running??? */ /* 拷贝数据到msghdr */ if (!tcp_copy_to_iovec(sk, skb, 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: */ /* 有时间戳选项&& 收到的数据段均已确认,更新时间戳 */ 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); /* 接收端RTT估算 */ __skb_pull(skb, tcp_header_len); /* 更新期望接收的序号 */ tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq); NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER); eaten = 1; } } /* 未拷贝数据到用户空间,或者拷贝失败----没有把数据放到ucopy中 */ if (!eaten) { if (tcp_checksum_complete(skb)) goto csum_error; /* skb长度> 预分配长度 */ if ((int)skb->truesize > sk->sk_forward_alloc) goto step5; /* 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); NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHPHITS); /* Bulk data transfer: receiver */ /* 数据加入接收队列 添加数据到sk_receive_queue中 */ eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen); } tcp_event_data_recv(sk, skb);//inet_csk_schedule_ack, 更新rtt /* 确认序号确认了数据 */ if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) { /* Well, only one small jumplet in fast path... */ tcp_ack(sk, skb, FLAG_DATA);/* 处理ack */ tcp_data_snd_check(sk); /* 检查是否有数据要发送,需要则发送 */ if (!inet_csk_ack_scheduled(sk)) /* 没有ack要发送 在tcp_event_data_recv标记过,但可能ack已经发出了,就不用检测是否要发送了*/ goto no_ack; } /* 检查是否有ack要发送,需要则发送 */ __tcp_ack_snd_check(sk, 0); no_ack: if (eaten) kfree_skb_partial(skb, fragstolen); sk->sk_data_ready(sk); return; } ------------------------------
慢速路径
pred_flags=0或者tcp包头匹配pred_flags失败的时候则为slow path处理
-
本地接受缓存不足通告0窗口的时候, 因为0窗口探测包需要在慢速路径处理
static u16 tcp_select_window(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); u32 old_win = tp->rcv_wnd; u32 cur_win = tcp_receive_window(tp); //根据过去接收窗口值,当前还能通告给对方的接收窗口配额 u32 new_win = __tcp_select_window(sk); //根据接收缓存计算出的新窗口值 ... /* If we advertise zero window, disable fast path. */ if (new_win == 0) { //cur_win scale也为0 tp->pred_flags = 0; //开启0窗口, 关闭快速路径 if (old_win) NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPTOZEROWINDOWADV); } else if (old_win == 0) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV); } return new_win; }
- 收到乱序包的时候
收到乱序包后会调用tcp_data_queue_ofo添加skb到ofo队列中
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb) { ... /* Disable header prediction. */ tp->pred_flags = 0; ... }
- 收到紧急数据
static void tcp_check_urg(struct sock *sk, const struct tcphdr *th) { ... tp->urg_data = TCP_URG_NOTYET; tp->urg_seq = ptr; /* Disable header prediction. */ tp->pred_flags = 0; }
- 接受缓存不足
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb, unsigned int size) { if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || //接收缓存不够 !sk_rmem_schedule(sk, skb, size)) { //并且超过系统设置的最大分配空间了 if (tcp_prune_queue(sk) < 0) //尝试合并ofo/sk_receive_queue来腾出空间 return -1; //还是不够,超过sk_rcvbuf, 返回失败 while (!sk_rmem_schedule(sk, skb, size)) { //再次确认 if (!tcp_prune_ofo_queue(sk)) //释放ofo队列中的数据 return -1; } } return 0; } static int tcp_prune_queue(struct sock *sk) { ... /* Massive buffer overcommit. */ tp->pred_flags = 0; //接收缓存还是不足,关闭快速路径 return -1; }
慢速路径包处理
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len) { ... if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags && // 快速路径包头检测 TCP_SKB_CB(skb)->seq == tp->rcv_nxt && // 非乱序包 !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) { // 确认的序号是已经发送的包 //快速路径处理 ... } slow_path: /* 长度错误|| 校验和错误 */ if (len < (th->doff << 2) || tcp_checksum_complete(skb)) goto csum_error; /* 无ack,无rst,无syn */ if (!th->ack && !th->rst && !th->syn) goto discard; /* * Standard slow path. /* 种种校验 */ if (!tcp_validate_incoming(sk, skb, th, 1)) return; step5: /* 处理ack */ if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0) goto discard; /* 计算rtt */ tcp_rcv_rtt_measure_ts(sk, skb); /* Process urgent data. */ /* 处理紧急数据 */ tcp_urg(sk, skb, th); /* step 7: process the segment text数据段处理 */ tcp_data_queue(sk, skb); tcp_data_snd_check(sk);/* 发送数据检查,有则发送 */ tcp_ack_snd_check(sk);/* 发送ack检查,有则发送 */ return;
tcp_data_queue
- tcp_data_queue主要把非乱序包copy到ucopy或者sk_receive_queue中,并调用tcp_fast_path_check尝试开启快速路径
- 对重传包设置dsack,并快速ack回去
- 对于乱序包,如果包里有部分旧数据也设置dsack,并把乱序包添加到ofo队列中
tcp_data_queue_ofo
在新内核的实现中ofo队列实际上是一颗红黑树。
在tcp_data_queue_ofo中根据序号,查找到合适位置,合并或者添加到rbtree中。
同时设置dsack和sack,准备ack给发送方。
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq) { struct tcp_sock *tp = tcp_sk(sk); int flag = 0; u32 nwin = ntohs(tcp_hdr(skb)->window); if (likely(!tcp_hdr(skb)->syn)) nwin <<= tp->rx_opt.snd_wscale; if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //更新滑动窗口,或者收到新的窗口通知 flag |= FLAG_WIN_UPDATE; tcp_update_wl(tp, ack_seq); //snd_wl1=ack_seq if (tp->snd_wnd != nwin) { //窗口更新 tp->snd_wnd = nwin; /* Note, it is the only place, where * fast path is recovered for sending TCP. */ tp->pred_flags = 0; tcp_fast_path_check(sk); //窗口更新了,要重新设置fast path检测条件 ... } } ... return flag; }