网络分层模型

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

sk_buff的操作

要想形成一个最终的数据包,即以太帧(不考虑其它的链路层)。要进行以下的操作:

  • 1.分配一个skb结构体
  • 2.分配数据包的数据区
  • 3.在skb数据区定位应用层起始位置
  • 4.拷贝数据到应用层(假设应用层协议没有在socket接口之上被封装)
  • 5.在skb数据区定位传输层起始位置
  • 6.设置传输层头部字段
  • 7.在skb数据区定位IP层起始位置
  • 8.设置IP层头部字段
  • 9.在skb数据区定位以太层起始位置
  • 10.设置以太头部字段

可以看出基本的模式,即“定位/设置”两步骤操作,有点区别的是应用层操作,这是因为应用层的操作一般都是在socket接口之上完成的。但是既然本文讲述的是skb的通用操作,就不再区分这个了

资料直通车:最新Linux内核源码资料文档+视频资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

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字节数据,同时更新尾指针和数据包长度;

Linux内核网络分层模型——skb核心操作相关推荐

  1. Linux 网络设备驱动开发(一) —— linux内核网络分层结构

    Linux内核对网络驱动程序使用统一的接口,并且对于网络设备采用面向对象的思想设计. Linux内核采用分层结构处理网络数据包.分层结构与网络协议的结构匹配,既能简化数据包处理流程,又便于扩展和维护. ...

  2. 深入Linux内核网络堆栈

    前一段时间看到这篇帖子,确实很经典,于是翻出了英文原版再读,顺便再翻译出来供大家学习,这篇文章的中文版也早都有了,不过出于完全理解的目的,我还是将它翻译了出来,加进了自己的代码,虽然在上一周的翻译过程 ...

  3. Linux内核网络(一)——初探内核网络

    本文将从宏观上介绍Linux内核网络协议栈和网络设备驱动程序,介绍了两个很重要的结构(net_device和sk_buff),更深入更详细的内容将在以后的文章中介绍. 首先,我们需要了解网络分层模型. ...

  4. Linux内核--网络栈实现分析(二)--数据包的传递过程--转

    转载地址http://blog.csdn.net/yming0221/article/details/7492423 作者:闫明 本文分析基于Linux Kernel 1.2.13 注:标题中的&qu ...

  5. Linux内核网络栈1.2.13-tcp.c概述

    参考资料 <<linux内核网络栈源代码情景分析>> af_inet.c文件中调用函数在协议层的实现 本文主要根据在af_inet.c文件中根据初始化不同的协议,来调用不同的协 ...

  6. linux内核网络协议栈--数据包的接收过程(二十二)

    与其说这篇文章分析了网卡驱动中中数据包的接收,还不如说基于Kernel:2.6.12,以e100为例,对网卡驱动编写的一个说明.当然,对数据包的接收说的很清楚. 一.从网卡说起 这并非是一个网卡驱动分 ...

  7. Linux内核网络数据包处理流程

    Linux内核网络数据包处理流程 from kernel-4.9: 0. Linux内核网络数据包处理流程 - 网络硬件 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO. ...

  8. 精通Linux内核网络 -(以)罗森

    讨论了Linux内核网络栈的实现原理,并对网络子系统及其体系结构进行了深入细致的分析.主要内容包括:核心网络的基础知识,Netlink socket.ARP.邻居发现和ICMP等重要协议的实现,IPv ...

  9. linux内核网络子系统初探---概述

    linux内核网络子系统初探-概述 一.网络模型 简单介绍 学习网络时,必定能在各种教材资料里见到以下三种网络模型: 三种模型间的差异: OSI七层模型是理论上的网络模型,从功能方面分成了相对独立的7 ...

  10. Linux内核网络栈1.2.13-icmp.c概述

    参考资料 <<linux内核网络栈源代码情景分析>> icmp协议 在实现的过程中, ICMP协议工作再IP协议之上,但又不与TCP协议工作再一级,而是在下一级,在一般ICMP ...

最新文章

  1. 大型网站演化发展历程之三
  2. 小而美的个人博客——前端——types and archives
  3. 【推荐系统入门】一窥推荐系统的原理
  4. 【学术相关】博士新生应该懂得哪些道理?
  5. Linux系统编程8-18总结项目:完成一个简单的自己的shell
  6. Hadoop学习笔记(三):作业调度器
  7. 蚂蚁金服实习Android岗,面试闯关记。
  8. android客户端同php服务端进行XML/JSON通信
  9. c++课后题,声明一个长方体类Box,该类有长度(length),宽度(width),高度(height)三个数据成员,类中有获取及修改长度…………
  10. 日常生活中使用的台式计算机,台式电脑鼠标不动应该怎么办
  11. 什么是Redis内存碎片率?碎片如何清理?
  12. 应用matlab仿真几类混沌电路,改进型二级Colpitts混沌电路的制作方法
  13. win10家庭版远程桌面控制解决
  14. Spark Streaming概述_大数据培训
  15. redmine修改主页
  16. Qt+opencv+android
  17. msp430 USB驱动
  18. 云服务器-裸金属介绍
  19. 关于lm393使用时的一些注意
  20. 执行perl时出现Undefined subroutine

热门文章

  1. python处理grd格式文件_python sklearn中,GBDT模型训练之后,可以查看模型中树的分裂路径吗?...
  2. 使用分身术变身术创建新进程
  3. scrapy框架爬虫
  4. themeforest 免费模板
  5. LVM -逻辑卷管理
  6. opencv实现靶纸弹孔识别计数功能
  7. Scikit-learn_聚类算法_K均值聚类
  8. python爬虫实战---网易云音乐评论抓取
  9. 游戏服务器级别分类及对应服务器架构
  10. 3D世界 ORGE SceneManager GetStart