linux内核协议栈研读设计感悟


前言

对于linux网络协议栈源码研读过程中,对于涉及到的一些设计思想,进行简单记录,同时写下自己的感悟,方便以后进行这种设计思想更加深入的研究、体会、践行。对于文中的观点,只是根据自己的理解和工作中经验进行了简述,会由于自己功力有限或理解偏差产生错误,若是有错误的地方希望能够留下您宝贵的意见和指正。源码主要参考linux-3.10.0-862.el7.x86_64版本。


设计思想

网络协议栈对skb的存储设计

网络协议栈中层与层的传输以及操作,对于系统中skb存储以及设计的要求比较高,主要包括以下几点:

  • 能够很方便地处理可变长缓存,因为接受和发送的数据报长度不是固定的。
  • 能够很容易地实现在头尾部添加和移除数据,因为这些缓存区需要再不同网络层间传递。
  • 在添加和移除数据时能够尽量避免数据的复制。

根据上述要求,提高网络处理性能。可以考虑从以下几点进行处理:

  • 申请一个固定大小的内存,能够存放各层之间所要的数据。
  • 对于每一层要添加或删除的数据,需要用一个指针进行指向,随着不同层间传输,偏移指针标识每一层的地址即可,再用相应的数据结构进行内存的翻译,随后对相应的变量进行操作。

如何定义一个数据结构

对于定义一个数据结构,主要通过自己要实现的业务,考虑一下四个点:

  • 声明组织相关的成员变量。
    主要是通过这些变量,使数据结构构成需要的有关联的结构,例如双向链表、树形结构、图等。linux内核协议栈SKB的相关成员,主要用来构成SKB双向链表。

  • 数据存储相关的成员变量。
    主要用于标示此数据结构要表示对象的纯属性和方法,例如数据存储的地方,数据存储的长度,构造函数,析构函数等。linux内核协议栈SKB数据存储相关的成员,主要用来表述SKB的宿主、数据的长度、SKB析构函数指针等。

  • 通用的成员变量。
    主要用来通过这些变量达到,一个通用的接口,例如文件操作,驱动的操作等。linux内核协议栈sk_buff结构中与内核特性无关的成员,他们主要和网络协议、网络设备等有关。

  • 标志行变量。
    主要用于对象的状态转换标识,展现对象的演变过程。例如linux内核协议栈的SKB标志相关的成员,主要用来标识payload是否被单独引用,是否允许切片,以及当前克隆的状态等。

  • 与特性相关的成员变量。
    主要用于通过宏定义,来控制是否包含其成员变量。例如linux内核是模块化的,在编译时可以选择包含或删除默写功能,因此,sk_buff结构里面的一些成员变量只在内核选择支持默写功能时才能有效,例如防火墙(netfilter)或Qos等。

采用轮询的方式对网络设备进行收包处理的设计

对于网络设备,有些需要处理的速度快,有些需要处理的速度慢,这时候要是用轮询的方式对不同速度的设备,处理的速度或是轮询的速率不一样如何实现?
linux内核NAPI中采用权重和处理不同数量的包来实现,将轮询多次的问题转化为一个处理数据包频率与多少的问题,这种解决问题的角度和思考方式,值得借鉴,具体过程如下所示:
内核以轮询方式处理链表上的所有设备:内核依次轮询各个设备,如果已经花费了一定的时间来处理某个设备,则选下一个设备进行处理。此外,每个设备都带有一个相对权重,表示与轮询表中其它设备相比,该设备的相对重要性。较快的设备权重大,较慢的设备权重小,再由权重指定了在一个轮询的循环中处理多少数据包,这样确保了内核将更多地关注速度较快的设备。

以内存池的方式存储数据

对于同一个数据结构类型的实例经常分配和回收时,回了提高效率,如何对数据进行申请内存?
内核组件为同一个数据结构类型分配几个实例时很常见的事情。当分配和回收经常发生时,相关联的内核组件初始化函数通常会分配一块特殊的内存缓存池,以作为分配之用。当一个内存块被释放时,实际上是返回到当初被分配的同一个缓存中。类似于平时我们熟悉的生产者消费者模型。内核协议栈采用此种方式的例子如:套接字缓冲区描述符、邻居协议映射、路由表等。

引用计数的使用

使用引用计数来管理对象的生命周期的想法非常普遍。核心思想就是使用一个计数器,当有一个新引用的时候就加一,释放引用的时候就减一。当计数器到达0的时候,这个对象所占用的所有资源(如用于存储它本身的内存)就都可以释放了。那什么情况下使用引用计数呢?
linux内核协议栈中主要以下方面使用:

  • 两种数据结构类型之间有很密切的关系。在这种情况下,其中的一个结构通常会维护一个被初始化为第二个结构的地址的指针。
  • 一个定时器启动,而该定时器的处理程序将访问该数据结构。当定时器启动时,该结构的应用计数值就会递增,因为不能够在定时器到期前该数据结构就被释放了。
  • 对列表或hash表的成功查询会返回一个指向匹配元素的指针。多数情况下,返回的结果被调用者用来执行某种任务。因此,常见的做法是,查询函数递增匹配元素引用计数值,然后必要时再让调用者予以释放。

对于通知,群组模式

对于本程序要发送一个通知,可以支持两种方式,一种是点对点,一种是一对多,对于那些多可以接受到消息呢,可以采用注册群组的方式。
linux内核协议栈中的Netlink对于消息的传递就是采用此方式:
Netlink套接字,代表用户空间与内核的IP网络配置之间的首选接口,同时也可作为内核内部以及多个用户空间进程之间的消息传输系统。其功能之一就是传送单播和多播消息:目的地终端点地址可以时一个PID,一个多播群组ID或两者的组合。内核定义Netlink多播群组的目的是传输特定种类事件的通知信息,而用户程序如果对这类通知信息感兴趣,可以向这个群组注册。例如,要是有一个修改IP的动作,那边这个修改ip的通知,会通知注册过RTMGRP_IPV4_ROUTE群组,即有关路由表以及L3地址映射的改变。

订阅与发布

内核的很多子系统之间具有很强的相互依赖性,因此,其中一个子系统侦测到的或者产生的事件,其它子系统可能都有兴趣。为了实现这种交互需求,Linux使用了所谓的通知链即订阅与发布。通知链是一份简单的函数列表,拥有通知链的称为通知者,向通知链注册回调函数的成为被通知者。当通知者子系统发生了一个事件或者侦测到一个事件,就执行此通知链中的所有回调函数来通知注册者。
子系统维护者不可能去追踪每个被添加到内核的子系统,然而每位子系统维护者都应该知道:

  • 自己对来自其它子系统的那种事件感兴趣(向那个链注册回调函数)。
  • 自己知道的事件有哪些(有几条链),并且其它子系统可能感兴趣的事件又是那种(什么是执行某个链中的所有回调函数)。

例如:改变本地IP配置以及改变本地设备的注册状态,都会影响路由表。


参考链接:
Linux kernel design patterns - part 1


转载请注明:HunterYuan的博客 » linux内核协议栈研读设计感悟

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦