网络编程调参学习————三次握手中的接收缓存和通告窗口

来源:互联网 发布:最新软件代理 编辑:程序博客网 时间:2024/06/06 09:48
一、查资料

        在我的机器上,

cat /proc/sys/net/core/rmem_default,输出212992;

cat /proc/sys/net/core/rmem_max,212992;

        cat /proc/sys/net/ipv4/tcp_mem,输出23343   31127   46686。
        cat /proc/sys/net/ipv4/tcp_rmem,输出4096    87380   6291456。
        网上众说纷纭,还是看看权威:https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt和https://www.kernel.org/doc/Documentation/sysctl/net.txt。
        大意是tcp_mem的三个数是tcp总共能用到的最小内存页数,默认页数,和最大页数。三个值还代表三种内存申请模式,后面也许会讲到。
        tcp_rmem是单个socket给tcp的内存大小参数(单位B)。最小大小(默认一页);默认大小(This value overrides net.core.rmem_default used by other protocols,说是能覆盖rmem_default,但是汉语网页我查到的是说会被后者覆盖,这就是得看英文资料的原因);最大大小是缓存不够时最大能够分配这个数的缓存(这个数能够被rmem_max覆盖)。限制(最大值)会被全局的覆盖,个性化配置(默认值)会覆盖全局值,这个可以理解。tcp_rmem用于操作系统动态管理读缓存,如果用setsockopt设置了如SO_RCVBUF以后读缓存就不变了,三个参数会被忽略。
        文档还提到了两个参数(这两个参数)cat /proc/sys/net/ipv4/tcp_app_win,32;cat /proc/sys/net/ipv4/tcp_adv_win_scale,1。
        tcp_adv_win_scale,最后还是查到了百度上,找到了一篇,瞄一眼就觉得质量很高的文章,原来一看博主就是《nginx源码剖析》的作者,这本书陪我在地铁上过了好几个月,果断收藏http://blog.csdn.net/russell_tao/article/details/18711023(这种好文章本来前面的关键字比如tcp_mem也能匹配上,但是垃圾匹配太多就被抵下去了,tcp_adv_win_scale这种真正写得深入的文章才涉及的小关键字才能把这篇牛文顶上去)。这个参数决定了缓存有多大的比例能够作为活动窗口,我先举一个例子:


        1.通告窗口大小为缓存的100%,假设接收缓存10MB,网速100MB/s(高延迟那种,比特从发出到到达目的地要0.99秒,所以发10MB和2MB时间差在0.1s内),10MB接收方处理速度1MB/s,一次处理10MB之后才重新读(这个在编程时很正常,比如简单的统计词频),发送方有足够多的数据待发送。可以预见:
        第一秒:发送方根据通告窗口大小发10MB(其实可能有个慢启动的过程,我们不妨假设当前已经过了这个阶段达到平衡状态),第一秒末接收方收到并立即读取,发送ack+10MB窗口通告;
        第二秒:接收方处理10MB数据第一秒,发送方收到窗口通告发送的10MB;
        第三秒:接收方处理10MB数据第二秒,并且收到10MB再次填满发送方接收缓存,接收方发送ack+0窗口通告(可能还有些细节,比如ack的发送时机可能是等待某个定时器而非收到10MB立即发送);
        第三--第11秒:接收方一步一步直到处理完整个10MB,再次从缓存中读取10MB到用户空间,此后tcp通知发送方10MB窗口通告;
        ......发送方再次发送10MB瞬间填满接收方缓存,再等10s等待下一次接收方处理完毕后又一股脑儿的发。


        2.硬环境同1,只是通告窗口大小为缓存20%:
        第一秒:接收方收到发送方2MB窗口通告,马上发送数据;
        第二秒:接收方接到2MB,用户程序此时的read或者recv等马上读取2MB开始处理,接收方发送2MB通告窗口;
        第三秒:发送方收到通告,继续发送2MB;
        第四秒:因为时隔两秒,用户程序已经处理完之前的2MB,所以又调用read把刚刚到达的数据读入,发送方发送ack+2MB窗口通告;
        ......基本按照发送速率等于处理速率的方法进行网络交互。


        3.硬环境同1,通告窗口大小为10%:
        第一秒:1MB通告窗口大小收到,发送1MB;
        第二秒:收到1MB,read读取,发送1MB通告,
        第三好吧,至少确定了缓存绝不是越大越好这一点好吧,至少确定了缓存绝不是越大越好这一点好吧,至少确定了缓存绝不是越大越好这一点好吧,至少确定了缓存绝不是越大越好这一点好吧,至少确定了缓存绝不是越大越好这一点好吧,至少确定了缓存绝不是越大越好这一点。。。。。。秒:发送方收到通告并发送1MB,但是在第二秒接收方已经处理完了1MB,所以整个第三秒接收方应用程序在等待数据的“饥饿”状态中。
        ......用户程序处理一秒等一秒,发送方发1MB等一秒窗口通告。
        很显然3应用程序工作不饱满不可取。对于工作同样饱满的1和2来说,1隔十秒“脉冲”发送一次10MB,2隔一秒发送2MB,对于这个100MB/s的网络来说,尽管10MB是每隔10秒才发一次,但是只用10个发送方同时发送就能打满,而对2来说能够容纳50个发送方。


        不过分析了半天,貌似和缓存比例没啥联系,都是说的通告窗口大小。好吧,至少确定了即使在系统内存足够大的前提下缓存也不是越大越好这一点。
        其实例子2稍微改一下就能说明tcp_adv_win_scale的用途了:
        硬件环境大部分类似,只不过接收方处理速度“平均”是1MB/s,就是说有些数据处理时间长,有的短,为方便讨论,按照0MB0MB3MB间隔来。
        如果没有用户接收缓存,就是通告窗口等于接收缓存等于2MB。上面的讨论可见在1MB/s的处理效率下是没有问题的,但是如果是1MB/s的平均处理速度:
        第一秒:接收方收到2MB,但是本秒不处理数据;
        第二秒:接收方用户程序不处理,本秒无事;
        第二秒:本秒处理2MB,只能read到2MB,用户处理能力只能发挥三分之二,因为缓存被读取,所以发送窗口通告(大概率不和ack一起发送)2MB;
        第三秒:发送方收到窗口通告,继续发送2MB,这一秒用户程序本来就不需要数据;
        第四秒:2MB到达,用户程序全部读取,当然也只能发挥三分之二。
        ......这样,其实用户工作是不饱和的。
        如果缓存为6MB,通告窗口为3MB,假设用户程序处理能力“更强”,间隔为0MB3MB:
        一秒:收到3MB,尽管这一秒用户没有读取,但是因为接收缓存有剩余,所以通告对方窗口大小为3MB,发送方收到通告窗口就看起来好像接收方用户进程read了一样;
        二秒:秒初用户进程读取3MB,但当前通告窗口最大就是3MB,所以不通告;秒末收到3MB后立即发送3MB通告窗口大小;
        三秒:用户不读数据,秒末发送方收到通告窗口发送3MB;
        四秒:秒初用户读到3MB,秒末收到3MB后立即发送3MB通告;
        五秒:用户不读,秒末发送端收到通告后发送3MB;
        六秒:用户读,秒末收到3MB后发送通告;
        ...就是说发送端隔一秒收到通告发3MB,接收端隔一秒读3MB,读写达到平衡。这样一来,用户程序效率满载,网络也是隔一秒发一次3MB。


        总之,非交互的场景下接收端数据处理能力和网络流量的平均负载是相互制约的。对一个用户来说,网络流量的考虑算是公德,stackoverflow上就有一兄弟提问说“如果我就操蛋(selfish bastard),什么慢启动拥塞避免啥的都不考虑,那么我的tcp实现能不能就有就狂发,没有就一直通告窗口催?”(https://stackoverflow.com/questions/27964952/why-would-you-ever-use-tcp-instead-of-udp-if-you-implement-your-own-error-checki/27966260#27966260)比较有意思可以看下。


二、实验
        从一中得到了一些关键字:rmem_default、tcp_mem、tcp_rmem、tcp_adv_win_scale、SO_RCVBUF,也从纸面上讨论了tcp_adv_win_scale引发的用户进程、内核待用户读取读缓存、通报窗口读缓存等的几个case讨论,下面还是尽量从实际的实验确认一下。
        先来个基本的探探路:
        server.c
#include <stdio.h>#include <sys/socket.h>#include <netinet/in.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>void main(){    int lfd = socket(AF_INET,SOCK_STREAM,0);    struct sockaddr_in sa;    memset(&sa,0,sizeof(sa));    sa.sin_family = AF_INET;    sa.sin_port = htons(8888);    sa.sin_addr.s_addr = htonl(INADDR_ANY);    int optVal = 1;    setsockopt(lfd,SOL_SOCKET,SO_REUSEPORT,&optVal,sizeof(optVal));    bind(lfd,(struct sockaddr*)&sa,sizeof(sa));    listen(lfd,5);    int connfd = accept(lfd,NULL,NULL);    printf("connection established,begin to sleep 20s\n");    sleep(20);    char buf[100000];    read(connfd,buf,87380);    printf("read in 87380,begin to sleep 20s\n");    sleep(20);    return;}


        client.c
#include <stdio.h>#include <sys/socket.h>#include <netinet/in.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>void main(){    int sfd = socket(AF_INET,SOCK_STREAM,0);    struct sockaddr_in sa;    memset(&sa,0,sizeof(sa));    sa.sin_family = AF_INET;    sa.sin_port = htons(8888);    inet_pton(AF_INET,"127.0.0.1",&(sa.sin_addr));    char buf[100000];    connect(sfd,(struct sockaddr*)&sa,sizeof(sa));    printf("connection established,begin to send 87380 and sleep 5s\n");    write(sfd,buf,87380);    sleep(5);    printf("wake up,begin to send 87380 and sleep 5s,again\n");    write(sfd,buf,87380);    sleep(5);    printf("wake up,try to send 1\n");    write(sfd,buf,1);    printf("done,good bye\n");    return;}


        最基本的收发场景,linux网络编程前五章的内容。tcpdump抓包的三次握手:
17:10:09.871847 IP localhost.50249 > localhost.8888: Flags [S], seq 3802356432, win 43690, options [mss 65495,sackOK,TS val 1119954726 ecr 0,nop,wscale 7], length 0
17:10:09.871855 IP localhost.8888 > localhost.50249: Flags [S.], seq 523740337, ack 3802356433, win 43690, options [mss 65495,sackOK,TS val 1119954726 ecr 1119954726,nop,wscale 7], length 0
17:10:09.871864 IP localhost.50249 > localhost.8888: Flags [.], ack 1, win 342, options [nop,nop,TS val 1119954726 ecr 1119954726], length 0
        代码没有用setsockopt显式地指出接收缓存,按照上面查资料,tcp_rmem的default设置为87380,tcp_adv_win_scale为1,所以初始窗口大小应该为87380/(2~tcp_adv_win_scale )(这是套用的公式,上面没讲),即87380除二为43690。第一二行中的win也指出是,但是tcp选项里面还有个wscale 7。按照tcpip详解的说法这是窗口扩大选项,前两个syn的包的窗口大小win是计算过后的(按stackoverflow一个回答的说法),第三行是原tcp头窗口大小值,实际扩大后的窗口大小是342*2~7=43776。43776-43690=86,相差的了86,要确定还是得看下tcp头详情。
        发现第一行的窗口大小为aaaa(43690);窗口扩大因子03 0307,03表示kind,03表示长度三字节,07表示位移为7;看来第一行的43690并非计算过的而是实实在在的,那么难道第一行说明发送方窗口为43690*128如此之大???查了查stackoverflow无果(极有可能关键词不对),最终只能请教传说中的rfc1323(https://tools.ietf.org/html/rfc1323#page-8),有一段话“The window field (SEG.WND) in the header of every incoming segment, with the exception of SYN segments, is left-shifted by Snd.Wind.Scale bits before updating SND.WND”,就是说syn中有wscale选项但是对syn本身不起作用,细想一下,syn中的wscale本来就是用作协商用,如果对端是老版tcp或者关闭了wscale那么wscale根本用不上。在都没确定是否用wscale的前提下肯定win是实际大小。
        第三行窗口大小的确就是342(0156);没有扩大因子选项。按tcpip详解的说法这是正常的,扩大因子选项只存在在syn报文段内,一经确认不再更改。
        没有解决的是86B的差值,这得看源码确认下到底以哪个为准。既然第一行是connect发出的,我们找下connect源码:
        1.用apt-get source下载linux内核源码和glibc源码,具体指令网上教程很多;
        2.man 2 connect有记录,man 3没有,说明connect可能是个比较纯的系统调用甚至glibc都没有包一层;
        3.gdb client.c编译出的可执行文件,在connect处打断点,可以看到Breakpoint 2, connect () at ../sysdeps/unix/syscall-template.S;
        4.去glibc源码目录查sysdeps/unix/syscall-template.S,汇编不大熟悉,大约知道是通过一系列define最后syscall-template.S这个系统调用模版会把syscalls.list里面的用户调用函数符号直接包成汇编的对应系统调用,比如connect的list:connect     -   connect     Ci:ipi  __libc_connect  __connect connect,第三列就是系统调用名connect;
        5.gdb 反汇编输出connect代码 disass connect,中间有关键的两行mov    $0x2a,%eax;syscall 这里的0x2a=42,去/usr/include/asm/unistd_64.h(应该是这下面)里面查得知#define __NR_connect 42,connect的系统调用号就是42。
        上诉过程中有些具体细节我没弄懂,但是至少说明connect调用的确要通过glibc,但是glibc里面根本就没有c代码,直接包成了汇编语言系统调用42。那就直接看linux内核源码:
        前面那些工具知识以前基本也用过涉及过,网络编程内核源码只在书上读过,真正的代码太绕了,入口找到都很难。好在这次目的不是内核本身,是connect源码,估计是在net/socket.c内,其实重新编译内核支持ftrace等可以看系统调用的详细内部调用情况,现在还是先学爬吧。socket.c中的SYSCALL_DEFINE3(connect定义处。这里还是和二十多年前的书里很像的,一些关键的结构:
        1.include/linux/net.h 里的struct socket,struct proto_ops和下面的int     (*connect)   (struct socket *sock;
        2.net/socket.c,SYSCALL_DEFINE3(connect,内调用了sock->ops->connect,connect在哪儿初始化的呢?应该是socket,但是代码太绕时间也不多,只能猜测代码在net/ipv4/tcp*内;
        3.找到这里也算是本来想找点野菜却入金山了。看到了mbuf的linux对应版sk_buf,之前见的tcp_wmem在这儿也成了static变量sysctl_tcp_wmem,教科书代码到处都是比如new_state这个数组就明确说了了在哪些状态上调用close会发送fin一目了然;
        4.和输入缓存相关的,tcp_init_sock函数内的
        sk->sk_rcvbuf = sysctl_tcp_rmem[1];        tcp_select_initial_window函数内的        (*rcv_wscale) = 0;        space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);        //6291456        /* window_clamp:Maximal window to advertise      */        space = min_t(u32, space, *window_clamp);        while (space > 65535 && (*rcv_wscale) < 14) {                space >>= 1;                (*rcv_wscale)++;        }


        到这一步没有gdb打印要去扒代码有些麻烦了,window_clamp是多少?如果space==6291456那么rcv_wscale的确该是7(先认了吧),space为49152(后面没用)。
        至于win和wscale填入tcp头部那一段,不好找,先放弃了。起码这次搞清了wscale的计算和内核走了一圈。


三、总结
        tcp_mem对内存占用不紧张是的个别tcp连接意义不大,tcp_rmem的三个参数是动态的,tcp_rmem[1]默认缓存大小覆盖rmem_default,tcp_rmem[2]最大缓存被rmem_max覆盖,tcp_adv_win_scale决定了接收缓存中最大滑动窗口的比例(也纸上谈兵地举例了不同比例的问题)。从实际的三次握手也能得到印证,三次握手中的wscale是由rmem_max和tcp_rmem[2]决定的,在syn包中win取原值,非syn包中不包含wscale信息(一经确定不会修改)。非syn包中的win是处理过的,需要通过wscale计算实值。至于具体syn中的win和非syn中win计算出的窗口大小差值,是bug还是有某种内部处理,后面通过进一步看内核源码得出。
阅读全文
0 0