第1、2章 Linux内核概述和网络包传输的关键数据结构——Socket Buffer

本文详细介绍了Linux内核中Socket Buffer的使用,包括进程与系统调用、硬件中断、互斥机制,特别是Socket Buffer的数据域设计和操作函数。重点讲解了Socket Buffer如何处理网络数据包,从接收时的中断处理到数据包的分片和分段。此外,还探讨了内核的内存资源管理和时间管理。
摘要由CSDN通过智能技术生成

1.进程和系统调用

进程是一个正在执行的程序实例,各进程拥有自己独立的地址空间。进程通常在执行某个应用程序时启动,应用执行完成后结束。创建、控制和结束进程是操作系统内核的一项重要任务。在用户地址空间执行的进程是互斥的,它们只能访问系统分配给它们的存储空间。用户地址空间的进程也不能直接访问内核功能。

当用户进程需要访问设备或使用操作系统内核的功能时,必须通过 系统调用来完成。系统调用将处理器切换到保护模式,随后访问内核的地址空间,在保护模式下,所有的设备和内存资源通过内核实现API访问。

2.硬件中断

外部设备用硬件中断来通知操作系统有重要的事情发生,中断发生后,CPU会暂时停止当前程序的执行,转去执行中断处理程序,中断处理程序结束后再恢复原来被停止程序的执行。

硬件中断是一种系统资源,当我们为设备编写中断处理程序来处理外部事件时,要向系统申请中断资源(即中断信号线,通常称为中断号),并将中断处理程序与中断源相关联。内核提供一对函数来申请和释放中断资源:

/*
* irq 中断号
* handler 中断处理程序
* flags 中断类型
* devname 设备名
* dev_id 设备索引号
*/
/*申请中断资源,将申请到的中断号irq与设备名为devname,设备索引号为dev_id的中断处理程序handler相连,该设备的中断类型为flags。*/
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *devname,void *dev_id)
/*释放中断资源*/
void free_irq(unsigned int irq,void *dev_id)
  1. 快速中断 fast interrup
    中断处理程序运行时间短,屏蔽其他中断,中断处理程序不会被其他中断打断,要将中断申请注册为快速中断,需要将中断标志flags设为SA_INTERRUPT

  2. 慢速中断 slow interrupt
    中断处理程序运行时间长,执行期间可以被别的中断打断

中断处理程序的执行通常可以暂停所有别的活动,不同的中断在几个CPU上可以同时运行,但某个中断的处理程序一次只能在一个CPU上执行。

网络数据包的接收处理(从网络适配器收到数据包到将数据包传送到用户接收进程)由中断触发,需要几千个CPU时钟才能完成,为了使中断处理程序运行时间尽可能短,这种费时的事件分为两部分完成:

  1. top half
    只完成中断触发后最重要的任务处理(中断处理程序):将收到的数据包复制到内核缓冲区后就立即结束返回,释放中断和CPU。对数据包的协议分析和处理不在中断处理程序中进行。
  2. bottom half
    完成所有非紧急部分的处理。由top half调度,放在以后某个安全的时间运行,如数据网络包复制到内核后的分析处理。在bottom执行期间,打开所有硬件中断。

top half将在设备缓冲区的数据复制到内核地址空间缓冲区,调度bottom half后退出,这个过程非常快。bottom half执行余下的处理任务,这样在bottom half工作过程中,CPU就可以响应新的外部中断请求。Linux有两种不同的机制来实现bottom half过程——tasklet和worqueue

tasklet是可以被调度执行的特殊函数,在系统某个特定的安全时间运行在软件中断的执行现场。实现tasklet的过程为:

  • 编写tasklet处理函数,如my_func
  • 用宏DECLARE_TASKLET(name,func,data)声明一个新的tasklet,name为该tasklet的名称,func为该tasklet要执行的函数,data是传送给tasklet处理函数的数据。
  • 用tasklet_schedule调度tasklet执行
void my_func(unsigned long); /*tasklet处理函数*/
char tasklet_data[] = "This is a new tasklet"; /*tasklet 处理程序需要的数据*/
DECLARE_TASKLET(my_tasklet, my_func, (unsigned long)&tasklet_data);/*声明tasklet*/
tasklet_schedule(&my_tasklet); /*调度新定义的tasklet*/

workqueue与tasklet类似,是可以由内核代码在将来某个时间调度执行的特殊函数。
workqueue与tasklet的不同在于:

  • tasklet在软中断执行现场运行,所有的tasklet代码必须是原子操作。workqueue在内核进程现场执行,更灵活,可以休眠
  • tasklet总是在最初调度它的处理器上执行,默认情况下workqueue与tasklet一样
  • 内核可以将workqueue函数推迟到一定时间以后才调用执行

3.软中断

软件中断是在硬件中断执行完成后由内核的调度器(scheduler)调度执行的活动,软件中断和硬件中断的主要区别在于:硬件中断可以随时立刻打断CPU现行活动(如中断允许);软件中断是由内核调度器调度执行的活动。软件中断必须要等到调度器调用它才能执行,软件中断的调度由内核函数do_softirq完成。

asmlinkage void do_softirq(void)

执行时间:

  • 系统调度结束后(在schedule中)被调度执行
  • 硬件中断结束后(在do_IRQ中)被调度执行

NET_RX_SOFTIRQ:处理网络接收到的数据包
NET_TX_SOFTIRQ: 处理要发送的网络数据包
特性:

  • 可以同时在多个处理器上运行,可重入
  • 不能被同类的软中断打断
  • 只能被硬件中断打断

4.互斥机制

当几个活动需要同时操作同一个数据结构时(这段代码通常称为critical section),这种操作必须是原子操作,即整个操作过程不能被打断,操作的几个步骤以不可分的方式执行。

原子操作

原子操作就是指某一个操作在执行过程中不可以被打断,要么全部执行,要不就一点也不执行。原子操作需要硬件的支持,与体系结构相关,使用汇编语言实现。原子操作主要用于实现资源计数,很多引用计数就是通过原子操作实现。Linux中提供了两种原子操作接口,分别是原子整数操作和原子位操作。

原子整数操作只对atomic_t类型的数据进行操作,不能对C语言的int进行操作,使用atomic_t只能将其作为24位数据处理,主要是在SPARC体系结构中int的低8为中设置了一个锁,避免对原子类型数据的并发访问。

原子位操作是针对由指针变量指定的任意一块内存区域的位序列的某一位进行操作。它只是针对普通指针的操作,不需要定义一个与该操作相对应的数据类型。
  
原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构structipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1,当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放Ip碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。

自旋锁spin lock

特点:

  • 只能由一个线程持有,如果一个线程已持有了锁,另一个执行线程想要获取锁时,就只能循环等待知道前一个线程将锁释放,也即在此期间处理器不做别的处理,一直在循环测试锁的状态。
  • spin lock 只适用于多处理器环境,而且通常用于预计锁可以在很短时间内就能获取的情况下。
  • 持有锁的线程不能休眠,否则会造成别的线程因不能获取锁而发生系统死锁的情况。
  • 适用于一段critical section代码部分非常短,执行快的情况

获取锁的地方标志着critical section部分的开始,释放锁处标志着critical section部分的结束,这时别的活动又可以来获取锁,操作共享的数据结构。

#include <linux/spinlock.h>
spinlock_t my_spinlock=SPIN_LOCK_UNLOCKED; /*初始化锁变量*/
spin_lock_init(&my_spinlock); /*将锁初始化为未锁定状态*/
/*获取锁*/
/* 当锁被别的进程持有,必须等待并不断测试锁的状态直到释放。锁释放后可立即获取。*/
spin_lock(spinlock_t *my_spinlock)
/* 自动屏蔽中断,将CPU的当前状态寄存器值保存到变量flags中*/
spin_lock_irqsave(spinlock_t *my_spinlock, unsigned log flags)
/*不保存CPU状态寄存器的值*/
spin_lock_irq(spinlock_t *my_spinlock)
/*阻止bottom half的运行*/
spin_lock_bh(spinlock_t *my_spinlock)
/*释放锁*/
spin_unlock(spinlock_t *my_spinlock)
/*释放锁,设置中断允许*/
spin_unlock_irqrestore(spinlock_t *my_spinlock)
/*释放锁,允许中断*/
spin_unlock_irq(spinlock_t *my_spinlock)
/*释放锁,并允许立即处理bottom half*/
spin_unlock_bh(spinlock_t *my_spinlock)

读/写自旋锁 spin lock

如果可以把操作明显地划分为只读、读写操作时,使用spin-lock更有效。

  • 允许多个读操作同时持有锁
  • 一次只有一个写操作可以获取锁
  • 当写操作持有锁时,读操作不能持有锁
  • 读操作优先级比写操作优先级高
read_lock ...()
read_unlock ...()
write_lock ...()
write_unlock ...()

在网络子系统中,所有表示网络设备的数据结构net_device的实例都存放在以dev_base为头指针的链表中。在系统初始化时,当探测到一个有效的网络适配器时,就将该网络设备的net_device实例插入到链表中,这时为写操作。在系统运行过程中,我们很少改变net_device结构变量的值,大部分操作是在dev_base链表中查找设备,获取设备属性,这属于读操作。
对这种数据结构的访问,一定要用锁定机制来保护,但对dev_base链表的读操作在没有写操作的情况下,不需要串行等待执行,就最适合读-写 spin lock锁定机制。它允许多个读操作的活动同时运行其critical section部分的代码,但一旦锁被写操作获取后,所有的活动只能等到写操作释放后才能访问共享数据。

BKL(Big Kernel Lock)

BKL即全局内核锁,也称大内核锁,它是一个全局自旋锁。大内核锁也是用来保护临界区资源的,避免出现多个处理器上的进程同时访问同一区域,整个内核中只有一个大内核锁。

BKL是一个名为kernel_flag的自旋锁,持有该锁的进程仍可以睡眠,当睡眠时持有的锁将被自动释放,该进程被唤醒时重新持有该锁。Linux允许一个进程可以递归的持有BKL,BKL是一个递归锁。

它的设计思想是,一旦某个内核路径获取了这把锁,那么其他所有的内核路径都不能再获取到这把锁。自旋锁加锁的对象一般是一个全局变量,大内核锁加锁的对象是一段代码,里面可能包含多个全局变量。那么他带来的问题是,虽然A只需要互斥访问全局变量a,但附带锁了全局变量b,从而导致B不能访问b了

mutex(互斥锁)

互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子API之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需求,那么它们将是您明智的选择。

读-复制-更新(Read-Copy-Update,RCU)

Linux中提供的最新互斥机制,在以下特定条件下执行效率非常高:

  • 相对于只读锁的要求,要求读-写锁的次数非常少
  • 持有锁的代码是以原子方式执行,绝不会休眠
  • 被锁保护的数据结构是通过指针访问的

5.内核模块

  • init_模块:向内核注册由模块提供的所有功能
  • cleanup_模块:撤销任何由init模块所做的功能
  1. insmod 加载模块,如果成功,模块的目标代码就被链接到内核,这样模块就可以访问内核的符号(函数和数据结构),发送此命令会引起以下的系统调用执行:
    sys_create_模块:给模块在内核地址空间分配其驻留所需的内存
    sys_get_kernel_sysms:返回内核的符号表,解决模块中尚未连接的对内核符号的引用
    sys_init模块:复制模块的目标代码到内核空间,并调用模块的初始化函数(init_模块)执行模块的初始化功能。

  2. 给模块传递参数时,参数需要在模块中用宏MODULE_PARM(arg,type,default)说明可以传递哪些参数。

  3. rmmod 卸载模块 系统调用sys_delet_模块执行模块的清除函数 cleanup_模块

自动装载模块:事先用内核函数request_模块申请需要的组件模块

内核和模块的符号表:Linux 内核中包含了一个符号表ksym,表中包含了所有符号(函数名、变量名)与地址的对应关系。
模块只能访问内核符号表中列出的函数和变量,EXPORT_SYMBOL(XXX)向符号表中加入内核的函数或变量。

6.内存资源

内核使用函数kmalloc和kfree来分配和释放内存块,当分配内存空间和释放内存的操作发生的非常频繁时,内核组件的初始化函数通常为自己初始化一个特殊的高速缓冲区来为其数据结构分配内存空间,在数据对象使用结束释放内存时,释放的内存返回给它分配的高速缓冲区。

  • 套接字缓冲区描述符(socket buffer descriptor) 用于为sk_buff缓冲区描述符分配内存
  • 邻居协议映射高速缓冲区 存放网络层至数据链路层(L3到L2)的地址映射关系
  • 路由表
/*创建和释放高速缓冲区*/
kem_cache_create
kmem_cache_destroy
/*从高速缓冲区中分配内存给数据结构对象*/
kmem_cache_alloc
/*释放内存对象,将缓冲区返回给高速缓存*/
kmem_cache_free

7.时间管理

在内核地址空间,时间以tick来衡量,一个tick是两次时钟中断之间的间隔值

  • 时钟中断产生间隔以每秒钟多少Hz数来衡量,不同CPU体系初始值不一样
  • 每次时钟中断产生时,时钟中断处理程序对全局变量jiffies递增1

8.Socket Buffer

Socket Buffer代表了一个网络数据包在内核中处理的整个生命周期,构成如下:
在这里插入图片描述
由两部分组成:

  • 数据包:存放实际要在网络中传送的数据缓冲区
  • 管理数据结构(struct sk_buff):管理数据包和操作数据包

本书中,Socket Buffer描述数据包和管理数据结构的整体,sk_buff 描述管理数据结构

发送数据包

  1. 数据从用户地址空间复制到内核地址空间,
  2. 在内核的套接字层创建相应的Socket Buffer实例将负载数据存放在数据包缓冲区中,sk_buff存放数据包的地址、长度、状态等信息
  3. 各层协议头信息插入到数据包中,sk_buff结构中描述协议头信息的地址指针被赋值

所以,在创建Socket Buffer时需要在数据包缓冲区前留足够的空间来存放各层协议的头信息。

接收网络数据包

  1. 网络适配器接收网络数据,产生中断通知内核收到了网络数据帧。
  2. 网络适配器的中断处理程序中调用dev_alloc_skb向系统申请一个Socket Buffer,将网络帧数据从设备硬件的缓冲区复制到Socket Buffer的数据缓冲区,填写sk_buff结构中的地址线、接收时间和协议等信息。
  3. Socket Buffer到达内核地址空间
  4. 各层协议头信息剥离, sk_buff描述头信息地址指针被复位,调整sk_buff->data指针

在这里插入图片描述
要传输的数据只需要复制两次:

  • 从应用程序的用户空间复制到内核地址空间
  • 从内核地址空间复制到网络适配器的硬件缓冲区

8.1 sk_buff数据域的设计和含义

Socket Buffer的数据包在穿越内核地址空间的TCP/IP协议栈过程中,数据内容通常不会修改,只会有数据包缓冲区中的协议头信息发生变化,大量操作是在sk_buff中进行的

sk_buff的设计:

  • 按系统的cache line的大小对齐,这样在系统总线上已burst方式传送,可以更快的读取sk_buff的数据
  • 增加新的数据域对新功能的支持

sk_buff数据域

  • 结构管理域
  • 常规数据域
  • 网络功能配置域

8.1.1 结构管理域

  1. *next : 指向链表的下一个成员
    *prev : 指向链表中的前一个成员
    双向链表的起始地址用如下数据结构表示:
struct sk_buff_head{
	struct sk_buff *next;
	struct sk_buff *prev;
	__u32 			qlen;//链表中sk_buff结构实例成员个数
	spinlock_t 		lock;//锁,防止并发访问
}

在这里插入图片描述
优点:某个Socket Buffer的状态变化了,需要将Socket Buffer在各队列之间移动时,无须复制整个缓冲区,只需要修改next和prev指针,就可将缓冲区从一个队列放入另一个队列中管理。

  1. struct sock *sk : 指向拥有该Socket Buffer 的套接字(socket)数据结构的指针。
    套接字就是端口号加IP地址。
    若数据包经本机继续向前传送时,值设置为NULL

  2. unsigned int len : 数据包的长度
    数据总长度 = 主缓存区中的数据长度 + 各分片数据长度

当数据大于网络适配器硬件一次能传送数据的最大传送单元MTU时,数据包会被分成更小的片段。

  1. unsigned int date_len:精确计算被分了片的数据块长度

  2. __u16 mac_len:数据链路层协议头的长度

  3. _u16 hdr_len :克隆的数据包的长度,克隆时可只做纯数据克隆。

  4. atomic_t users:对sk_buff结构的引用计数
    dataref 记录数据包缓冲区的使用计数

  5. unsigned int truesize
    记录整个Socket Buffer的大小,初始化为len + sizeof(sk_buff)

  6. sk_buff_data_t tail;
    sk_buff_data_t end;
    unsigned char *head,*data;
    head和end之前指向整个数据包缓冲区的起始和结束地址,data和tail指向实际数据的起始和结束地址
    在这里插入图片描述

  7. void (*destructor)(…)
    指向Socket Buffer的析构函数,释放Socket Buffer时,完成具体的清除工作。

8.1.2 常规数据域

  1. ktime _ttstamp 接收数据包到达内核的时间
    网络驱动程序收到网络数据时 ,调用数据包处理函数netif_rx->net_timestamp(skb)对该数据域赋值

  2. struct net_device *dev : 接收或发送该数据包的网络设备
    int iif : 接收/发送数据包的网络设备索引号(eth0,eth1)

  3. union {
    struct dst_entry *dst;
    struct rtable *rtable;
    }
    路由子系统使用,当数据包目标地址不是本机时,该数据结构指明应该如何将数据包向前发送

  4. char cb[48] : 控制缓冲区(Control Buffer)
    各层协议在处理数据包时存放私有信息或变量的地方。
    例:UDP协议用控制缓冲区存放它的udp_skb_cb数据结构

struct udp_skb_cb{
	union{
		struct inet_skb_parm h4;
#ifdefined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	struct inet6_skb_parm h6;
#endif
	}header;
	_u16 cscov;
	_u8 partial_cov;
}
  1. csum 存放数据包的校验和,csum_start : 以skb->head为起始地址的偏移量,指出校验和从数据的什么位置开始计算,csum_offset : 以csum_start为起始地址的偏移量,指明校验和存放的地址
    _u8 ip_summed :网络设备是否可以用硬件来对IP数据进行校验或解码

  2. _u32 priority 质量服务(Qos)功能特性,描述了数据包传送的优先级。数据包本地产生:由套接字层填priority的值。前送数据包: 由路由子系统函数根据数据包中的IP协议头信息Tos(Type of Service 服务类型)填写

  3. _be16 protocol 接收数据包的网络层协议,标志了网络数据包应传给TCP/IP协议栈网络层的哪个协议处理函数

  4. _u16 queue_mapping 发送网络数据包所在队列与设备硬件发送队列的映射关系

  5. _u32 mark 常规数据包标志

  6. _u 16 vlan_tci 虚拟局域网的标记控制信息Vlan Tag Control Information

  7. struct sec_path *sp 安全路径Security Path,用于IPsec协议跟踪网络数据包的传送路径。

  8. sk_buff_data_t transport_header 传输层协议头在网络数据包中的地址
    sk_buff_data_t network_header 网络层协议头在网络数据包中的地址
    sk_buff_data_t mac_header 数据链路层协议头在网络数据包中的地址

8.1.3 网络功能域

Linux内核的网络子功能的实现是模块化的

  1. 连接跟踪: 记录有什么数据包经过主机以及它们是如何进入网络连接的
  2. 桥防火墙
  3. 流量控制: 决定哪个数据包先送,哪个后送,哪个数据包丢掉

8.2 操作sk_buff的函数

Linux源码中有很多同名函数有两个版本,一个是不带下划线(do_something),一个带双下滑线(__do_something),带双下划线的是实际功能,不带下划线的是带双下划线的包装函数。包装函数在裸函数基础上增加了安全监测、实现凡是并发访问的锁定制等功能。

8.2.1 创建和释放Socket Buffer

创建Socket Buffer比常规内存分配复杂,在高速网络的环境下,每秒有几千个网络数据包接收/发送,需要频繁的创建/释放Socket Buffer。为此系统初始化时由skb_init创建两个sk_buff的内存对象池

  • skbuff_head_cache
  • skbuff_fclone_cache

每当需要为sk_buff数据结构分配内存时,根据所需的sk_buff是克隆的还是非克隆的分别从上面两个cache中获取内存对象。

1.创建Socket Buffer
创建Socket Buffer函数原型所在文件
__alloc_skb(unsigned int size, gfp_t gfp_mask, int fclone, int node)skbuff.c
alloc_skb(unsigned int size, gfp_t priority)skbuff.h
alloc_skb_fclone(unsigned int size, gfp_t priority)skbuff.h
dev_alloc_skb(unsigned int length)skbuff.c
__dev_alloc_skb(unsigned int length, gfp_t gfp_mask)skbuff.h
__netdev_alloc_dev(struct net_device *dev, unsigned int length, gfp_t gfp_mask)skbuff.c
netdev_alloc_dev(struct net_device *dev, unsigned int length)skbuff.h
/*
 *size : 存放数据包需要的内存空间的大小
 *gfp_mask : 可否被别的进程中断
      GFP_ATOMIC ——在中断处理程序中申请内存
      GFP_KERNEL ——常规内核函数申请内存分配
 *fclone : 是否对该sk_buff克隆。
 *  0 : 从skbuff_head_cache 中获取内存空间
 *  1 : 从skbuff_fclone_cache中获取内存空间
 */
__alloc_skb(unsigned int size, gftp_t gfp_mask, int fclone, int node )

在这里插入图片描述
创建好的Socket Buffer为:
在这里插入图片描述

/*为非克隆Socket Buffer分配内存*/
static inline struct sk_buff *alloc_skb(unsigned int size ,gfp_t priority)
{
	return _alloc_skb(size,priority,0,-1);
}
/*为克隆Socket Buffer分配内存*/
static inline struct sk_buff *alloc_skb_fclone(unsigned int size ,gfp_t priority)
{
	return _alloc_skb(size,priority,1,-1);
}

当网络设备收到数据包时,调用dev_alloc_skb向系统申请缓冲区来存放数据包,NET_SKB_PAD为数据包缓冲区预留的16个字节(以太网协议头长度为14个字节,预留16个字节是为了保证数据在2^n的边界对其),GFP_ATOMIC表明不能休眠

static inline sk_buff *_dev_alloc_skb(unsigned int length,gtp_t gfp_mask)
{
	struct sk_buff *skb=alloc_skb(length+NET_SKB_PAD,gfp_mask);
	if(likely(skb))
	{
		skb_reserve(skb,NET_SKB_PAD);
	}
	return skb;
}
static sk_buff *dev_alloc_skb(unsigned int length)
{
	return _dev_alloc_skb(length,GFP_ATOMIC);
}

接收数据包的网络设备分配

struct sk_buff *_netdev_alloc_skb(struct net_device *dev, unsigned int length, gtp_t gfp_mask)
{
	...
	struct sk_buff *skb;
	skb= _alloc_skb(length+NET_SKB_PAD, gfp_mask,0,node);
	if(likely(skb))
	{
		skb_reserve(skb,NET_SKB_PAD);
		skb->dev = dev;
	}
	return skb;
}
static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev,unsigned int length)
{
	return _netdev_alloc_skb(dev,length,GFP_ATOMIC);
}
2. 释放Socket Buffer
函数名函数功能
kfree_skb释放Socket Buffer的内存,skb->user为1时释放内存,否则只做对skb->user减1操作
kfree_release_all释放与sk_buff相连的其他数据结构的引用计数
kfree_release_data释放数据包的主缓冲区和数据片缓冲区
kfree_skbmem将sk_buff数据结构返回内存对象池
dst_release释放对路由表的引用

8.2.3 数据空间的预留和对齐

函数名函数功能
skb_reserve(struct sk_buff *skb, int len)用于在数据包存储区插入网络协议头或强制数据边界对齐,skb->data+=len;skb->tail+=len;
skb_put(struct sk_buff *skb, unsigned int len)在skb的尾部预留指定长度的空间 ,skb->tail+=len;
skb_push(struct sk_buff *skb , unsigned int len)在skb的头部预留指定长度的空间,skb->data+=len;
skb_pull(struct sk_buff *skb , unsigned int len)在skb的头部移走指定长度的数据skb->data - =len;
skb_trin(struct sk_buff *skb , unsigned int len)在skb的尾部移走指定长度的数据skb->tail - =len;

8.2.4 复制和克隆

1.Socket Buffer的克隆

时机:同一个Socket Buffer由不同的进程独立处理,但这些进程所需要操作的只是sk_buff数据结构描述符,不需要对数据包本身做改动。这时为了提高性能,内核不需要对整个Socket Buffer做完全复制,只对sk_buff数据结构做完全的复制,并将数据包的引用计数(dataref)加1。

struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gtp_mask)

skb_clone产生一个对skb的克隆数据结构,返回指向克隆出来的sk_buff数据结构的指针。克隆的sk_buff特点:

  • 不放入任何sk_buff的管理队列
  • 不属于任何套接字,即没有任何套接字拥有克隆的sk_buff
  • 两个sk_buff结构的skb->cloned域置为1,克隆的sk_buff的sk->users域置为1
  • 当一个sk_buff被克隆后,数据包中的值就不能再修改了
    在这里插入图片描述
2.Socket Buffer的复制

时机:多个进程对同一个Socker Buffer的操作为既要修改sk_buff数据结构中的内容,也要修改数据包内容时

  • 既要修改主数据包中的内容,又要操作分片数据段中的值——sk_buff *skb_copy(struct sk_buff *skb)
  • 只修改主数据包中的内容,不需要读/写分片数据段中的值 —— struct sk_buff *pskb_copy(struct sk_buff *skb)

在这里插入图片描述
在这里插入图片描述

8.2.5 操作队列的函数

函数执行必须是原子操作,在操作队列前它们必须首先获取sk_buff_dead结构中的spinlock。

函数名函数功能
skb_queue_head_init初始化sk_buff的等待队列,这时队列为一个空队列
skb_dequeue从list队列的头读取一个Socket Buffer,返回值为读取的Socket Buffer的地址
skb_dequeue_tail从list队列的结尾读取一个Socket Buffer
skb_queue_purge清空list队列,依次从list队列中读取Socket Buffer并释放缓冲区,直到list队列为空
skb_queue_head将一个新的Socket Buffer 放入list队列的头
skb_queue_tail将一个Socket Buffer放入队列尾
skb_unlink从指定的队列中去掉一个Socket Buffer
skb_append将newsk放入队列指定的某个成员后

8.3 数据分片和分段

  • struct skb_shared_info用于支持IP数据分片和TCP数据分段的数据结构。
  • 当一个数据包长度大于网络适配器能传送的最大传送单元MTU时,将数据包分割成更小的数据片,存放在由struct skb_shared_info数据结构管理的Socket Buffer组成的单向链表中。
   struct skb_shared_info
   {
   atomic_t dataref;//对主数据包缓冲区的引用计数
   unsigned short nr_frags;//被分割的数据片计数
   unsigned short gso_size;//TCP数据包被分段的数量
   unsigned short gso_sges;
   unsigned short gso_type;
   _be32 ip6_frag_id;
   struct sk_buff *frag_list;//指向分片数据链表起始地址
   skb_frag_t frags[MAX_SKB_FRAGS];//页表入口数组
	} 

使用struct skb_shared_info数据结构目的:

  • 支持IP数据分片
  • 支持TCP数据分段
  • 跟踪数据包的引用计数

操作skb_shared_info的函数有如下几种:

  • skb_is_nonlinear: 查看Socket Buffer的数据包是否被分片
  • skb_linearize : 将分了片的数据包组装成一个完整的大数据包
  • skb_shinfo : 访问struct skb_shared_info数据结构
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值