Linux TCP数据接收 - 快路径与慢路径

一、快路径与慢路径简介

在Linux内核的TCP/IP协议栈实现中,TCP数据接收分为快路径处理与慢路径进行处理,快路径用于处理预期的、理想情形的输入数段,TCP连接中最常见的情形应该被尽可能地检测并最优化处理,达到快速处理的目的。慢路径用于处理那些非预期、非理想情况下的数据段,如乱序数据段、socket内存管理和紧急数据等。

快路径与慢路径在Linux内核中的处理流程:tcp握手完成后,收到数据包后,调用路径为:

tcp_v4_rcv

->tcp_v4_do_rcv

->tcp_rcv_established,

在tcp_rcv_establisheed函数中处理TCP_ESTABLISHED状态的包 ,并根据pred_flags预测字段来选择着采用快路径或慢路径。

二、首部预测字段-pred_flags

预测字段存储在struct tcp_sock中,pred_flag为0表示关闭首部预测使用慢速路径,非0表示开启快速路径的前提,如果开启会对该变量进行设定。

struct tcp_sock { ....../* Header prediction flags * 0x5?10 << 16 + snd_wnd in net byte order */ __be32 pred_flags;......}

可以看到pred_flags与网络传输时一致采用大端存储,其大小为32位。该32位的pred_flags和TCP首部的第3个32位字(第0个32位:16位源端口号,16位目的端口号;第1个32位:32位序列号;第2个32位:32位确认号;第3个32位:首部字段、标志、窗口大小等,即首部预测字段需要的字段)对应,**如下图1为TCP首部字段分布与第3个32位的细节,**pred_flag变量的赋值通过调用tcp_fast_path_on函数或tcp_fast_path_on函数(include et cp.h)中进行设定,其中tcp_fast_path_on间接调用__tcp_fast_path_on,只不过是在调用__tcp_fast_path_on针对携带窗口扩大因子的TCP传输进行还原发送窗口的大小。

static inline void tcp_fast_path_on(struct tcp_sock *tp){ __tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);}
static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd){  //pred_flags中有三部分:首部长度、ACK标记、发送窗口(即对端的接收窗口) tp->pred_flags = htonl((tp->tcp_header_len << 26) |          ntohl(TCP_FLAG_ACK) |          snd_wnd);}

**pred_flags由三个部分组成:【首部长度、ACK标记、发送窗口大小】也就是与TCP首部字段(13-16字节)中相应的字段,******如图2 pred_flags图示,****内核中的解释如下:

 /* 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.  */

进行快速路径判断的时候只需要将预测字段与TCP首部中对应的部分进行对比即可。

__tcp_fast_path_on函数中:tp->header_len<<26的解释如下:

1、关于header_len:是指TCP首部的字节数(TCP首部固定部分为20字节),在TCP头部对应的是4位的"首部长度"字段,TCP的首部字节数 header_len= 首部长度*4,要得到”首部长度“字段,需要将header_len右移两位,即除以4:header_len>>2

2、”首部长度“字段是4位,现在要得到的pred_flags应该与TCP首部的第4个32位的位置(第13-16字节)进行对应,所以4位的首部字段左移28位得到:32位初始化(仅包含首部长度字段)的pred_flags,即header_len右移两位后,再左移28位:header_len<<26,即XXXX0000000000000000000000000000

其中XXXX表示首部长度字段的值。

tp->header_len<<26得到了含首部长度字段的pred_flags,但pred_flags除了首部长度字段外还应含有ACK标记,发送窗口:

tp->tcp_header_len << 26) | ntohl(TCP_FLAG_ACK) | snd_wnd

其中TCP_FLAG_ACK定义如下:

enum {  TCP_FLAG_CWR = __constant_cpu_to_be32(0x00800000), TCP_FLAG_ECE = __constant_cpu_to_be32(0x00400000), TCP_FLAG_URG = __constant_cpu_to_be32(0x00200000), TCP_FLAG_ACK = __constant_cpu_to_be32(0x00100000), TCP_FLAG_PSH = __constant_cpu_to_be32(0x00080000), TCP_FLAG_RST = __constant_cpu_to_be32(0x00040000), TCP_FLAG_SYN = __constant_cpu_to_be32(0x00020000), TCP_FLAG_FIN = __constant_cpu_to_be32(0x00010000), TCP_RESERVED_BITS = __constant_cpu_to_be32(0x0F000000), TCP_DATA_OFFSET = __constant_cpu_to_be32(0xF0000000)}; 

TCP_FLAG_ACK字段为0x00100000,即对应ACK标志位为1。


更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂


三、首部预测字段的设定

首部预测字段的设定分为两种过程

1、直接调用__tcp_fast_path_on函数进行设定首部预测字段相应的值

2、直接调用tcp_fast_path_on函数进行设定首部预测字段相应的值

2、先进行条件检验,检验通过后调用__tcp_fast_path_on函数进行设定首部预测字段相应的值
直接调用__tcp_fast_path_on 的时机:

客户端connect系统调用即将结束的时

在tcp_finish_connect中没有开启窗口扩大因子的时,调用__tcp_fast_path_on来设置快速路径条件,这时候客户端进入TCP_ESTABLISHED状态,服务端还在等待客户端最后一次ACK才能发送数据,因此不会收到服务端的数据,也就不用考虑快速路径。

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb){  ...... if (sock_flag(sk, SOCK_KEEPOPEN))  inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp)); if (!tp->rx_opt.snd_wscale)//对端没有开启wscale,则开启快速路径  __tcp_fast_path_on(tp, tp->snd_wnd); else  tp->pred_flags = 0;  //目前不会收到服务端数据,不用开启快速路径}

直接调用tcp_fast_path_on的时机:

服务器在收到SYN请求后的处理过程中,如下tcp_rcv_state_process函数中

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb){  ...... switch (sk->sk_state) { case TCP_SYN_RECV: ......  tcp_fast_path_on(tp);  break;  ......}

进行检验后调用__tcp_fast_path_on函数:

检验函数检查条件是否满足,满足后才能设置预测标记,条件:**
**

条件1:乱序队列是否为空(没有乱序数据时设定)

条件2:接收窗口是否还有剩余空间(接收窗口不为0时设定)

条件3:接收内存是否受限(接收缓存未耗尽时设定)

条件4:是否有紧急数据需要传输(无紧急数据时设定)

检验函数是tcp_fast_path_check,逻辑主要是进行4个条件判断,决定是否设定首部预测字段,如下所示:

static inline void tcp_fast_path_check(struct sock *sk){ struct tcp_sock *tp = tcp_sk(sk);  /*  条件1:乱序队列为空  条件2:接收窗口还有剩余空间  条件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_check函数检测后调用t cp_fast_path_on的时机:

1、读完紧急数据后:紧急数据是由慢路径处理的,在慢速路径上收完紧急数据后检查是否可用开启快速路径模式

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,  int flags, int *addr_len){    ......  /* Do we have urgent data here? */  if (tp->urg_data) {   u32 urg_offset = tp->urg_seq - *seq;   if (urg_offset < used) {    if (!urg_offset) {     if (!sock_flag(sk, SOCK_URGINLINE)) {      ++*seq;      urg_hole++;      offset++;      used--;      if (!used)       goto skip_copy;     }    } else     used = urg_offset;   }  } ......skip_copy:  if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {   tp->urg_data = 0;   tcp_fast_path_check(sk);//检验后重新设定首部预测字段  }  if (used + offset < skb->len)   continue;  if (TCP_SKB_CB(skb)->has_rxtstamp) {   tcp_update_recv_tstamps(skb, &tss);   has_tss = true;  }  if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)   goto found_fin_ok;  if (!(flags & MSG_PEEK))   sk_eat_skb(sk, skb);  continue;......}

2、当发送发收到ACK并调用tcp_ack_update_window更新窗口时,通告窗口发生了变化,则必须更新预测标记,以免后续的输入报文因为窗口不符而进入慢速路径:

static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack,     u32 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);//检验后重新设定首部预测字段   if (!tcp_write_queue_empty(sk))    tcp_slow_start_after_idle_check(sk);   if (nwin > tp->max_window) {    tp->max_window = nwin;    tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie);   }  } }  ......}

3、当调用tcp_data_queue将数据放入接收队列时,这时可用的接收缓存大小发生变化,即将输入数据放入到接收队列后,更新了字节的内存占用量,tcp_fast_path_check会检查这俄格缓存的变化是否允许开启快速路径模式。只有当前包是非乱序包,且接收窗口非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, 尝试开启快速路径        ...    }    ...}

四、进入快速路径与慢速路径处理

设置预测标记后,使用它是在处理已连接TCP数据段的唯一入口函数:tcp_rcv_estblished

是否能够执行快速路径,pred_flags匹配只是前提条件,还有一些其他的判断条件,在内核中有相关的定义(net\ipv4 cp_input.c):

 *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. */

在tcp_rcv_established函数中,关于快速路径检查与执行部分如下:

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,    const struct tcphdr *th){ unsigned int len = skb->len; struct tcp_sock *tp = tcp_sk(sk); tcp_mstamp_refresh(tp); if (unlikely(!sk->sk_rx_dst)) tp->rx_opt.saw_tstamp = 0; 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;  /* 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;    if (len <= tcp_header_len) {  //无数据   /* Bulk data transfer: sender */   if (len == tcp_header_len) {    if (tcp_header_len ==        (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&        tp->rcv_nxt == tp->rcv_wup)//有时间戳选项&&所有接收的数据段均确认完毕     tcp_store_ts_recent(tp); //保存时间戳    tcp_ack(sk, skb, 0);  //快路径ACK处理    __kfree_skb(skb);    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 (tcp_checksum_complete(skb))    goto csum_error;   if ((int)skb->truesize > sk->sk_forward_alloc)     goto step5;            //有时间戳选项,且数据均确认完毕,则更新时间戳   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);   //数据加入接收队列,添加数据到sk_receive_queue中   eaten = tcp_queue_rcv(sk, skb, tcp_header_len,           &fragstolen);   tcp_event_data_recv(sk, skb);//更新RTT   if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {        tcp_ack(sk, skb, FLAG_DATA); //处理ACK    tcp_data_snd_check(sk);    if (!inet_csk_ack_scheduled(sk)) //检查是否有数据要发送,需要则发送     goto no_ack;   } ...... }slow_path:  //进入慢路径 ......}

其中:

 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)) { //确认号没有超出本端最新发生的数据的序列号}

tcp_flag_word(th)获取的是TCP首部的第13-16字节,也就是第3个32位(第0个32位:16位源端口号,16位目的端口号;第1个32位:32位序列号;第2个32位:32位确认号;第3个32位:首部字段、标志、窗口大小等,需要的首部预测字段)

union tcp_word_hdr {  struct tcphdr hdr; __be32     words[5];}; #define tcp_flag_word(tp) ( ((union tcp_word_hdr *)(tp))->words [3]) 

得到TCP首部的第3个32位后,还不是首部预测字段,还要继续屏蔽掉PSH字段,即 tcp_flag_word(th) & TCP_HP_BITS

122 #define TCP_HP_BITS (~(TCP_RESERVED_BITS|TCP_FLAG_PSH))

慢速路径处理:

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;

原文地址:Linux TCP数据接收 | 快路径与慢路径 - 进程管理 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛

数据   Linux   TCP
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章