网络知识 娱乐 Linux TCP数据接收 | 快路径与慢路径

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函数(includenettcp.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匹配只是前提条件,还有一些其他的判断条件,在内核中有相关的定义(netipv4tcp_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数据接收 | 快路径与慢路径 - 进程管理 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛