Linux内核工程导论——网络:下半部分:core

时间:2022-08-28 21:48:54

总览

Linux内核工程导论——网络:下半部分:core


         最外层是3个文件:socket、compat;、sysctl_net。socket文件定义了操作系统暴漏给用户程序的接口,compat是兼容性考虑的特殊socket接口(主要服务于sparc),sysctl_net向内核的sysctl接口注册服务(并没有具体的实现节点,具体的实现在各个内部模块)。

         目录中最重要的是core文件夹,其内部是net整个部分的基础架构,其他文件下面大都是具体的某一种网络协议。这些网络协议有的是我们所知的因特网上的,例如ipv4、sctp,有的是专用网上的,例如x25、can等,有的是无线网络,例如wimax、wireless、irda等,有的甚至是硬件与硬件之间的局部通信,例如caif,有的甚至只是一个文件系统的抽象网络,例如ceph。所以net目录下的网络一般是抽象层次比较高的概念,是指一切对外暴漏socket接口的功能模块。目录的组织更多的是一种技术上的安排,而不是产品上的(这符合开源软件的一贯作风)。另外,很多内容都是定义在对应的.h文件中。

CORE

         最核心的core部分,这部分的主要工作就是实现socket机制所需要的基础设置,包括:

l  用来放网络数据的skbuf,用来在内核不完全启动时的手法框架netpoll,用来过滤数据包的filter,对ethtool的支持(是的,你没看错,就是用户端那个ethtool命令对应的内核代码),缓存目标地址的dst_entry机制,用来监测丢包的drop_monitor,与网络设备(net_device)相关交互,一些通用的网络相关的概念(tso、timestamp、stream),测试调试相关的接口(pkgen),邻居通用管理(毕竟,任何一种网络都有邻居的概念)

l  通用的socket部分(针对各个不同的协议sock结构体会派生)和一些针对sock的操作的封装实现(请求分配)

l  对内核各个功能模块的支持工作(netlink、sysctl、sysfs、trace、procfs、cgroup、内核通知链、名称空间)

        

         core的这些实现里,大部分都是辅助性的,核心就是sock结构体和skbuff。

概念

         值得注意的是,多数人事首先在用户端使用linux,看到内核代码的时候已经有了很久的linux发行版的使用经验了。你或许精通socket编程,但是对于内核来说,你就要忘记socket了,因为内核里面根本不存在socket,可以叫做sock。socket是内核暴漏给用户端的编程接口,在内核的网络协议栈里,socket会被转化为sock。

         由于TCP/IP网络的应用实在太广了,所以导致即使在具体协议无关的core里面也存在很多TCP、UDP和IP的影子。但是不要因为这些文件组织就怀疑了网络多样化的事实。内核的代码组织更多的是技术性的,而不是产品性。虽然代码要被那么多人看,那明显是个产品,但更多的,代码是技术黑客的玩具。在core里面,有stream和datagram两种传输层概念的定义,虽然,并不是所有的网络都是有传输层的。还是那句话,core看起来更多的是为TCP/IP这个强者服务的。这也是为什么你在创造socket的时候又得指明SOCK_STREAM,又得指明是IPPROTO_TCP。按照函数的解释是类型是SOCK_STREAM,具体协议是IPPROTO_TCP。但是这对用户来说,拜托,谁都知道IPPROTO_TCP的类型是SOCK_STREAM,需要在这么重量级的参数上指定吗?这就是接口和底层的矛盾所在。接口要假装自己可以适应多种情况,底层也确实有多种情况,而常用的基本只有一种。那么为其他不常用的情况留出一个参数就是必要的吗?这让多少初学socket编程的人莫名其妙。

skbuff

         我们来推倒skbuff。skbuff用来存放网络中的数据包,这些数据包可以是任意的网络协议,所以skbuff也必须支持。这个数据包会从socket的最上层一直下到硬件设备,因此其又必须考虑要能同时被各个层次的功能操作。不同的层次对数据包的处理会有各种奇葩的需求,例如发送了一个IP包并不直接删掉这个包,而是系统还担心万一发送不成功,还得重新构造一遍,不如保留,那就涉及到怎么保留的问题。用户向skbuff中写数据(通常调用的是系统调用write或send),可能是一段一段写的,这又要求skbuff有灵活的扩展性。

         像这种结构体如果在C++这种面向对象的编程思想里,如果要实现,一定会采用类似decorator的设计模式。但在C里面,不好意思,各个部分的需求功能的支持数据我只能都列在一个结构体里,实现一个看起来像component的设计模式。下面列出对skbuff有使用要求的内核模块。

l  netfilter要识别和对skbuff进行过滤(难免会在skbuff中添加辅助这个功能的加速域)

l  网络协议的各个层次要对数据进行修改,计算校验,添加头部等灵活的数据修改需求

l  硬件

n  DMA要使用skbuff中的数据

n  GSO、TSO、LRO、GRO等硬件加速功能要能识别和使用skbuff

l  克隆需求。例如TCP不知道是否发送成功会事先克隆一份(为啥不重用?因为skbuff向下走的时候会被修改,既然要克隆,你就得决定哪些东西要克隆,哪些东西可以共享)

l  对skbuff打时间戳的需求(这种被传来传去的各个操作不打时间戳如何安民心?)

l  与sock和dev的关联。任何一个skbuff必定是来自某个用户的socket(对应一个内核的sock),去往或者从某个net_device到达

l  出于资源管理考虑的组织(要做成链表或rb_tree)

        

         我们可能以为这些需求那就自己去使用skbuff就好了,关skbuff结构体什么事?错,这就是linux内核的做事方式(除非你能想出效率更高的),所有的这些看似外部的需求几乎都会在skbuff的结构体中添加域。好像每个实现特定部分的人都以在skbuff中添加了自己的域为荣一样(不为了钱总得为点什么)。最后导致skbuff结构体正如你看到的那样,巨大无比。

         但是,我们知道,skbuff作为数据的载体,最大的功能就是如果组织数据的。其结构体中记录了各个头部在数据中的位置,是否被克隆过,是否计算过校验等等状态和指示信息(当然这些信息都是分别在上百万内核代码的某处被更新和维护的),但最关键的是,其数据是以可灵活调整大小的fragment组织的(类似scatter list)。其定义的大部分函数也都是为了更加方便的修改结构体中的,你当然可以不用函数而直接修改来完成几乎全部的工作。

         所以,我们看出来,skbuff被虐待成什么样了不要紧,重要的是虐待他的功能模块。他只是用来记录这些模块战果的记事本和战场。

sock

         skbuff是可以跟踪数据包的整个生命周期,sock则是跟踪一个socket的整个生命周期。什么是socket?那是内核中的一个资源实体,利用这个实体就可以访问网络。也就是说,网络协议栈对外呈现的并不是一个面向过程的函数调用,而在概念上反而是一个由类生成的一个个socket对象,通过这一个个对象,用户可以调用对象的方法访问网络。

         那么问题来了。为什么需要这个对象?因为每个应用程序对网络的使用都是个事务,这个事务可能包含很多个不同的步骤,也可能是很长期的。而任何一个函数调用只能完成整个事务的一部分。所以每个事务就需要变量来存储当前的状态。对于网络调用来说,就是监听的是哪个端口,这个端口是否允许重用,这个事务的所有者是哪个或哪些进程,绑定的是哪个设备,使用的是什么协议族,源地址和一些与这个事务有关的缓存、内存、资源(例如可以同时监听的连接数backlog)、超时时间等。这些参数对每个函数调用来说都只是变量,但是对于单个事务来说,就是这个事务存续期间的不可变量。这就是sock结构体存在的意义。

         综上,sock结构体代表的是网络事务。

net

         在linux中,协议栈看起来只有一个,但是随着虚拟化的流行,linux中也支持多个协议栈。这个多个协议栈自然没必要把协议代码拷贝多份,还是利用的面向对象的思想。struct net这个结构体本身就代表了一个协议栈,生成一个对象就是一个协议栈。说白了,由一个协议栈变成多个也就是定义一个变量集。换句话说,一个协议栈本身也描述了一个事务,从这个协议栈的出生到死亡。同时存在多个也就不足为奇了。

         多个net结构体的存在的最大用途在虚拟化,这是一个命名空间概念在网络协议栈的体现(其他的还是文件系统空间、进程PID空间)。不过在别的地方看到网络的命名空间就不要被专业术语给迷惑了。其实就是协议栈对象。