Linux Network Related Drive
catalog
1. 通过套接字通信 2. 网络实现的分层模型 3. 网络命名空间 4. 套接字缓冲区 5. 网络访问层 6. 网络层 7. 传输层 8. 应用层 9. 内核内部的网络通信
1. 通过套接字通信
Linux的设计思想是"万物皆文件",从开发者角度来看,外部设备在Linux(以及UNIX)中都是普通文件,通过正常的读写操作即可访问,但是对于网卡而言,情况会复杂的多。网卡的运作方式与普通的块设备和字符设备完全不同,一个原因是(所有层次)使用了许多不同的通信协议,为建立连接需要指定许多选项,且无法在打开设备文件时完成这些任务,因此,在/dev目录下没有与网卡对应的项
为此,Linux采用的解决方案是将一种称为套接字的特殊结构用作到网络实现的接口,这种方案现在已经成为工业标准,POSIX标准中也定义了套接字
1. 套接字用于定义和建立网络连接,以便可以用操作inode的普通方法(特别是读写操作)来访问网络 2. 从开发者角度,创建套接字的最终结果是一个文件描述符,它不仅提供所有的标准函数,还包括几个增强的函数 3. 用于实际数据交换的接口对所有的协议和地址族都是相同的 4. 在创建套接字时,不仅要区分地址和协议族,还要区分基于流的通信、基于数据报的通信
0x1: 创建套接字
套接字不仅可以用于各种传输协议的IP连接,也可以用于内核支持的所有其他地址和协议类型(例如: IPX、Appletalk、本地UNIX套接字、DECNet、Netlink、以及在<socket.h>中列出的许多其他类型)。因此,在创建套接字时,必须指定所需要的地址和协议类型的组合
需要注意的是,每个地址族都只支持一个协议族(因此在创建socket的时候第三个参数常常为0即可,即通知函数使用适当的默认协议),而且只能区分面向流的通信和面向数据报的通信
0x2: 使用套接字
0x3: 数据报套接字UDP是建立在IP连接之上的第二种大量使用的传输协议,UDP标识User Datagram Protocol(用户数据报协议)
Relevant Link:
http://www.cnblogs.com/LittleHann/p/3875451.html
2. 网络实现的分层模型
内核网络子系统的实现和TCP/IP参考模型非常相似,相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信,这种做法的好处在于
1. 可以组合使用各种设备、传输机制和协议 2. 通常的以太网卡不仅可用于建立因特网(IP)连接,还可以在其上传输其他类型的协议,如Appletalk、IPX,而无须对网卡的设备驱动程序做任何类型的修改
分层模型不仅反映在网络子系统的设计上,而且也反映在数据传输的方式上(对各层产生和传输的数据进行封装的方式)
3. 网络命名空间
内核的许多部分包含在命名空间中,通过命名空间可以建立系统的多个虚拟视图,并且彼此分隔开来,每个实例看起来像是一台运行Linux的独立机器,但是在一台物理机器上,可以同时运行许多这样的实例,从内核2.6.24开始,Linux内核也开始对网络子系统采用命名空间,这对网络子系统增加了一些额外的复杂性
4. 套接字缓冲区
在内核分析(收到的)网络分组时,底层协议的数据将传递到更高的层,发送数据时顺序相反,各种协议产生的数据(首部和载荷)依次向更低的层传递,直至最终发送。这些操作的速度对网络子系统的性能有决定性的影响,因此内核使用了一种特殊的结构,称为套接字缓冲区(socket buffer)
/source/include/linux/skbuff.h
struct sk_buff { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; struct sock *sk; ktime_t tstamp; struct net_device *dev; unsigned long _skb_dst; #ifdef CONFIG_XFRM struct sec_path *sp; #endif /* * This is the control buffer. It is free to use for every * layer. Please put your private variables there. If you * want to keep them across layers you have to do a skb_clone() * first. This is owned by whoever has the skb queued ATM. */ char cb[48]; unsigned int len, data_len; __u16 mac_len, hdr_len; union { __wsum csum; struct { __u16 csum_start; __u16 csum_offset; }; }; __u32 priority; kmemcheck_bitfield_begin(flags1); __u8 local_df:1, cloned:1, ip_summed:2, nohdr:1, nfctinfo:3; __u8 pkt_type:3, fclone:2, ipvs_property:1, peeked:1, nf_trace:1; __be16 protocol:16; kmemcheck_bitfield_end(flags1); void (*destructor)(struct sk_buff *skb); #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct nf_conntrack *nfct; struct sk_buff *nfct_reasm; #endif #ifdef CONFIG_BRIDGE_NETFILTER struct nf_bridge_info *nf_bridge; #endif int iif; #ifdef CONFIG_NET_SCHED __u16 tc_index; /* traffic control index */ #ifdef CONFIG_NET_CLS_ACT __u16 tc_verd; /* traffic control verdict */ #endif #endif kmemcheck_bitfield_begin(flags2); __u16 queue_mapping:16; #ifdef CONFIG_IPV6_NDISC_NODETYPE __u8 ndisc_nodetype:2; #endif kmemcheck_bitfield_end(flags2); /* 0/14 bit hole */ #ifdef CONFIG_NET_DMA dma_cookie_t dma_cookie; #endif #ifdef CONFIG_NETWORK_SECMARK __u32 secmark; #endif __u32 mark; __u16 vlan_tci; sk_buff_data_t transport_header; sk_buff_data_t network_header; sk_buff_data_t mac_header; /* These elements must be at the end, see alloc_skb() for details. */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; atomic_t users; };
套接字缓冲区用于在网络实现的各个层次之间交换数据,而无须来回复制分组数据,对性能带来了很大的提高。套接字结构是网络子系统的基石之一,因为在产生和分析分组时,在各个协议层次上都需要处理该结构
0x1: 使用套接字缓冲区管理数据
套接字缓冲区通过其中包含的各种指针与一个内存区域相关联,网络分组的数据就位于该区域中,套接字缓冲区的基本思想是
通过操作指针来增删协议首部
1. head和end指向数据在内存中的起始和结束位置: 这个区域可能大于实际需要的长度,因为在产生分组时,商不清楚分组的长度 2. data和tail指向协议数据区域的起始和结束位置 3. mac_header指向MAC协议首部的起始 4. network_header指向网络层协议首部的起始 5. transport_header指向传输层协议首部的起始
这使得内核可以将套接字缓冲区用于所有协议类型,正确地解释数据需要做简单的类型转换,为此内核提供了几个辅助函数
data和tail使得在不同协议层之间传递数据时,无须显式的复制操作
1. 在一个新分组产生时,TCP层首先在用户空间中分配内存来容纳该分组数据(首部和载荷),分配的空间大于数据实际需要的长度,因此较低的协议层可以进一步增加首部 2. 分配一个套接字缓冲区,使得head和end分别指向新分配的较低层协议内存区的起始和结束地址,而TCP数据位于data和tail之间 3. 在套接字缓冲区传递到互联网网络层时,必须增加一个新层,只需要向已经分配但尚未占用的那部分内存空间写入数据即可,除了data之外的所有指针都不变,data现在指向IP首部的起始处,再往下的各层会重复同样的操作,直至分组完成,即将通过网络发送 4. 对接收的分组进行分析的过程是类似的,分组数据复制到内核分配的一个内存区中,并在整个分析期间一直处于该内存区中,与该分组相关联的套接字缓冲区在各层之间顺序传递,各层依次将其中的各个指针设置为正确值 //套接字缓冲区之所以可以使用头尾指针进行跨层传递处理的原因在于,TCP/IP协议栈各层之间本质上是在进行逐层扩大的数据包装,从内存空间视角看到的就是从栈顶逐渐向栈底发展,所以可以通过预申请一块大内存,在逐层封装的时候通过移动头尾指针实现协议层切换
套接字缓冲区需要很多指针来表示缓冲区中内容的不容部分,由于网络子系统必须保证较低的内存占用和较高的处理速度,因而对于struct sk_buff来说,我们需要保持该结构的长度尽可能小
0x2: 管理套接字缓冲区数据
5. 网络访问层
网络访问层主要负责在计算机之间传输信息,与网卡的设备驱动程序直接协作
0x1: 网络设备的表示
在内核中,每个网络设备都表示为net_device结构的一个实例,在分配并填充该结构的一个实例之后,必须用net/core/dev.c中的register_netdev函数将其注册到内核,该函数完成一些初始化任务,并将该设备注册到通用设备机制内,这会创建一个sysfs项
[root@iZ23er0navtZ ~]# ll /sys/class/net total 0 lrwxrwxrwx 1 root root 0 Dec 9 20:00 eth0 -> ../../devices/vif-0/net/eth0 lrwxrwxrwx 1 root root 0 Dec 9 20:00 eth1 -> ../../devices/vif-1/net/eth1 lrwxrwxrwx 1 root root 0 Dec 9 20:00 lo -> ../../devices/virtual/net/lo
0x2: 接收分组
分组到达内核的时间是不可预测的,所有现代的设备驱动程序都使用中断来通知内核有分组到达,网络驱动程序对特定于设备的中断设置了一个处理例程,因此每当该中断被触发时(即分组到达),内核都会自动调用该处理例程,将数据从网卡传输到物理内存,或通知内核在一定时间后进行处理
几乎所有的网卡都支持DMA模式,能够自行将数据传输到物理内存,但这些数据仍然需要解释和处理,这会在稍后进行
1. 传统方法
下面学习一个分组到达网络适配器之后,该分组穿过内核到达网络层函数的路径
1. 分组是在中断上下文中接收到的,所以处理例程只能执行一些基本的任务,避免系统(或当前CPU)的其他任务延迟太长时间 2. net_interrupt是由设备驱动程序设置的中断处理程序,它将确定该中断是否真的是由接收到的分组引发的,如果确实如此,则控制将转移到net_rx 3. net_rx函数也是特定于网卡的 1) 首先创建一个新的套接字缓冲区 2) 分组的内容接下来从网卡传输到缓冲区(即进入物理内存) 3) 然后使用内核源码中针对各种传输类型的库函数来分析首部数据,这项分析将确定分组数据所使用的网络层协议,例如IP协议 4. netif_rx函数不是特定于网络驱动程序的,该函数位于net/core/dev.c,调用该函数,标志着控制由特定于网卡的代码转移到了网络层的通用接口部分。该函数的作用在于,将接收到的分组放置到一个特定于CPU的等待队列上,并退出中断上下文,使得CPU可以执行其他任务 5. 内核在全局定义的softnet_data数组中管理进出分组的等待队列,数据项类型为softnet_data
2. 对高速接口的支持
0x3: 发送分组
在网络层中特定于协议的函数通知网络访问层处理由套接字缓冲区定义的一个分组时,将发送完成的分组
6. 网络层
网络访问层仍然收到传输介质的性质以及相关适配器的设备驱动程序的很大影响,网络层(具体地说是IP协议)与网络适配器的硬件性质几乎是完全分离的
需要明白的是,硬件的性质是需要将较大的分组分隔为较小的单位的首要原因,因为每一种传输技术所支持的分组长度都有一个最大值,IP协议必须将较大的分组划分为较小的单位,由接收方重新组合,更高层协议不会注意到这一点,划分后分组的长度取决于特定传输协议的能力
0x1: IPv4
ip_rcv函数是网络层的入口点,分组向上穿过内核的路线如下
发送和接收操作的程序流程并不总是分离的,如果分组只通过当前计算机转发,那么发送和接收操作是交织的,这种分组不会传递到更高的协议层(或应用程序),而是立即离开计算机,发往新的目的地
0x2: 接收分组
1. 在分组(以及对应的套接字缓冲区,其中的指针已经设置了适当的值)转发到ip_rcv之后,必须检查接收到的信息,确保它是正确的 1) 主要检查计算的校验和与首部中存储的校验和是否一致 2) 其他的检查包括分组是否达到了IP首部的最小长度 3) 以及分组的协议是否确实是IPv4(IPv6的接收例程是另一个) 2. 在进行了这些检查之后,内核并不立即继续对分组的处理,而是调用一个netfilter挂钩,使得用户空间可以对分组数据进行操作,netfilter挂钩插入到内核源代码中定义好的各个位置,使得分组能够被外部动态操作,挂钩存在于网络系统的各个位置,每种挂钩都有一个特别的标记,例如: NF_IP_POST_ROUTING 3. 在内核到达一个挂钩位置时,将在用户空间调用对该标记支持的例程,接下来,在另一个内核函数中继续内核端的处理(分组可能在这个环节被修改) 4. 在下一步中,接收到的分组到达一个十字路口,此时需要判断该分组的目的地是本地系统还是远程计算机。根据对分组目的地的判断,需要将分组转发到更高层、或转到互联网络层的输出路径上 5. ip_route_input负责选择路由,判断路由的结果是,选择一个函数,进行进一步的分组结果,可用的函数是 1) ip_local_deliver: 分组是交付到本地计算机下一个更高层的 2) ip_forward: 分组是转发到网络中的另一台计算机
0x3: 交付到本地传输层
如果分组的目的地是本地计算机,ip_local_deliver必须设法找到一个适当的传输层函数,将分组转送过去,IP分组通常对应的传输层协议是TCP或UDP
0x4: 分组转发
IP分组可能如上所述交付给本地计算机处理,它们也可能离开互联网络层,转发到另一台计算机,而不牵涉本地计算机的高层协议实例,分组的目标地址可分为以下两类
1. 目标计算机在某个本地网络中,发送计算机与该网络有连接 2. 目标计算机在地理上属于远程计算机,不连接到本地网络,只能通过网关访问
该信息由路由表(routing table)提供,路由表由内核通过多种数据结构实现并管理。在接收分组时调用的ip_route_input函数充当路由实现的接口,这一方面是因为该函数能够识别出分组是交付到本地还是转发出去,另一方面是该函数能够找到通向目标地址的路由。目标地址存储在套接字缓冲区的dst字段中
0x5: 发送分组
内核提供了几个通过互联网络层发送数据的函数,可由较高协议层使用,其中ip_queue_xmit是最常使用的一个
0x6: netfilter
netfilter是一个Linux内核框架,使得可以根据动态定义的条件来过滤和操作分组,这显著增加了可能的网络选项的数目,从简单的防火墙,到对网络通信数据的详细分析,甚至更复杂的依赖于状态的分组过滤器
1. 扩展网络功能
1. 根据状态及其条件,对不同数据流方向(进入、外出、转发)进行分组过滤(packet filtering) 2. NAT(network address translation 网络地址转换)根据某些规则来转换源地址和目标地址(通常用于IP伪装或透明代理) 3. 分组处理(packet manghing)和操作(manipulation),根据特定的规则拆分和修改分组
可以通过在运行期间动态向内核载入模块来增强netfilter功能,一个定义好的规则集,告知内核在何时使用各个模块的代码。内核和netfilter之间的接口保持在很小的规则上,尽可能使两个领域彼此隔离,避免二者的互相干扰并改进网络代码的稳定性
netfilter挂钩位于内核中的各个位置,以支持netfilter代码的执行,这些不仅用于IPv4,也用于IPv6和DECNET协议,netfilter实现划分为如下几个部分
1. 内核代码中的挂钩,位于网络实现的核心,用于调用netfilter代码 2. netfilter模块,其代码挂钩内部调用,但其独立于其余的网络代码,一组标准模块提供了常用的函数,但可以在扩展模块中定义用户相关的函数
2. 调用挂钩函数
在通过挂钩执行netfilter代码时,网络层的函数将会被中断。挂钩的一个重要特性是,它们将一个函数划分为两部分
1. 前一部分在netfilter代码调用前运行 2. 后一部分在netfilter代码调用后运行
3. 扫描挂钩表
如果至少注册了一个挂钩函数并需要调用,那么会调用nf_hook_slow,所有挂钩都保存在二维数组nf_hooks中
4. 激活挂钩函数
每个hook函数都返回下列值之一
1. NF_ACCEPT: 表示接受分组,这意味着所述例程没有修改数据,内核将继续使用未修改的分组,使之穿过网络实现中剩余的协议层(或通过后续的挂钩) 2. NF_STOLEN: 表示挂钩函数"窃取"了一个分组并处理该分组,此时,该分组已经与内核无关,不必再调用其他挂钩,还必须取消其他协议层的处理 3. NF_DROP: 通过内核丢弃该分组,如同NF_STOLEN,其他挂钩或网络层的处理都不再需要了,套接字缓冲区(和分组)占用的内存空间可以释放,因为其中包含的数据可以被丢弃,例如挂钩可能认定分组是损坏的 4. NF_QUEUE: 将分组置于一个等待队列上,以便其数据可以由用户空间代码处理,不会执行其他挂钩函数 5. NF_REPEAT: 表示再次调用该挂钩
除非所有挂钩函数都返回NF_ACCEPT(NF_REPEAT不是最终结果),否则分组不会在网络子系统进一步处理,所有其他的分组,不是被丢弃,就是由netfilter子系统处理
内核提供了一个挂钩函数的集合,使得不必为每个场合都单独定义挂钩函数,这些称为iptables,用于分组的高层处理,它们使用用户空间工具iptables配置
0x7: IPv6
7. 传输层
两个基于IP的主要传输协议分别是UDP和TCP,前者用于发送数据报,后者可建立安全的、面向连接的服务
0x1: UDP
0x2: TCP
TCP提供的函数比UDP多得多,因此在其内核中的实现要困难得多,也牵涉更广的内容。TCP连接总是处于某个明确定义的状态,各个状态之间遵循一定的规范进行迁移
8. 应用层
套接字将UNIX隐喻"万物皆文件"应用到了网络连接上,内核与用户空间套接字之间的接口实现在C标准库中,使用了socketcall系统调用(老的Linux内核采用多路复用方式,新内核已经将多路复用拆解到了单独的系统调用中了)
Linux采用了内核套接字的概念,使得与用户空间中的套接字的通信尽可能简单,对程序使用的每个套接字来说,都对应于
1. socket结构实例: 充当向下(到内核) 2. sock结构实例: 充当向上(到用户空间)接口
0x1: socekt数据结构
0x2: 套接字和文件
在连接建立后,用户空间进程使用普通的文件操作来访问套接字,由于VFS层的开放结构,内核只需要很少的工作。在VFS虚拟文件系统的VFS inode中,每个套接字都分配了一个该类型的inode,inode又关联到另一个与普通文件相关的结构,用于操作文件的函数保存在一个单独的指针表中
<fs.h> struct inode { .. struct file_operations *i_fop; .. }
因此,对套接字文件描述符的文件操作,可以透明地重定向到网络子系统的代码
9. 内核内部的网络通信
与其他主机通信,不只是用户层应用程序的需求,内核同样需要与其他计算机通信,例如网络文件系统如CIFS或NCPFS都依赖于内核内部提供的网络通信支持。同时,内核各组件之间也需要进行通信,以及用户层和内核之间的通信,netlink机制提供了所需的框架
0x1: 通信函数
0x2: netlink机制
netlink是一种基于网络的机制,允许在内核内部以及内核与用户层之间进行通信,它的思想是,基于BSD的网络套接字使用网络框架在内核和用户层之间进行通信,但netlink套接字大大扩展了可能的用途,该机制不仅仅用于网络通信,其更重要的用户是对象模型,它使用netlink套接字将各种关于内核事务的状态信息传递到用户层,其中包括新设备的注册和移除、硬件层次上发生的特别事件等
内核中还有其他一些可选的方法能够实现类似的功能,例如procfs或sysfs中的文件,但与这些方法相比,netlink机制有一些很明显的优势
1. 任何一方都不需要轮询,如果通过文件传递状态信息,那么用户层需要不断检查是否有新消息到达 2. 系统调用和ioctl也能够从用户层向内核传递信息,但比简单的netlink连接更难于实现 3. 使用netlink不会与模块有任何冲突,但模块和系统调用显然配合的不是很好 4. 内核可以直接向用户层发送信息,而无须用户层事先请求,使用文件也可以做到,但系统调用和ioctl是不可能的 5. 除了标准的套接字,用户空间应用程序不需要使用其他东西来与内核交互
netlink只支持数据报信息,但提供了双向通信,另外,netlink不仅支持单播消息,也可以进行多播,类似于其他套接字的机制,netlink的工作方式是异步的
Relevant Link:
Copyright (c) 2015 LittleHann All rights reserved