C 非0为真 0为假

int n=3;
while(n)
printf("%2d is true\n",n--);
n=-3;
while(n)
printf("%2d is true\n",n++);
return 0;

RAW是什么意思


“RAW”是个英语形容词,意思是:原始的,未加工的,未成熟的。

简单的说,RAW格式文件,就是相机(扫描机,......)生成的“数码”文件,不是“图像”文件。因为没有经过任何处理,RAW文件带有相机所能记录的全部信息。相机上出来的jpg文件,是相机对RAW数据作了“后期”处理,并压缩后得到的“图像”文件。

如果你的相机能给你RAW格式的文件,尽量用它。道理很简单:你的电脑和电脑上的软件,和相机上的软件相比,功能上不知要强大多少倍。RAW文件的后期处理要多一道从“数码”文件到“图像”文件的转换,但完全在你的控制之下。如果你想得到更好的照片,值。

顺便说一句,任何数码照片(图像文件,例如jpg,tiff,psd,bmp......)都是经过“后期处理”才得到的,不管是你处理的,还是相机处理的。

滑动窗口协议

    TCP的首部中有一个很重要的字段就是16位长的窗口大小,它出现在每一个TCP数据报中,配合32位的确认序号,用于向对端通告本地socket的接收窗口大小。也就是说,如果本地socket发送一个TCP数据,其32位确认序号是5,窗口大小是5840,则用于告诉对端,对端已经发出的4个字节的数据已经收到并确认,接下来,本地socket最多能够接收从第5个字节开始的5840个字节长度的数据。这是由接收方进行的一种流量控制,接收方通过告诉发送方自己所能够接收数据的大小,达到控制发送方发送速度的目的。

    结构体struct tcp_sock中有很多成员数据跟滑动窗口协议相关,需要注意的是这里讲的滑动窗口都是指本地socket的接收窗口。

    成员window_clamp表示滑动窗口的最大值,滑动窗口的大小在变化的过程中不能超出这个值。它在TCP连接建立的时候被初始化,被置为最大的16位整数左移窗口的扩大因子,因为滑动窗口在TCP首部中以16位表示,window_clamp太大会导致滑动窗口不能在TCP首部中表示。

    成员rx_opt是一个struct tcp_options_received结构体,它有两个成员snd_wscale和rcv_wscale,分别表示来自对端通告的滑动窗口扩大因子(本地发送数据报时需要遵守),和本地接收滑动窗口的扩大因子。snd_wscale从来自对端的第一个SYN中获取。rcv_wscale在本地socket建立连接时初始化,它赋值的原则是使16位整数的最大值左移rcv_wscale后,至少可以达到整个接收缓存的最大值。接收缓存最大值在协议栈中由全局变量mysysctl_rmem_max表示,它是256*(256+sizeof(struct sk_buff))后的值,为107520,但sysctl_tcp_rmem[3]所表示的接收缓存的上限更大,为174760,所以,取后者,这样的话,rcv_wscale的值几乎可以说是固定的,为2。所以window_clamp的值就是 65535 << 2 = 262140。可见,window_clamp的值超出了接收缓存的最大值,但这没有关系,因为在滑动窗口增长的时候,会考虑接收缓存的大小这个因素的。

    rcv_wnd表示当前的接收窗口的大小,这个值在接收到来自对端的数据后,会变动的。它的初始值取接收缓存大小的3/4跟MAX_TCP_WINDOW之间的最小值,MAX_TCP_WINDOW在系统中的定义为32767U。然后,还要根据mss的值作一个调整,调整逻辑是:如果mss大于3*1460,则如果当前的rcv_wnd大于两倍的mss,就取两倍的mss作为rcv_wnd的值;如果mss大于1460,则如果当前的rcv_wnd大于3倍的mss,就取3倍的mss作为rcv_wnd的新值;否则,如果rcv_wnd大于4倍的mss,就取4倍的mss作为rcv_wnd的新值,我们的实验环境的mss值为1448(因为tcp首部有12字节的时间戳选项),所以rcv_wnd最后被调整为1448*4=5792。

    rcv_ssthresh是当前的接收窗口大小的一个阀值,其初始值就置为rcv_wnd。它跟rcv_wnd配合工作,当本地socket收到数据报,并满足一定条件时,增长rcv_ssthresh的值,在下一次发送数据报组建TCP首部时,需要通告对端当前的接收窗口大小,这时需要更新rcv_wnd,此时rcv_wnd的取值不能超过rcv_ssthresh的值。两者配合,达到一个滑动窗口大小缓慢增长的效果。

    rcv_wup记录滑动窗口的左边沿,即落在滑动窗口中的最小的一个序号。这样的话,rcv_wup+rcv_wnd即为滑动窗口的右边沿,rcv_wup+rcv_wnd-rcv_nxt即为滑动窗口的空白部分。它的初始值为0,在移动滑动窗口时被更新。    以上是关于接收滑动窗口的几个相关数据,下面我们看看它们是如何运用在TCP协议的通讯中的。

    每次发送一个TCP数据报,都要构建TCP首部,这时,会调用mytcp_select_window选择窗口大小,窗口大小选择的基本思想是接收缓存剩余空间大小的3/4,但是不能超过rcv_ssthresh的大小。但是,如果这个新选择的窗口大小比当前窗口的剩余大小还小,则以当前窗口的剩余大小作为新窗口的大小。同时右移左边沿,令rcv_wup=rcv_nxt。这个新选择的窗口是受rcv_ssthresh限制的,一般不会有什么问题,但我们可以看到代码中还是作了一些上限判断,如果扩大因子为0,则窗口大小不能超过32767U,否则不能超过65535左移扩大因子后的值。

    每次接收到来自对端的一个TCP数据报,且数据报长度大于128字节时,我们需要调用mytcp_grow_window,增加rcv_ssthresh的值,一般每次为rcv_ssthresh增长两倍的mss,增加的条件是rcv_ssthresh小于window_clamp,并且rcv_ssthresh小于接收缓存剩余空间的3/4,同时mytcp_memory_pressure没有被置位(即接收缓存中的数据量没有太大)。mytcp_grow_window中对新收到的skb的长度还有一些限制,并不总是增长rcv_ssthresh的值。具体见函数代码。

    以上是关于接收窗口,下面简单看一下发送窗口。关于发送窗口,在struct tcp_sock中也有一些成员数据相关。

    snd_wl1记录发送窗口更新时,造成窗口更新的那个ACK数据报的第一个序号。它主要用于在下一次判断是否需要更新发送窗口。

    snd_wnd是发送窗口的大小,直接取值于来自对端的数据报的TCP首部。

    max_window记录来自对端通告的窗口的最大值。

    snd_una表示当前正等待ACK的第一个序号,而发送窗口实际上是在每次收到来自对端的ACK后,都会更新,所以,实际上snd_una成了发送窗口的左边沿。


LInux Tcp 延迟确认问题

案例一:同事随手写个压力测试程序,其实现逻辑为:每秒钟先连续发N个132字节的包,然后连续收N个由后台服务回显回来的132字节包。其代码简化如下:

char sndBuf[132];

char rcvBuf[132];

while (1) {

    for (int i = 0; i < N; i++){

        send(fd, sndBuf, sizeof(sndBuf), 0);

        ...    

    }

    for (int i = 0; i < N; i++) {

        recv(fd, rcvBuf, sizeof(rcvBuf), 0);

        ...

    }

    sleep(1);

}

在实际测试中发现,当N大于等于3的情况,第2秒之后,每次第三个recv调用,总会阻塞40毫秒左右,但在分析Server端日志时,发现所有请求在Server端处理时耗均在2ms以下。

当时的具体定位过程如下:先试图用strace跟踪客户端进程,但奇怪的是:一旦strace attach上进程,所有收发又都正常,不会有阻塞现象,一旦退出strace,问题重现。经同事提醒,很可能是strace改变了程序或系统的某些东西(这个问题现在也还没搞清楚),于是再用tcpdump抓包分析,发现Server后端在回现应答包后,Client端并没有立即对该数据进行ACK确认,而是等待了近40毫秒后才确认。经过Google,并查阅《TCP/IP详解卷一:协议》得知,此即TCP的延迟确认(Delayed Ack)机制。

其解决办法如下:在recv系统调用后,调用一次setsockopt函数,设置TCP_QUICKACK。最终代码如下:

char sndBuf[132];

char rcvBuf[132];

while (1) {

    for (int i = 0; i < N; i++) {

        send(fd, sndBuf, 132, 0);

        ...    

    }

    for (int i = 0; i < N; i++) {

        recv(fd, rcvBuf, 132, 0); 

        setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int)); 

    }

    sleep(1);

}

 

案例二:在营销平台内存化CDKEY版本做性能测试时,发现请求时耗分布异常:90%的请求均在2ms以内,而10%左右时耗始终在38-42ms之间,这是一个很有规律的数字:40ms。因为之前经历过案例一,所以猜测同样是因为延迟确认机制引起的时耗问题,经过简单的抓包验证后,通过设置TCP_QUICKACK选项,得以解决时延问题。

 

延迟确认机制

在《TCP/IP详解卷一:协议》第19章对其进行原理进行了详细描述:TCP在处理交互数据流(即Interactive Data Flow,区别于Bulk Data Flow,即成块数据流,典型的交互数据流如telnet、rlogin等)时,采用了Delayed Ack机制以及Nagle算法来减少小分组数目。

书上已经对这两种机制的原理讲的很清晰,这里不再做复述。本文后续部分将通过分析TCP/IP在Linux下的实现,来解释一下TCP的延迟确认机制。

 

1、为什么TCP延迟确认会导致延迟?

其实仅有延迟确认机制,是不会导致请求延迟的(初以为是必须等到ACK包发出去,recv系统调用才会返回)。一般来说,只有当该机制与Nagle算法或拥塞控制(慢启动或拥塞避免)混合作用时,才可能会导致时耗增长。我们下面来详细看看是如何相互作用的:

延迟确认与Nagle算法

我们先看看Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):

1)如果包长度达到MSS,则允许发送;

2)如果该包含有FIN,则允许发送;

3)设置了TCP_NODELAY选项,则允许发送;

4)未设置TCP_CORK选项时,若所有发出去的包均被确认,或所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送。

对于规则4),就是说要求一个TCP连接上最多只能有一个未被确认的小数据包,在该分组的确认到达之前,不能发送其他的小数据包。如果某个小分组的确认被延迟了(案例中的40ms),那么后续小分组的发送就会相应的延迟。也就是说延迟确认影响的并不是被延迟确认的那个数据包,而是后续的应答包。

1 00:44:37.878027 IP 172.25.38.135.44792 > 172.25.81.16.9877: S 3512052379:3512052379(0) win 5840 <mss 1448,wscale 7>

2 00:44:37.878045 IP 172.25.81.16.9877 > 172.25.38.135.44792: S 3581620571:3581620571(0) ack 3512052380 win 5792 <mss 1460,wscale 2>

3 00:44:37.879080 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 1 win 46

......

4 00:44:38.885325 IP 172.25.38.135.44792 > 172.25.81.16.9877: P 1321:1453(132) ack 1321 win 86

5 00:44:38.886037 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 1321:1453(132) ack 1453 win 2310

6 00:44:38.887174 IP 172.25.38.135.44792 > 172.25.81.16.9877: P 1453:2641(1188) ack 1453 win 102

7 00:44:38.887888 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 1453:2476(1023) ack 2641 win 2904

8 00:44:38.925270 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 2476 win 118

9 00:44:38.925276 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 2476:2641(165) ack 2641 win 2904

10 00:44:38.926328 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 2641 win 134

从上面的tcpdump抓包分析看,第8个包是延迟确认的,而第9个包的数据,在Server端(172.25.81.16)虽然早就已放到TCP发送缓冲区里面(应用层调用的send已经返回)了,但按照Nagle算法,第9个包需要等到第个7包(小于MSS)的ACK到达后才能发出。

 

延迟确认与拥塞控制

我们先利用TCP_NODELAY选项关闭Nagle算法,再来分析延迟确认与TCP拥塞控制是如何互相作用的。

慢启动:TCP的发送方维护一个拥塞窗口,记为cwnd。TCP连接建立是,该值初始化为1个报文段,每收到一个ACK,该值就增加1个报文段。发送方取拥塞窗口与通告窗口(与滑动窗口机制对应)中的最小值作为发送上限(拥塞窗口是发送方使用的流控,而通告窗口则是接收方使用的流控)。发送方开始发送1个报文段,收到ACK后,cwnd从1增加到2,即可以发送2个报文段,当收到这两个报文段的ACK后,cwnd就增加为4,即指数增长:例如第一个RTT内,发送一个包,并收到其ACK,cwnd增加1,而第二个RTT内,可以发送两个包,并收到对应的两个ACK,则cwnd每收到一个ACK就增加1,最终变为4,实现了指数增长。

在Linux实现里,并不是每收到一个ACK包,cwnd就增加1,如果在收到ACK时,并没有其他数据包在等待被ACK,则不增加。

本人使用案例1的测试代码,在实际测试中,cwnd从初始值2开始,最终保持3个报文段的值,tcpdump结果如下:

1 16:46:14.288604 IP 172.16.1.3.1913 > 172.16.1.2.20001: S 1324697951:1324697951(0) win 5840 <mss 1460,wscale 2>

2 16:46:14.289549 IP 172.16.1.2.20001 > 172.16.1.3.1913: S 2866427156:2866427156(0) ack 1324697952 win 5792 <mss 1460,wscale 2>

3 16:46:14.288690 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 1 win 1460

......

4 16:46:15.327493 IP 172.16.1.3.1913 > 172.16.1.2.20001: P 1321:1453(132) ack 1321 win 4140

5 16:46:15.329749 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1321:1453(132) ack 1453 win 2904

6 16:46:15.330001 IP 172.16.1.3.1913 > 172.16.1.2.20001: P 1453:2641(1188) ack 1453 win 4140

7 16:46:15.333629 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1453:1585(132) ack 2641 win 3498

8 16:46:15.337629 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1585:1717(132) ack 2641 win 3498

9 16:46:15.340035 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1717:1849(132) ack 2641 win 3498

10 16:46:15.371416 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 1849 win 4140

11 16:46:15.371461 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1849:2641(792) ack 2641 win 3498

12 16:46:15.371581 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 2641 win 4536

上表中的包,是在设置TCP_NODELAY,且cwnd已经增长到3的情况,第7、8、9发出后,受限于拥塞窗口大小,即使此时TCP缓冲区有数据可以发送亦不能继续发送,即第11个包必须等到第10个包到达后,才能发出,而第10个包明显有一个40ms的延迟。

 

注:通过getsockopt的TCP_INFO选项(man 7 tcp)可以查看TCP连接的详细信息,例如当前拥塞窗口大小,MSS等。

 

2、为什么是40ms?这个时间能不能调整呢?

首先在redhat的官方文档中,有如下说明:

一些应用在发送小的报文时,可能会因为TCP的Delayed Ack机制,导致一定的延迟。其值默认为40ms。可以通过修改tcp_delack_min,调整系统级别的最小延迟确认时间。例如:

         # echo 1 > /proc/sys/net/ipv4/tcp_delack_min

即是期望设置最小的延迟确认超时时间为1ms。

不过在slackware和suse系统下,均未找到这个选项,也就是说40ms这个最小值,在这两个系统下,是无法通过配置调整的。

 

linux-2.6.39.1/net/tcp.h下有如下一个宏定义:

#define TCP_DELACK_MIN    ((unsigned)(HZ/25))    /* minimal time to delay before sending an ACK */

注:Linux内核每隔固定周期会发出timer interrupt(IRQ 0),HZ是用来定义每秒有几次timer interrupts的。举例来说,HZ为1000,代表每秒有1000次timer interrupts。HZ可在编译内核时设置。在我们现有服务器上跑的系统,HZ值均为250。

以此可知,最小的延迟确认时间为40ms。

TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。具体调整算法,可以参考linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564的tcp_event_data_recv函数。

 

3、为什么TCP_QUICKACK需要在每次调用recv后重新设置?

在man 7 tcp中,有如下说明:

TCP_QUICKACK

         Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are         sent immediately, rather than delayed if needed in accordance to normal TCP operation.   This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent         operation of the TCP protocol will once again enter/leave quickack mode depending on      internal protocol processing and factors such as delayed ack timeouts occurring and data         transfer. This option should not be used in code intended to be portable.

手册中明确描述TCP_QUICKACK不是永久的。那么其具体实现是如何的呢?参考setsockopt函数关于TCP_QUICKACK选项的实现:

case TCP_QUICKACK:

    if (!val) {

        icsk->icsk_ack.pingpong = 1;

    } else {

        icsk->icsk_ack.pingpong = 0;

        if ((1 << sk->sk_state) &

        (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&

        inet_csk_ack_scheduled(sk)) {

            icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;

            tcp_cleanup_rbuf(sk, 1);

            if (!(val & 1))

            icsk->icsk_ack.pingpong = 1;

            }

    }

    break;

其实linux下socket有一个pingpong属性来表明当前链接是否为交互数据流,如其值为1,则表明为交互数据流,会使用延迟确认机制。但是pingpong这个值是会动态变化的。例如TCP链接在要发送一个数据包时,会执行如下函数(linux-2.6.39.1/net/ipv4/tcp_output.c, Line 156):

/* Congestion state accounting after a packet has been sent. */

static void tcp_event_data_sent(struct tcp_sock *tp,

                                     struct sk_buff *skb, struct sock *sk)

{

         ......

         tp->lsndtime = now;

         /* If it is a reply for ato after last received

          * packet, enter pingpong mode.

          */

         if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)

                   icsk->icsk_ack.pingpong = 1;

}

最后两行代码说明:如果当前时间与最近一次接受数据包的时间间隔小于计算的延迟确认超时时间,则重新进入交互数据流模式。也可以这么理解:延迟确认机制被确认有效时,会自动进入交互式。

通过以上分析可知,TCP_QUICKACK选项是需要在每次调用recv后重新设置的。

 

4、为什么不是所有包都延迟确认?

TCP实现里,用tcp_in_quickack_mode(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 197)这个函数来判断是否需要立即发送ACK。其函数实现如下:

/* Send ACKs quickly, if "quick" count is not exhausted

 * and the session is not interactive.

 */

static inline int tcp_in_quickack_mode(const struct sock *sk)

{

         const struct inet_connection_sock *icsk = inet_csk(sk);

         return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;

}

要求满足两个条件才能算是quickack模式:

1、pingpong被设置为0。

2、快速确认数(quick)必须为非0。

关于pingpong这个值,在前面有描述。而quick这个属性其代码中的注释为:scheduled number of quick acks,即快速确认的包数量,每次进入quickack模式,quick被初始化为接收窗口除以2倍MSS值(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 174),每次发送一个ACK包,quick即被减1。

 

5、关于TCP_CORK选项

TCP_CORK选项与TCP_NODELAY一样,是控制Nagle化的。

1、打开TCP_NODELAY选项,则意味着无论数据包是多么的小,都立即发送(不考虑拥塞窗口)。

2、如果将TCP连接比喻为一个管道,那TCP_CORK选项的作用就像一个塞子。设置TCP_CORK选项,就是用塞子塞住管道,而取消TCP_CORK选项,就是将塞子拔掉。例如下面这段代码:

int on = 1;

setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //set TCP_CORK

write(sockfd, ...);    //e.g., http header

sendfile(sockfd, ...); //e.g., http body

on = 0;

setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //unset TCP_CORK

当TCP_CORK选项被设置时,TCP链接不会发送任何的小包,即只有当数据量达到MSS时,才会被发送。当数据传输完成时,通常需要取消该选项,以便被塞住,但是又不够MSS大小的包能及时发出去。如果应用程序确定能一起发送多个数据集合(例如HTTP响应的头和正文),建议设置TCP_CORK选项,这样在这些数据之间不存在延迟。为提升性能及吞吐量,Web Server、文件服务器这一类一般会使用该选项。

著名的高性能Web服务器Nginx,在使用sendfile模式的情况下,可以设置打开TCP_CORK选项:将nginx.conf配置文件里的tcp_nopush配置为on。(TCP_NOPUSH与TCP_CORK两个选项实现功能类似,只不过NOPUSH是BSD下的实现,而CORK是Linux下的实现)。另外Nginx为了减少系统调用,追求性能极致,针对短连接(一般传送完数据后,立即主动关闭连接,对于Keep-Alive的HTTP持久连接除外),程序并不通过setsockopt调用取消TCP_CORK选项,因为关闭连接会自动取消TCP_CORK选项,将剩余数据发出。



tcp要点学习-数据发送一


tcp要点学习-数据发送一

1. 什么是delayed ack algorithm
   delayed ack algorithm也就是中所谓的"经受时延的确认"(翻译得真饶舌 = =||)。在RFC1122中提到delayed ack
   的概念:
 

  "
      A host that is receiving a stream of TCP data segments can
      increase efficiency in both the Internet and the hosts by
      sending fewer than one ACK (acknowledgment) segment per data
      segment received; this is known as a "delayed ACK" [TCP:5].
    "

   我在之前提到过,TCP在收到每一个数据包时,都会发送一个ACK报文给对方,用以告诉对方"我接收到你刚才发送的数据了"。并
   且会在报文的确认号字段中标志希望接收到的数据包。

   但是,如你所想,如果为每一个接收到的报文都发送一个ACK报文,那将会增加网络的负担。于是,为了解决这个问题,delayed
   ack被提出。也就是说,实现了delayed ack的TCP,并不见得会对每一个接收到的数据包发送ACK确认报文。

   实际情况是,TCP延迟发送这个ACK。延迟多久?中说的是200ms,在RFC1122中说的则是500ms。delayed ack有时候
   还会附加到数据报文段一起发送,如果在延迟时间内有报文段要发送的话,如果没有,那么当延迟时间到时,就单独发送ACK。

   在另一份文档中,作者讲到delayed ack的好处:
   a) to avoid the silly window syndrome;
   b) to allow ACKs to piggyback on a reply frame if one is ready to go when the stack decides to do the ACK;
   c) to allow the stack to send one ACK for several frames, if those frames arrive within the delay period.

   a) 所谓的糊涂窗口综合症(别人都这样翻译的,似乎有点搞笑:D)
   b) 将ACK与将要发送的数据报文一起发送
   c) 一个ack确认多个报文段,如果这几个报文段在延迟时间内到达

2. 什么是Nagle algoritm ?
   简而言之,nagle算法主要目的是减少网络流量,当你发送的数据包太小时,TCP并不立即发送该数据包,而是缓存起来直到数据包
   到达一定大小后才发送。(improving the efficiency of TCP/IP networks by reducing the number of packets that need to
   be sent over the network.)

   关于这个算法,我觉得wikipedia上讲的比较好。具体点说,当上层提交数据给TCP时,TCP觉得你的数据太小了(套用一般的例子,
   如果你要发送1一个字节的数据,当附加上TCP和IP头后,数据包通常就会增加到41字节,那么这显然是低效的),就缓存你的数据,
   当数据缓存到一定长度后,如果之前发送的数据得到了ACK确认且接收方有足够空间容纳数据,就发送这些数据,否则继续等待。

   wikipedia上给了一段nagle的伪代码:

if there is new data to send
     if the window size >= MSS and available data is >= MSS
       send complete MSS segment now
     else
       if there is unconfirmed data still in the pipe
         enqueue data in the buffer until an acknowledge is received
       else
         send data immediately
       end if
     end if
   end if

 
   TCP socket提供了关闭nagle算法的接口,你可以通过TCP_NODELAY选项决定是否开启该算法。不过MSDN上建议不要关闭此算法。如果
   你发送的数据不至于很小的话(<40byte),我也不建议你关闭。

TCP数据传输策略


目前建立在TCP协议上的网络协议特别多,有telnet,ssh,有ftp,有http等等。这些协议又可以根据数据吞吐量来大致分成两大类:(1)交互数据类型,例如telet,ssh,这种类型的协议在大多数情况下只是做小流量的数据交换,比如说按一下键盘,回显一些文字等等。(2)数据成块类型,例如ftp,这种类型的协议要求TCP能尽量的运载数据,把数据的吞吐量做到最大,并尽可能的提高效率。针对这两种情况,TCP给出了两种不同的策略来进行数据传输。

1.TCP的交互数据流

对于交互性要求比较高的应用,TCP给出两个策略来提高发送效率和减低网络负担:(1)捎带ACK。(2)Nagle算法(一次尽量多的发数据)。通常,在网络速度很快的情况下,比如用lo接口进行telnet通信,当按下字母键并要求回显的时候,客户端和服务器将经历 发送按键数据->服务器发送按键数据的ack -> 服务器端发送回显数据->客户端发送回显数据的ACK的过程,而其中的数据流量将是40bit + 41bit+41bit+40bit = 162bit,如果在广域网里面,这种小分组的TCP流量将会造成很大的网络负担。

1.1.捎带ACK的发送方式

这个策略是说,当主机收到远程主机的TCP数据报之后,通常不马上发送ACK数据报,而是等上一个短暂的时间,如果这段时间里面主机还有发送到远程主机的TCP数据报,那么就把这个ACK数据报“捎带”着发送出去,把本来两个TCP数据报整合成一个发送。一般的,这个时间是200ms。可以明显地看到这个策略可以把TCP数据报的利用率提高很多。

1.2.Nagle算法

上过bbs的人应该都会有感受,就是在网络慢的时候发贴,有时键入一串字符串以后,经过一段时间,客户端“发疯”一样突然回显出很多内容,就好像数据一下子传过来了一样,这就是Nagle算法的作用。

Nagle算法是说,当主机A给主机B发送了一个TCP数据报并进入等待主机B的ACK数据报的状态时,TCP的输出缓冲区里面只能有一个TCP数据报,并且,这个数据报不断地收集后来的数据,整合成一个大的数据报,等到B主机的ACK包一到,就把这些数据“一股脑”的发送出去。虽然这样的描述有些不准确,但还算形象和易于理解,我们同样可以体会到这个策略对于低减网络负担的好处。

在编写插口程序的时候,可以通过TCP_NODELAY来关闭这个算法。并且,使用这个算法看情况的,比如基于TCP的X窗口协议,如果处理鼠标事件时还是用这个算法,那么“延迟”可就非常大了。

2.TCP的成块数据流

对于FTP这样对于数据吞吐量有较高要求的要求,将总是希望每次尽量多的发送数据到对方主机,就算是有点“延迟”也无所谓。TCP也提供了一整套的策略来支持这样的需求。TCP协议中有16个bit表示“窗口”的大小,这是这些策略的核心。

2.1.传输数据时ACK的问题

在解释滑动窗口前,需要看看ACK的应答策略,一般来说,发送端发送一个TCP数据报,那么接收端就应该发送一个ACK数据报。但是事实上却不是这样,发送端将会连续发送数据尽量填满接受方的缓冲区,而接受方对这些数据只要发送一个ACK报文来回应就可以了,这就是ACK的累积特性,这个特性大大减少了发送端和接收端的负担。

2.2.滑动窗口

滑动窗口本质上是描述接受方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接受方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接受方发送窗口大小不为0的数据报的到来。书中的P211和P212很好的解释了这一点。

关于滑动窗口协议,书上还介绍了三个术语,分别是:

  1. 窗口合拢:当窗口从左边向右边靠近的时候,这种现象发生在数据被发送和确认的时候。
  2. 窗口张开:当窗口的右边沿向右边移动的时候,这种现象发生在接受端处理了数据以后。
  3. 窗口收缩:当窗口的右边沿向左边移动的时候,这种现象不常发生。

TCP就是用这个窗口,慢慢的从数据的左边移动到右边,把处于窗口范围内的数据发送出去(但不用发送所有,只是处于窗口内的数据可以发送。)。这就是窗口的意义。图20-6解释了这一点。窗口的大小是可以通过socket来制定的,4096并不是最理想的窗口大小,而16384则可以使吞吐量大大的增加。

2.3.数据拥塞

上面的策略用于局域网内传输还可以,但是用在广域网中就可能会出现问题,最大的问题就是当传输时出现了瓶颈(比如说一定要经过一个slip低速链路)所产生的大量数据堵塞问题(拥塞),为了解决这个问题,TCP发送方需要确认连接双方的线路的数据最大吞吐量是多少。这,就是所谓的拥塞窗口。

拥塞窗口的原理很简单,TCP发送方首先发送一个数据报,然后等待对方的回应,得到回应后就把这个窗口的大小加倍,然后连续发送两个数据报,等到对方回应以后,再把这个窗口加倍(先是2的指数倍,到一定程度后就变成现行增长,这就是所谓的慢启动),发送更多的数据报,直到出现超时错误,这样,发送端就了解到了通信双方的线路承载能力,也就确定了拥塞窗口的大小,发送方就用这个拥塞窗口的大小发送数据。要观察这个现象是非常容易的,我们一般在下载数据的时候,速度都是慢慢“冲起来的”




看了以前的一个文档,不禁老泪纵横

时间

故障

解决方法

2008-1-10

从电源开关出去,各路电压输出均不正常。

经分析应该是接地不对导致,把GND同电源负极相接即解决,各路电压输出正常。

2008-1-14

CPU时钟无输出导致以太芯片无法工作

初步怀疑由于FLASH未焊接,导致CPU没有启动,从而没有输出25MPHY信号。(后来证明是CPU没有被正确配置寄存器,所以时钟输出不正常,连好SPI线并从EEPROM获得配置代码后,CPU即成功输出25MHZ时钟,以太芯片工作正常)

2008-1-14

以太芯片工作不正常,开机瞬间插座无亮灯指示,芯片过热,插入网线没有接通显示

1:经分析,由于CPU没有起来,导致复位的GPIOpin为低电平以致以太芯片始终处于复位状态,所以把以太芯片ADM6996复位pin直接接3v3,即解决开机无亮灯指示的问题。

2:由于CPU25MHZ时钟没有输出,故还无法解决网线无法接通问题

3:芯片过热问题尚未解决怀疑是因为第一条原因,即芯片不停地复位造成芯片过热。

2008-1.17

CPU没有正常运行,串口打印数据出现乱码

。。。

2008-1-18

串口电平不正常

经检查,是原理图有一点接地有误

此处没有接地,导致MAX3221转换电压 不正常

2008-1-21

超级终端成功打印boot信息(证明CPU已经成功启动并且开始BOOT),然而并不稳定,关闭板子再打开就没有消息打印了

怀疑是某点电平不稳定,检查电路中……

2008-1-22

串口打印boot消息终于正常

终于发现原来是jtag信号的原因,现在串口终于可以打印出boot信号了

以太时钟有输出,但是以太芯片并未正常工作

故障寻找中

EEPROM工作正常、以太芯片工作正常

加载片选信号后eeprom即工作正常

Eeprom加载后,时钟输出正常,以太芯片正确复位后工作正常,可以实现交换功能

2008-1-25

可以实现从网口boot,但是启动到一定阶段,超级终端就死了。

怀疑是Conexant提供的flash.bin中的部分驱动有问题,准备重新编译我们自己的bin

2008-1-28

配置rehhat linux。配置TFTP,配置DHCP,以便使得板子可以通过网口下载flash.bin到内存中执行之

 通过无数次测试,更改,板子终于可以在linux环境下利用DHCP服务器获取IP地址并获得boot文件的位置,然后利用本机启动的TFTP服务器下载flash.bin到内存中(虽然执行到一定程度时会死机)

2008-1-29

完成flash内核编译,并且成功down到板子上

1:利用Conexant提供的flash.bin会在启动时死机,怀疑是由于isos中会初始化一些硬件,而这些硬件可能在我的板子上还没有起来,所以考虑自己编译flash.bin

2:参考科胜讯的编译方法,无法成功编译完成CX94615flash.bin,然后通过分析Makefile,mkfflash文件,找出问题所在,成功编译成功flash.bin

3:把第二步中编译好的flash.bin通过TFTPDHCP下载到板子上,linux boot正常,但是会卡死在一个叫EHCI的进程上,通过查阅资料得知,EHCIusb支持热插拔的一个特性。所以重新编译linux内核,让其不加载usb部分,重新生成flash.bindownload到板子上,成功启动linux。(由此怀疑是USB部分有问题,而我的USB部分的电源管理芯片没有焊接)。

4:在linux命令行下输入startbsp启动conexant的驱动后,网口工作正常,可以ifconfig eth0.

 

2008-1-30

测试WLAN

Wlan可以工作,但信号较弱

2008-1-31

以太芯片port4工作不正常。表现为,有时插上网线没反应,有时正常。

由于其他四个LAN口工作正常,而仅仅port4工作不正常,所以通过比较,最终觉得可能是对port4的配置不正确。由MAC MII改成PHY interface后,port4即工作正常。

2008-2-15

编译内核,测试科胜讯demo板,可以正常读取flashchip id,也可以通过MTDdriver来实现烧写flash。但是对于科胜讯方案的intel flash,依旧无法读取flashid。所以开始检查硬件电路。

 

 

2008-2-19

SLIC部分硬件ID无法读取,怀疑是SPI原因。因为SPIEEPROM已经相接。

 

2008-2-21

USB部分电源管理芯片未焊接,12MHZ晶体连线错误

晶体连线错误解决方法考虑中…..

可以在xtaloutxtalin之间并联1M欧姆电阻解决.

2008-2-26

里程碑式进展,终于读取出flash芯片的chip id

片选信号CE接错(多方面导致接错,最主要的原因还是由于自己画图时大意,过于相信intel的技术支持,调试时固执地以为自己的片选是正确的),去掉非门,低电平有效,则flash成功选中,chip id成功读出

2008-2-27

Boot.bin无法成功从flash启动

1cat boot.bin >/dev/mtdblock0而不是按照文档中所说的至/dev/mtd0

2:几经周折,copy cofig.bin/tftpboot,运行configflash,成功boot

文件系统jffs2.bin无法烧录入flash

分析是由于内存不足,所以先删除部分文件(内存中的部分文件),然后成功烧写,也成功启动linux至文件系统

文件系统不可写,mkdir后出现错误

怀疑是内存不足,问题发现解决中….

2008-2-28

文件系统成功可写

证明是flash容量不足所致 分析过程:

Lxcmdlineinfo set 768k(kernel) 文件系统不可写而set 704k(kernel)文件系统可写,由此判断是flash容量不足所致。因此精简文件系统和内核,成功实现文件系统可写。

就其原因:

 

1intelflash的质量太次,相比spansion64K/block intel的是128k/block 所以空间利用率降低

2flash容量选择偏小^_^

2008-2-29

集中调试slic部分。

 

由于conexant默认是把le88221连接在spi1上而我是连接在spi0上的。所以修改spi_CS信号后,重新编译驱动,startbsp后,可以读出le88221chipid

2008-3-3

Slic部分,pcm时钟无输出

由于可以读出chip id,所以认为spi和硬件部分没问题。问题转向对于pcm时钟设置部分(设置CPUpcm时钟。)

2008-3-4

USB初始化成功Slic初始化失败

1:在usb部分的12MHZ时钟输入输出脚并联一个1M欧姆电阻,晶体正常起振。由于电源芯片已经焊接,所以USB设备可以正常初始化。并且可以mount到文件系统上。

2:尝试加载ISOSflash.bin(USB部分未焊好之前,加载ISOS,会死机),可以正常启动。PCM时钟正常为2.048Mhz。但是slic部分初始化依旧失败,有可能是因为isosslic部分驱动是利用spi1来配置le88221的。

2008-3-10

Pcm时钟正常输出。Slic设备有馈电

Slic部分电路有很大噪声

1Conexant技术支持修改了so4615-rd.hw硬件配置文件,然后pcm时钟可以正常输出

2:修改slic设备的参数,有馈电输出

3:噪声问题暂没找到原因,怀疑是某电容有问题

2008-3-11

Slic部分噪声消失

…..噪声消失原因未明

2008-3-12

slic部分结合lvoice进行调试

 

2008-3-13

运行lvoice有馈电,但是没有拨号音,没有振铃

 

1:加载我们自己的le88221参数没有拨号音,lvoice工作不正常。

2:加载conexant的配置参数,运行lvoice,一切正常,可以拨号,可以通话

最后分析,发现,我写的参数配置文件不对,在参数文件中,不能包括对通道12的初始化,因为lvoice中会包含这些。

修改我们的配置文件,也可以出现拨号音以及振铃

 

3:通过分析,觉得电路板噪声应该是slic没有正确初始化所致。因为运行lvoice后就不会有噪声出现。

截至2008-3-14,完成R1A所有硬件的调测,包括cpu,以太芯片,slicwifiusb功能调试均已完成

2008-5-12

可以读取flashchipid,无法擦除flash以及烧写flash

 

由于R1bfflash容量增加至16MB,需要conexant提供gpio扩展的代码

2008-5-17

Conexant修改代码后,可以成功写入flash,但是Boot.bin写入到flash后,依旧无法从flashboot起来

Stun_gpio3未正确下拉

2008-5-20

Wififirmware加载后出现乱码,cpu必须重启才能正常

发现是由于时钟的问题:原因是由于插入wifi驱动会复位50321芯片导致cpu输入时钟消失。只需修改高速非门器件即可,原理图不必更改

2008-5-21

Usb时钟未正确输出

需要在晶体输出输入端并联一个106的电阻

2008-5-25

内核启动会卡死在ehci部分,usb无法挂在

1.8V电源同3.3V电源相比,必须加一个延迟

R1B方案所有模块调试完成

 


总结(按模块)

 

1CPU模块

串口电平不正常,打印乱码

Max3221有一处电容没有接地

串口信息可以成功打印boot,但是极不稳定

Jtag信号没有接地,导致CPU启动后进入JTAG test模式

 

2:电源模块 

   上电后,各路电平均不正常,经检查,是由于GND最终没有同CGND相连

 

3flash模块

 无法读出chip id 无法烧写flash

片选信号接错,低电平有效错误地接成了高电平有效。加上修改源代码flash.c中的读写时序,即可实现对flash chipid的读取。配合linuxmtd driver,可以实现flash的读写

 

 4:以太芯片模块

以太芯片四个口均不亮,时钟信号不稳定,仅15MHZ

加载EEPROM后,时钟正常,五口均正常工作,eeprom中包含了对CPU时钟配置的代码

单个MII,五口均属于交换,但是"wan"口不稳定

修改配置电阻.使一台芯片工作于phy mode,即稳定工作

 

5slic模块

编译驱动,加载,无时钟,无馈电,内核打印slic设备初始化失败

conexant技术支持确认,修改硬件配置文件,可以实现slic设备的正确初始化

-------然后经证明,以前的编译方法并没有错,conexant默认的slic驱动并没有初始化时钟和配置参数,只要加载lvoiceok了。

电路板有噪声

经分析,认为是slic初始化不对,加载lvoice后电路板即没有噪声

加载lvoice,时钟,馈电均正常,但是无法实现对呼,无振铃,无拨号音

经多次实验比较,最后用conexnat默认的配置参数可以成功实现对呼,比较两者参数后发现我们自己写的参数由于多加了对端口12的初始化导致lvoice无法完成

 

6USB模块

   12MHZ时钟无法稳定输出--------电路接错,去掉串联电阻,在xtaloutxtalin之间并联1M欧姆电阻,时钟即稳定正常输出。

   焊接好电源管理芯片后,USB模块即正常工作,可以发现USB设备,可以在linux内核下挂载,读写

 

7WIFI模块

   功能上没有问题,无线设备可以搜索到。但是信号太弱

 

8DSL模块

   暂未测试

 

9:内核编译

Flash.bin无法编译出来

Conexant的一系列文件有问题

默认的配置参数,flash有诸多错误

由于intelflash默认的块大小是128k,修改文件系统的配置即可

Flash可以烧写后,加载的文件系统不可写

由于flash容量较小,需要精简内核和文件系统