ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

cgroup 子系统之 net_cls 和 net_prio

在分析net_clsnet_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

设有三个优先级的队列,优先级由高到低分别是

  1. “interactive”
  2. “best effort”
  3. “bulk”

先消费 0 队列再消费 1 队列,依次类推,一般的packet都是属于 1。

pfifo

用 IP 的 ToS 可以映射到这些队列,对应的关系如图。

tos

他的实现依赖下面这个结构,bands 一般是 3 个,代表三个不同优先级的队列,然后filter_list是他的过滤器列表,最后prio2band就是上图的ToS To Queue 的映射中的Linux priority 到 Band的那部分,queues保存的是三个fifoqdisc,也就是三个最简单的队列。

1
2
3
4
5
6
7
struct prio_sched_data {
int bands;
struct tcf_proto __rcu *filter_list;
u8 prio2band[TC_PRIO_MAX+1];
struct Qdisc *queues[TCQ_PRIO_BANDS];
};

这里虽然和 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 的状态

  1. Red ( R > CR ) 也就是超速

  2. Yellow (R <= CR && R > AR) 也就是合理超速

  3. Green 就是没有超过保障速率

下面我们有几个等式

1
Rc = min(CRc, ARc + Bc)        [eq1]

Bc 表示从祖先那里借到的速率,下面这段公式表示的意思是,如果我是当前 prio 最小的,从优先级的兄弟节点中加权平均借到父节点的速率,其他的 prio 更大的节点都不能借到速率。

1
2
3
4
5
       Qc Rp
Bc = ----------------------------- iff min[Pi over D(p)]>=Pc [eq2]
sum[Qi over D(p) where Pi=Pc]

Bc = 0 otherwise [eq3]

如果没有父节点的话,Bc 就是 0。这样算出来代表的意义是什么呢,就是说先服务优先级更高的节点,并且按照量子的大小在同优先级的节点中分配速率。

然后我们具体看一下 HTB 调度器是如何工作的。

htb_sch_1

htb_sch_2

每个 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 里面把绿色的包按顺序取出来就可以。

feed3

feed4

然后我们看一下更复杂的情况,在图 3 中,我们从 D 中出队一个 packet(htb_dequeue),然后htb_charge_class会增加 D 的速率,导致 D 变成 yellow,离开 self slot (通过htb_deactivate_prioshtb_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,从这里就可以看到一个借取的过程。

htb_sch_5

htb_sch_6

现在说一个更复杂一点的例子,在图 5 中,A 已经被消耗光了,E 开始工作,然后 C 也能开始工作,变成图 6 的样子。注意即使 D 没有被使用但是他的优先级还会被 class slot 维持,也就是红线。但是 C 和 E 都是同一个优先级,这样的话,就要使用 DRR 算法(也就是在 RR 算法上给每个变量加一个权重,也就是之前的那个量子 quantom)。然后也可以发现一个 class 可以对不同的优先级(红色和蓝色)保持 active。

下面是一个 HTB 的全貌图,抽象的可以理解成一个从父 class 中根据优先级带权重的分享令牌的一个算法。

htb arch

下面三个值对应的就是三种颜色。

1
2
3
4
5
6
/* used internaly to keep status of single class */
enum htb_cmode {
HTB_CANT_SEND, /* class can't send and can't borrow */
HTB_MAY_BORROW, /* class can't send but may borrow */
HTB_CAN_SEND /* class can send */
};

HTB 的实现依赖于

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
struct htb_sched {
struct Qdisc_class_hash clhash;
int defcls; /* class where unclassified flows go to */
int rate2quantum; /* quant = rate / rate2quantum */

/* filters for qdisc itself */
struct tcf_proto __rcu *filter_list;

#define HTB_WARN_TOOMANYEVENTS 0x1
unsigned int warned; /* only one warning */
int direct_qlen;
struct work_struct work;

/* non shaped skbs; let them go directly thru */
struct qdisc_skb_head direct_queue;
long direct_pkts;

struct qdisc_watchdog watchdog;

s64 now; /* cached dequeue time */
struct list_head drops[TC_HTB_NUMPRIO];/* active leaves (for drops) */

/* time of nearest event per level (row) */
s64 near_ev_cache[TC_HTB_MAXDEPTH];

int row_mask[TC_HTB_MAXDEPTH];

struct htb_level hlevel[TC_HTB_MAXDEPTH];
};


其中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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// handle 是一组用户指定的标示符,格式为 major:minor。
// 如果是一条 queueing discipline,minor 需要一直为 0。
# tc qdisc add dev eth0 root handle 1: htb

// parent 指明该新增的 class 添加到那一个父 handle 上去
// classid 指明该 class handle 的唯一 ID,minor 需要是非零值
// ceil 定义 rate 的上界
# tc class add dev eth0 parent 1:1 classid 1:6 htb rate 256kbit ceil 512kbit

// 新建一个带宽为 100kbps 的 root class, 其 classid 为 1:1
# tc class add dev eth0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps
// 接着建立两个子 class,指定其 parent 为 1:1,ceil 用来限制子类最大的带宽
# tc class add dev eth0 parent 1:1 classid 1:10 htb rate 40kbps ceil 100kbps
# tc class add dev eth0 parent 1:1 classid 1:11 htb rate 60kbps ceil 100kbps
// 随后建立 filter 指定哪些类型的 packet 进入那个 class
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10
# tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 flow 1:11
// 最后为这些 class 添加 queuing disciplines
# tc qdisc add dev eth0 parent 1:10 handle 20: pfifo limit 5
# tc qdisc add dev eth0 parent 1:11 handle 30: sfq perturb 10

实现

用 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 了,最后清空 qdiscRUNNING状态。

在此结构中,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
2
3
4
5
6
7
8
dev_queue_xmit ->
__dev_queue_xmit ->
__dev_xmit_skb ->
qdisc->enqueue
__qdisc_run ->
qdisc_restart ->
qdisc->dequeue ->
sch_direct_xmit

enqueue的时候会主动唤起dequeue也可能是硬件发送就绪会唤醒发送软中断来dequeue,描述的整个过程大概是图中的这部分。

补充

netfilter

netfilter 在数据包传输中有一些 hook 可以在其中注册回调函数。

netfilter

iptables

主要是四表五链,是 netfilter 的用户态工具。

deep dive

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
2
3
4
mkdir /sys/fs/cgroup/net_cls
mount -t cgroup -onet_cls net_cls /sys/fs/cgroup/net_cls
mkdir /sys/fs/cgroup/net_cls/0
echo 0x100001 > /sys/fs/cgroup/net_cls/0/net_cls.classid

设置一个 10:1 handle.

1
2
cat /sys/fs/cgroup/net_cls/0/net_cls.classid
1048577

配置 tc:

1
2
tc qdisc add dev eth0 root handle 10: htb
tc class add dev eth0 parent 10: classid 10:1 htb rate 40mbit

创建 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_stateclassid并且sock_cgroup_set_classid(&sock->sk->sk_cgrp_data,(unsigned long)v)来设置sockclassid

cgroup net_prio 子系统

网络优先权(net_prio)子系统可以为各个 cgroup 中的应用程序动态配置每个网络接口的流量优先级。

net_prio.prioidx

只读文件。它包含一个特有整数值,kernel 使用该整数值作为这个 cgroup 的内部代表。

net_prio.ifpriomap

包含优先级图谱,这些优先级被分配给源于此群组进程的流量以及通过不同接口离开系统的流量。回顾pfifo里优先级映射,对应的就是这个值。该图用 的形式以成对列表表示:

1
2
3
4
~]# cat /cgroup/net_prio/iscsi/net_prio.ifpriomap
eth0 5
eth1 4
eth2 6

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->priomapprioid->prio的映射记录这个优先级和 cgroup 的关系。

net_prio 使用每个 cgroup 的 id(cgroupo->id)作为 sequence number,并将这个存储在 sk_cgrp_prioidx 中。sk_cgrp_prioidx 这个是单纯的用于设置网络包的优先级,使用这个之后将会覆盖之前通过 SO_PRIORITY socket 选项或者其他方式设置的值。