可以查看文章Part1和Part2eBPF(Extended Berkeley Packet Filter)的核心是驻留在kernel的高效虚拟机。最初的目的是高效网络过滤框架，前身是BPF，所以我们先了解下BPF。BPF框架上图是BPF的位置和框架，需要注意的是kernel和user使用了buffer来传输数据，避免频繁上下文切换。BPF虚拟机非常简洁，由累加器、索引寄存器、存储、隐式程序计数器组成。示例接下来我们看一下示例，过滤所有ip报文，可以使用tcpdump -d ip查看：(000) ldh [12] // 链路层第12字节的数据加载到寄存器，ethertype字段 (001) jeq #0x800 jt 2 jf 3 // 比较寄存器的ethertype字段是否为IP类型，true跳到2，false跳到3 (002) ret #65535 // 返回true (003) ret #0 // 返回0BPF只使用了4条虚拟机指令，就能提供非常有用的IP报文过滤。tcpdump -d tcp (000) ldh [12] // 链路层第12字节的数据(2字节)加载到寄存器，ethertype字段 (001) jeq #0x86dd jt 2 jf 7 // 判断是否为IPv6类型，true跳到2，false跳到7 (002) ldb [20] // 链路层第20字节的数据(1字节)加载到寄存器，IPv6的next header字段 (003) jeq #0x6 jt 10 jf 4 // 判断是否为TCP，true跳到10，false跳到4 (004) jeq #0x2c jt 5 jf 11 // 可能是IPv6分片标志，true跳到5，false跳到11 (005) ldb [54] // 我编不下去了... (006) jeq #0x6 jt 10 jf 11 // 判断是否为TCP，true跳到10，false跳到11 (007) jeq #0x800 jt 8 jf 11 // 判断是否为IP类型，true跳到8，false跳到11 (008) ldb [23] // 链路层第23字节的数据(1字节)加载到寄存器，next proto字段 (009) jeq #0x6 jt 10 jf 11 // 判断是否为TCP，true跳到10，false跳到11 (010) ret #65535 // 返回true (011) ret #0 // 返回0以上是freebsd的BPF，Linux中应该不叫这个，叫LSF，自己看吧。eBPFeBPF初识Linux kernel 3.18版本开始包含了eBPF，相对于BPF做了一些重要改进，首先是效率，这要归功于JIB编译eBPF代码；其次是应用范围，从网络报文扩展到一般事件处理；最后不再使用socket，使用map进行高效的数据存储。根据以上的改进，内核开发人员在不到两年半的事件，做出了包括网络监控、限速和系统监控。目前eBPF可以分解为三个过程：以字节码的形式创建eBPF的程序。编写C代码，将LLVM编译成驻留在ELF文件中的eBPF字节码。将程序加载到内核中，并创建必要的eBPF-maps。eBPF具有用作socket filter，kprobe处理器，流量控制调度，流量控制操作，tracepoint处理，eXpress Data Path(XDP)，性能监测，cgroup限制，轻量级tunnel的程序类型。将加载的程序attach到系统中。根据不同的程序类型attach到不同的内核系统中。程序运行的时候，启动状态并且开始过滤，分析或者捕获信息。2016年10月的NetDev 1.2大会上，Netronome的Jakub Kicinski和Nic Viljoen发表了标题为“eBPF / XDP硬件卸载到SmartNIC”。 Nic Viljoen在其中介绍了Netronome SmartNIC上每个FPC每秒达到300万个数据包，每个SmartNIC有72到120个FPC，可能最大支持eBPF吞吐量4.3 Tbps！（理论上）eBPF入口接下来我们以内核版本4.14版本为例进行查看。kernel/bpf/syscall.c bpf的系统调用include/uapi/linux/bpf.h bpf系统调用的头文件入口函数int bpf(int cmd, union bpf_attr *attr, unsigned int size);，从kernel/bpf/syscall.c中的宏定义展开。eBPF命令Linux系统的BPF系统调用有10个命令，其中man page中列出了6个：BPF_PROG_LOAD 验证并且加载eBPF程序，返回一个新的文件描述符。BPF_MAP_CREATE 创建map并且返回指向map的文件描述符BPF_MAP_LOOKUP_ELEM 通过key从指定的map中查找元素，并且返回value值BPF_MAP_UPDATE_ELEM 在指定的map中创建或者更新元素(key/value 配对)BPF_MAP_DELETE_ELEM 通过key从指定的map中找到元素并且删除BPF_MAP_GET_NEXT_KEY 通过key从指定的map中找到元素，并且返回下个key值以上的命令可以分为两大类，加载eBPF程序和eBPF-maps操作。eBPF-maps操作有很大的自主性，用于创建eBPF-maps，从中查找、更新和删除元素，遍历eBPF-maps(BPF_MAP_GET_NEXT_KEY)接下来列一下剩下的4个命令，在代码中可以看到：BPF_OBJ_PIN 4.4版本新加的，属于持久性eBPF。有了这个，eBPF-maps和eBPF程序可以放入/sys/fs/bpfBPF_OBJ_GET 同上，在这之前，没有工具能创建eBPF程序，并且结束，因为会破坏filter，而文件系统可以在创建他们的程序退出后依然保留eBPF-maps和eBPF程序BPF_PROG_ATTACH 4.10版本中添加的，将eBPF程序attach到cgroup，这样适用于containerBPF_PROG_DETACH 同上。eBPF-map 类型BPF_MAP_TYPE_UNSPECBPF_MAP_TYPE_HASH eBPF-maps hash表，是主要用的前两种方式之一BPF_MAP_TYPE_ARRAY 和上面类似，除了索引像数组一样BPF_MAP_TYPE_PROG_ARRAY 将加载的eBPF程序的文件描述符保存其值，常用的是使用数字识别不同的eBPF程序类型，也可以从一个给定key值的eBPF-maps找到eBPF程序，并且跳转到程序中去BPF_MAP_TYPE_PERF_EVENT_ARRAY 配合perf工具，CPU性能计数器，tracepoints，kprobes和uprobes。可以查看路径samples/bpf/下的tracex6_kern.c，tracex6_user.c，tracex6_kern.c，tracex6_user.cBPF_MAP_TYPE_PERCPU_HASH 和BPF_MAP_TYPE_HASH一样，除了是为每个CPU创建BPF_MAP_TYPE_PERCPU_ARRAY 和BPF_MAP_TYPE_ARRAY一样，除了是为每个CPU创建BPF_MAP_TYPE_STACK_TRACE 用于存储stack-tracesBPF_MAP_TYPE_CGROUP_ARRAY 检查skb的croup归属BPF_MAP_TYPE_LRU_HASHBPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_LPM_TRIE 最专业的用法，LPM(Longest Prefix Match)的一种trieBPF_MAP_TYPE_ARRAY_OF_MAPS 可能是针对每个port的BPF_MAP_TYPE_HASH_OF_MAPS 可能是针对每个port的BPF_MAP_TYPE_DEVMAP 可能是定向报文到dev的BPF_MAP_TYPE_SOCKMAP 可能是连接socket的先写这么多，反正还是迷糊，接下来看看代码。
版本说明Linux版本： 3.10.103网卡驱动： ixgbe协议头注意上图中的五个非常重要的东西：Sequence Number是包的序号，用来解决网络包乱序（reordering）问题。Acknowledgement Number就是ACK——用于确认收到，用来解决不丢包的问题。Window又叫Advertised-Window，也就是著名的滑动窗口（Sliding Window），用于解决流控的。TCP Flag ，也就是包的类型，主要是用于操控TCP的状态机的。TCP Options，主要是用于协商信息用的。flagsoptions数据是tlv形式，主要有kind-len-content，一些kind类型如下：KindMeaning0End of Option List1No-Operation2Maximum Segment Size3WSOPT - Window Scale4SACK Permitted5SACK6Echo (obsoleted by option 8)7Echo Reply (obsoleted by option 8)8TSOPT - Time Stamp Option9Partial Order Connection Permitted10Partial Order Service Profile11CC12CC.NEW13CC.ECHO14TCP Alternate Checksum Request15TCP Alternate Checksum Data16Skeeter17Bubba18Trailer Checksum Option19MD5 Signature Option20SCPS Capabilities21Selective Negative Acknowledgements22Record Boundaries23Corruption experienced24SNAP25Unassigned (released 12/18/00)26TCP Compression Filter状态转换图连接建立和销毁对于建链接的3次握手，主要是要初始化Sequence Number的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN：Inital Sequence Number)——所以叫SYN，全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号，以保证应用层接收到的数据不会因为网络上的传输的问题而乱序（TCP会用这个序号来拼接数据）。对于4次挥手，其实你仔细看是2次，因为TCP是全双工的，所以，发送方和接收方都需要Fin和Ack。只不过，有一方是被动的，所以看上去就成了所谓的4次挥手。如果两边同时断连接，那就会就进入到CLOSING状态，然后到达TIME_WAIT状态。数据传输该图是我从Wireshark中截了个我在访问百度时的有数据传输的图，看下SeqNum是怎么变的。SeqNum的增加是和传输的字节数相关的。该图中，三次握手后，来了个Len:351的包，而第二个包的SeqNum就成了352。然后第一个ACK回的是352，表示第一个352收到了。重传机制TCP要保证所有的数据包都可以到达，所以，必需要有重传机制。重传机制：超时重传机制。快速重传机制。SACK(Selective Acknowledgment)。Duplicate SACK – 重复收到数据的问题。超时重传接收端给发送端的Ack确认只会确认最后一个连续的包。比如，发送端发了1,2,3,4,5一共五份数据，接收端收到了1，2，于是回ack 3，因为数据包3丢失了，所以收不到ACK，等到一定的时间超时之后就会重新发送。需要关注的问题就是我们设置超时时间多长？设长了，重发就慢，没有效率，性能差。设短了，重发的就快，会增加网络拥塞，导致更多的超时，更多的超时导致更多的重发。这时TCP也比较纠结到底设置多长时间(后续也会讲具体的超时依据，但也不是完美设置超时时间)，所以我们还需要找到一种方式来缓解超时带来的误差。快速重传TCP引入了一种叫Fast Retransmit 的算法，不以时间驱动，而以数据驱动重传。如果包没有连续到达，就ack最后那个可能被丢了的包，如果发送方连续收到3次相同的ack，就重传。Fast Retransmit的好处是不用等timeout了再重传。比如：发送方发出了1，2，3，4，5份数据，第一份先到送了，于是就ack回2，结果2因为某些原因没收到，3到达了，于是还是ack回2，后面的4和5都到了，但是还是ack回2，因为2还是没有收到，于是发送端收到了三个ack=2的确认，知道了2还没有到，于是就马上重转2。然后，接收端收到了2，此时因为3，4，5都收到了，于是ack回6。Fast Retransmit缓解了超时误差的问题，但是我是要重传2还是重传2、3、4、5？因为发送端并不清楚这连续的3个ack(2)是谁传回来的？也许发送端发了20份数据，是#6，#10，#20传来的呢。对此有两种选择：仅重传2。这样会节省带宽，但是慢，要继续等ACK。重传2之后所有的数据。这样不用等，比较快，但是会浪费带宽，也可能会有无用功。所以TCP急需知道对方丢掉的具体包是哪个。SACKSelective Acknowledgment(SACK)，这种方式需要在TCP头里加一个SACK的东西，ACK还是Fast Retransmit的ACK，SACK则是汇报收到的数据碎片。这样，在发送端就可以根据回传的SACK来知道哪些数据到了，哪些没有到。于是就优化了Fast Retransmit的算法。当然，这个协议需要两边都支持。在 Linux下，可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。这里还需要注意一个问题——接收方Reneging ，所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的，因为这个事会把问题复杂化了，但是，接收方这么做可能会有些极端情况，比如要把内存给别的更重要的东西。所以，发送方也不能完全依赖SACK，还是要依赖ACK，并维护Time-Out，如果后续的ACK没有增长，那么还是要把SACK的东西重传，另外，接收端这边永远不能把SACK的包标记为Ack。注意：SACK会消耗发送方的资源，试想，如果一个攻击者给数据发送方发一堆SACK的选项，这会导致发送方开始要重传甚至遍历已经发出的数据，这会消耗很多发送端的资源。传输控制TCP需要保证尽量合理使用带宽资源，不能抢太多资源，也不能空闲太多资源。抢太多资源，就容易造成负荷太重而丢包，太空闲何必买这么大的带宽，败家。传输控制机制：TCP的RTT算法。TCP滑动窗口协议。TCP的拥塞处理。其它拥塞控制算法简介RTT从前面的TCP的重传机制我们知道Timeout的设置对于重传非常重要。这个超时时间在不同的网络的情况下，有不同的时间，根本没有办法设置一个固定值。只能动态地设置。 为了动态地设置，TCP引入了RTT——Round Trip Time，也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间，从而可以方便地设置Timeout——RTO（Retransmission TimeOut），以让我们的重传机制更高效。 听起来似乎很简单，好像就是在发送端发包时记下t0，然后接收端再把这个ack回来时再记一个t1，于是RTT = t1 – t0。没那么简单，这只是一个采样，不能代表普遍情况。RTT算法：经典算法。Karn / Partridge 算法。Jacobson / Karels 算法(Linux当前使用)经典算法RFC793 中定义的经典算法是这样的：首先，先采样RTT，记下最近好几次的RTT值。然后做平滑计算Smoothed RTT。公式为: SRTT = (αSRTT) + ((1-α)RTT)(其中的 α 取值在0.8 到 0.9之间，这个算法叫加权移动平均(Exponential weighted moving average))开始计算RTO。公式如下： RTO = min[UBOUND, max[LBOUND, (β*SRTT)]]其中：UBOUND是最大的timeout时间，上限值。LBOUND是最小的timeout时间，下限值。β 值一般在1.3到2.0之间。经典算法在重传的时候会出有一个终极问题——你是用第一次的时间和ack回来的时候做RTT样本，还是用重传的时间和ACK的时间做RTT样本？如图所示：是ack没回来，所发重传。如果你计算第一次发送和ACK的时间，那么，明显算大了。是ack回来慢了，重传不一会，之前ACK就回来了。如果你是算重传的时间和ACK回来的时间，就会短了。Karn / Partridge 算法1987年的时候，搞了一个叫Karn/Partridge的算法，这个算法的最大特点是——忽略重传，不把重传的RTT做采样(眼不见心不烦)。但是，这样一来，又会引发一个大BUG：如果在某一时间，网络闪动，突然变慢了，产生了比较大的延时，这个延时导致要重转所有的包(因为之前的RTO很小)，于是，因为重转的不算，所以，RTO就不会被更新，这是一个灾难。于是Karn算法用了一个取巧的方式——只要一发生重传，就对现有的RTO值翻倍(这就是所谓的指数退避(Exponential backoff))。Jacobson / Karels 算法前面两种算法用的都是“加权移动平均”，这种方法最大的毛病就是如果RTT有一个大的波动的话，很难被发现，因为被平滑掉了。1988年，又有人推出来了一个新的算法，这个算法叫Jacobson / Karels Algorithm（参看RFC6289）。这个算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。公式如下：SRTT = SRTT + α (RTT – SRTT) DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) //(其中的DevRTT是Deviation RTT的意思) RTO= µ * SRTT + ∂ *DevRTT //(其中：在Linux下，α = 0.125，β = 0.25， μ = 1，∂ = 4 ——这就是算法中的“调得一手好参数”，nobody knows why, it just works…)最后的这个算法在被用在今天的TCP协议中（Linux的源代码在：tcp_rtt_estimator）。滑动窗口TCP使用一种窗口（window）机制来控制数据流，TCP头里有一个字段叫Window，这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据，而不会导致接收端处理不过来。图中的1,2,3号数据已经发送成功。4,5,6号数据帧已经被发送出去，但是未收到关联的ACK。7,8,9帧则是等待发送。可以看出发送端的窗口大小为6，这是由接受端告知的。此时如果发送端收到4号ACK，则窗口的左边缘向右收缩，窗口的右边缘则向右扩展，此时窗口就向前“滑动了”，即数据帧10也可以被发送。Zero Window从上一张图，我们可以看到一个处理缓慢的Server是怎么把TCP Sliding Window给降成0的。如果Window变成0了，TCP会使用Zero Window Probe技术(缩写为ZWP)，发送端会发ZWP的包给接收方，让接收方来ack他的Window尺寸，一般这个值会设置成3次，第3次大约30-60秒（依实现而定）。如果3次过后还是0的话，有的TCP实现就会发RST把链接断了。注意：只要有等待的地方都可能出现DDoS攻击，Zero Window也不例外，一些攻击者会在和HTTP建好链发完GET请求后，就把Window设置为0，然后服务端就只能等待进行ZWP，于是攻击者会并发大量的这样的请求，把服务器端的资源耗尽。现在接收窗口的大小是根据对端接收能力来确定的，那么网络中间状态或者说我们的带宽是否需要考虑？拥塞处理TCP通过一个timer采样了RTT并计算RTO，如果网络上的延时突然增加，那么，TCP对这个事做出的应对只有重传数据，而重传会导致网络的负担更重，于是会导致更大的延迟以及更多的丢包，于是，这个情况就会进入恶性循环被不断地放大。试想一下，如果一个网络内有成千上万的TCP连接都这么行事，那么马上就会形成“网络风暴”，TCP这个协议就会拖垮整个网络。这是一个灾难。 所以，TCP不能忽略网络上发生的事情，而无脑地一个劲地重发数据，对网络造成更大的伤害。对此TCP的设计理念是：TCP不是一个自私的协议，当拥塞发生的时候，要做自我牺牲。就像交通阻塞一样，每个车都应该把路让出来，而不要再去抢路了。拥塞控制主要是四个算法：慢启动。拥塞避免。拥塞发生。快速恢复。这四个算法不是一天都搞出来的，这个四算法的发展经历了很多时间，到今天都还在优化中。慢启动慢启动的意思是，刚刚加入网络的连接，一点一点地提速，不要一上来就像那些特权车一样霸道地把路占满。慢启动的算法如下(cwnd全称Congestion Window)：连接建好的开始先初始化cwnd = 1，表明可以传一个MSS大小的数据。每当收到一个ACK，cwnd++或者每当过了一个RTTcwnd = cwnd*2呈指数上升。还有一个ssthresh(slow start threshold)，是一个上限，当cwnd >= ssthresh时，就会进入“拥塞避免算法”。如果网速很快的话，ACK也会返回得快，RTT也会短，那么，这个慢启动就一点也不慢。Linux 3.0后把cwnd 初始化成了 10个MSS。 而Linux 3.0以前，比如2.6，Linux采用了RFC3390，cwnd是跟MSS的值来变的，如果MSS < 1095，则cwnd = 4；如果MSS > 2190，则cwnd=2；其它情况下，则是3。ssthreshold初始值为0x7fffffff。拥塞避免前面说过，还有一个ssthresh(slow start threshold)，是一个上限，当cwnd >= ssthresh时，就会进入“拥塞避免算法”。一般来说ssthresh的值是65535，单位是字节，当cwnd达到这个值时后，算法如下收到一个ACK时，cwnd = cwnd + 1/cwnd OR 当每过一个RTT时，cwnd = cwnd + 1；线性增长。这样就可以避免增长过快导致网络拥塞，慢慢的增加调整到网络的最佳值。拥塞发生前面我们说过，当丢包的时候，会有两种情况：等到RTO超时，重传数据包。TCP认为这种情况太糟糕，反应也很强烈。sshthresh = cwnd /2 cwnd = 1进入慢启动过程。Fast Retransmit算法，也就是在收到3个duplicate ACK时就开启重传，而不用等到RTO超时。TCP Tahoe的实现和RTO超时一样。TCP Reno的实现是：cwnd = cwnd /2 sshthresh = cwnd进入快速恢复算法——Fast Recove上面我们可以看到RTO超时后，sshthresh会变成cwnd的一半，这意味着，如果cwnd<=sshthresh时出现的丢包，那么TCP的sshthresh就会减了一半，然后等cwnd又很快地以指数级增涨爬到这个地方时，就会成慢慢的线性增涨。我们可以看到，TCP是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。快速恢复快速恢复算法是认为，你还有3个Duplicated Acks说明网络也不那么糟糕，所以没有必要像RTO超时那么强烈。 注意，正如前面所说，进入Fast Recovery之前，cwnd 和 sshthresh已被更新。cwnd = cwnd /2 sshthresh = cwnd然后，真正的Fast Recovery算法如下：cwnd = sshthresh + 3 * MSS //3的意思是确认有3个数据包被收到了重传Duplicated ACKs指定的数据包如果再收到 duplicated Acks，那么cwnd = cwnd +1如果收到了新的Ack，那么，cwnd = sshthresh ，然后就进入了拥塞避免的算法了。如果你仔细思考一下上面的这个算法，你就会知道，上面这个算法也有问题，那就是——它依赖于3个重复的Acks。注意，3个重复的Acks并不代表只丢了一个数据包，很有可能是丢了好多包。但这个算法只会重传一个，而剩下的那些包只能等到RTO超时，于是，进入了恶梦模式——超时一个就减半一下，多个超时会超成TCP的传输速度呈级数下降，而且也不会触发Fast Recovery算法了。 通常来说，正如我们前面所说的，SACK或D-SACK的方法可以让Fast Recovery或Sender在做决定时更聪明一些，但是并不是所有的TCP的实现都支持SACK（SACK需要两端都支持），所以，需要一个没有SACK的解决方案。TCP New Reno算法提出来，主要就是在没有SACK的支持下改进Fast Recovery算法。当sender这边收到了3个Duplicated Acks，进入Fast Retransimit模式，开发重传重复Acks指示的那个包。如果只有这一个包丢了，那么，重传这个包后回来的Ack会把整个已经被sender传输出去的数据ack回来。如果没有的话，说明有多个包丢了。我们叫这个ACK为Partial ACK。一旦Sender这边发现了Partial ACK出现，那么，sender就可以推理出来有多个包被丢了，于是乎继续重传sliding window里未被ack的第一个包。直到再也收不到了Partial Ack，才真正结束Fast Recovery这个过程。我们可以看到，这个“Fast Recovery的变更”是一个非常激进的玩法，他同时延长了Fast Retransmit和Fast Recovery的过程。FACKFACK全称Forward Acknowledgment，算法是其于SACK的。snd.una就是还没有收到ack的序列号，unack。snd.fack SACK中最大的序列号，更新由ack触发，如果网络一切安好则和snd.una一样。然后触发Fast Recovery 的条件：((snd.fack – snd.una) > (3*MSS)) || (dupacks == 3))这样一来，就不需要等到3个duplicated acks才重传，而是只要sack中的最大的一个数据和ack的数据比较长了（3个MSS），那就触发重传。snd.nxt 表示下一个要发送的包重传流程：cwnd不变。当第一次丢包的snd.nxt<=snd.una(也就是重传的数据都被确认了)，进入拥塞避免机制——cwnd线性上涨。优点：我们可以看到如果没有FACK在，那么在丢包比较多的情况下，原来保守的算法会低估了需要使用的window的大小，而需要几个RTT的时间才会完成恢复，而FACK会比较激进地来干这事。缺点：FACK如果在一个网络包会被 reordering的网络里会有很大的问题。拥塞算法类型算法丢包BIC/CUBIC, HSTCP, STCP, MulTCP, TFRC时延Vegas, FastTcp综合Veno, WestWood, Compound TCP, UDT, TFRC VenoBIC ：曾经的linux默认tcp拥塞算法，拥塞避免阶段，每RTT加1，丢包时降一半。CUBIC ：现在的linux默认tcp拥塞算法，慢启动阶段，每RTT乘2，然后进入拥塞避免阶段，每RTT加1。HSTCP ：相对于CUBIC添加了参数，增得快，降得慢。拥塞避免时的窗口增长方式： cwnd = cwnd + α(cwnd) / cwnd，丢包后窗口下降方式：cwnd = (1- β(cwnd))*cwnd，当α(cwnd)=1，β(cwnd)=0.5时就和cubic一致了。Vegas ：RTT增大代表拥堵，减小拥塞窗口。FastTcp ：相对于Vegas主要引入了一个动态参数alpha。Veno ：基于Vegas和Reno提出的，Veno提升了TCP在单跳无线网络中的性能，大量研究已经表明，Veno在蜂窝通信众性能得到较好的提升。DCTCP ：DataCenter TCP使用ECN。HTCP ：根据时延抖动。Hybla ：主要解决不同RTT的公平性问题，基于时延补偿的方法，大时延网络下，这种算法可以提高传输效率。illinois：基于RTT估计，调整AI、MD因子。STCP ：快速回复，与当前窗口无关，是常数。WestWood：适用于无线网络，丢包后重新调整阈值。vegas这个算法通过对RTT的非常重的监控来计算一个基准RTT。然后通过这个基准RTT来估计当前的网络实际带宽，如果实际带宽比我们的期望的带宽要小或是要多的活，那么就开始线性地减少或增加cwnd的大小。如果这个计算出来的RTT大于了Timeout后，那么，不等ack超时就直接重传。(Vegas 的核心思想是用RTT的值来影响拥塞窗口，而不是通过丢包)关于这个算法实现，你可以参看Linux源码：/net/ipv4/tcp_vegas.h， /net/ipv4/tcp_vegas.cHSTCP(High Speed TCP)使得Congestion Window涨得快，减得慢。其中：拥塞避免时的窗口增长方式：cwnd = cwnd + α(cwnd) / cwnd丢包后窗口下降方式：cwnd = (1- β(cwnd))*cwnd注：α(cwnd)和β(cwnd)都是函数，如果你要让他们和标准的TCP一样，那么让α(cwnd)=1，β(cwnd)=0.5就可以了。 对于α(cwnd)和β(cwnd)的值是个动态的变换的东西。 关于这个算法的实现，你可以参看Linux源码：/net/ipv4/tcp_highspeed.cBIC在Linux 2.6.8中是默认拥塞控制算法。BIC的发明者发这么多的拥塞控制算法都在努力找一个合适的cwnd – Congestion Window，而且BIC-TCP的提出者们看穿了事情的本质，其实这就是一个搜索的过程，所以BIC这个算法主要用的是Binary Search——二分查找来干这个事。 关于这个算法实现，你可以参看Linux源码：/net/ipv4/tcp_bic.cWestwoodwestwood采用和Reno相同的慢启动算法、拥塞避免算法。westwood的主要改进方面：在发送端做带宽估计，当探测到丢包时，根据带宽值来设置拥塞窗口、慢启动阈值。 那么，这个算法是怎么测量带宽的？每个RTT时间，会测量一次带宽，测量带宽的公式很简单，就是这段RTT内成功被ack了多少字节。因为，这个带宽和用RTT计算RTO一样，也是需要从每个样本来平滑到一个值的——也是用一个加权移平均的公式。 另外，我们知道，如果一个网络的带宽是每秒可以发送X个字节，而RTT是一个数据发出去后确认需要的时候，所以，X RTT应该是我们缓冲区大小。所以，在这个算法中，ssthresh的值就是est_BD min-RTT(最小的RTT值)，如果丢包是Duplicated ACKs引起的，那么如果cwnd > ssthresh，则 cwin = ssthresh。如果是RTO引起的，cwnd = 1，进入慢启动。 关于这个算法实现，你可以参看Linux源码： /net/ipv4/tcp_westwood.c传输层数据调用UDP报文接收__udp4_lib_rcv找到匹配的sock，根据skb的net、四元组和入端口信息，sock会记录连接的状态。__udp4_lib_rcv->udp_queue_rcv_skb->__udp_queue_rcv_skb->sock_queue_rcv_skb添加到sk_receive_queue队列中。TCP报文接收tcp_v4_rcv找到匹配的sock，根据skb的net、四元组和入端口信息，sock会记录连接的状态。｀tcp_v4_do_rcv-->tcp_rcv_established-->tcp_queue_rcv｀添加到｀sk_receive_queue｀队列中应用层接收用户态调用recvfrom会调用kernel代码的系统用SYSCALL_DEFINE6(recvfrom,…)，net/socket.c。这里面调用sock_recvmsg-> __sock_recvmsg-> __sock_recvmsg_nosec。__sock_recvmsg_nosec调用不同协议的对应recvmsg接口，比如TCP的tcp_recvmsg和UDP的udp_recvmsg。里面分别从sk_receive_queue拿到skb，然后将skb的数据部分拷贝到buffer上传。应用层收到数据。应用层发送用户态调用sendto会调用kernel代码的系统用SYSCALL_DEFINE6(sendto,…)。首先通过参数fd查找对应的sock，找到之后调用sock_sendmsg。这里面调用sock_sendmsg > __sock_sendmsg > __sock_sendmsg_nosec。__sock_sendmsg_nosec调用不同协议的对应sendmsg接口，比如TCP的tcp_sendmsg和UDP的udp_sendmsgTCP报文发送tcp_sendmsg将buffer转换成skb，如果超过mss，则生成多个skb，存入sock的sk_write_queue和sk_send_head 。调用tcp_push_one-> tcp_write_xmit从sk_send_head拿到skb之后调用tcp_transmit_skb-> ip_queue_xmit-> ip_local_out发送报文。ip_output-->ip_finish_output2-->dst_neigh_output-->neigh_hh_output-->dev_queue_xmit-->__dev_xmit_skb-->sch_direct_xmit-->dev_hard_start_xmit-->ndo_start_xmit 。备忘录google search的了解增加初始发送窗口到10个mss，内核已经支持。Proportional Rate Reduction for TCP是指修改快速重传的算法，目前内核已经支持。fastopen是指在三次握手的时候增加数据的传输，不止需要修改内核，客户端和服务端都需要，目前已经提交到rfc，但是目前内核还未确认是否支持。内核一些默认值linux3.10.102为准net/ipv4/tcp.c的tcp_init_sock会初始化拥塞窗口和ssthresh。#define TCP_INIT_CWND 10 tp->snd_cwnd = TCP_INIT_CWND; #define TCP_INFINITE_SSTHRESH 0x7fffffff tp->snd_ssthresh = TCP_INFINITE_SSTHRESH;tcp_input.c的tcp_ack快速重传的判定tcp_fastretrans_alert。inet_init->proto_register会注册tcp_prot和udp_prot等。tcp_prot->init=tcp_v4_init_sock。(void)sock_register(&inet_family_ops);注册了socket创建的函数inet_create，其中调用了init对应tcp的tcp_v4_init_sock->tcp_init_sock。bind会调用inet_bind，如果tcp_prot存在bind则调用对应的函数，可惜没有。listen会调用inet_listen。accept会调用inet_accept，会调用tcp_prot的inet_csk_accept。recv应该是调用inet_recvmsg，会调用tcp_prot的tcp_recvmsg，从sock的sk_receive_queue上拿报文。send应该是调用inet_sendmsg，会调用tcp_prot的tcp_sendmsg，将报文放入sock的sk_write_queue
版本说明Linux版本： 3.10.103网卡驱动： ixgbe位置框架图ip_rcv调用NF_HOOK开始进入netfilter框架对数据进行处理NF_HOOK(pf, hook, skb, indev, outdev, okfn)pf ：协议族名，Netfilter架构可以用于IP层之外，ipv4、arp、bridge、ipv6等。hook ：HOOK点的名字，对于IP层，就是取上面的五个值，其他的大同小异。skb ：不解释。indev ：数据包进来的设备，以struct net_device结构表示。outdev：数据包出去的设备，以struct net_device结构表示。okfn :是个函数指针，当所有的该HOOK点的所有登记函数调用完后，转而走此流程。Hook点PRE_ROUTING ：在进入网络层之初就会经过。LOCAL_IN ：数据进入本地时经过。FORWARD ：转发数据经过。LOCAL_OUT ：本地发送数据时经过。POST_ROUTING：数据出设备时调用比如我们做数据统计的话：如果需要统计流入和流出流量的话，只需要在1和5两个点统计skb即可。如果我们要分别统计本地流量和转发，需要在2、3、4三个点统计。Hook点的选取和我们的操作息息相关，主要看我们改变网络特性的情况，比如我们要改变连接属性的话，需要考虑conntrack的影响，以IP做策略匹配的时候，需要考虑NAT。优先级协议族的引入是为了支持更多的协议优先级的引入是因为同一个hook点可以挂很多钩子，不同的Hook函数用不同的优先级表示调用的先后顺序，数值越小的优先被调用，最大值是0x7fffffff，最小值是-0x80000000实现netfilter定义struct list_head nf_hooks[PROTO][HOOKS]，建立一个二维数组的链表，分别是协议族和hook点，然后以优先级为基准进行链表的创建和排序。我们在挂Hook函数的时候会指定协议、hook点、优先级和执行函数，直接将这些信息加入相应的链表中，协议栈的数据包进入的时候会根据相应的协议和当前执行到的hook点去遍历链表，执行相应的函数。比如，我们设置协议为ipv4， hook点为pre_routing，优先级-100，函数为dnatfor{ a = get_list(nf_hooks[ipv4][prerouting]); a.func(skb); }当遍历到优先级为-100的时候，就会执行dnat函数。挂钩子Netfilter处于kernel层，推荐直接编写module，比直接修改内核代码方便调试和编译static struct nf_hook_ops my_nfhook[] = { { .hook=my_hook_func, //Hook函数 .owner=THIS_MODULE, .pf=PF_INET, //协议族 .hooknum=NF_INET_LOCAL_IN, //Hook点 .priority=NF_IP_PRI_FIRST, //优先级 },//可以写多个 };挂Hook的接口nf_register_hooks(my_nfhook, ARRAY_SIZE(my_nfhook))取消Hook的接口nf_unregister_hooks(my_nfhook, ARRAY_SIZE(my_nfhook))unsigned int my_hook_func(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff*)) { //此处skb任你处置 return NF_ACCEPT; }需要注意的是比较老的内核版本的函数参数skb是二维指针**skb, 新的是*skb，老内核我遇到过的是2.6.18之前的，2.6.32之后是新的返回值Netfilter将skb交给Hook函数进行处理，处理的结果如何，需要通过返回值的形式告知协议栈：NF_ACCEPT继续正常传输数据报。这个返回值告诉 Netfilter：到目前为止，该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。NF_DROP丢弃该数据报，不再传输。NF_STOLEN模块接管该数据报，告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理，并且Netfilter应当放弃对该数据包做任何的处理。但是，这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效，只是回调函数从Netfilter 获取了该数据包的所有权。NF_QUEUE对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)。NF_REPEAT再次调用该回调函数，应当谨慎使用这个值，以免造成死循环。注意：在此阶段数据包的存储空间还是位于DMA的空间中，尽量不要做特别耗时的操作，以免影响网卡收取数据。
版本说明Linux版本： 3.10.103网卡驱动： ixgbe路由路由项通过两种途径加入内核：通过用户态路由协议进程或者用户静态配置配置加入。主机自动发现的路由。自动发现的路由实际上是“发现了一个路由项和一个转发表”，其含义在主机某一个网卡启动的时候生效，比如eth0启动，那么系统生成下列路由表项/转发项：往eth0同一IP网段的包通过eth0发出。路由实现内核路由存在两种查找算法：HASH算法(Linux之前默认)。LC-trie算法(Linux现在默认，更适用在超大路由表的情况，它在这种情况提高查找效率的同时，大大地增加了算法本身的复杂性和内存的消耗)。HASH路由算法路由的性能很大程度上由路由表的设计决定，好的设计能减少系统资源的消耗，这两方面尤其体现在路由表的查找上。Linux使用了多个路由表，一个用于上传给本地上层协议，另一个则用于转发， 其他用于策略路由。不同策略的路由存放在不同的表中，有效地被免了查找庞大的路由表，在一定程度上提高了查找了效率。HASH路由表由多个结构组合而成，是一个分层的结构组合：第一层是指表头fib_table->tb_data指向fn_hash，里面包含了33个fn_zone。第二层根据子网掩码(netmask)的长度(0~32)分成33个部分(struct fn_zone)。第三层根据子网的不同(如10.1.1.0/24和10.1.2.0/24)划分 (struct fib_node)。第四层是一个路由表项(struct fib_info) ，因为不同的子网可能是对应着同一个下一跳，所以fib_node和fib_info是多对一的关系。LC-Trie树路由算法trie树本质上是一棵检索树。CheckNode{ int pos; int bits; Node children[1<<bits]; } union Node{ Leaf entry; CheckNode node; } //Pos表示位置 //Bits表示位数 //Children表示子节点RT_TABLE_LOCAL优先级 0选择器 = 匹配任何数据报动作＝察看本地路由表(routing table local)，ID为255。local表是保留路由表，包含了到本地和广播地址已及NAT地址的路由。规则0是特殊的规则，由系统自动维护，不可被删除或修改。RT_TABLE_MAIN优先级 32766选择器 = 匹配所有数据报动作 = 察看主路由表(routing table main)， ID为254。main路由表是默认的标准路由表，其包含所有非策略路由，main表是存放旧的路由命令(route命令)创建的路由。而且任何由ip route命令创建的没有明确指定路由表的路由都被加入到该路由表中。该规则不能被删除和被其他规则覆盖。RT_TABLE_DEFAULT优先级 32767选择器 = 匹配所有数据报动作 = 察看默认路由表(routing table default)，ID为253。default路由表是空的，为最后处理所预留，若前面的默认规则没有选择该数据报时保留用作最后的处理。该规则可以被删除。USER_DEFINE 策略路由优先级 1-32765选择器 = 自定义动作 = 自动以静态路由(LOCAL、MAIN、DEFAULT)匹配字段： 目的地址操作： 转发策略路由(USER_DEFINE)匹配字段： 各个字段（常用五元组：源IP、目标IP、协议、源端口、目标端口）4操作： 数据包任意操作可以实现ACL的功能，不过这些功能目前的linux中用了更高效的nf-HiPAC，另外也可以了解一下ipset和nftables
版本说明Linux版本： 3.10.103网卡驱动： ixgbe网络协议注册inet_init主要是注册各种协议注册TCP协议proto_register(&tcp_prot, 1)继续注册UDP、RAW、PINGarp_init, ip_init, tcp_init, udp_init, ping_init, icmp_initdev_add_pack(&ip_packet_type)主要是注册ip报文处理函数ip_rcv到pttype_base。arp_init-->dev_add_pack(&arp_packet_type)主要是注册arp报文处理函数arp_rcv到pttype_base。报文处理网卡调用__netif_receivve_skb_core后，会调用deliver_skb(skb, pt_prev, orig_dev)处理对应的３层协议函数。ip协议ip_rcv主要获取报文头，报文健康检查，最后进入NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,...,ip_rcv_finish)。ip_rcv_finish进行路由查找，ip_route_input_noref-->ip_route_input_slow进行慢速路由判定。ip_route_input_slow判定报文是本地的话，就给input装载ip_local_deliver函数，如果不是本地继续调用ip_mkroute_input-->__mkroute_input查找路由，并且给input装载ip_forward函数，output装载ip_output。ip_local_deliver函数直接进入NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,..., ip_local_deliver_finish)。ip_local_deliver_finish调用ipprot->handler进入4层的协议处理函数。ip_forward进入NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,..., ip_forward_finish)。ip_forward_finish调用output装载的ip_output。ip_output进入NF_HOOK(NFPROTO_IPV4, NF_INET_POST_ROUTING,...,ip_finish_output)ip_finish_output进行分片的判断和操作，此处涉及到GSO的判定，最后调用ip_finish_output2。ip_finish_output2调用__ipv4_neigh_lookup_noref进行邻居子系统的表查找，ipv4主要是arp表，查找到arp表，则调用dst_neigh_output-->neigh_hh_output-->dev_queue_xmit进行报文发送。如果没有查找到，则调用__neigh_create-->arp_constructor进行发送等函数的装载，最后也调用dst_neigh_output，然后也调用装载的发送函数，将报文修改为request，调用dev_queue_xmit进行发送。arp协议arp_rcv健康检查后，进入NF_HOOK(NFPROTO_ARP, NF_ARP_IN, ..., arp_process)arp_process完成了所有的处理操作，包括是reply报文则更新arp表；如果是request本地的话，则更新或者创建arp表，并且调用arp_send回复arp报文；如果不是本地不支持arp proxy的话，丢弃报文；支持的话转发报文。arp_send调用arp_create创建报文，并且调用arp_xmit发送报文。arp_xmit进入NF_HOOK(NFPROTO_ARP, NF_ARP_OUT, ..., dev_queue_xmit_sk)，最后调用dev_queue_xmit发送报文。转发流程邻居子系统Generic neighbouring interface(VFT)，为上层协议提供了一个统一的输出接口neigh->output()。Generic neighbouring interface，为下层提供的也是一个统一的接口dev_queue_xmit()。ARP是为IPV4设计的地址解析协议，而ND（neighbour detect）则是为IPv6设计的。这些地址解析协议可以说是“嵌入”在邻居子系统里面，但是又可以自由灵活的拆卸，非常方便。为了加速数据包的发送速度，会将路由表和邻居缓存进行绑定，这个绑定其实就是把邻居缓存中的每个项的结构体嵌入路由表中的一个路由项中，这样报文在查找到路由以后，其实也相当于已经在邻居子系统中查到了缓存，减少了查找的次数。这样在进入邻居子系统后的处理流程就很短。struct neigh_table ：邻居表，每个地址解析协议就会创建这样的一个表(比如arp)struct neighbour ：邻居项代表一个邻居。邻居项用哈希表+链表链的组织方式struct hh_cache ：这个字段存的就是封装好的二层协议头部，每个报文在进入邻居子系统前都会查找路由，路由项中就会包含这hh_cache这个结构状态机邻居项存在一种状态机，邻居项都有一个对于管理和维护邻居表来说非常重要的成员，nud_state，用来表示该邻居项当前所处的状态。下面依依介绍这几个状态：NUD_NONE：邻居项刚建立时处于的状态，在该状态下，还没有硬件地址可以用，所以还不能发送请求报文。一旦有报文要输出到该邻居，便会出发对该邻居硬件地址的请求，进入NUD_INCOMPLETE状态，并缓存发送的报文。NUD_INCOMPLETE：该状态是请求报文已发送，但尚未收到应答的状态。该状态下还没解析到硬件地址，因此尚无可用硬件地址，如果有报文要输出到该邻居，会将其缓存起来。这个状态会启动一个定时器，如果在定时器到期时还没有接收到邻居的回应，则会重复发送请求报文，否则发送请求报文的次数打到上限，便会进入NUD_FAILED。NUD_REACHABLE：该状态以及得到并缓存了邻居的硬件地址。进入该状态首先设置邻居项相关的output函数(该状态使用neighbors_ops结构的connectd_outpt)，然后查看是否存在要发送给该邻居的报文。如果在该状态下闲置时间达到上限，便会进入NUD_STATLE。NUD_STATLE：该状态一旦有报文要输出到该邻居，则会进入NUD_DELAY并将该报文输出。如果在该状态下闲置时间达到上限，且此时的引用计数为1，则通过垃圾回收机制将其删除，在该状态下，报文的输出不收限制，使用慢速发送过程。NUD_DELAY：该状态下表示NUD_STATE状态下发送的报文已经发出，需得到邻居的可达性确认的状态。在为接收到邻居的应答或确认时也会定时地重发请求，如果发送请求报文的次数到上限，如果收到邻居的应答，进入NUD_REACHABLE,否则进入NUD_FAILED，在该状态下，报文的输出不收限制，使用慢速发送过程。操作neigh_create:创建一个neighbour项结构，当以下情况发生：传输请求:向一台L2地址未知的主机传输请求，就需要对这个地址进行解析。收到solicitation请求：收到这个请求的主机会假定两个系统有通信发生，会创建一个缓存项。手工添加：手工添加一个邻居缓存项。neigh_release:这个函数会减少neighbour的引用计数，当引用计数为0时，才真正删除neighbour结构，这个函数真正调用neigh_destroy。以下是函数调用的原因：内核企图向一个不可到达的主机发送报文。与这个邻居结构相关的L2地址改变了。邻居结构存在的时间太长，内核需要它占用的内存。neigh_update:更新neighbour状态和链路层地址。流程如下:更新状态不是NUD_VALID态的邻居发生的变化，主要是状态机和neigh->output。更新L2地址，给状态不是NUD_VALID态的邻居使用。设置一个新的链路层地址。改变NUD状态。处理arp_queue队列。arp例子
版本说明Linux版本： 3.10.103网卡驱动： ixgbe网络协议注册br_add_if主要是注册桥处理函数br_handle_frame给skb->edv->rx_handler。netdev_create-->netdev_rx_handler_register主要是注册接收函数netdev_frame_hook，此处是给openvswitch的datapath用的。报文接收网卡调用__netif_receivve_skb_core后，会调用skb->dev->rx_handler处理２层协议函数，此处只关注桥的，暂不关注ovs的。br_handle_frame首先判定mac是否本地，是本地的报文则进入到NFPROTO_BRIDGE的NF_HOOK，即NF_BR_LOCAL_IN，进入处理链，最终完成后调用br_handle_local_finish完成操作，等着进入再上一层的协议。如果不是本地，则进入转发流程，如果直接转发状态，则调用br_should_route_hook，即ebt_route；如果是学习状态是NF_HOOK，即NF_BR_PRE_ROUTING，进入处理链，最终完成调用br_handle_frame_finish。然后调用br_fdb_update学习源MAC地址，更新MAC地址表，__br_fdb_get或者目的MAC对应的端口。如果查找到了端口，调用br_forward转发到对应的端口。找不到端口，调用br_flood_forward进行广播。br_forward继续调用NF_HOOK的NF_BR_FORWARD，完成后调用br_forward_finish，继续调用NF_HOOK的NF_BR_POST_ROUTING，最终调用br_dev_queue_push_xmit。br_dev_queue_push_xmit-->dev_queue_xmit进行L2的发送。报文发送dev_queue_xmitnetdev_pick_tx主要是选择发送设备设置的映射关系，查找当前CPU对应的队列，如果多个则计算hash，确定队列。支持入队的话，调用__dev_xmit_skb进行下一步的操作。不支持队列的话调用dev_hard_start_xmit进行数据的发送。__dev_xmit_skb如果队列的状态是非活跃的，则丢包。如果是支持bypass，则调用sch_direct_xmit直接发送数据包，没有发送完成，继续调用__qdisc_run。其他情况入队并且调用__qdisc_run。__qdisc_runwhile循环调用qdisc_restart，进行的操作为skb出队，并且调用sch_direct_xmit直接发送数据包，没有发送完成，返回值非0，继续参与循环。一直循环到数据包发送完毕，或者权重耗完或者需要重新调度，此时调用__netif_schedule-->__netif_reschedule，将当前的队列记录在CPU的softnet_data，触发软中断NET_TX_SOFTIRQ，调用net_tx_action。sch_direct_xmit直接调用dev_hard_start_xmit-->ops->ndo_start_xmit-->ixgbe_xmit_frame发送数据包。如果发送成功，则返回值为队列剩下的数据包量。如果被自己CPU锁了则报错，被其他CPU锁了则重新入队，等以后再发送。如果busy的话重新入队，等以后发送。所以其实就是发送完成则返回成功0，否则发送非0。
版本说明Linux版本： 3.10.103网卡驱动： ixgbe报文收发简单流程网卡驱动默认采用的是NAPI的报文处理方式。即中断+轮询的方式，网卡收到一个报文之后会产生接收中断，并且屏蔽中断，直到收够了netdev_max_backlog个报文(默认300)或者收完网卡上的所有报文之后，重新打开中断。网卡数据处理网卡初始化内核启动时会调用do_initcalls，从而调用注册的初始化接口net_dev_init，net_dev_init注册软中断的回调函数,分别为接收和发送的：NET_RX_SOFTIRQ = net_rx_action，NET_TX_SOFTIRQ = net_tx_action。网卡驱动加载时调用ixgbe_init_module注册一个PCI驱动。接着调用probe对应的ixgbe_probe做一些准备工作如下：创建ixgbe_adapter数据结构，这是网卡的一个实例，其中包含了网卡的所有数据和接口(netdev)。netdev注册了网卡的所有操作：ixgbe_netdev_ops和网卡的features。ixgbe_init_interrupt_scheme主要是设置网卡的NAPI对应的poll接口，这里主要是ixgbe_poll。网卡激活时会调用netdev上的open函数ixgbe_open，完成工作如下：设置接收和发送队列，通过DMA讲PCI网卡地址和队列建立映射。给网卡注册硬中断：ixgbe_open-->ixgbe_request_irq-->ixgbe_request_msix_irqs注册中断回调ixgbe_msix_clean_rings。(MSIX中断时)ixgbe_close函数所做的事情：关闭中断并且释放中断向量号。释放申请的相应空间。网卡收发数据当网卡接收到数据帧时，DMA将数据搬运到对应的rx_ring，然后产生一个硬中断，调用接口ixgbe_msix_clean_rings。硬中断回调函数ixgbe_msix_clean_rings会调用当前CPU对应的NET_RX_SOFTIRQ，即net_rx_action。其中的主要实现NAPI操作如下：关闭硬中断。调用该CPU的poll_list中的所有poll函数，即ixgbe_poll。开启硬中断。轮询函数ixgbe_poll主要是处理接收发送任务：循环处理每个ring中的数据，ixgbe_clean_tx_irq，此处发送tx_work_limit个报文(128)或者没有报文后完成，并且调用netif_wake_subqueue触发发送软中断NET_TX_SOFTIRQ，即net_tx_action。循环处理每个ring中的数据，ixgbe_clean_rx_irq，此处接收weight个报文(64)，总共收取netdev_budget(300)个，并且调用ixgbe_rx_skb进行下一步的处理。ixgbe_rx_skb最终会处理过GRO之后调用到netif_receive_skb，此处忽略rps。netif_receive_skb最终调用__netif_receivve_skb_core进入协议栈处理各种协议。首先根据下面介绍的rx_handler类型调用ovs或者桥处理函数进入L2的处理。然后根据pttype_base处理相关的L3协议处理。备注ixgbe拥有64个rx_ring，每个ring默认有512个buffer，最小64，最大4096。由以上可知网卡默认可以缓存32768个报文，当网卡接收报文的速度过快，接收软中断无法处理的时候，系统进入扼流(throttle)状态，所有后续的报文都被丢弃，直到rx_ring有空间了。通过DMA地址映射的方式，网卡接收数据相当于零拷贝。TUN虚拟网卡网卡驱动加载时调用tun_init注册一个MISC驱动，生成/dev/net/tun的设备文件。接着调用open对应的tun_chr_open做主要创建socket。随后调用ioctl对应的tun_chr_ioctl，完成一些需要的配置和信息获取，包括校验、连接状态、offload、mac等。通过调用write发送报文，主要是调用tun_chr_aio_write生成skb格式报文，最终通过调用netif_rx进入协议栈，通过三层转发到对应的端口发送。通过调用read接收报文，主要是调用tun_chr_aio_read，从内核对应的sock队列上拿到报文，然后将数据上传到用户层。注:因为网卡接收报文后最终经过四层的时候，会将报文根据协议几元组找到对应的sock结构，放入队列。上层调用read或者recv的时候都是从socket对应的sock结构队列上取数据。VETH虚拟网卡网卡驱动加载时调用veth_init注册一个netlink驱动。接着调用newlink对应的veth_newlink，创建两个网络设备，两者互为对方的peer。发送报文时会调用ndo_start_xmit对应的veth_xmit，实际操作为将skb的dev设置为自己的peer，调用到netif_rx接收到协议栈。QEMU虚拟机网络通信主机vhost驱动加载时调用vhost_net_init注册一个MISC驱动，生成/dev/vhost-net的设备文件。主机qemu-kvm启动时调用open对应的vhost_net_open做主要创建队列和收发函数的挂载，接着调用ioctl启动内核线程vhost，做收发包的处理。主机qemu通过ioctl配置kvm模块，主要设置通信方式，因为主机vhost和virtio只进行报文的传输，kvm进行提醒。虚拟机virtio模块注册，生成虚拟机的网络设备，配置中断和NAPI。虚拟机发包流程如下：直接从应用层走协议栈最后调用发送接口ndo_start_xmit对应的start_xmit，将报文放入发送队列，vp_notify通知kvm。kvm通过vmx_handle_exit一系列调用到wake_up_process唤醒vhost线程。vhost模块的线程激活并且拿到报文，在通过之前绑定的发送接口handle_tx_kick进行发送，调用虚拟网卡的tun_sendmsg最终到netif_rx接口进入主机内核协议栈。虚拟机收包流程如下：tap设备的ndo_start_xmit对应的tun_net_xmit最终调用到wake_up_process激活vhost线程，调用handle_rx_kick，将报文放入接收队列。通过一系列的调用到kvm模块的接口kvm_vcpu_kick，向qemu虚拟机注入中断。虚拟机virtio模块中断调用接口vp_interrupt，调用virtnet_poll，再调用到netif_receive_skb进入虚拟机的协议栈。Offload技术发送数据TSO(TCP Segmentation Offload)使得网络协议栈能够将大块buffer推送至网卡，然后网卡执行分片工作，这样减轻了CPU 的负荷，但TSO需要硬件来实现分片功能，也叫LSO。UFO(UDP Fragmentation Offload)类似。GSO(Generic Segmentation Offload)，它比TSO更通用，基本思想就是尽可能的推迟数据分片直至发送到网卡驱动之前，此时会检查网卡是否支持分片功能(如TSO、UFO)，如果支持直接发送到网卡，如果不支持就进行回调分片后再发往网卡。这样大数据包只需走一次协议栈，而不是被分割成几个数据包分别走，这就提高了效率。接收数据LRO(Large Receive Offload)通过将接收到的多个TCP数据聚合成一个大的数据包，然后传递给网络协议栈处理，以减少上层协议栈处理开销，提高系统接收TCP数据包的能力。GRO(Generic Receive Offloading)，LRO使用发送方和目的地IP地址，IP封包ID，L4协议三者来区分段，对于从同一个SNAT域的两个机器发向同一目的IP的两个封包可能会存在相同的IP封包ID(因为不是全局分配的ID)，这样会有所谓的卷绕的bug。GRO采用发送方和目的地IP地址，源/目的端口，L4协议三者来区分作为改进。所以对于后续的驱动都应该使用GRO的接口，而不是LRO。另外，GRO也支持多协议。
了解版本为主要版本为1.5.12。主要从四个方面了解：主循环逻辑、TCP连接处理流程、HTTP连接处理流程。主循环处理流程主循环run_poll_loopHAproxy的主循环在haproxy.c中的run_poll_loop()函数，代码如下：/* Runs the polling loop */ void run_poll_loop() { int next; tv_update_date(0,1); while (1) { /* check if we caught some signals and process them */ signal_process_queue(); /* Check if we can expire some tasks */ wake_expired_tasks(&next); /* Process a few tasks */ process_runnable_tasks(&next); /* stop when there's nothing left to do */ if (jobs == 0) break; /* The poller will ensure it returns around <next> */ cur_poller.poll(&cur_poller, next); fd_process_cached_events(); } }主循环的结构比较清晰，就是循环的调用几个函数，并在适当的时候结束循环并退出：处理信号队列。超时任务。处理可运行的任务。检测是否可以结束循环。执行 poll 处理 fd 的 IO 事件。处理可能仍有 IO 事件的 fd.signal_process_queue - 处理信号队对列haproxy实现了自己的信号处理机制。接受到信号之后，将该信号放到信号队列中。在程序运行到signal_process_queue()时处理所有位于信号队列中的信号。wake_expired_tasks - 唤醒超时任务haproxy的顶层处理逻辑是task，task上存储着要处理的任务的全部信息。task的管理是采用队列方式，同时分为wait_queue和run_queue。顾名思义，wait_queue是需要等待一定时间的task的集合，而run_queue则代表需要立即执行的task的集合。该函数就是检查wait_queue中那些超时的任务，并将其放到run_queue中。haproxy在执行的过程中，会因为一些情况导致需要将当前的任务通过调用task_queue等接口放到wait_queue中。process_runnable_tasks - 处理可运行的任务处理位于run_queue中的任务。前面提到，wake_expired_tasks可能将一些超时的任务放到run_queue中。此外，haproxy执行的过程中，还有可能通过调用task_wakeup直接讲某个task放到run_queue中，这代表程序希望该任务下次尽可能快的被执行。对于TCP或者HTTP业务流量的处理，该函数最终通过调用process_session来完成，包括解析已经接收到的数据， 并执行一系列load balance的特性，但不负责从socket收发数据。jobs == 0 - 无任务可执行，结束循环haproxy中用jobs记录当前要处理的任务总数，一个listener也会被计算在内。因此， 如果jobs为0的话，通常意味着haproxy要退出了，因为连listener都要释放了。 jobs的数值通常在process_session时更新。因此，是否可以退出循环，就放在了所有任务的process_session执行之后。cur_poller.poll() - 执行poll处理fd的IO事件haproxy启动阶段，会检测当前系统可以启用那种异步处理的机制，比如select、poll、epoll、kqueue等，并注册对应poller的poll方法。epoll的相关函数接口在ev_epoll.c中。这里就是执行已经注册的poller的poll方法，主要功能就是获取所有活动的fd，并调用对应的handler，完成接受新建连接、数据收发等功能。处理可能仍有IO事件的fdpoller的poll方法执行时，程序会将某些符合条件以便再次执行IO处理的的fd放到fd_spec list[]中，fd_process_cached_events()函数会再次执行这些fd的io handler。TCP连接处理流程关键数据结构 sessionhaproxy负责处理请求的核心数据结构是struct session，本文不对该数据结构进行分析。从业务的处理的角度，简单介绍一下对session的理解：haproxy每接收到client的一个连接，便会创建一个session结构，该结构一直伴随着连接的处理，直至连接被关闭，session才会被释放。haproxy其他的数据结构，大多会通过引用的方式和session进行关联。一个业务session上会存在两个TCP连接，一个是client到haproxy，一个是haproxy到后端server.此外，一个session，通常还要对应一个task，haproxy最终用来做调度的是通过task.相关初始化在haproxy正式处理请求之前，会有一系列初始化动作。这里介绍和请求处理相关的一些初始化。初始化处理TCP连接的方法初始化处理TCP协议的相关数据结构，主要是和socket相关的方法的声明。详细见下面proto_tcpv4 (proto_tcp.c)的初始化：/* Note: must not be declared <const> as its list will be overwritten */ static struct protocol proto_tcpv4 = { .name = "tcpv4", .sock_domain = AF_INET, .sock_type = SOCK_STREAM, .sock_prot = IPPROTO_TCP, .sock_family = AF_INET, .sock_addrlen = sizeof(struct sockaddr_in), .l3_addrlen = 32/8, .accept = &listener_accept, .connect = tcp_connect_server, .bind = tcp_bind_listener, .bind_all = tcp_bind_listeners, .unbind_all = unbind_all_listeners, .enable_all = enable_all_listeners, .get_src = tcp_get_src, .get_dst = tcp_get_dst, .drain = tcp_drain, .pause = tcp_pause_listener, .listeners = LIST_HEAD_INIT(proto_tcpv4.listeners), .nb_listeners = 0, };初始化 listenerlistener，顾名思义，就是用于负责处理监听相关的逻辑。在haproxy解析bind配置的时候赋值给listener的proto成员。函数调用流程如下：cfgparse.c -> cfg_parse_listen -> str2listener -> tcpv4_add_listener -> listener->proto = &proto_tcpv4;由于这里初始化的是listener处理socket的一些方法。可以推断，haproxy接收client新建连接的入口函数应该是protocol结构体中的accpet方法。对于tcpv4来说，就是listener_accept()函数。listener的其他初始化cfgparse.c -> check_config_validity -> listener->accept = session_accept; listener->frontend = curproxy; (解析 frontend 时，会执行赋值： curproxy->accept = frontend_accept） listener->handler = process_session;整个haproxy配置文件解析完毕，listener也已初始化完毕。可以简单梳理一下几个accept方法的设计逻辑：stream_sock_accept(): 负责接收新建TCP连接，并触发listener自己的accept方法session_accept()。session_accept(): 负责创建session，并作session成员的初步初始化，并调用frontend的accept方法front_accetp()。frontend_accept(): 该函数主要负责session前端的TCP连接的初始化，包括socket设置，log设置，以及session部分成员的初始化。下文分析TCP新建连接处理过程，基本上就是这三个函数的分析。绑定所有已注册协议上的listenershaproxy.c -> protocol_bind_all -> all registered protocol bind_all -> tcp_bind_listeners (TCP) -> tcp_bind_listener -> [ fdtab[fd].iocb = listener->proto->accept ]该函数指针指向proto_tcpv4结构体的accept成员，即函数listener_accept。启用所有已注册协议上的listeners把所有 listeners 的 fd 加到 polling lists 中 haproxy.c -> protocol_enable_all -> all registered protocol enable_all -> enable_all_listeners (TCP) -> enable_listener 函数会将处于 LI_LISTEN 的 listener 的状态修改为 LI_READY，并调用 cur poller 的 set 方法， 比如使用 sepoll，就会调用 __fd_set.TCP 连接的处理流程接受新建连接前面几个方面的分析，主要是为了搞清楚当请求到来时，处理过程中实际的函数调用关系。以下分析TCP建连过程。haproxy.c -> run_poll_loop -> cur_poller.poll -> _do_poll (如果配置使用的是 sepoll，则调用 ev_sepoll.c 中的 poll 方法) -> fd_process_polled_events -> fdtab[fd].iocb (TCP 协议的该函数指针指向 listener_accept ) -> listener_accept -> 按照 global.tune.maxaccept 的设置尽量可能多执行系统调用 accept，然后再调用 l->accept()，即 listener 的 accept 方法 session_accept -> session_acceptsession_accept主要完成以下功能:调用conn_new分配一个connection结构。cli_conn->t.sock.fd = cfd: 系统调用返回的 cfd 记录到连接。cli_conn->addr.from = *addr: 记录ip地址。调用pool_alloc2分配一个session结构。调用task_new分配一个新任务。将新分配的session加入全局sessions链表中。si_attach_conn将si_conn_cb绑定到connection(si_conn_recv_cb和si_conn_send_cb)。conn_attach将sess_conn_cb绑定到connection(全为NULL)。conn_ctrl_init主要是fdtab[fd].iocb = conn_fd_handler。session_complete中的session和task的初始化，若干重要成员的初始化如下：t->process = l->handler:即t->process指向process_session。t->context = s:任务的上下文指向session。s->listener = l:session的listener成员指向当前的listener。s->si[]的初始化，将si_conn_cb绑定connection(si_conn_recv_cb和si_conn_send_cb)。为s->req和s->rep分别分配内存，并作对应的初始化。s->req = pool_alloc2(pool2_buffer)。s->rep = pool_alloc2(pool2_buffer)。从代码上来看，应该是各自独立分配tune.bufsize + sizeof struct buffer大小的内存。初始化s->txn。p->accept执行proxy的accept方法即frontend_accept。设置session结构体的log成员。根据配置的情况，分别设置新建连接套接字的选项，包括TCP_NODELAY/KEEPALIVE/LINGER/SNDBUF/RCVBUF等等。如果mode是http的话，将session的txn成员做相关的设置和初始化。TCP连接上的接收事件haproxy.c -> run_poll_loop -> cur_poller.poll -> _do_poll (如果配置使用的是 sepoll，则调用 ev_sepoll.c 中的 poll 方法) -> fd_process_polled_events -> fdtab[fd].iocb(fd) (该函数在建连阶段被初始化为四层协议的 read 方法，对于 TCP 协议，为 conn_fd_handler) -> conn_fd_handler -> conn->data->recv(对应si_conn_recv_cb) -> si_conn_recv_cbsi_conn_recv_cb主要完成以下功能:找到当前连接的读缓冲，即当前 session 的 req buffer:struct channel *chn = si->ib。根据配置，调用splice或者recv读取套接字上的数据，并填充到读缓冲中，即填充到从b->r（初始位置应该就是b->data）开始的内存中。ret = conn->xprt->rcv_pipe(conn, chn->pipe, chn->to_forward); -> raw_sock_to_buf。如果读取到0字节，则意味着接收到对端的关闭请求，调用stream_sock_shutr进行处理。读缓冲标记si->ib->flags的BF_SHUTR置位，清除当前fd的epoll读事件，不再从该fd读取。如果写缓冲si->ob->flags的BF_SHUTW已经置位，说明应该是由本地首先发起的关闭连接动作。将fd从fdset[]中清除，从epoll中移除fd，执行系统调用close(fd)，fd.state置位FD_STCLOSE。 *stream interface的状态修改si->state = SI_ST_DIS。唤醒任务task_wakeup，把当前任务加入到run queue中。随后检测runnable tasks时，就会处理该任务。TCP连接上的发送事件haproxy.c -> run_poll_loop -> cur_poller.poll -> _do_poll (如果配置使用的是 sepoll，则调用 ev_sepoll.c 中的 poll 方法) -> fdtab[fd].iocb(fd) (该函数在建连阶段被初始化为四层协议的 write 方法，对于 TCP 协议，为 conn_fd_handler) -> conn_fd_handler -> conn->data->send(对应si_conn_send_cb) -> si_conn_send_cbsi_conn_send_cb主要完成以下功能:找到当前连接的写缓冲，即当前session的rep buffer:struct channel *chn = si->ob;。将待发送的数据调用send系统调用发送出去。或者数据已经发送完毕，需要发送关闭连接的动作stream_sock_shutw->系统调用shutdown。唤醒任务task_wakeup，把当前任务加入到run queue中。随后检测runnable tasks时，就会处理该任务。HTTP 请求的处理haproxy.c -> run_poll_loop -> process_runnable_tasks，查找当前待处理的任务所有 tasks， 然后调用 task->process（大多时候就是 process_session） 进行处理 -> process_sessionprocess_session主要完成以下功能:处理连接需要关闭的情形，分支resync_stream_interface。处理请求，分支resync_request (read event)。根据s->req->analysers的标记位，调用不同的analyser进行处理请求。ana_list & AN_REQ_WAIT_HTTP:http_wait_for_request。ana_list & AN_REQ_HTTP_PROCESS_FE:http_process_req_common。ana_list & AN_REQ_SWITCHING_RULES:process_switching_rules。处理应答，分支resync_response(write event)。根据s->rep->analysers的标记位，调用不同的analyser进行处理请求。ana_list & AN_RES_WAIT_HTTP:http_wait_for_response。ana_list & AN_RES_HTTP_PROCESS_BE:http_process_res_common。处理forward buffer的相关动作。关闭req和rep的buffer，调用pool2_free释放session及其申请的相关内存，包括读写缓冲 (read 0 bytes)。pool_free2(pool2_buffer, s->req);pool_free2(pool2_buffer, s->rep);pool_free2(pool2_session, s);task从运行任务队列中清除，调用pool2_free释放 task申请的内存:task_delete(); task_free();本文简单分析了TCP连接的处理过程，不侧重细节分析，而且缺少后端server的选择以及建连等，重在希望展示出一个haproxy处理TCP连接的框架。HTTP请求处理初始化session数据处理相关的设置建连的处理基本上就是_do_poll->listener_accept->session_accept->fronend_accept()。其中session_accept()会设置新建fd的io handler。/* Add the various callbacks. Right now the transport layer is present * but not initialized. Also note we need to be careful as the stream * int is not initialized yet. */ conn_prepare(s->si[0].conn, &sess_conn_cb, l->proto, l->xprt, s); fdtab[cfd].owner = s->si[0].conn; /*fd 对应的 owner 为 connection 结构*/ fdtab[cfd].iocb = conn_fd_handler; conn_data_want_recv(s->si[0].conn); if (conn_xprt_init(s->si[0].conn) < 0) goto out_free_task;IPv4 http对应的listener的xprt和proto分别被初始化为l->xprt = &raw_sock; l->proto = &proto_tcpv4;conn_prepare()就是将相关数据收发以及连接处理的函数都赋值到connection结构体上：/* Assigns a connection with the appropriate data, ctrl, transport layers, and owner. */ static inline void conn_assign(struct connection *conn, const struct data_cb *data, const struct protocol *ctrl, const struct xprt_ops *xprt, void *owner) { conn->data = data; conn->ctrl = ctrl; conn->xprt = xprt; conn->owner = owner; } /* prepares a connection with the appropriate data, ctrl, transport layers, and * owner. The transport state and context are set to 0. */ static inline void conn_prepare(struct connection *conn, const struct data_cb *data, const struct protocol *ctrl, const struct xprt_ops *xprt, void *owner) { conn_assign(conn, data, ctrl, xprt, owner); conn->xprt_st = 0; conn->xprt_ctx = NULL; }经过初始化，session client端的connection结构体初始化完成：conn->data指向sess_conn_cb。后面调用session_complete()会被再次赋值。conn->ctrl指向l->proto, IPv4下为proto_tcpv4。conn->xprt执向l->xprt, 不启用SSL时为raw_sock，启用SSL时为ssl_sock。conn->owner指向session。接着调用session_complete完成建立一个session所需要的最后的初始化工作，其中包含调用frontend_accept，并将当前session对应的task放入runqueue中以待下次执行：... si_takeover_conn(&s->si[0], l->proto, l->xprt); ... t->process = l->handler; ... if (p->accept && (ret = p->accept(s)) <= 0) { /* Either we had an unrecoverable error (<0) or work is * finished (=0, eg: monitoring), in both situations, * we can release everything and close. */ goto out_free_rep_buf; } ... task_wakeup(t, TASK_WOKEN_INIT);其中si_takeover_conn完成为si分配连接的处理函数，实现如下：static inline void si_takeover_conn(struct stream_interface *si, const struct protocol *ctrl, const struct xprt_ops *xprt) { si->ops = &si_conn_ops; conn_assign(si->conn, &si_conn_cb, ctrl, xprt, si); }si_conn_cb的定义如下：struct data_cb si_conn_cb = { .recv = si_conn_recv_cb, .send = si_conn_send_cb, .wake = si_conn_wake_cb, };因此，si->conn->data指向了si_conn_cb。这个结构用在随后的recv/send中。此外，session所对应的任务task在session_complete的最后通过调用task_wakeup()是在随后的循环中被执行。task的处理函数初始化为l->handler即process_session()。至此，一个新建session的client fd的io处理函数conn_fd_handler()及session的处理函数process_session()都已经正确初始化好了。以后基本上就是这两个函数分别负责数据的读取，以及业务的处理。接收client发送的请求数据epoll中考虑的新建连接通常会尽可能快的传输数据，因此对于新建的fd，通常会尽快的执行io handler，即调用conn_fd_handler。是在ev_epoll.c中的_do_poll()中进行：gettimeofday(&before_poll, NULL); status = epoll_wait(epoll_fd, epoll_events, global.tune.maxpollevents, wait_time); tv_update_date(wait_time, status); measure_idle(); /* process polled events */ for (count = 0; count < status; count++) { unsigned int n; unsigned int e = epoll_events[count].events; fd = epoll_events[count].data.fd; ... /* Save number of updates to detect creation of new FDs. */ old_updt = fd_nbupdt; fdtab[fd].iocb(fd); ... for (new_updt = fd_nbupdt; new_updt > old_updt; new_updt--) { fd = fd_updt[new_updt - 1]; ... if (fdtab[fd].ev && fdtab[fd].iocb && fdtab[fd].owner) fdtab[fd].iocb(fd); ... }上面代码中第一处执行iocb()的是由epoll_wait()返回的fd触发的。而第二次的iocb()则就是在前面iocb的执行过程中新建的fd，为了提高效率，则直接调用该fd的iocb()，也 就是conn_fd_handler()函数。int conn_fd_handler(int fd) { struct connection *conn = fdtab[fd].owner; ... if ((fdtab[fd].ev & (FD_POLL_IN | FD_POLL_HUP | FD_POLL_ERR)) && conn->xprt && !(conn->flags & (CO_FL_WAIT_RD|CO_FL_WAIT_ROOM|CO_FL_ERROR|CO_FL_HANDSHAKE))) { /* force detection of a flag change : it's impossible to have both * CONNECTED and WAIT_CONN so we're certain to trigger a change. */ flags = CO_FL_WAIT_L4_CONN | CO_FL_CONNECTED; conn->data->recv(conn); } ... }根据的session_complete的初始化，上面代码conn->data->recv指向si_conn_recv_cb()。 该函数就是haproxy中负责接收数据的入口函数。相同的，si_conn_send_cb()就是haproxy中负责发送数据的入口函数。si_conn_recv_cb()函数简单介绍如下：if (conn->xprt->rcv_pipe && chn->to_forward >= MIN_SPLICE_FORWARD && chn->flags & CF_KERN_SPLICING) { ... ret = conn->xprt->rcv_pipe(conn, chn->pipe, chn->to_forward); ... } ... while (!chn->pipe && !(conn->flags & (CO_FL_ERROR | CO_FL_SOCK_RD_SH | CO_FL_DATA_RD_SH | CO_FL_WAIT_RD | CO_FL_WAIT_ROOM | CO_FL_HANDSHAKE))) { ... ret = conn->xprt->rcv_buf(conn, chn->buf, max); ... }该函数主要根据数据的接收情况，选择调用xprt的rcv_pipe还是rcv_buf。前面已经分析过，conn->xprt指向了listner的xprt，不启用SSL就是raw_sock数据结构。因此，数据的接收最终是通过调用raw_sock的raw_sock_to_pipe或/和raw_sock_to_buf完成的。连接处理过程haproxy.c -> run_poll_loop 调用 cur_poller.poll， 实际为_do_poll (如果配置使用的是 sepoll，则调用 ev_sepoll.c 中的 poll 方法) --> _do_poll 中监听事件，触发之后根据事件数量依次调用 fd_process_polled_events ---> fd_process_polled_events 调用 fdtab[fd].iocb (TCP协议的该函数指针指向listener_accept) ----> listener_accept 按照 global.tune.maxaccept 的设置尽量可能多执行系统调用 accept，然后再调用 l->accept()，即 listener 的 accept 方法 session_accept -----> session_accept 调用 session_complete ------> session_complete 调用 p->accept 即 frontend_accept -------> frontend_accept 调用完毕后退回到 fd_process_polled_events ---> fd_process_polled_events 进入循环判定是否有新的fd加入，有的话调用fd对应的fdtab[fd].iocb(TCP协议的该函数指针指向 conn_fd_handler) ----> conn_fd_handler 调用 conn->data->recv(conn) (指向 si_conn_recv_cb) -----> si_conn_recv_cb 调用 conn->xprt->rcv_buf(conn, chn->buf, max) (指向 raw_sock_to_buf) ------> raw_sock_to_buf 调用 recv(conn->t.sock.fd, bi_end(buf), try, 0) 接收数据，退回到 conn_fd_handler ----> conn_fd_handler 调用 conn->data->wake(conn) (指向 si_conn_wake_cb) -----> si_conn_wake_cb 退回到 run_poll_loop -> run_poll_loop 调用 process_runnable_tasks
测试方式如下：20个线程同时进行malloc和free的操作，每个线程进行malloc和free的操作1M次，每次申请的大小是操作次数的大小(比如第10次操作，就malloc(10))。测试每个线程操作1M次malloc和free的时间。glibc malloc的结果如下：time use 20118516 second time use 20159695 second time use 20209392 second time use 20243706 second time use 20314329 second time use 20409412 second time use 20497020 second time use 20485955 second time use 20536799 second time use 20530233 second time use 20615596 second time use 20697397 second time use 20783369 second time use 20798414 second time use 20832314 second time use 20844732 second time use 20847341 second time use 20877054 second time use 20919291 second time use 20924166 secondtcmalloc的结果如下：time use 6547655 second time use 6586277 second time use 6595014 second time use 6602379 second time use 6603297 second time use 6617556 second time use 6606473 second time use 6594718 second time use 6625851 second time use 6610081 second time use 6627009 second time use 6632563 second time use 6632408 second time use 6614826 second time use 6616527 second time use 6639291 second time use 6640707 second time use 6641525 second time use 6616567 second time use 6643258 second代码如下：#include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <time.h> #include <pthread.h> #define LOOP_NUM (1024 * 1024) #define THREAD_NUM 20 static void *fun_thread(void *argv) { struct timeval start, end; gettimeofday(&start, NULL); uint32_t i; for(i = 0; i < LOOP_NUM; i++) { uint8_t *a = malloc(i); free(a); } gettimeofday(&end, NULL); printf("time use %ld second\n", (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec); return NULL; } void main(void) { pthread_t a[THREAD_NUM]; uint8_t i; for(i = 0; i < THREAD_NUM; i++) pthread_create(&a[i], NULL, fun_thread, NULL); for(i = 0; i < THREAD_NUM; i++) pthread_join(a[i], NULL); }编译如下：@gcc $< -o $@ -lpthread @gcc $< -o $@ -lpthread -ltcmalloc_minimal