• 套接字相关数据据结构及分层模型


    主要参考了《深入linux内核架构》和《精通Linux内核网络》相关章节

    套接字及分层模型

    与网络相关的头文件的数目巨大,使得内核开发者将这些头文件存储到一个专门的目录include/net中

    image-20220719152358328

    对某些问题来说,划分为7层过于详细了。因此,实际上通常使用另一种参考模型,其中将ISO/OSI模型的一些层合并为新层。该模型只有4层,因此其结构更为简单。这种模型称为TCP/IP参考模型,IP表示Internet Protocol(网际协议),而TCP表示Transmission Control Protocol(传输控制协议)。当今因特网上的大部分通信都是基于该模型的。两个模型的各个层的比较见图12-1。

    • 传输层:定义传输数据的协议端口号,以及流控和差错校验;
    • 网络层:进行逻辑地址建起(IP)、实现不同网络之间的路由选择;
    • 网络接口层:它处理传输介质的物理性质,并将数据流划分为定长的帧(frame),以便在发生传输错误时重传数据块。如果几台计算机共享同一传输线路,网络接口卡必须有一个唯一的 ID号,称之为MAC地址(MAC address),通常烧进硬件中 。各厂商之间的协议保证该ID是全球唯一的 。

    内核网络子系统的实现与刚介绍的TCP/IP参考模型非常相似。相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信。这种做法的好处在于,可以组合使用各种设备、传输机制和协议。

    appletalk(AT)是由Apple公司创建的一组网络协议的名称,它用于Apple系列的个人计算机。IPX是指互联分组交换协议:提供分组寻址和选择路径功能,保证可靠到达,相当于数据报的功能;SPX是顺序报文分组交换协议,这可保证信息流按序、可靠地传送。IPX/SPX为Novell在网络层和传输采用的协议;SDLC是SNA中数据链路层协议,后修改为HDLC(高级数据链路控制)。

    核心基本术语

    • 数据帧(Frame):指起始点和目的点都是数据链路层的信息单元。

      数据帧:帧数据由两部分组成:帧头部和帧数据,帧头部包括接收方主机物理地址的定位及其它网络信息,帧数据区含有一个数据体。

    • 数据包(Packet):指起始点和目的地是网络层的信息单元。

    • 数据报(Datagram):指起始点和目的地都使用无连接网络服务的网络层的信息单元。

    • 段(Segment):指起始点和目的地都是传输层的信息单元。

    • 消息(Message):指起始点和目的地都在网络层以上的信息单元(经常在应用层)。

    • 元素(Cell):指的是一种固定长度的信息,它的起始点和目的地都是数据链路层。元素通常用于异步传输模式(ATM)和交换多兆位数据服务(SMDS)网络等交换环境。

    • 数据单元(Data unit):常用的数据单元有服务数据单元(SDU)、协议数据单元(PDU)。

    各协议层的数据划分为首部和数据

    数据帧(数据链路层):帧数据由两部分组成:帧头部和帧数据,帧头部包括接收方主机物理地址的定位及其它网络信息,帧数据区含有一个数据体。
    IP数据体(网络层)由两个部分组成:数据体头部和数据体的数据区,数据体头部包括IP源地址和IP目标地址及其它信息,数据体的数据区包括用户数据协议、传输控制协议,还有数据包及其它信息。

    img

    套接字

    BSD网络软件中包含两个重要的函数:inet_addr,inet_ntoa。**用来在二进制地址格式和点分十进制字符串格式之间转换,仅支持IPv4。**也有两个函数同时支持IPv4和IPv6:inet_ntop,inet_pton。

    网络顺序(大端)和主机顺序(一般为小端)
    htonl()–“Host to Network Long”
    ntohl()–“Network to Host Long”
    htons()–“Host to Network Short”
    ntohs()–“Network to Host Short”

    struct sockaddr {
    	sa_family_t	sa_family;	/* address family, AF_xxx	AF_INET - TCP/IP协议族 */
    	char		sa_data[14];	/* 14 bytes of protocol address	*/
    };
    
    • 1
    • 2
    • 3
    • 4

    include\uapi\linux\in.h

    struct sockaddr_in {
      __kernel_sa_family_t	sin_family;	/* Address family		*/
      __be16		sin_port;	/* Port number			*/
      struct in_addr	sin_addr;	/* Internet address		*/
    
      /* Pad to size of `struct sockaddr'.  无用的部分*/
      unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) -
    			sizeof(unsigned short int) - sizeof(struct in_addr)];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用例子

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define BUFFLEN 1024
    #define SERVER_PORT 8888
    #define BACKLOG 5
    #define PIDNUMB 3
    static void handle_connect(int s_s)
    {	
    	int s_c;	
    	struct sockaddr_in from;				
    	socklen_t len = sizeof(from);
    	
    	while(1)
    	{
    		s_c = accept(s_s, (struct sockaddr*)&from, &len);
    		time_t now;								
    		char buff[BUFFLEN];						
    		int n = 0;
    		memset(buff, 0, BUFFLEN);			
    		n = recv(s_c, buff, BUFFLEN,0);		
    		if(n > 0 && !strncmp(buff, "TIME", 4))	
    		{
    			memset(buff, 0, BUFFLEN);			
    			now = time(NULL);					
    			sprintf(buff, "%24s\r\n",ctime(&now));	
    			send(s_c, buff, strlen(buff),0);	
    		}		
    		close(s_c);	
    	}
    		
    }
    void sig_int(int num)
    {
    	exit(1);	
    }
    int main(int argc, char *argv[])
    {
    	int s_s;									
    	struct sockaddr_in local;					
    	signal(SIGINT, sig_int);
    	
    	s_s = socket(AF_INET, SOCK_STREAM, 0);
    	
    	memset(&local, 0, sizeof(local));			
    	local.sin_family = AF_INET;					
    	local.sin_addr.s_addr = htonl(INADDR_ANY);	
    	local.sin_port = htons(SERVER_PORT);		
    	
    	bind(s_s, (struct sockaddr*)&local, sizeof(local));
    	listen(s_s, BACKLOG);				
    	
    	pid_t pid[PIDNUMB] = {0};
    	int i =0;
    	for(i=0;i<PIDNUMB;i++)
    	{
    		pid[i] = fork();
    		if(pid[i] == 0)						
    		{
    			handle_connect(s_s);
    		}
    	}
        
    	while(1);	
    	close(s_s);
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    网络实现的分层模型

    内核网络子系统的实现与本章开头介绍的TCP/IP参考模型非常相似。

    相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信。这种做法的好处在于,可以组合使用各种设备、传输机制和协议。例如,通常的以太网卡不仅可用于建立因特网(IP)连接,还可以在其上传输其他类型的协议,如Appletalk或IPX,而无须对网卡的设备驱动程序做任何类型的修改。

    image-20220710232635358

    网络子系统是内核中涉及面最广、要求最高的部分之一。为什么是这样呢?答案是,该子系统处理了大量特定于协议的细节和微妙之处,穿越各层的代码路径中有大量的函数指针,而没有直接的函数调用。这是不可避免的,因为各个层次有多种组合方式,这显然不会使代码路径变得更清楚或更易于跟踪。此外,其中涉及的数据结构通常彼此紧密关联。

    Linux网络核心架构分为三层:用户空间的应用层,内核空间的网络协议栈层,物理硬件层。其中最重要的核心是内核空间的协议栈层。

    在整个栈按照严格分层设计思想可分为五层:系统调用接口层–>协议无关的接口层–>网络协议实现层–>驱动接口层–>驱动程序层。

    1. 系统调用接口层,实质是一个面向用户空间应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。

    2. 协议无关的接口层,就是SOCKET层,这一层的目的是屏蔽底层的不同协议(更准确的来说主要是TCP与UDP,当然还包括RAW IP, SCTP等),以便与系统调用层之间的接口可以简单,统一。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个SOCKET,这个SOCKET其实是一个巨大的sock结构,它和下面一层的网络协议层联系起来,屏蔽了不同的网络协议的不同,只吧数据部分呈献给应用层(通过系统调用接口来呈献)。

    3. 网络协议实现层,毫无疑问,这是整个协议栈的核心。这一层主要实现各种网络协议,最主要的当然是IP,ICMP,ARP,RARP,TCP,UDP等。这一层包含了很多设计的技巧与算法,相当的不错。

    4. 与具体设备无关的驱动接口层,这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如open,close,init等,这一层可以屏蔽底层不同的驱动程序。

    5. 驱动程序层,这一层的目的就很简单了,就是建立与硬件的接口层。

    L2 链路层〔例如,Ethernet )
    L3 网络层(例如,IP)
    L4 传输层(例如,UDP/TCP/ICMP)
    BH 下半部( Bottom Half)
    IRQ 中断(事件)
    RX 接收
    TX 传输

    image-20220711010934914

    图1-2显示了Linux内核网络栈所涉及的3层。其中,L2、L3和L4这三层分别对应于OSI 7层模型中的数据链路层、网络层和传输层。

    从本质上说,Linux内核栈的任务就是将接收到的数据包从L2(网络设备驱动程序)传递给L3(网络层,通常为IPv4或IPv6)。接下来,如果数据包目的地为当前设备,Linux内核网络栈就将其传递给L4(传输层,应用TCP或UDP协议侦听套接字);如果数据包需要转发,就将其交还给L2进行传输。对于本地生成的出站数据包,将从L4依次传递给L3和L2,再由网络设备驱动程序进行传输。这个过程分很多阶段,期间可能会发生如下行为。

    • 根据协议规则(如IPsec规则或NAT规则),可能需要对数据包进行修改。数据包可能被丢弃。
    • 数据包可能导致设备发送错误消息。
    • 可能会对数据包进行分段。
    • 可能需要重组数据包。
    • 需要计算数据包的校验和。

    套接字缓冲区及net_device

    套接字缓冲区

    struct sk_buf

    Linux内核中的数据包 —— sk_buf

    在内核分析(收到)网络分组时,底层协议的数据将传递到更高的层。发送数据时顺序相反,各种协议产生的数据(首部和净荷)依次向更低的层传递,直至最终发送。这些操作的速度对网络子系统的性能有决定性的影响,因此内核使用一种特殊的结构,称为套接字缓冲区(socket buffer),具体源码分析如下:

    struct sk_buff {
    	union {
    		struct {
    			/* These two members must be first. */
    			struct sk_buff		*next;
    			struct sk_buff		*prev;
    
    			union {
    				ktime_t		tstamp; // 报文收到的时间戳
    				struct skb_mstamp skb_mstamp;
    			};
    		};
    		struct rb_node	rbnode; /* used in netem & tcp stack */
    	};
    	struct sock		*sk; // 本网络报文所属的sock结构,此值仅在本机发出的报文中有效,从网络收到的报文此值为空
    
    	union {
            // 每个SKB都有一个dev成员—-一个net_device结构实例。对于到来的数据包,
            // 这个成员表示接收它的网络设备;而对于外出的数据包,这个成员表示发送它的网络设备。
    		struct net_device	*dev;
    		/* Some protocols might use this space to store information,
    		 * while device pointer would be NULL.
    		 * UDP receive path is one user.
    		 */
    		unsigned long		dev_scratch;
    	};
        
    ...
        
        unsigned int		len, // 有效数据长度
    				data_len; // 数据长度
    	__u16			mac_len, // 连接层头部长度,对于以太网,指MAc地址的用的长度,为6(MAc报文的长度)
    				hdr_len; // skb的可写头部长度(用于clone时,表示clone的skb的头长度)
    
    ...
            
        __be16			protocol;
    	__u16			transport_header; // 传输层头部(指向四层帧头部的指针)
    	__u16			network_header; // 网络层头部(指向三层IP结构体的指针)
    	__u16			mac_header; // 链路层头部(指向二层MAC头部的指针)
    	
        /* private: */
    	__u32			headers_end[0];
    	/* public: */
    
    	/* 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; // skb被克隆引用的次数,在内存申请和克隆时会用到
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    套接字缓冲区管理数据

    套接字缓冲区管理数据的基本思想是,通过操作指针来增删协议首部。

    在字长32/64位的系统上,数据类型sk_buff_data_t用来表示各种类型为简单指针的数据,具体结构sk_buff_data_t如下所示:
    typedef unsigned char *sk_buff_data_t;

    • head和end指向数据在内存中的起始和结束位置。

    • data和tail指向协议数据区域的起始和结束位置。

      • data不同层指向的不同

        当数据包位于网络设备驱动程序接收路径的L2时,skb->data指向的是L2(以太网)报头;调用方法eth_type_trans()后,数据包即将进入第3层,因此skb->data应指向网络层(L3)报头,而这个报头紧跟在以太网报头后面。

    • mac_header指向MAC协议首部的起始,而network_header和transport_header分别指向网络层和传输层协议首部的起始。

    image-20220711145354834

    在套接字缓冲区传递到互联网层时,必须增加一个新层。只需要向已经分配但尚未占用的那部分内存空间定稿的数据即可,除了data之外所有的指针都不变,data现在指向IP首部的起始处。对接收分组进行分析过程类似:分组数据复制到内核分配的一个内存区中,并在整个分析期间一直处于该内存区中。与该分组关联的套接字缓冲区在各层之间顺序传递。

    对套接字缓冲区的操作

    • alloc_skb 分配一个新的sk_buff实例
    • skb_copy 创建套接字缓冲区和相关数据的一个副本
    • skb_clone 创建套接字缓冲区的一个副本,但原本和副本将使用同一分组数据
    • skb_tailroom 返回数据末端空闲空间的长度
    • skb_headroom 返回数据起始处空闲空间的长度
    • skb_realloc_headroom 在数据起始处创建更多的空闲空间。现存数据不变

    套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分。由于网络子系统必须保证较低的内存占用和较高的处理速度,因而对struct sk_buff来说,我们需要保持该结构的长度尽可能小。在64位CPU上,可使用一点小技巧来节省一些空间。sk_buff_data_t的定义改为整型变量:(data和head仍然是常规的指针,而所有sk_buff_data_t类型的成员现在都解释为相对于前两者的偏移量。这种做法是有效的,因为4字节偏移量足以描述长达4 GiB的内存区,套接字缓冲区不可能超过这个长度。)

    #ifdef NET_SKBUFF_DATA_USES_OFFSET
    typedef unsigned int sk_buff_data_t;
    #else
    typedef unsigned char *sk_buff_data_t;
    #endif 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    管理套接字缓冲区

    套接字缓冲区结构不仅包含上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员。下面列出的是一些最重要的成员。

    • tstamp保存了分组到达的时间。
    • dev指定了处理分组的网络设备。
    • sk是一个指针,指向用于处理该分组的套接字对应的socket实例。
    • dst表示接下来该分组通过内核网络实现的路由。
    • next和prev用于将套接字缓冲区保存到一个双链表中。

    使用一个表头来实现套接字缓冲区的等待队列。其结构类型定义如下:

    struct sk_buff_head {
    	/* These two members must be first. */
    	struct sk_buff	*next;
    	struct sk_buff	*prev;
    
    	__u32		qlen;
    	spinlock_t	lock;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    qlen指定了等待队列的长度,即队列中成员的数目。sk_buff_head和sk_buff的next和prev用于创建一个循环双链表,套接字缓冲区的list成员指回到表头。

    image-20220711151613216
  • 相关阅读:
    Qt OpenGL 蒙板
    华为设备配置VRRP负载分担
    MySQL数据库的MVCC详解
    Etcd-v3.4.27集群部署
    【JVM笔记】年轻代与老年代
    2022年华中杯数学建模挑战赛B题量化投资问题求解全过程文档及程序
    单元测试与自测
    C#文件目录
    FTP主动模式和被动模式(2)- 防火墙对FTP的影响 ASPF
    MongoRepository查询数据常用语法
  • 原文地址:https://blog.csdn.net/qq_53111905/article/details/126250841