在分析net_cls
和net_prio
之前先要解释几个东西,一个是网络的 QoS 以及 netfilter。
网络 QoS
IP 服务模型是尽力而为的,这样的模型不能体现某些流量的重要性,所以诞生了QOS
技术,Linux 很早就提供了流量控制接口,命令行工具是tc
。
协议栈的 QoS 主要由三部分组成。
qdisc 队列规则(queueing discipline),class 控制策略,filter 根据 filter 划入具体的控制策略(class)。
一般由流程是这样的,当一个 qdisc 被入队的时候,会循环匹配其中的 filter 如果匹配到了的话,就会将 packet 入队到对应的 class 当中,大部分情况下被 class 的“所有者”代表的 qdisc 入队。一些没有匹配的 packet 会进入默认的 class。下面将会介绍几种常用的 queue discpline。
pfifo
默认 qdisc 就是 pfifo,实现在 net/sched/sch_prio.c
。
设有三个优先级的队列,优先级由高到低分别是
- “interactive”
- “best effort”
- “bulk”
先消费 0 队列再消费 1 队列,依次类推,一般的packet
都是属于 1。
用 IP 的 ToS 可以映射到这些队列,对应的关系如图。
他的实现依赖下面这个结构,bands 一般是 3 个,代表三个不同优先级的队列,然后filter_list
是他的过滤器列表,最后prio2band
就是上图的ToS To Queue
的映射中的Linux priority 到 Band
的那部分,queues
保存的是三个fifo
的qdisc
,也就是三个最简单的队列。
1 | struct prio_sched_data { |
这里虽然和 IP 的 ToS 暂时没有关系,但是最后cgroup
的部分会提到怎么联系起来的。
pfifo enqueue 的时候会调用prio_classify
根据skb->priority
来选择队列进行入队,dequeue 的时候则 round robin 每次依次从优先级高到低取出一个 packet。
HTB (Hierarchical Token Bucket)
HTB 是一个层级令牌桶的 qdisc,而且可以加入 class,HTB 的整体结构如下,HTB 的作者在这里 解释了他的设计。
这里简单解释一下,每个 class 有一个 AR(保障速率)CR(最大速率),P(优先级),level(在树中的层次),quantum(量子,一个动态参数),和实际的速率 R,中间层的 class 通过计算子层的速率获得。
Leaf 没有子节点,并且只有 Leaf 有传输功能的 queue,其他只是帮助构造层级关系。
Mode class 的状态
Red ( R > CR ) 也就是超速
Yellow (R <= CR && R > AR) 也就是合理超速
Green 就是没有超过保障速率
下面我们有几个等式
1 | Rc = min(CRc, ARc + Bc) [eq1] |
Bc 表示从祖先那里借到的速率,下面这段公式表示的意思是,如果我是当前 prio 最小的,从优先级的兄弟节点中加权平均借到父节点的速率,其他的 prio 更大的节点都不能借到速率。
1 | Qc Rp |
如果没有父节点的话,Bc 就是 0。这样算出来代表的意义是什么呢,就是说先服务优先级更高的节点,并且按照量子的大小在同优先级的节点中分配速率。
然后我们具体看一下 HTB 调度器是如何工作的。
每个 class 有个 slot 包含不同的 prio 级别,指向 yellow 的子节点,然后每个层级包含一个 slot 也有不同的 prio 级别指向这一层中 green 的节点,上图展示了的是有两个 prio 的 slot,右边的 self slot 有个白色的是用于 yellow red 的 wait list。
假设当前如图 1 的状态,所有节点都是绿的没有 packet 到来,现在有有两个包到达 C 和 D,然后激活它们,并且它们现在都是绿的,所以 self slot 指向他们,然后因为 D 的优先级更高,所以先把 D 出队。所以你可以发现,出队的顺序很简答,就是按照优先级从 self slot 里面把绿色的包按顺序取出来就可以。
然后我们看一下更复杂的情况,在图 3 中,我们从 D 中出队一个 packet(htb_dequeue
),然后htb_charge_class
会增加 D 的速率,导致 D 变成 yellow,离开 self slot (通过htb_deactivate_prios
和htb_remove_class_from_row
),然后添加到 B 的 slot 里面 (htb_activate_prios
),并且递归向上添加htb_add_class_to_row
,D 会在一段时间后进入 self slot 的白色等待区,然后 D 又会变回绿色。现在如果选择的话,就会从 C 出队,因为虽然 C 的优先级低但是 C 不需要借别人的速率。
在图 4,假设 C 已经完全消耗了速率达到了最大限速,这个时候 D 就会开始工作然后把 B 消耗完,B 被消耗了以后就会去消耗 A,从这里就可以看到一个借取的过程。
现在说一个更复杂一点的例子,在图 5 中,A 已经被消耗光了,E 开始工作,然后 C 也能开始工作,变成图 6 的样子。注意即使 D 没有被使用但是他的优先级还会被 class slot 维持,也就是红线。但是 C 和 E 都是同一个优先级,这样的话,就要使用 DRR 算法(也就是在 RR 算法上给每个变量加一个权重,也就是之前的那个量子 quantom)。然后也可以发现一个 class 可以对不同的优先级(红色和蓝色)保持 active。
下面是一个 HTB 的全貌图,抽象的可以理解成一个从父 class 中根据优先级带权重的分享令牌的一个算法。
下面三个值对应的就是三种颜色。
1 | /* used internaly to keep status of single class */ |
HTB 的实现依赖于
1 | struct htb_sched { |
其中hlevel
对应的就是self slot
,而clhash
可以通过 classid 找到 hub_class
,其中的->un.inner
就是对应的class slot
,->un.leaf
对应的就是叶子结点。
使用
qdisc 参数
parent major:minor 或者 root。
一个 qdisc 是根节点就是 root,否则其他的情况指定 parent。其中 major:minor 是 class 的 handle id,每个 class 都要指定一个 id 用于标识。
handle major: ,这个语法有点奇怪,是可选的,如果 qdisc 下面还要分类(多个 class),则需要指定这个 hanlde。对于 root,通常是”1:”。
1 | // handle 是一组用户指定的标示符,格式为 major:minor。 |
实现
用 rtnetlink 接口实现 API。
在include/net/sch_generic.h
下有Qdisc_ops
,Qdisc_class_ops
,tcf_proto_ops
的定义。
同一个文件下还有Qdisc_class_ops
的定义。
qdisc_run
首先检查设备的运行状态然后调用__qdisc_run
,这个函数的主体就是调用 qdisc_restart
, 直到超过限制或者需要让出时间 CPU 了,最后清空 qdisc
的 RUNNING
状态。
在此结构中,enqueue 和 dequeue 两个函数是整个 QoS 调度的入口函数。其中的Qdisc_class_ops
用于对此 Qdisc
的 filter list 进行操作,添加删除等,通过对 Qdisc 添加 fliter,filter 对 enqueue 到此 Qdisc 的 pkt 进行分类,从而归类到此 Qdisc 的子 class 中,而每个子 class 都有自己的 Qdisc 进行 pkt enqueue 的管理,因此实现一个树形的 filter 结构。
callgraph Qdisc 在从来自上层的dev_queue_xmit
主动发送开始起作用,对出口数据包作限制。
1 | dev_queue_xmit -> |
在enqueue
的时候会主动唤起dequeue
也可能是硬件发送就绪会唤醒发送软中断来dequeue
,描述的整个过程大概是图中的这部分。
补充
netfilter
netfilter 在数据包传输中有一些 hook 可以在其中注册回调函数。
iptables
主要是四表五链,是 netfilter 的用户态工具。
cgroup 子系统 net_cls (Network classifier cgroup)
net_cls 可以给 packet 打上 classid 的标签,用于过滤分类,有了上面的详细解释,这个 classid 的作用也非常明显了,就是用于标记skb
所属的 qdisc class 的。
有了这个标签,流量控制器(tc)可以对不同的 cgroup 的 packet 起作用,Netfilter(iptables)也可以基于这个标签有对应的动作。创建一个 net_cls cgroup 对应的是创建一个 net_cls.classid 文件,这个文件初始化为 0。可以写 16 进制的 0xAAAABBBB 到这个文件里面,AAAA 是 major 号,BBBB 是 minor 号。读这个文件返回的是十进制的数字。
例子
1 | mkdir /sys/fs/cgroup/net_cls |
设置一个 10:1 handle.
1 | cat /sys/fs/cgroup/net_cls/0/net_cls.classid |
配置 tc:
1 | tc qdisc add dev eth0 root handle 10: htb |
创建 traffic class 10:1
1 | tc filter add dev eth0 parent 10: protocol ip prio 10 handle 1: cgroup |
配置 iptables,也可以用于这个 classid。
1 | iptables -A OUTPUT -m cgroup ! --cgroup 0x100001 -j DROP |
对应的实现在net/core/netclassid_cgroup.c
下面。起作用的方式是css_cls_state
的classid
并且sock_cgroup_set_classid(&sock->sk->sk_cgrp_data,(unsigned long)v)
来设置sock
的classid
。
cgroup net_prio 子系统
网络优先权(net_prio)子系统可以为各个 cgroup 中的应用程序动态配置每个网络接口的流量优先级。
net_prio.prioidx
只读文件。它包含一个特有整数值,kernel 使用该整数值作为这个 cgroup 的内部代表。
net_prio.ifpriomap
包含优先级图谱,这些优先级被分配给源于此群组进程的流量以及通过不同接口离开系统的流量。回顾pfifo
里优先级映射,对应的就是这个值。该图用
1 | ~]# cat /cgroup/net_prio/iscsi/net_prio.ifpriomap |
net_prio.ifpriomap
文件的目录可以使用上述格式,通过将字符串回显至文件的方式来修改。例如:
1 | ~]# echo "eth0 5" > /cgroup/net_prio/iscsi/net_prio.ifpriomap |
上述指令将强制设定任何源于 iscsi
net_prio
cgroup 进程的流量和 eth0
网络接口传出的流量的优先级为 5
。父 cgroup 也有可写入的 net_prio.ifpriomap
文件,可以设定系统默认优先级。
对应的实现在net/core/netprio_cgroup.c
下面。实现方式是通过扩展dev->priomap
的prioid->prio
的映射记录这个优先级和 cgroup 的关系。
net_prio 使用每个 cgroup 的 id(cgroupo->id)作为 sequence number,并将这个存储在 sk_cgrp_prioidx 中。sk_cgrp_prioidx 这个是单纯的用于设置网络包的优先级,使用这个之后将会覆盖之前通过 SO_PRIORITY socket 选项或者其他方式设置的值。