sk_buff封装和解封装网络数据包的过程详解

可以说sk_buff结构体是Linux网络协议栈的核心中的核心,几乎所有的操作都是围绕sk_buff这个结构体进行的,它的重要性和BSD的mbuf类似(看过《TCP/IP详解 卷2》的都知道),那么sk_buff是什么呢?
       sk_buff就是网络数据包本身以及针对它的操作元数据。
       想要理解sk_buff,最简单的方式就是凭着自己对网络协议栈的理解封装一个直到以太层的数据帧并且成功发送出去,个人认为这比看代码/看文档或者在网上搜资料强多了。当然,网上已经有了大量的这方面的文章,但是我认为很多都太复杂了,它们都细化到了sk_buff结构体的每一个指针字段,并且还都画出了图,但一般都逃不过《深入理解Linux网络技术内幕》这本书的圈子。试想,如果以后内核版本升级了,字段新增了或者名字变了,怎么办?这些文章包括那本经典的《ULN》还能有帮助吗?
       因此,本文绝不深入到sk_buff的细节,但是相信这种简单的方式可以让自己在多年以后早已忘了什么是Linux协议栈的情况下,瞬间理解Linux是如何通过sk_buff封装数据包的。我们从网络的分层模型开始。

网络分层模型

这是一切的本质。网络被设计成分层的,所以网络的操作就可以称作一个“栈”,这就是网络协议栈的名称的由来。在具体的操作上,数据包最终形成的过程就是一层一层封装的过程,在栈上形成一段连续的数据,我们可以称作是一层一层的push操作。同样的,数据包的解封装的过程,则可以认为是一层一层的pop操作。

sk_buff的操作

要想形成一个最终的数据包,即以太帧(不考虑其它的链路层)。要进行以下的操作:
1.分配一个skb结构体
2.分配数据包的数据区
3.在skb数据区定位应用层起始位置
4.拷贝数据到应用层(假设应用层协议没有在socket接口之上被封装)
5.在skb数据区定位传输层起始位置
6.设置传输层头部字段
7.在skb数据区定位IP层起始位置
8.设置IP层头部字段
9.在skb数据区定位以太层起始位置
10.设置以太头部字段
可以看出基本的模式,即“定位/设置”两步骤操作,有点区别的是应用层操作,这是因为应用层的操作一般都是在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操作将写指针定位到应用层开始处,这等于说压入了应用层栈帧。
       skb_push接口是将一个协议栈帧压入协议栈的接口,它返回一个position,该position就是skb数据包的写指针,告诉调用者,这里开始按照你的封装逻辑封装数据包,写多少字节呢?由skb_push的参数n指示。应用层的压栈操作如下图所示:

将应用层栈帧压入协议栈之后,就可以在写指针位置开始,往后连续写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接口压入一个栈帧,返回写指针,然后按照该层的协议逻辑从写指针开始写入栈帧长度的数据。
       在skb_push返回的那一刻,一个栈帧被压入了协议栈,然后该栈帧还仍未被写入数据,也就是说还没有完成封装过程,具体的封装过程由调用者自己实现。
       skb_push导致了skb数据包缓冲区写指针位置的前推,连带的改变了好几个变量,首先数据包的长度增加了n个字节,其次缩小了headroom的空间,然后通过reset_XXX_header的调用,skb记住了某层协议头在数据包中的位置(这点特别重要!比如在TSO/UFO的情况下,网卡驱动需要协议头的位置信息,用以计算校验值,所以虽然skb不记住协议头的位置,一个数据包也能完成封装,但是对于协议栈的完整实现而言,却是不正确的做法,毕竟网卡计算校验码已经成了一种事实上的标准[即便它违背了严格的分层原则!])

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);

解封装的过程和封装的过程相反,解封装的过程是协议栈栈帧逐层pop的过程,但是Linux协议栈并没有用栈的术语来定义接口名字,而是使用了push的反义词,即pull来定义的,skb_pull就是核心接口,和skb_push严格相对。我就不再一一画图了。

按照接口编码而不是按照实现编码

这好像是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
这是怎么回事?原因就在于skb纪录的协议头位置是错误的!难道以上的设置skb的network_header字段的方式有何不妥吗?当然不妥!这就是没有按照接口编码的恶果。
       原因在于,系统设置skb的network_header字段的方式有两种,通过一个宏来识别:NET_SKBUFF_DATA_USES_OFFSET。也就是说,可以通过相对于skb的head指针的偏移来定位协议头的位置,也可以通过绝对地址来定位,具体使用哪一种取决于系统有没有定义NET_SKBUFF_DATA_USES_OFFSET宏,以上的skb->network_header = p明显是通过绝对地址来定位的,一旦系统定义了NET_SKBUFF_DATA_USES_OFFSET宏,肯定就不对了。既然宏定义在编译期确定,那么通过定义接口就可以在编译期唯一确定一种实现,程序员不必在乎是否定义了NET_SKBUFF_DATA_USES_OFFSET宏,这就是通过接口编程的益处。如果基于skb的实现来编程,你不得不针对所有的情况编写好几套实现,而以上错误的实现只是其中一种,而且还用错了场景!这是多么痛的领悟!
       NET_SKBUFF_DATA_USES_OFFSET宏是一个细节问题,如果使用接口编程便不必关注这个细节,否则你就必须搞清楚系统为何这么设计,即便这并不是你所关注的!为何呢?
       由于指针的长度大小在32位系统和64位系统中是不一样的,所以按理说skb中的指针型的元数据大小也会不同,且64位系统的将会是32位系统的两倍,为了平滑掉这个差别,使元数据大小一致,就必须让64位系统的对应指针类型变为4个字节,而这是不可能的。因此在64位系统中,使用偏移来定位元数据,而偏移的类型为固定不变的unsigned int,即4个字节。为了支持上述说法,skb中加入了一个新的层次,即定义了一种新的数据类型sk_buff_data_t,该类型在编译期确定:

#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还有更多的,相当多的细节,但是不能再一一描述了,因为那样就违背了本文一开始的初衷,即用最简单的方式揭露本质,如果一一描述了,那么本文将成为一个文档而非一篇感悟,时隔多年以后,相信自己也不会看下去的。
       关于sk_buff还有超级多的内容,仅仅结构体里面丰富字段的含义就够折腾好久的了,加上它如何配合Linux各层协议的实现,内容就更加丰富了。不过最基本的,就是本文讲述的,你得知道数据是怎样塞到一个skb并封装成一个可以被网卡实际发送的数据包的。好了,基本就是这些。最后我来总结一下本文提到的几个接口:
alloc_skb:分配一个skb;
skb_reserver:写指针向后移动到一个位置p,确定为数据包尾部,自始,写指针开始从该位置前移封装数据包;
skb_push:写指针前移n,更新数据包长度,从它返回的位置可以写n个字节数据-即封装n字节的协议;
skb_put:写指针移动到数据包尾部,返回尾部指针,可以从此位置写n字节数据,同时更新尾指针和数据包长度;
...

时间: 2024-08-08 17:52:08

sk_buff封装和解封装网络数据包的过程详解的相关文章

sk_buff封装和解封装网络数据包的过程详解(转载)

http://dog250.blog.51cto.com/2466061/1612791 可以说sk_buff结构体是Linux网络协议栈的核心中的核心,几乎所有的操作都是围绕sk_buff这个结构体进行的,它的重要性和BSD的mbuf类似(看过<TCP/IP详解 卷2>的都知道),那么sk_buff是什么呢?       sk_buff就是网络数据包本身以及针对它的操作元数据.       想要理解sk_buff,最简单的方式就是凭着自己对网络协议栈的理解封装一个直到以太层的数据帧并且成功发

HBase 与Hive数据交互整合过程详解

Hive和Hbase整合理论 1.为什么hive要和hbase整合 2.整合的优缺点 优点: (1).Hive方便地提供了Hive QL的接口来简化MapReduce的使用, 而HBase提供了低延迟的数据库访问.如果两者结合,可以利 用MapReduce的优势针对HBase存储的大量内容进行离线的计算和分析. (2).操作方便,hive提供了大量系统功能 缺点: 性能的损失,hive有这样的功能, 他支持通过类似sql语句的语法来操作hbase 中的数据, 但是速度慢. 3.整合需要做什么样的

网络数据包分析 网卡Offload

http://blog.nsfocus.net/network-packets-analysis-nic-offload/ 对于网络安全来说,网络传输数据包的捕获和分析是个基础工作,绿盟科技研究员在日常工作中,经常会捕获到一些大小远大于MTU值的数据包,经过分析这些大包的特性,发现和网卡的offload特性有关,本文对网卡Offload技术做简要描述. 文章目录 网络分片技术 网卡offload机制 发送模式 接收模式 网卡offload模式的设置 Linux windows 网卡Offload

Linux程序设计学习笔记----网络编程之网络数据包拆封包与字节顺序大小端

网络数据包的封包与拆包 过程如下: 将数据从一台计算机通过一定的路径发送到另一台计算机.应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示: 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据包(packet),在链路层叫做帧(frame).数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理. 上图对应两台计算机在同一网段中的情况,

捕获网络数据包并进行分析的开源库-WinPcap

什么是WinPcap WinPcap是一个基于Win32平台的,用于捕获网络数据包并进行分析的开源库. 大多数网络应用程序通过被广泛使用的操作系统元件来访问网络,比如sockets.  这是一种简单的实现方式,因为操作系统 已经妥善处理了底层具体实现细节(比如协议处理,封装数据包等等),并且提供了一个与读写文件类似的,令人熟悉的接口. 然而,有些时候,这种“简单的方式”并不能满足任务的需求,因为有些应用程序需要直接访问网 络中的数据包.也就是说,那些应用程序需要访问原始数据包,即没有被操作系统利

Linux内核中网络数据包的接收-第一部分 概念和框架

与网络数据包的发送不同,网络收包是异步的的,因为你不确定谁会在什么时候突然发一个网络包给你,因此这个网络收包逻辑其实包含两件事:1.数据包到来后的通知2.收到通知并从数据包中获取数据这两件事发生在协议栈的两端,即网卡/协议栈边界以及协议栈/应用边界:网卡/协议栈边界:网卡通知数据包到来,中断协议栈收包:协议栈栈/应用边界:协议栈将数据包填充socket队列,通知应用程序有数据可读,应用程序负责接收数据.本文就来介绍一下关于这两个边界的这两件事是怎么一个细节,关乎网卡中断,NAPI,网卡poll,

CORE网络数据包接收传递过程分析

能够接收实际网络流量是CORE的一个显著优点,这使得已有的系统能方便地接入虚拟网络进行模拟.CORE对网络设备的虚拟是通过LXC技术来实现的,而对网络的虚拟则是通过虚拟网卡(veth).网桥(Bridge).Quagga来实现的.本文档主要通过分析CORE中网络数据传递过程,来理解CORE网络模拟. 拓扑结构 为了方便描述,以如图1所示拓扑结构为例子,分析数据流从网卡eth0到虚拟节点n2的过程. 图1 示例拓扑 虚拟网络创建由CORE后台根据前台的拓扑结构和配置,执行相应的命令进行实现,如下:

一个最简单的通过WireShark破解SSL加密网络数据包的方法

原文地址: http://article.yeeyan.org/view/530101/444688 一般来说,我们用WireShark来抓取包进行分析是没有多大问题的.但这里有个问题是,如果你碰到的是用SSL/TLS等加密手段加密过的网络数据的时候,往往我们只能束手无策.在过去的话,如果我们拥有的该传输会话的私钥的话我们还是可以将它提供给WireShark来让其对这些加密数据包进行解密的 1. 简介 相信能访问到这篇文章的同行基本上都会用过流行的网络抓包工具WireShark,用它来抓取相应的

用C++实现网络编程,抓取网络数据包的实现方法和介绍

做过网管或协议分析的人一般都熟悉sniffer这个工具,它可以捕捉流经本地网卡的所有数据包.抓取网络数据包进行分析有很多用处,如分析网络是否有网络病毒等异常数据,通信协议的分析(数据链路层协议.IP.UDP.TCP.甚至各种应用层协议),敏感数据的捕捉等.下面我们就来看看在windows下如何实现数据包的捕获. 下面先对网络嗅探器的原理做简单介绍. 嗅探器设计原理 嗅探器作为一种网络通讯程序,也是通过对网卡的编程来实现网络通讯的,对网卡的编程也是使用通常的套接字(socket)方式来进行.但是,