加载中…
个人资料
老衲五木
老衲五木
  • 博客等级:
  • 博客积分:0
  • 博客访问:352,549
  • 关注人气:330
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

TCP超时与重传《LwIP协议栈源码详解——TCP/IP协议的实现》

(2012-08-24 19:23:32)
标签:

lwip

tcp

超时

重传

it

分类: 嵌入式网络那些事

在TCP两端交互过程中,数据和确认都有可能丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重传该数据。对任何TCP协议实现而言,怎样决定超时间隔和如何确定重传的频率是提高TCP性能的关键。

这节讲解TCP的超时重传机制,TCP控制块tcp_pcb内部的相关字段为rtime、rttest、rtseq、sa、sv、rto、nrtx,太多了,先不要晕!

与超时时间间隔密切相关的是往返时间(RTT)的估计。RTT是某个字节的数据被发出到该字节确认返回的时间间隔。由于路由器和网络流量均会变化,因此RTT可能经常会发生变化,TCP应该跟踪这些变化并相应地改变其超时时间。

在某段时间内发送方可能会连续发送多个数据包,但发送方只能选择一个发送包启动定时器,估计其RTT值,另外,一个报文段被重发和该报文的确认到来之前不应该更新估计器。协议中利用一些优化算法平滑RTT的值,并根据RTT值设置RTO的值,即下一个数据包的重传超时时间。

先来看看超时重传机制是怎样实现的,再来重点介绍与RTT估计密切相关的部分。前面讲过tcp_output从unsent队列上取下第一个数据段,并调用函数tcp_output_segment将数据段发送出去,发送完毕后,tcp_output将该数据段挂接到unacked队列上,至于挂在unacked队列上的什么位置,那是后话。tcp_output_segment负责将数据段发送出去,发送出去后它要做的工作如下面的代码所示:

if(pcb->rtime == -1)

pcb->rtime = 0;

if (pcb->rttest == 0) {

pcb->rttest = tcp_ticks;

pcb->rtseq = ntohl(seg->tcphdr->seqno);

}

rtime用于重传定时器的计数,当其值为-1时表示计数器未被使能;当值为非0时表示计数器使能,在这种情况下,rtime的值每500ms被内核加1,当rtime超过rto的值时,在unacked队列上的所有数据段将被重传。rto已经提及过多次,就是我们为数据包所设置的超时重传时间。接下来的rttest字段与RTT估计密切相关,当rttest值为0时表示RTT估计未启动,否则若要启动RTT估计,则应在发送数据包出去后,将rttest的值设置为tcp_ticks(全局变量,系统当前滴答数),并用rtseq字段记录要进行RTT估计的数据段的起始数据编号。当接收到对方返回的ACK编号后,就可以根据rttest与rtseq的值计算RTT了,字段sa、sv与rto值的计算密切相关,放在后续讨论。数据段发送就是这么多了,主要是针对发送出去的数据段启动重传定时器。当然如过数据段发送出去的时候,重传定时器是启动的,即rtime不等于-1,此刻不对重传定时器做任何操作。同理,如果rttest不等于0,则说明RTT正在进行,此时不会对RTT的各个字段做任何操作。

TCP慢定时器每500ms产生一次中断处理,在中断处理中,若TCP控制块的重传计数器被启动(即rtime不为0),则rtime值被加1。同时,当rtime值大于rto时,调用重传函数对未被确认的数据进行重传,代码如下(仅列出了与重传相关的部分):

if (pcb->unacked != NULL && pcb->rtime >= pcb->rto) {

……

pcb->rtime = 0;  // 复位计数器

tcp_rexmit_rto(pcb);  // 函数调用进行重传

…….

}

tcp_rexmit_rto函数实现重传的机制很简单,它将unacked链表上的所有数据段插入到unsent队列的前端,并将控制块重传次数字段nrtx加1,最后调用tcp_output重发数据包。

这里你可能会发现一个问题,假设我们刚刚从unsent队列上取下一个数据段发送出去,并将该数据段挂接在unacked链表上等待确认,接着前面某个数据段处设置的重传定时器超时,这样整个unacked链表上又被放到了unsent队列上进行重传。不可避免,我们刚刚发送出去的那个数据段又回到了unsent队列上,这岂不是悲剧,它又得重发一遍?尽管这种情况是可能发生的,但是LWIP通过窗口的控制以及收到确认号后遍历unsent队列(下面讲解)这两种方式使得这种可能性降到了最小。这里注意,在较老版本的LWIP协议栈中,每个数据段结构tcp_seg中都对应有一个rtime字段,用于记录某个数据段的超时情况,这样可以避免重传时将整个unacked链表上放回到unsent队列上。而新版本的中,整个TCP控制块公用了一个rtime字段。

在数据接收上,tcp_receive函数提取收到的数据段中的ackno,并用该ackno来处理unacked队列,即当该ackno确认了某个数据段中的所有数据,则将该数据段从unacked队列中移除,并释放数据段占用的空间。同时,函数要检查unacked队列,如果unacked队列中没有被需要确认的数据段了,此时需要停止重传定时器,否则要复位重传定时器。很简单,用下面的代码:

if(pcb->unacked == NULL)

pcb->rtime = -1;

else

pcb->rtime = 0;

接下来,tcp_receive函数还要根据收到的确认号遍历unsent队列,以处理那些正被等待重传的数据段。unsent队列上那些是被重传的数据段?很明显就是数据段内数据编号小于控制块snd_max字段值的那些数据段。能被确认号确认的数据段会从unsent链表中移除,同时数据段占用的空间被释放。

关于数据段的超时设置与重传就是这么多了,下面到了很重要的内容,即RTT的估计。这里的代码有点难度啊!先来看看《TCP/IP详解1》里面是怎样描述RTT的。

TCP超时与重传中最重要的部分就是对一个给定连接的往返时间(RTT)的测量。由于路由器和网络流量均会变化,因此我们认为这个时间可能经常会发生变化,TCP应该跟踪这些变化并相应地改变其超时时间。TCP必须测量在发送一个带有特别序号的字节和接收到包含该字节的确认之间的RTT,同时应注意,发出去的数据段与返回的确认之间并没有一一对应的关系。

在往返时间变化起伏很大时,基于均值和方差来计算RTO能提供更好的响应,下面这个算法是Jacobson提出的,目前广泛应用在了TCP协议的实现中,当然LWIP也不例外。

TCP超时与重传《LwIP协议栈源码详解——TCP/IP协议的实现》
其中M表示某次测量的RTT的值,A表示测得的RTT的平均值,A值的更新如第二式所示,D值为RTT的估计的方差,其更新如第三式所示。二式和三式中g和h都为常数,一般g取1/8,h取1/4。这样取值是为了便于计算,从后面可以看出,通过简单的移位操作就可以完成上述计算了。RTO的计算第四式所示,初始时,RTO取值为6,即3s,A值为0,D值为6。

现在我们将上面的四个表达式做简单的变化,就得到了LWIP中计算RTT的表达式:

Err = M-A

A = A+ Err/8= A+(M-A)/8  ——>  8A = 8A + M-A

D =D + (|Err |-D)/4= D + (|M-A |-D)/4  ——>  4D = 4D + (|M-A |-D)

RTO = A+4D

令sa = 8A,sv = 4D,这就是TCP控制块中的两个字段。带入上面变换后的表达式,得到:

sa = sa + M - sa>>3

sv = sv + (|M - sa>>3 |-sv>>2)

RTO = sa>>3 + sv

这样我们就得到了最关心的RTO值,还有一个疑问:M值怎么得到?M表示某次测量的RTT的值,在LWIP中它就是系统当前tcp_ticks值减去数据包被发送出去时的tcp_ticks值。tcp_ticks也是在内核的500ms周期性中断处理中被加1。

如果到这里你都还很清醒,那说明对RTT的理解就没什么问题了。这就来看看源代码是怎样进行RTT估算的,这也是在函数tcp_receive中进行的。

if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno)) {  // 有RTT正在进行且该数据段被确认

m = (s16_t)(tcp_ticks - pcb->rttest);   //计算M值

m = m - (pcb->sa >> 3);  // M - sa>>3

pcb->sa += m;   //更新sa

if (m < 0) {

m = -m;    //  |M - sa>>3

}

m = m - (pcb->sv >> 2);  // (|M - sa>>3 |-sv>>2)

pcb->sv += m;   // 更新sv

pcb->rto = (pcb->sa >> 3) + pcb->sv; //计算rto

pcb->rttest = 0;  // 停止RTT估计

}

这段代码基本是前面讲的公式的翻译了,不解释!还有这样一个问题,当以某个RTO为超时值发送数据包后,在RTO时间后未收到对该数据段的确认,则该数据包被重发,若重发后仍收不到关于该数据包的确认,这种情况下,协议栈该怎么办呢,是每次都按照原来的RTO重发数据包吗?答案是否定的,因为当多次重传都失败时,很可能是网络不通或者网络阻塞,如果这时再有大量的重发包被投入到网络,这势必使问题越来越严重,可能数据包永远的被阻塞在网络中,而无法到达目的端。与标准里面描述的一样,LWIP是这样做的:如果重发的数据包超时,则接下来的重发包必须按照2的指数避让,即将RTO值设置为前一次的2倍,当重发超过一定次数后,不再对数据包进行重发。这是在500ms定时处理函数tcp_slowtmr中完成的,看看源代码。

if(pcb->rtime >= 0)     // 若重传定时器是开启的

++pcb->rtime;     // 则增加定时器的值

if (pcb->unacked != NULL && pcb->rtime >= pcb->rto) { //如果定时器超时,且有数据未确认

if (pcb->state != SYN_SENT) { // 对处于SYN_SENT状态的数据不做避让处理

pcb->rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[pcb->nrtx];  // rto值退让

}

pcb->rtime = 0;    // 复位定时器

……

tcp_rexmit_rto(pcb);  // 重传数据

}

上面这小段代码有三个地方想说一下:一是对处于SYN_SENT状态的控制块不进行超时时间的避让,可能是由于考虑到SYN_SENT状态一般发送出去的是SYN握手包,每次按照固定的RTO值进行重发,为什么要这样呢?不解,貌似标准里面也是进行避让了的啊!第二点是避让使用一个数组tcp_backoff通过移位的方式实现,tcp_backoff定义如下:

const u8_t tcp_backoff[13] = { 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7};

在这里面,当重传次数多于6次时,RTO值将不再进行避让。最后一点是函数tcp_rexmit_rto,该函数真正完成数据包的重传工作:

void tcp_rexmit_rto(struct tcp_pcb *pcb)

{

struct tcp_seg *seg;

if (pcb->unacked == NULL) {

return;

}

for (seg = pcb->unacked; seg->next != NULL; seg = seg->next);   // 将unacked队列全部放到

seg->next = pcb->unsent;                                  // 将unsent队列前端

pcb->unsent = pcb->unacked;

pcb->unacked = NULL;

pcb->snd_nxt = ntohl(pcb->unsent->tcphdr->seqno);  // 下一个要发送的数据编号指向队列

//  unsent首部的数据段

++pcb->nrtx;      // 重传次数加1

pcb->rttest = 0;    // 重发数据包期间不进行RTT估计

tcp_output(pcb);   // 发送一个数据包

}

从这个代码和上面的避让算法里你可以很清楚的看到字段nrtx的作用了,它是多次重传时设置rto值的重要变量。另外注意,在重传期间不应该进行RTT估计,因为这种情况下的估计值往往是不准确的。这就是传说中的Karn算法,Karn算法认为由于某报文即将重传,则对该报文的计时也就失去了意义。即使收到了ACK,也无法区分它是对第一次报文,还是对第二次报文的确认。因此,TCP只对未重传报文计时。还有一个要注意,经过上面的代码,snd_nxt的值很可能就会小于snd_max的值咯,相信你隐约中感觉到snd_max的作用了吧,哈哈,收工!

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有