目录
1 ip_append_data 函数功能概述
2 skb数据组织策略
2.1 情形1
2.2 情形2
2.3 情形3
ip_append_data() 主要是由 udp 使用。因为 ip_append_data() 的实现非常的复杂,涉及 skb 对数据的组织方式,以及各种出于性能考虑的分配策略,所以在分析其代码实现之前,有必要先讨论下内核实际采取的数据组织方式。
首先需要明确 ip_append_data() 要实现的功能:
ip_append_data() 的任务就是将多个片段合并成一个大的IP报文,该函数仅仅做合并工作,并不实际发送报文,如果要发送,调用者后面应该主动调用 ip_push_pending_frames();这里要注意区分IP报文和 IP片段的不同,发送端可以组织一个很大的IP报文,但是在实际发送时,为了适配设备的MTU,需要将这一个IP报文拆封成多个IP片段,这些拆分的IP片段的ipid相同,片内偏移不同;该函数将参数指定的数据(from,length)按照MTU大小组织成一个个的方便后面IP协议处理的skb,并且将这些skb放入传输控制块的发送缓冲区中,这些skb在代码中被称之为 pending数据;下面我们来看一些可能的情况。
如上图所示,这是一种不需要分段的情形,即传输的数据长度小于PTMU,只需要分配一个skb就可以容纳下要发送的数据了。这里有些关键点需要说明:
实际分配skb时,会将L2所需头部、IP头部、L4的头部和数据部分都考虑进来,这样后续协议在处理该数据包是就无需再次分配缓冲区了;示例中还有IPsec header和IPsec tailer两部分,这属于扩展首部和扩展末尾,当启用IPsec时,需要在IP报文的首尾追加IPsec特有的信息,所以如果是这种情况,在分配skb时也会将这种开销考虑进来;实际分配时,如果一个IP数据包总长度小于PMTU,分配的skb长度是按照需要的大小来分配的,不会造成浪费,这点同样适用于需要分段时的最后一个片段(可能小于PMTU)的情况。额外需要说明的是:ip_append_data()在分配了skb后,会拷贝L4 payload部分,还会初始化skb中的某些字段,如上图缓冲区左侧的图示;其余的如L4 header由调用ip_push_pending_frames()的函数负责填充、IP header由ip_push_pending_frames()填充。
这个例子比上一个稍微复杂一点,这个例子我们不考虑IPsec的情形。左下角为L4协议想要传输的报文,长度为length并且大于PMTU,一次调用ip_append_data()需要将这整个报文拆分成skb放入到套接字的发送缓冲区中,这里假设分配两个skb可以容纳length长度的数据,这时第一个skb大小为PTMU,它拷贝了x字节的数据;第二个skb拷贝了剩余的y字节(x+y=length),并且第二个skb的长度小于PMTU。
由于调用者可能会连续调用ip_append_data()多次,然后再调用ip_push_pending_frames()启动发送,所以如果按照上图的方式分配最后一个skb,再来数据时就需要重新分配skb,然后将最后一个skb的数据拷贝到新的缓冲区中,然后删除最后一个缓冲区,执行这样的动作是耗时的,会带来性能的损耗。为了处理这种问题,引入了MSG_MORE标记,如果在前一次调用ip_append_data()时指定了MSG_MORE标记,那么表示后面马上还会添加新的数据,这时就可以在处理最后一部分数据时预先分配一个容纳PMTU大小的skb,如下图所示: 虽然当前无法填满最后一个skb,但是因为很快有数据到达,所以这么分配没有问题。
case1和case2所举的例子都是设备不支持S/G IO时能够采取的解决方案,如果设备支持S/G IO,那么即使L4是多次传入小片段调用ip_append_data(),ip_append_data()也可以按照下图所示方式组织数据:
上图a表示支持S/G IO的情形下第一次调用ip_append_data()后的缓冲区结构,注意skb的分配是按需分配的,其大小并不是PMTU。图b是第二次调用ip_append_data()后,新增的数据保存在了前一个skb的frags数组中,这两块区域并不连续,但是由于设备支持S/G IO,所以这么分配是没有问题的。
要特别注意的是,上图中的x+S1的大小依然要小于PMTU,如果超过,那么还是需要分配一个新的skb,这是因为一个skb对应一个IP片段的原则不能打破。