54、Linux 网络流量控制:CBQ 机制深入解析

Linux 网络流量控制:CBQ 机制深入解析

1. 引言

在 Linux 系统中,网络流量控制是保障网络服务质量(QoS)的关键技术。其中,基于类的队列调度(Class - based Queueing,CBQ)机制能够将网络链路的带宽分配给不同的流量类,并根据优先级进行数据包调度。本文将深入探讨 CBQ 机制的相关命令、函数实现以及工作流程。

2. 创建 CBQ 类层次结构的 tc 命令

在 Linux 中,可以使用 tc 命令来创建 CBQ 类层次结构。以下是两个示例命令:

# tc class add dev eth0 parent 1:1 classid 1:2 cbq bandwidth 10 Mbit rate 3 Mbit 
allot 1514 cell 8 weight 100 Kbit prio 3 maxburst 20 avpkt 1000 split 1:0  

# tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 10 Mbit rate 7 Mbit 
allot 1514 cell 8 weight 800 Kbit prio 7 maxburst 20 avpkt 1000 split 1:0  

当执行这些创建类的 tc 命令时, rtnetlink_rcv_msg() 中的 doit 函数指针会指向 tc_ctl_tclass() 函数。

3. tc_ctl_tclass() 函数

tc_ctl_tclass() 函数的主要功能是处理创建类的操作,其具体步骤如下:
- 查找网络接口设备 :在第 852 行调用 dev_get_by_index() 函数,以 tcm→tcm_ifindex 作为参数,该参数在命令提示符中指定。 dev_get_by_index() 根据传入的索引搜索网络接口,并返回指向该设备的指针。
- 确定类的类型和查找 qdisc :基于 tcm→tcm_parent 的值,判断该类是根类(无父类)还是层次结构中的节点类。在第 895 行调用 qdisc_lookup() 函数查找 qdisc,并在第 899 行检查该 qdisc 是否支持类。
- 检查类 ID :在第 904 行根据命令提示符中设置的值检查类 ID。如果类 ID 为零且等于 TC_H_ROOT ,则为父类;否则为子类。
- 查找类 :在第 911 行调用 cbq_get() 函数,该函数通过调用 cbq_class_lookup() 函数来检查是否已存在具有相同类 ID 的类。如果存在,则返回该类;否则返回 NULL
- 更改类 :在第 939 行调用 cbq_change_class (cops→change) 函数。
- 通知用户空间 :最后调用 tclass_notify() 函数,将消息(skb)发送到用户空间。

以下是 tc_ctl_tclass() 函数的流程:

graph TD;
    A[开始] --> B[调用 dev_get_by_index() 查找网络接口设备];
    B --> C[根据 tcm→tcm_parent 确定类类型和查找 qdisc];
    C --> D[检查类 ID];
    D --> E[调用 cbq_get() 查找类];
    E --> F[调用 cbq_change_class() 更改类];
    F --> G[调用 tclass_notify() 通知用户空间];
    G --> H[结束];
4. cbq_change_class() 函数

cbq_change_class() 函数的主要功能包括:
- 分配内存 :为 cbq_class 数据结构分配内存,并在第 1914 和 191 行进行初始化。
- 创建默认 qdisc :在第 1921 行调用函数为该类创建默认的 qdisc。
- 设置类的属性 :在第 1923 行设置类 ID,第 1924 行设置类的父类,第 1925 行设置 qdisc。在第 1926 和 1927 行设置类的分配和量子值,这些值用于 cbq_dequeue() 函数对该类及其兄弟类进行调度。在第 1932 行设置兄弟链接。

5. 过滤器

过滤器的主要功能是将传入的数据包分配到队列规则(qdisc)的不同类中,数据包的分类基于 IP 地址、端口号等信息。常见的过滤器类型包括:
- RSVP
- U32
- Route
- Police
- Estimator
- Firewall - based

本文将重点讨论 U32 和 Route 过滤器。可以使用以下命令设置这两种过滤器:

# tc filter add dev eth0 parent 1:0 protocol ip prio 100 route or  
# tc filter add dev eth0 parent 1:0 protocol ip prio 100 u32  

当执行设置过滤器的 tc 命令时, rtnetlink_rcv_msg() 中的 doit 函数指针会指向 tc_ctl_tfilter() 函数。

6. tc_ctl_tfilter() 函数

tc_ctl_tfilter() 函数的主要功能是添加、删除、更改或获取过滤器。其具体步骤如下:
- 识别设备 :在第 146 行调用 _dev_get_by_index() 函数,使用 tcm_ifindex 值来识别设备。
- 查找 qdisc :调用 qdisc_lookup() 函数,根据父 ID( tcm_parent )查找队列规则。
- 识别过滤器列表 :使用队列规则类操作的 tcf_chain 在第 168 行识别队列规则过滤器列表。
- 检查过滤器 :通过循环遍历列表(第 174 - 183 行)检查过滤器。如果未找到,则创建或分配一个新的过滤器节点。
- 初始化过滤器节点 :在第 199 行创建或分配新的过滤器节点,并在第 201 行调用 tcf_proto_lookup_ops() 函数初始化过滤器节点操作结构 tp_ops 。然后使用过滤器节点操作结构的值从第 220 - 226 行初始化过滤器节点。
- 调用更改函数 :最后,调用过滤器的更改函数,如 u32_change route4_change

以下是 tc_ctl_tfilter() 函数的流程:

graph TD;
    A[开始] --> B[调用 _dev_get_by_index() 识别设备];
    B --> C[调用 qdisc_lookup() 查找 qdisc];
    C --> D[使用 tcf_chain 识别过滤器列表];
    D --> E[遍历列表检查过滤器];
    E --> F{过滤器是否存在};
    F -- 是 --> G[结束];
    F -- 否 --> H[创建或分配新的过滤器节点];
    H --> I[调用 tcf_proto_lookup_ops() 初始化操作结构];
    I --> J[初始化过滤器节点];
    J --> K[调用更改函数];
    K --> G;
7. U32 过滤器实现

U32 过滤器根据目的 IP、目的 TCP/IP 端口、源 IP 地址、源 TCP/IP 端口、TOS 字节和协议对数据包进行分类。设置 U32 过滤器的命令示例如下:

/root/work/iproute/iproute2 - ss050607/tc/tc filter add dev eth1 parent 1:0 protocol ip prio 1 u32 match ip dst 192.168.2.101 match ip sport 23 0xfff flowid 1:2  
/root/work/iproute/iproute2 - ss050607/tc/tc filter add dev eth1 parent 1:0 protocol ip prio 1 u32 match ip dst 192.168.2.102 match ip sport 80 0xfff flowid 1:3  
8. u32_change() 函数

U32 过滤器存储在哈希表中,相关的数据结构包括 struct tc_u_hnode (哈希节点)、 struct tc_u_knode (键节点)和 struct tc_u32_key (键结构)。 u32_change() 函数的主要步骤如下:
- 分配和初始化哈希节点 :如果需要新的哈希节点(第 523 行条件为真),根据除数(第 524 行)在第 535 行分配新的 struct tc_u_hnode 哈希节点,并在第 538 行进行初始化。在第 539 行初始化新哈希节点的 tp_c 指针,指向 tc_u_common tp_c ,并在第 540 行将引用计数设置为 0。根据 tc 用户参数在第 541 - 542 行设置除数和句柄值,最后在第 544 行更新 struct tc_u_common 的哈希列表。
- 分配和初始化哈希键节点 :如果需要新的哈希键节点(第 549 行条件为真),首先获取 tc_u_hnode 的 ID,然后从 TCA_U32_SEL 表项中获取 struct tc_u32_sel 及其关联键的信息(第 578 行)。在第 579 行分配新哈希键节点的内存,内存空间大小取决于 tc_u32_key→nkeys 中指定的键数量,并在第 582 行进行初始化。在第 583 行使用 memcpy 函数将 TCA_U32_SEL 的内容复制到新键节点的键中。在第 584 - 585 行将 tc_u_node (ht) 和句柄分配给新键节点。最后在第 586 行调用 u32_set_params() 函数设置新键节点内的类特定信息。

9. Route 过滤器实现

Route 过滤器根据路由表对数据包进行分类,根据路由表中的信息为特定目的地设置路由过滤器。设置路由过滤器的命令示例如下:

[root@localhost root]# ip route add 192.168.2.101 via 192.168.2.100 realm 2  
[root@localhost root]# ip route add 192.168.2.102 via 192.168.2.100 realm 3  
[root@localhost root]# tc filter add dev eth1 parent 1:0 protocol ip prio 100 route to 3 flowid 1:3  
[root@localhost root]# tc filter add dev eth1 parent 1:0 protocol ip prio 100 route to 2 flowid 1:2  
10. route4_change() 函数

路由过滤器的主要数据结构包括 struct route4_head (路由头)、 struct route4_filter (路由过滤器)和 struct route4_bucket (路由桶)。 route4_change() 函数的主要步骤如下:
- 解析参数 :在第 381 行调用 rtattr_parse() 函数,对 struct rtattr 中的命令参数进行排序,并将特定信息整理成表格形式。
- 分配和初始化内存 :检查 struct route4_head 是否为 NULL 。如果为 NULL ,则在第 414 行分配 struct route4_head 的内存空间,并在第 417 行进行初始化。同时在第 424 行分配 struct route4_filter 的内存空间,并在第 428 行进行初始化。
- 设置过滤器属性 :将 struct rtattr TCA_ROUTE4_TO 表项中的领域 ID 信息分配给 (struct route4_filter) f→id (第 437 行)。检查参数表中的类 ID 条目,如果存在,则将 TCA_ROUTE4_TO 条目将该类 ID 分配给 f→res.classid
- 计算索引和插入表项 :使用 f→handle 值调用 to_hash() 函数计算 route4_bucket 表的索引(第 475 行)。检查该索引处的条目是否为 NULL ,如果为 NULL ,则使用 f→handle 值分配 struct route4_bucket 的内存空间,并在第 478 - 481 行进行初始化。最后在第 484 行将分配的 route4_bucket 条目插入到 head→table[h1] 表中。再次调用 from_hash() 函数计算 route4_bucket 表的索引值(第 490 行),并使用该索引值计算 route4_bucket 表条目的地址,将 route4_filter 分配到该地址(第 506 行)。

11. 入队操作

入队函数的作用是将数据包( sk_buff )放入队列规则的调度队列中。当调用入队函数时,IP 层的 dev_queue_xmit() 函数会在第 1028 行调用队列规则的入队函数。如果默认队列规则未被其他队列规则覆盖,则默认调用 pfifo_fast_enqueue() 函数。这里主要讨论 CBQ 队列规则的 cbq_enqueue() 函数。

12. cbq_enqueue() 函数

cbq_enqueue() 函数的参数包括 struct sk_buff (待排队的数据包)和 struct Qdisc (设备队列规则)。该函数的主要步骤如下:
- 分类数据包 :在第 397 行调用 cbq_classify() 函数,以缓冲区 skb 和指向队列规则(调度器)的指针作为参数。 cbq_classify() 函数的主要目的是应用已设置的过滤器来识别数据包应入队的类。如果过滤器匹配成功, cbq_classify() 函数返回用于入队数据包的类。
- 入队数据包 :在第 404 行检查类是否存在,如果存在,则在第 408 行调用该类所属队列规则的入队函数。如果数据包入队成功,则在第 409 行更新队列长度,在第 410 和 411 行更新数据包统计信息,在第 412 行调用 cbq_mark_toplevel() 函数标记类树的顶级,最后在第 414 行调用 cbq_activate_class() 函数激活该类以进行调度。

13. cbq_classify() 函数

cbq_classify() 函数的主要步骤如下:
- 检查优先级 :在第 253 和 254 行检查 skb→priority (prio) 是否指向某个类,并调用 cbq_class_lookup() 函数。如果指向某个类,则返回该类给调用的入队函数。
- 使用过滤器查找类 :如果根据 skb→priority 未找到类,则在第 265 行检查过滤器列表并调用 tc_classify() 函数,根据过滤器参数(如 IP 地址、TCP/IP 源端口等)查找类。 tc_classify 是一个函数指针,根据过滤器类型指向相应的分类函数(如 U32 过滤器的 u32_classify() 函数,路由过滤器的 route4_classify() 函数等)。

以下是 cbq_enqueue() 函数的流程:

graph TD;
    A[开始] --> B[调用 cbq_classify() 分类数据包];
    B --> C{是否找到类};
    C -- 是 --> D[调用类所属队列规则的入队函数];
    D --> E[更新队列长度];
    E --> F[更新数据包统计信息];
    F --> G[调用 cbq_mark_toplevel() 标记顶级];
    G --> H[调用 cbq_activate_class() 激活类];
    H --> I[结束];
    C -- 否 --> I;
14. CBQ 出队操作

CBQ 机制将网络链路的带宽分配给不同的流量类,并通过同一物理网络链路提供链路共享方法。CBQ 机制中的流量类具有不同的优先级,根据优先级对每个类进行数据包传输调度。

15. CBQ 出队机制的主要组成部分
  • 通用调度器 :CBQ 通用调度器使用修改后的加权轮询(WRR)调度算法。CBQ 维护一个活动类的循环链表,根据优先级使用 WRR 算法调度一个类进行数据包传输。只有当一个类有数据包需要传输时,它才是活动的。每个类在一轮中被分配一定数量的字节(量子)。当一个类传输完分配的字节后,它会移动到循环链表中的下一个活动类。
  • 链路共享调度器 :链路共享算法的主要功能是检查每个类的状态,并根据类的空闲时间分配多余的带宽。
  • 估计器 :估计器用于测量类使用的带宽。它使用类的某些参数(如空闲时间和平均空闲时间)来确定带宽消耗。空闲时间参数是两个数据包之间的间隔时间,平均空闲时间参数值用于确定类是超过限制、低于限制还是达到限制。该值使用指数加权移动平均(EWMA)函数计算。
    • 当一个类使用的带宽超过其分配的带宽时,它处于超过限制状态。
    • 当一个类使用的带宽少于其分配的带宽时,它处于低于限制状态。
    • 当一个类使用的带宽等于其分配的带宽时,它处于达到限制状态。
16. 类基于队列的层次结构

类基于队列的方式以层次结构排列,层次结构的顶部是根队列规则类,它定义了整个类层次结构的总带宽。该带宽会进一步分配给层次结构中的其他类。CBQ 为层次结构中的每个类分配优先级,根据优先级,一个类将有机会将数据包发送到接口。此外,如果父类有多余的带宽,CBQ 类可以配置为从其父类借用带宽。

17. qdisc_run() 函数

在数据包成功入队到 CBQ 层次结构的适当类后, dev_queue_xmit() 函数会调用 qdisc_run() 函数。 qdisc_run() 函数在第 439 - 440 行检查 qdisc_restart(dev) ,直到输出队列中没有更多的数据包,或者直到网络设备不再接受更多的数据包(即 !netif_queue_stopped(dev) )。 qdisc_restart(dev) 函数负责从网络设备的队列中获取下一个数据包,并通过调用 hard_start_xmit() 函数将其发送出去。

18. qdisc_restart() 函数

qdisc_restart() 函数负责使用网络设备的队列规则从网络设备的队列中获取下一个数据包。它在第 83 行调用设备的出队函数(函数指针 q→dequeue(q) ),在这种情况下,该函数指针初始化为 cbq_dequeue() 函数。 cbq_dequeue() 函数从适当的类中获取下一个数据包。如果数据包成功出队, cbq_dequeue() 函数会调用网络设备的 hard_start_xmit() 函数将出队的数据包发送到网络上。如果设备的 hard_xmit() 函数成功传输数据包,则在第 100 行返回 -1 qdisc_run() 函数, qdisc_run() 函数中的循环会继续从类中出队下一个数据包。如果 hard_xmit() 函数失败或出队函数失败,则在这两种情况下,数据包都会被重新入队到队列中,并使用 NET_TX_SOFTIRQ net_if_schedule() 函数的第 137 行触发,以便在 do_softirq() 函数被调用时进行数据包传输。

综上所述,CBQ 机制通过一系列的命令、函数和数据结构,实现了对网络流量的精细控制和调度,能够根据不同的需求和优先级对数据包进行分类、入队和出队操作,从而提高网络服务质量。在实际应用中,可以根据网络环境和业务需求,灵活配置 CBQ 机制的参数和过滤器,以达到最佳的网络性能。

Linux 网络流量控制:CBQ 机制深入解析

19. CBQ 机制的数据结构和流程总结

为了更清晰地理解 CBQ 机制,下面对涉及的数据结构和主要流程进行总结。

数据结构 描述
cbq_class 表示 CBQ 中的一个类,包含类的各种属性,如类 ID、父类、qdisc 等
tc_u_hnode U32 过滤器哈希表的节点
tc_u_knode U32 过滤器哈希表的键节点
tc_u32_key 用于保存 U32 过滤器的相关信息,如 IP 地址、端口等
route4_head 路由过滤器的头结构
route4_filter 路由过滤器的过滤器结构
route4_bucket 路由过滤器的桶结构

CBQ 机制的主要流程包括:
1. 创建类层次结构 :使用 tc 命令创建类,调用 tc_ctl_tclass() 函数处理创建操作,最终调用 cbq_change_class() 函数完成类的创建和初始化。
2. 设置过滤器 :使用 tc 命令设置过滤器,调用 tc_ctl_tfilter() 函数处理,根据过滤器类型调用 u32_change() route4_change() 函数。
3. 数据包入队 :IP 层调用 dev_queue_xmit() 函数,进而调用 cbq_enqueue() 函数,通过 cbq_classify() 函数确定数据包所属的类,将数据包入队到相应类的队列中。
4. 数据包出队 dev_queue_xmit() 调用 qdisc_run() 函数, qdisc_run() 调用 qdisc_restart() 函数, qdisc_restart() 调用 cbq_dequeue() 函数从类中获取数据包并发送。

20. CBQ 机制的性能优化

在实际应用中,为了提高 CBQ 机制的性能,可以考虑以下几个方面的优化:

  • 合理配置类的带宽和优先级 :根据不同业务的需求,合理分配每个类的带宽和优先级。例如,对于实时性要求高的业务(如视频会议),可以分配较高的优先级和足够的带宽,以保证其服务质量。
  • 优化过滤器设置 :减少不必要的过滤器,避免过滤器匹配的复杂性过高。可以根据实际情况,选择合适的过滤器类型,如 U32 过滤器适用于基于 IP 地址和端口的分类,路由过滤器适用于基于路由表的分类。
  • 调整调度算法参数 :对于 CBQ 的通用调度器和链路共享调度器,可以调整相关参数,如量子值、空闲时间阈值等,以优化带宽分配和调度效率。
21. CBQ 机制的应用场景

CBQ 机制在多种网络场景中都有广泛的应用,以下是一些常见的应用场景:

  • 企业网络 :在企业网络中,可以使用 CBQ 机制对不同部门的网络流量进行分类和控制。例如,为研发部门分配较高的带宽,以保证其对网络资源的需求;对市场部门的非关键流量进行限制,以避免占用过多的网络带宽。
  • 数据中心网络 :在数据中心网络中,CBQ 机制可以用于对不同应用的流量进行调度。例如,对于关键业务应用(如数据库服务),可以分配较高的优先级和带宽,以保证其性能和可靠性。
  • 无线网络 :在无线网络中,由于带宽资源有限,CBQ 机制可以用于对不同用户的流量进行管理。例如,对高优先级用户(如 VIP 用户)分配更多的带宽,以提供更好的服务体验。
22. 与其他 QoS 机制的比较

除了 CBQ 机制,Linux 中还有其他一些 QoS 机制,如 HTB(Hierarchical Token Bucket)、PFIFO(Packet First - In - First - Out)等。下面对 CBQ 机制与这些机制进行比较:

机制 优点 缺点 适用场景
CBQ 支持复杂的层次结构和优先级调度,能够根据类的空闲时间分配多余带宽 配置相对复杂,性能开销较大 对流量分类和调度要求较高的场景
HTB 配置相对简单,能够有效地控制带宽 缺乏对类空闲时间的考虑,带宽分配不够灵活 对带宽控制要求较高,对调度灵活性要求较低的场景
PFIFO 实现简单,性能开销小 不支持流量分类和优先级调度 对网络性能要求不高,不需要复杂流量控制的场景
23. 未来发展趋势

随着网络技术的不断发展,CBQ 机制也将面临新的挑战和机遇。未来,CBQ 机制可能会朝着以下几个方向发展:

  • 智能化调度 :结合人工智能和机器学习技术,实现对网络流量的智能分类和调度。例如,通过分析网络流量的特征和趋势,自动调整类的带宽和优先级,以提高网络性能和资源利用率。
  • 与软件定义网络(SDN)的融合 :SDN 技术可以实现网络的集中控制和灵活配置。CBQ 机制可以与 SDN 结合,通过 SDN 控制器对 CBQ 进行远程配置和管理,提高网络的可管理性和灵活性。
  • 支持新的网络协议和应用 :随着新的网络协议(如 IPv6)和应用(如物联网)的不断涌现,CBQ 机制需要不断扩展和优化,以支持这些新的协议和应用的流量控制需求。
24. 总结

CBQ 机制作为 Linux 中一种重要的 QoS 机制,通过一系列的命令、函数和数据结构,实现了对网络流量的精细控制和调度。它能够根据不同的需求和优先级对数据包进行分类、入队和出队操作,从而提高网络服务质量。

在实际应用中,需要根据网络环境和业务需求,合理配置 CBQ 机制的参数和过滤器,以达到最佳的网络性能。同时,为了提高性能和适应未来网络的发展,还可以考虑对 CBQ 机制进行优化和扩展。

通过深入理解 CBQ 机制的原理和实现,网络管理员可以更好地管理和优化网络流量,为用户提供更稳定、高效的网络服务。

希望本文能够帮助读者更好地理解 CBQ 机制,并在实际应用中发挥其作用。如果读者在使用过程中遇到问题或有进一步的需求,可以参考相关的文档和资料,或者在社区中寻求帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值