关键词不能为空

当前您在: 主页 > 英语 >

linux tcpip协议栈分析

作者:高考题库网
来源:https://www.bjmy2z.cn/gaokao
2021-02-05 22:24
tags:

-

2021年2月5日发(作者:粗糙)


sk_buff


结构可能是


linux


网络代码中最重要的数据结构,它表示接收或发送数据包的包头信息。它在



中定义,并包含很多成员变量供 网络代码中的各子系统使用。




这个 结构在


linux


内核的发展过程中改动过很多次,或者是增加 新的选项,或者是重新组织已存在的成员


变量以使得成员变量的布局更加清晰。它的成员 变量可以大致分为以下几类:




?



Layout


布局



?



General


通用



?



Feature- specific


功能相关



?



Management functions


管理函数



这个 结构被不同的网络层(


MAC


或者其他二层链路协议,三层的< /p>


IP


,四层的


TCP


UDP


等)使用,并


且其中的成 员变量在结构从一层向另一层传递时改变。


L4



L3


传递前会添加一个


L4


的头部,同样,


L3



L2

< p>
传递前,会添加一个


L3


的头部。添加头部比在不 同层之间拷贝数据的效率更高。由于在缓冲区的头


部添加数据意味着要修改指向缓冲区的 指针,这是个复杂的操作,所以内核提供了一个函数


skb_reserve

< p>
(在后面的章节中描述)来完成这个功能。协议栈中的每一层在往下一层传递缓冲区前,第一件事就 是调



skb_reserve


在缓冲 区的头部给协议头预留一定的空间。



skb_reserv e


同样被设备驱动使用来对齐接收到包的包头。如果缓冲区向上层协议传递,旧的协议层 的头


部信息就没什么用了。


例如,


L2


的头部只有在网络驱动处理


L2


的协议 时有用,


L3


是不会关心它的信息的。


但是,内核并没有把


L2


的头部从缓冲区中删除,而是把有效荷 载的指针指向


L3


的头部,这样做,可以节


CPU


时间。



1.


网络参数和内核数据结构


< /p>


就像你在浏览


TCP/IP


规范或者配置 内核时所看到的一样,网络代码提供了很多有用的功能,但是这些功


能并不是必须的,比 如说,防火墙,多播,还有其他一些功能。大部分的功能都需要在内核数据结构中添


加自 己的成员变量。因此,


sk_buff


里面包含了很多像


#ifdef


这样的预编译指令。例如,在


s k_buff


结构


的最后,你可以找到:




struct sk_buff {


... ... ...


#ifdef CONFIG_NET_SCHED


_ _u32 tc_index;


#ifdef CONFIG_NET_CLS_ACT


_ _u32 tc_verd;


_ _u32 tc_classid;


#endif


#endif


}




它表明,


tc_index


只有在编译时定义了


CONFIG_NET_SCHED


符号才有效。这个符号可以通过选择特


定的编译选项来定义(例如:

< p>


options


QoS


and/or


fair queueing


)。这些编译选项可以由管理员通过


make config


来选择,或者通过一些自动安装工具


来选择。




前面的例子有两个嵌套的选项:


CONFIG_NET_CLS_ACT


(包分类器)


只有在选择支持


“QoS


and/or fair


queueing”


时才能生效。



顺便提一下,


QoS


选项不能被编译成 内核模块。原因就是,内核编译之后,由某个选项所控制的数据结构


是不能动态变化的。 一般来说,如果某个选项会修改内核数据结构(比如说,在


sk_buff

< p>
里面增加一个项


tc_index


),那么,包含 这个选项的组件就不能被编译成内核模块。



你可能经常需要查找是哪个


make config

< p>
编译选项或者变种定义了某个


#ifdef


标记, 以便理解内核中包


含的某段代码。在


2.6

内核中,最快的,查找它们之间关联关系的方法,



就是查 找分布在内核源代码树中



kconfig

文件中是否定义了相应的符号(每个目录都有一个这样的文件)。在


< p>
2.4


内核中,你需要查看


Documentat ion/


文件。




2. Layout Fields



有些


sk_buff


成员变量的作用是方便查找或者是连接数据 结构本身。


内核可以把


sk_buff


组织成一个双向


链表。当然,这个链表的结构要比常见的双向链表的结构复杂一点。




就像任何一个双向链表一样,


sk_buff


中有两个指针


next



prev


,其中,


n ext


指向下一个节点,而



pre v


指向上一个节点。但是,这个链表还有另一个需求:每个


sk _buff


结构都必须能够很快找到链表头


节点。为了满足这个 需求,在第一个节点前面会插入另一个结构


sk_buff_head

< br>,这是一个辅助节点,它


的定义如下:




struct sk_buff_head {


/* These two members must be first. */ struct sk_buff *


next;


struct sk_buff * prev;


_ _u32 qlen;


spinlock_t lock;


};


qlen


代表链表元素的个数 。


lock


用于防止对链表的并发访问。




sk_buff



sk_buff_head


的前两个元素是一样的:


next



prev


指针。

< p>
这使得它们可以放到同一个链表


中,尽管


sk_b uff_head


要比


sk_buff


小得多。另外,相同的函数可以同样应用于


sk_buff


和< /p>


sk_buff_head




为了使这个数据结构更灵活,


每个


sk_buf f


结构都包含一个指向


sk_buff_head


的指针。


这个指针的名字



list


。图


1


会帮助你理解它们之间 的关系。



Figure 1. List of sk_buff elements





其他有趣的成员变量如下:



struct sock *sk


这是一个指向拥有这个


sk_buff



sock


结构的指针。这个指针在网络包由本机发出或者由本机进程接收


时有效,


因为插口相关的信息被


L4(TCP



UDP)


或者用户空间程序使用。


如果


sk_buff


只在转发中使用


(

< p>


意味着,源地址和目的地址都不是本机地址


)< /p>


,这个指针是


NULL





unsigned int len

< p>
这是缓冲区中数据部分的长度。它包括主缓冲区中的数据长度


(data< /p>


指针指向它


)


和分片中的数据长度。它< /p>


的值在缓冲区从一个层向另一个层传递时改变,因为往上层传递,旧的头部就没有用了,而 往下层传递,


需要添加本层的头部。


len

同样包含了协议头的长度。




unsigned int data_len


< p>
len


不同,


data_len

< br>只计算分片中数据的长度。




unsigned int mac_len


这是

< p>
mac


头的长度。




atomic_t users


这是一个引用计数,用于计算 有多少实体引用了这个


sk_buff


缓冲区。它的主要用途是 防止释放


sk_buff


后,还有其他实体引用这个

< p>
sk_buff


。因此,每个引用这个缓冲区的实体都必须在适当的时候增 加或减小


这个变量。这个计数器只保护


sk_buff


结构本身,而缓冲区的数据部分由类似的计数器


(dataref)


来保护


.


有时可以用

atomic_inc



atomic_dec

< p>
函数来直接增加或减小


users



但是,


通常还是使用函数


skb_get

< p>


kfree_skb


来操作这个变量。




unsigned int truesize


这是缓冲区的总长度,包括


sk_buff


结构和数据部分。如果申请一个


len


字节的缓冲区,


alloc_skb


函数


会把它初始化成


len+sizeof(sk_buff)





struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)


{


... ... ...


skb->truesize = size + sizeof(struct sk_buff);


... ... ...


}



skb->len


变化时,这个变量也会变化。




unsigned char *head


unsigned char *end


unsigned char *data


unsigned char *tail


它们表示缓冲区和 数据部分的边界。


在每一层申请缓冲区时,


它会分配比协议头或 协议数据大的空间。


head



end


指向缓冲区的头部和尾部,



data



tail


指向实际数据的头部和尾部 ,


参见图


2



每一层会在


head



data


之间填充协议头,或者在


tail



end


之间添加新的协议数据。图


2

< p>
中右边数据部分会在尾部包含


一个附加的头部。




Figure 2. head/end versus data/tail pointers





void (*destructor)(...)


这个函数指针可以初始化成一个在缓冲区释放时完成某些动作的函数。


如果缓冲区不属于一个


socket




个函数指针通常是不会被赋值的。如果缓冲区属于一个


soc ket




这个函数指针会被赋值为< /p>


sock_rfree



sock_wf ree(


分别由


skb_set_owner_r



skb_set_owner_w


函数初始化


)


。这两个


sock_xxx



数用于更新


socket


的队列中的内存容量。



3. General Fields


本节描述


sk_buff


的主要成员变量,这些成员变量与特定的内核功能无关:




struct timeval stamp


这个变量只对接 收到的包有意义。它代表包接收时的时间戳,或者有时代表包准备发出时的时间戳。它在


netif_rx


里面由函数


net_timestamp


设置,而


netif_rx


是设备驱动收到 一个包后调用的函数。



struct net_device *dev


这个变量的类型是


net_device

< p>


net_device


它代表一个网络设备。< /p>


dev


的作用与这个包是准备发出的包


还 是刚接收的包有关。


当收到一个包时,


设备驱动会把

< p>
sk_buff



dev


指针指向收到这个包的设备的数据


结构,就像下面的


vorte x_rx


里的一段代码所做的一样,这个函数属于


3c59x< /p>


系列以太网卡驱动,用于


接收一个帧。


( drivers/net/3c59x.c)





static int vortex_rx(struct net_device *dev)


{


... ... ...


skb->dev = dev;


... ... ...


skb->protocol = eth_type_trans(skb, dev);


netif_rx(skb); /* Pass the packet to the higher layer


*/


... ... ...


} < /p>


当一个包被发送时,这个变量代表将要发送这个包的设备。在发送网络包时设置这个值的代 码


要比接收网络包时设置这个值的代码复杂。有些网络功能可以把多个网络设备组成一个 虚拟的


网络设备


(


也就是说,这些设备 没有和物理设备直接关联


)


,并由一个虚拟网络设备驱动管理。


当虚拟设备被使用时,


dev


指针指向 虚拟设备的


net_device


结构。而虚拟设备驱动会在一 组


设备中选择一个设备并把


dev


指针 修改为这个设备的


net_device


结构。


因此,


在某些情况下,



指向传输设备的指针会在包处理过程中被改变。





struct net_device *input_dev


这是收到包的网络设备的指针。如果包 是本地生成的,这个值为


NULL


。对以太网设备来说,这个值 由


eth_type_trans


初始化


,


它主要被流量控制代码使用。





struct net_device *real_dev


这个变量只对虚拟设备有意义,它代表与虚拟设备关联的真实设备。例如,


Bonding



VLAN


设备都使用


它来指向收到包的真实设备。




union {...} h


union {...} nh


union {...} mac


这些是指向


TCP/IP


各层 协议头的指针:


h


指向


L4

< p>


nh


指向


L3



mac


指向


L2


。每个指针的


类型都是一个联合,


包含多个 数据结构,


每一个数据结构都表示内核在这一层可以解析的协议。


例如,


h


是一个包含内核所能解析的


L4


协议的数据结构的联合。每一个联合都有一个


raw



量用于初始化,后续的访问都是通过协议相关的变量进行的。




当接收一个包时,处理

< br>n


层协议头的函数从


n-1


层收 到一个缓冲区,它的


skb->data


指向

< br>n


层协议的头。


处理


n


层协议的函数把本层的指针


(


例如,

< p>
L3


对应的是


skb->nh

指针


)


初始化为


skb->dat a



因为这个指针的值会在处理下一层协议时改变


(


skb->data


将被初始化成缓冲区


里的其他地址


)


。在处理


n


层协议的函数结束时,在把包传递给


n+1


层的处理函数前,它会把


skb->data


指针指向


n


层协议头的末尾,这正好是


n+1< /p>


层协议的协议头


(


参见图


3)




< br>发送包的过程与此相反,但是由于要为每一层添加新的协议头,这个过程要比接收包的过程复


杂。




Figure 3. Header's pointer initializations while moving from layer


two to layer three





struct dst_entry dst


这个变量在路由子系统中使用。




char cb[40]


这是一个


“control buffer”

< br>,或者说是一个私有信息的存储空间,由每一层自己维护并使用。它在分配


sk_ buff


结构时分配


(


它目前的大小是


40


字节,已经足够为每一层存储必要的私有信息了

< p>
)


。在每一层中,


访问这个变量的代码通常用宏实 现以增强代码的可读性。例如,


TCP


用这个变量存储


tcp_skb_cb


结构,


这个结构在


include/net/tcp.h


中定义:



struct tcp_skb_cb {


... ... ...


_


_u32 seq; /*


Starting


sequence


number


*/


_


_u32 end_seq; /*


SEQ


+


FIN


+


SYN


+


datalen*/


_ _u32 when; /* used to compute


rtt's */


_ _u8 flags; /* TCP header


flags. */


... ... ...


};

< p>
下面这个宏被


TCP


代码用来访问


cb


变量。在这个宏里面,有一个简单的类型转换:



#define TCP_SKB_CB(_ _skb) ((struct tcp_skb_cb *)&((_


_skb)->cb[0]))


下面的例子是


TCP


子系统在收到一个分段时填充相 关数据结构的代码:



int tcp_v4_rcv(struct sk_buff *skb)


{


... ... ...


th = skb->;


TCP_SKB_CB(skb)->seq = ntohl(th->seq);


TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq +


th->syn + th->fin +


skb->len - th->doff * 4);


TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);


TCP_SKB_CB(skb)->when = 0;


TCP_SKB_CB(skb)->flags = skb->->tos;


TCP_SKB_CB(skb)->sacked = 0;


... ... ...


}


如果想要了解

< br>cb


中的参数是如何被取出的,可以查看


net/ipv 4/tcp_output.c


中的


tcp_transmit _skb


函数。这个函数被


TCP


用于 向


IP


层发送一个分段。



unsigned int csum


unsigned char ip_summed


表示校验和以及相关状态标记。




unsigned char cloned


一个布尔标记, 当被设置时,表示这个结构是另一个


sk_buff


的克隆。在



克隆和拷贝缓冲区



一节中有描


述。




unsigned char pkt_type


这个变量表 示帧的类型,


分类是由


L2


的目的地址 来决定的。


可能的取值都在


include/linux/if _packet.h


中定义。对以太网设备来说,这个变量由


e th_type_trans


函数初始化。



类型的可能取值如下:




PACKET_HOST


包的目的地址与收到它的网络设备的


L2


地址相等。换句话说,这个包是发给本机的。




.PACKET_MULTICAST


包的目的地址是一个多播地址,而这个多播地址是收到这个包的网络设备所注册的多播地址。< /p>




PACKET_BROADCAST


包的目的地址是一个广播地址,而这个广播地址也是收到这个包的网络设备的广播地址。




PACKET_OTHERHOST


包的目的地址与收到它的 网络设备的地址完全不同


(


不管是单播,多播还是广播


)


,因此,如果


本机的转发功能没有启用,这个 包会被丢弃。




PACKET_OUTGOING


这个包将被发出。用到这个 标记的功能包括


Decnet


协议,或者是为每个网络


tap


都复制一份


发出包的函数。




PACKET_LOOPBACK

< p>
这个包发向


loopback


设备。由于有这个标 记,在处理


loopback


设备时,内核可以跳过一些


真实设备才需要的操作。




PACKET_FASTROUTE


这个包由快速路由代码查 找路由。快速路由功能在


2.6


内核中已经去掉了。

< p>




_ _u32 priority


这个变量描述发送或转发包的


QoS


类别。如果包是本地生成的,


socket


层 会设置


priority


变量。如果包


是将要被转发的,


rt_tos2priority


函数会根据


ip


头中的


Tos

域来计算赋给这个变量的值。这个变量的值



DSCP(D iffServ CodePoint)


没有任何关系。



unsigned short protocol


这个变量 是高层协议从二层设备的角度所看到的协议。典型的协议包括


IP



IPV6



ARP


。完整的列表在


include/linux/if_ether.h


中。由于每个协议都有自己的协议处理函数来处理接收到的包,因此,这个域

被设备驱动用于通知上层调用哪个协议处理函数。


每个网络驱动都调用


netif_rx


来通知上层网络协议的协


议处理函 数,因此


protocol


变量必须在这些协议处理函数调用之 前初始化。




unsigned short security


这是包的安全级别。这个变量最初由

< br>IPSec


子系统使用,但现在已经作废了。



4. Feature-Specific Fields


l inux


内核是模块化的,


你可以选择包含或者删除某些功能。


因此,


sk_buff


结构里面的一些 成员变量只有


在内核选择支持某些功能时才有效,比如防火墙


( netfilter)


或者


qos


:< /p>



unsigned long nfmark


_ _u32 nfcache


_ _u32 nfctinfo


struct nf_conntrack *nfct


unsigned int nfdebug


struct nf_bridge_info *nf_bridge


这些变量被

< br>netfilter


使用


(


防火 墙代码


)


,内核编译选项是


“Devi c


e Drivers->Networking support->


Networking options-


> Network packet filtering”


和两个子选项


“Network packet filtering


debugging”



“Bridged IP/ARP packets filtering”



union {...} private


这个联合结构被高性能并行接口


(HIPPI)


使用。


相应的内核编译选项是


“Device


->Drivers ->Networking


support ->Network device support -


>HIPPI driver support”



_ _u32 tc_index


_ _u32 tc_verd


_ _u32 tc_classid


这些变量被流量控制代码使用,内核编译选项是


“Device Drivers


->Networking->support


->Networking options -


>QoS and/or fair queueing”


和它的子选项


“ Packetclassifier API”



struct sec_path *sp


这个变量被


IPSec


协议用于跟踪传输的信息。



5. Management Functions


有很多函 数,通常都比较短小而且简单,内核用这些函数操作


sk_buff

的成员变量或者


sk_buff


链表。图


4


会帮助我们理解其中几个重要的函数。我们首先来看分配和释放缓冲区的函数 ,然后是一些通


过移动指针在缓冲区的头部或尾部预留空间的函数。



如果你看过


include/ linux/skbuff.h



net/core/skbu ff.c


中的函数,


你会发现,


基本上 每个函数都有两


个版本,名字分别是


do_something



__do_something


。通 常第一种函数是一个包装函数,它会在第


二种函数的基础上增加合法性检查或者锁。一般 来说,类似


__do_something


的函数不能被直接调 用


(



非满足特定的条件,比如说锁< /p>


)


。那些违反这条规则而直接引用这些函数的不良代码会最终被更 正。




Figure 4. Before and after: (a)skb_put, (b)skb_push, (c)skb_pull, and


(d)skb_reserve


-


-


-


-


-


-


-


-



本文更新与2021-02-05 22:24,由作者提供,不代表本网站立场,转载请注明出处:https://www.bjmy2z.cn/gaokao/603536.html

linux tcpip协议栈分析的相关文章