zoukankan      html  css  js  c++  java
  • sk_buff封装和解封装网络数据包的过程详解

    转自:http://www.2cto.com/os/201502/376226.html

    可以说sk_buff结构体是Linux网络协议栈的核心中的核心,几乎所有的操作都是围绕sk_buff这个结构体进行的,它的重要性和BSD的mbuf类似(看过《TCP/IP详解 卷2》的都知道),那么sk_buff是什么呢?
    网络分层模型这是一切的本质。网络被设计成分层的,所以网络的操作就可以称作一个“栈”,这就是网络协议栈的名称的由来。在具体的操作上,数据包最终形成的过程就是一层一层封装的过程,在栈上形成一段连续的数据,我们可以称作是一层一层的push操作。同样的,数据包的解封装的过程,则可以认为是一层一层的pop操作。
    sk_buff的操作要想形成一个最终的数据包,即以太帧(不考虑其它的链路层)。要进行以下的操作:
    1.分配一个skb结构体
    可以看出基本的模式,即“定位/设置”两步骤操作,有点区别的是应用层操作,这是因为应用层的操作一般都是在socket接口之上完成的。但是既然本文讲述的是skb的通用操作,就不再区分这个了。
    skb的核心操作在上面一小节,我们展示了skb的封装逻辑,但是具体到接口层面,就涉及到了skb的核心操作。
    1.分配skb这个是由alloc_skb完成的,完成同一任务的接口形成一个接口族,但是alloc_skb是最基本的接口。
    该alloc_skb接口完成两件事,即分配skb结构体以及skb数据包缓冲区,设置初始值。size参数表示skb的数据包缓冲区的大小,这个大小包括所有层的总和。如果该函数成功返回,那么就相当于你已经有了一个大小为size的空数据包缓冲区以及操作该数据包缓冲区的skb元数据。如下图所示:

    2.初始定位(skb_reserve)

    skb的逐层封装的关键在于写指针的定位,即这一层从哪个位置开始写。从协议封装的压栈形象来看,这个定位应该是顺序有规律的。初始定位十分重要,后面的定位就是例行公事了。初始定位当然是定位到应用层的末端,从这里开始,逐层将协议头push到skb的数据包缓冲区。初始定位图示如下:

    3.拷贝应用层数据(skb_push/copy)

    当skb分配好了之后,需要将协议“栈”的位置定位在数据包的“最低处”,这是初始定位,这样才可以把每一层的数据或者协议头push到栈上,这个操作由skb_reserve来完成。应用层数据已经在socket之上封装好了,那么就把skb的数据包缓冲区写指针定位到应用数据的开始处,此时的写指针在应用层缓冲区的末尾,因此需要使用skb_push操作将写指针定位到应用层开始处,这等于说压入了应用层栈帧。


    将应用层栈帧压入协议栈之后,就可以在写指针位置开始,往后连续写n字节的应用层数据了,一般而言,这些数据来自socket。
    4.设置传输层头部和应用层的操作类似,这次需要把传输层栈帧压入协议栈中,如下图所示:


    接下来就可以愉快地在skb_push返回的位置设置传输层头部了,UDP,TCP,就看你对传输层的理解了。设置传输层头部其实就是在skb_push返回的位置开始写数据,写入的长度由skb_push的参数指定,即n。
    5.设置IP层头部和应用层以及传输层操作类似,这次需要把IP层的栈帧压入协议栈中,如下图所示:


    接下来就可以愉快地在skb_push返回的位置设置IP层头部了,如何设置,就看你对IP层的理解了。由于只是演示skb如何封装,因此没有涉及IP层相当重要的IP路由过程。
    6.设置以太帧头部这个就不说了,和上述的类似...如下图所示:


    到此为止,我封装了一个完整的以太帧,可以直接通过dev_queue_xmit发送的那种。一路下来,你会发现,skb数据包缓冲区以“压栈(push)”的方式逐渐被填充,每一层,都是通过skb_push接口压入一个栈帧,返回写指针,然后按照该层的协议逻辑从写指针开始写入栈帧长度的数据。
    7.在应用数据后面追加PADDING目前为止,从最后的图示上可以看到,在skb数据包缓冲区中,还有两块区域没有使用,一个headroom,一个是tailroom,这些是干什么用的呢?作为一个练习的例子,由于存在某种对齐原则,在封装完成后,我需要在数据包的最后追加一些填充,或者说我需要在最前面加一个前导码,或者最常见的,我要在数据包的最后加一个纠错码,此时应该怎么办呢?
    这个时候就需要headroom或者tailroom了,以在数据包最后追加数据为例,请看下图:


    实际上,skb_put的操作就是,在数据包的末尾追加数据。至于说headroom如何使用,我就不多说了,其实还是skb_push,headroom有什么用呢?前导码,X over Y封装,不一而足。

    实际的例子

    下面我给出一个实际的例子,封装一个以太帧,然后发送出去:

        skb = alloc_skb(1500, GFP_ATOMIC);
        skb->dev = dev;
        // 例行填充skb元数据
    
        /* 保留skb区域 */
        skb_reserve (skb, 2 + sizeof(struct ethhdr) +
                sizeof(struct iphdr) +
                sizeof(struct iphdr) +
                sizeof(app_data));
    
        /* 构造数据区 */
        p = skb_push(skb, sizeof(app_data));
        memcpy(p, &app_data[0], sizeof(app_data));
    
        p = skb_push(skb, sizeof(struct udphdr));
        udphdr = (struct udphdr *)p;  
        // 填充udphdr字段,略
        skb_reset_transport_header(skb);
    
        /* 构造IP头 */
        p = skb_push(skb, sizeof(struct iphdr));
        iphdr = (struct iphdr*)p;
        // 填充iphdr字段,略
        skb_reset_network_header(skb);
    
        /* 构造以太头 */
        p = skb_push(skb, sizeof(struct ethhdr));
        ethhdr = (struct ethhdr*)p;
        // 填充ethhdr字段,略
        skb_reset_mac_header(skb);
    
        /* 发射 */
        dev_queue_xmit(skb);
    


    按照接口编码而不是按照实现编码这好像是Effective C++里面的一条,同样也适合于skb的操作场景。典型的就是“如何让skb记住IP层协议头,传输层协议头,mac头的位置”,接口是:
    skb_reset_mac_header skb_reset_network_header skb_reset_transport_header 调用时机为skb_push返回的当时。曾几何时,我按照下面的方式设置了协议头的位置:
    /* 构造IP头 */ p = skb_push(skb, sizeof(struct iphdr)); iphdr = (struct iphdr*)p; // 填充iphdr字段,略 //skb_reset_network_header(skb); skb->network_header = p; 有错吗?咋一看是没错的,但是却报错了:
    protocol 0008 is buggy, dev eth2
    #if BITS_PER_LONG > 32 #define NET_SKBUFF_DATA_USES_OFFSET 1 #endif #ifdef NET_SKBUFF_DATA_USES_OFFSET typedef unsigned int sk_buff_data_t; #else typedef unsigned char *sk_buff_data_t; #endif 节约空间之外,对于和大小相关的操作,接口实现也更加统一。这就是细节,而这些细节并不是玩网络协议栈的人所要关注的,不是吗?这完全是系统实现的层面,和业务逻辑是无关的。
    为何未竟全功本文讲述到此为止。事实上,sk_buff还有更多的,相当多的细节,但是不能再一一描述了,因为那样就违背了本文一开始的初衷,即用最简单的方式揭露本质,如果一一描述了,那么本文将成为一个文档而非一篇感悟,时隔多年以后,相信自己也不会看下去的。
    alloc_skb:分配一个skb;
    skb_reserver:写指针向后移动到一个位置p,确定为数据包尾部,自始,写指针开始从该位置前移封装数据包;
    skb_push:写指针前移n,更新数据包长度,从它返回的位置可以写n个字节数据-即封装n字节的协议;
    skb_put:写指针移动到数据包尾部,返回尾部指针,可以从此位置写n字节数据,同时更新尾指针和数据包长度;

  • 相关阅读:
    代码书写过程中的一些需要培养的好习惯(持续更新)
    arm linux 移植 PHP
    arm linux 支持 wifi (wpa_supplicant)
    arm linux 移植 OpenCV
    使用FFmpeg处理视频文件:视频转码、剪切、合并、播放速调整
    视频编解码 基本概念:GOP
    arm linux 移植 python3.6
    读懂反向传播算法(bp算法)
    FFmpeg命令详解
    (转)浅谈 Linux 内核无线子系统
  • 原文地址:https://www.cnblogs.com/x_wukong/p/6026494.html
Copyright © 2011-2022 走看看