第二十一章:因特网协议第四版(IPv4):传输

本章讨论L3层封包的传输,即封包离开本地前往另一台主机,传输可以由L4进行,也可以作为转发的最后阶段被调用。本章所讨论的所有函数都在dst_output函数之前执行,也就是替该函数把封包准备好。

上图显示L4传输和L3的最后步骤间的主要函数。

处理分段的两组函数:

ip_queue_xmit:

L4协议把PMTU纳入考虑后,已经把数据分层大小合适的分段,IP层的工作就是把IP报头加上。

ip_push_pending_frames及相关函数:

调用此函数的那些L4协议不会考虑分段或协助分段,L4协议把数据直接传给IP层。L4协议可以多次调用ip_append_data来存储好几个传输请求。而不实际传输任何东西,同时还会产生一些最佳大小的数据片段,使得IP层容易处理分段。当L4协议必须刷新ip_append_data创建的输出队列时,该协议会调用ip_push_pending_frames,通过此函数做必要的分段,然后把封包传给dst_output。

本地流量相关的套接字数据接口:

struct inet_sock{

    struct sock sk;

    ...

struct {

    ...

}cork;//cork字段存储ip_append_data和ip_append_page做分段所需的信息

}

只要传输是由本地产生,每个sk_buff缓冲区都会和其sock实例相关联。skb->sk指向一个sock实例。

下面这些函数可用于读取sock和inet_sock字段的值:

    sk_dst_set和_ _sk_dst_set:连接套接字后,这些函数可以把用于抵达目的地址的路径存储在sock结构中。

    sk_dst_check和_ _sk_dst_check:路径的有效性可以由这两个API测试。

    skb_set_owner_w:把一个sk_buff缓冲区指派给特定的sock结构。

    sock_alloc_send_skb和sock_wmalloc:分配sk_buff缓存区。

    

ip_queue_xmit函数:

原型:int ip_queue_xmit(struct sk_buff *skb ,int ipfragok)

注意,该函数用于处理本地产生的封包,转发的封包没有相关的套接字。

和skb相关的套接字包含一个opt指针,改指针指向的结构存储着ip选项的解析结果。

    struct sock *sk = skb->sk;

    struct inet_sock *inet = inet_sk(sk);

    struct ip_options *opt = inet->opt;

当缓存区已经设定了正确的路由信息,就没必要查询路由表。

    rt = (struct rtable *)skb->dst;

    if(rt != NULL)

        goto packet_routed;

其他情况下,检查套接字结构中是否缓存了一条路径,如果有的话,判断是否有效。

    rt = (struct rtable *)_ _sk_dst_check(sk,0);

如果套接字没有路径可以使用,或者ip层一直使用的路径由于路由表更新等原因失效了,需要调用ip_route_output_flow寻早一条新路径,然后存储到sk结构中。

以上操作若没有问题,接下来ip_queue_xmit函数会开始构建ip头。

到目前为止,skb只含IP有效载荷(一般是L4的报头和数据部分)。当ip_queue_xmit接收skb时,skb->data指向L3有效载荷,所以需要使用skb_push把skb->data往前移到指向ip头。

    iph = (struct iphdr *) skb_push(skb , sizeof(struct iphdr) + (opt ?opt->optlen:0));

接着对ip报头中的一些字段初始化,如果ip头中包含了一些选项,调用ip_options_build来处理这些选项。

接着,调用ip_select_ident_more函数,该函数会根据封包是否可能被分段在报头中设定ip id。然后调用ip_send_check计算ip头的校验和。流量控制使用skb->priority来决定把封包排入哪一个外出队列。

最后,调用Netfilter来决定该封包是否有权跳至后续步骤(dst_output)继续传输.

    return NF_HOOK(PF_INET,NF_IP_LOCAL_OUT,skb,NULL,rt->u.dev,dst_output);

ip_append_data函数:

此函数不传输数据,只是把数据放在一些缓存区中,要刷新这些缓冲区,L4必须显示调用ip_push_pending_frames(该函数也会处理ip 头)。

原型:int ip_append_data(struct sock *sk ,int getfrag(void *from,char *to,int offset,int len,int odd,struct sk_buff *skb),void *from,int length ,int transhdrlen,struct ipcm_cookie *ipc,struct rtable *rt,unsigned int flags)

输入参数的意义:

    sk:此封包相关的套接字

    from :指向L4层正试着传输的数据(有效载荷,没有L4头)的指针。getfrag函数的工作就是正确处理这个指针

    getfrag:把L4的有效载荷拷贝到即将建立的一些数据片段中

    length:要传输的数据长度,包括L4报头和L4有效载荷

    transhdrlen:要传输的L4报头的尺寸

    ipc:参见二十三章ipcm_cookie一节

    rt:路由表缓存项目

    flags:

        MSG_MORE:应用层使用,告诉L4层马上就有更多其他传输。

        MSG_DONTWAIT:对ip_data_append的调用一定不能受到阻塞。

        MSG_PROBE:表示用于不行传输任何东西,只是探测路径。例如,测试通网给定ip地址PMTU。

ip_append_data的基本内存分配和缓冲区组织:

ip_append_data可以建立一个或多个sk_buff实例,而每一个实例代表一个独立的ip封包或片段。假设我们传输的数据不必分段,且使用了IPsec套件,且分配缓冲区时不用达到内存最优化,因此,ip_append_data的结果如下:

    只有一个缓冲区;

    IPsec套件的协议可能需要一个头部和尾部(trailer)来包住原有缓冲区(包括其原有的IP报头)    

    给L2的报头预先分配空间。

    L4报头由调用ip_push_pending_frames的函数填写;

    L3报头由ip_push_pending_frames填写;

上图中,缓冲区左边的指针是sk_buff的字段,右边的是局部变量。

现在考虑需要分段的情况,结果如下图所示:

上图中,左下方的对象是ip_append_data从输入中所接收的缓冲区。此函数所建立的两个缓冲区在右边。第一个具有最大尺寸(PMTU,注意pmtu不包括L2头部),第二个包含剩余的数据,且尺寸小于pmtu。此时,若L4再次调用ip_append_data,由于第二个缓冲区已满,需要再次分配一个缓冲区,这样无法实现最佳分段(即除了最后一个分段,其他分段都达到pmtu)。所以,MSG_MORE的标志作用在于,如果知道马上就有下一次调用,我们在分配第二个缓冲区时,直接分配最大的尺寸pmtu,这样下一次调用可以先填补第二个缓冲区的剩余部分。

ip_append_data(有分散/聚集I/O)的内存分配和缓冲区组织:

当设备支持分散/聚集IO时,L3层可以不去管L4层存放数据的那些缓冲区,让设备把这些缓冲区结合起来去做传输,减少了分配内存和考不数据的消耗。举例来说,当上层协议连续产生很多小数据项,L4层将其存在内核内存的不同缓冲区,然后L3层被要求用一个IP封包来传输,此时,若没有分散/聚集I/O,L3得把数据拷贝到一个缓冲区中,若设备支持分散/聚集I/O,则数据可以留在原地直到离开主机。

上图a表示第一次调用ip_append_data,b表示第二次调用。

处理片段缓冲区的重要函数:

skb_is_nonlinear:当缓冲区被分段时返回true。

skb_headlen:给定一个分段缓冲区,返回主要缓冲区中的数据长度(即不算frags片段和frag_list列表)

skb_pagelen:分段缓冲区的尺寸,把主要缓冲区的数据以及frags片段中的数据计算进来,但是不考虑frag_list列表。

注意,frags向量里的数据是主缓冲区中数据的扩展,而frags_list里的数据代表的是独立缓冲区。(也就是说,每一个缓冲区都必须作为单独的ip片段传输)

缓冲区的后续处理:

每当ip_append_data分配一个新的sk_buff结果来处理一个新数据片段(将会变成一个新的ip片段),就会把该片段排入名为sw_write_queue的队列,次队列就是ip_append_data函数的输出。

了解了ip_append_data会产生什么输出了之后,接下来看看程序代码做了那些工作。

设定上下文:

该函数的第一个代码块是对一些本地变量做初始化,可能会修改一些输入参数。实际所作的工作取决于该函数是否建立封包内的第一个ip片段或者后续片段。若是第一个片段,会对inet->cork和inet做初始化。下面是一些重要本地变量的意义:

    rt:用于传输此IP数据段的路由表缓存项,此结构包括下个跳点,出口设备以及pmtu等。

    mtu:和rt相关的pmtu

    opt:要加入ip头的选项。

    exthdrlen:外部报头长度(IPsec套件的协议需要加上的头部)

    transhdrlen:传输报头长度

注意,只有第一个片段必须包含传输报头和可选的外部报头,因此,建立第一个片段之后,transhdrlen和exthdrlen都会变零。

准备产生片段:

此函数会接着定义三个本地变量,hh_len表示L2报头长度,fraghdrlen是ip头长度(包括选项),maxfraglen是ip片段的最大尺寸(基于pmtu)。

把数据拷贝到片段:getfrag

不同的协议可能必须对所拷贝的数据执行不同的操作,而且数据的源头可能是用户空间或者内核空间。为了让ip_append_data尽可能通用,此函数允许通过输入参数getfrag指定要用于拷贝的数据,也就是说,ip_append_data使用getfrag把输入数据拷贝到缓冲区中,拷贝的结果就是上面的图中标有“L4有效载荷”的内存区域。如果传入的缓冲区skb->ip_summed被初始化为CHECKSUM_NONE,那么getfrag也会计算L4有效载荷的校验和。

缓冲区分配:

ip_append_data根据下列因素选择要分配的缓冲区尺寸:

单一传输与多次传输:如果ip_append_data知道很快就会有其他传输请求,就会分配大一点的缓冲区。

分散/聚集IO。

主要循环:

上图中的NETIF_F_SG    表示设备是否支持分散/聚集IO。

L4校验和:

当设备支持硬件校验和计算时,是否使用硬件校验和,取决于何时调用ip_append_data处理第一个片段。当下列条件符合时,才使用硬件校验和计算:

    由ip_append_data所构建的ip封包不会被分段

    出口设备支持硬件校验和计算

    没有IPsec集组的协议。

ip_append_page函数:

当传输请求来自用户空间时,需要把要传输的数据从用户空间拷贝到内核空间。这个拷贝是由ip_append_data的getfrag参数完成的。内核提供另一个接口给用户空间应用程序:sendfile,此接口可以优化传输,通常称为零拷贝TCP/UDP。只有当出口设备支持分散聚集IO时,可以使用sendfile。这种情况下,ip_append_data的实现逻辑可以简化,不需要拷贝(即用户空间传给内核的数据可以留在那),内核只需用输入数据中所接收的数据缓冲区位置对frag向量做初始化,然后必要时处理L4校验和。这种简化的逻辑就是ip_append_page所提供的东西。

ip_push_pending_frames函数:

当L4决定把排在sw_write_queue里的那些片段打包起来传输时,就会调用ip_push_pending_frames函数。

原型:int ip_push_pending_frames(struct sock *sk)

根据是否使用分散聚集IO,封包中数据的组织和在sk_buff的结构中的组织会有所不同。上图是没有使用分散聚集IO的情况。注意,图中,nr_frags指的是分散聚集IO缓冲区的数目,不是IP片段的数目。

这部分的程序会把第一个缓冲区之后的所有缓冲区排入一个frag_list链表,并清空sw_write_queue队列,使得L4层认为数据已经传输,此时,数据已经流出L4层,完全在ip层的掌控之下了。

接着,会填写ip报头,如果有多个片段,则只有第一个片段的ip头由p_append_page函数填写,其余片段稍后处理(第二十二章会说明)。

如果报头有IP选项,则会用ip_options_build处理那些选项。 ip_options_build的最后一个参数是0,表示正在处理第一个片段的选项。

处理DF标志。

设定ip包的id。

设定tos,这会决定Traffic Control把该封包排入哪一个外出队列。

NF_HOOK(PF_INET,NF_IP_LOCAL_OUT,skb,NULL,skb->dst->dev,dst_output)。只会为一个封包中的所有片段查询一次Netfilter。

返回之前,此函数事cork结构的内容失效,因为后续的传往相同目的的包会重复使用cork。

整合传输函数:

要了解ip_append_data,ip_push_pending_frames如何合作,可以参考UDP层调用的函数udp_sendmsg。

raw套接字:

raw套接字把IP报头包含在传给IP层的数据内是有可能的。也就是说,可以要求ip层传送一段数据,而该数据内包含一个已经初始化的ip头。为此,raw IP使用IP_HDRINCL  选项。当设定了此选项后,raw IP会直接调用dst_output。

衔接邻居子系统:

如第十八章图18-1所示,传输会以调用ip_finish_output结束,在其末尾,

return NF_HOOK(PF_INET,NF_IP_POST_ROUTING,skb,NULL,dev,ip_finish_output2)

当一切终于就绪时(包括L2报头),就会调用dev_queue_xmit函数去做真正的传输工作。第十一章有该函数的细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值