Linux 内核 net_cls cgroup(v1):用 classid 标签网络报文实现基于 cgroup 的 QoS 与过滤
Linux 内核 net_cls cgroupv1用 classid 标签网络报文实现基于 cgroup 的 QoS 与过滤【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxnet_cls 是 Linux cgroup v1 体系中的「网络分类」控制器它为进程所在的 cgroup 关联一个 32 位 classid并把这个标签随 socket 打到出站网络报文上供流量控制tc与 Netfilter/iptables 按标签做差异化处理。本文基于本仓库Linux kernel source tree中的 Documentation/admin-guide/cgroup-v1/net_cls.rst 整理并深入其核心实现 net/core/netclassid_cgroup.c 与 include/net/cls_cgroup.h带你掌握从挂载、配置 classid到与 tc、iptables 联动的完整实战方法。一、net_cls 控制器是什么给网络报文打上 cgroup 标签net_clsnetwork classifier是内核自带的 cgroup 子系统controller其作用非常单一而明确为每个 net_cls cgroup 实例维护一个 classid类标识符并把该标识关联到从这个 cgroup 中的任务所创建的 socket 上进而通过 socket 传递给由该 socket 发出的网络报文skb。有了这一层「进程分组 → 网络包」的标签映射内核网络路径中的两个关键模块就可以消费它流量控制 tc为来自不同 cgroup 的报文分配不同优先级或带宽例如 HTB 层级队列。Netfilter / iptables使用 cgroup 匹配扩展-m cgroup对该标签执行 DROP、限速等动作。在源码层面该子系统在内核配置项CONFIG_CGROUP_NET_CLASSID开启后编译。子系统结构体定义于 net/core/netclassid_cgroup.cstruct cgroup_subsys net_cls_cgrp_subsys { .css_alloc cgrp_css_alloc, .css_online cgrp_css_online, .css_free cgrp_css_free, .attach cgrp_attach, .legacy_cftypes ss_files, };注意这里使用legacy_cftypes说明 net_cls 是一个典型的 cgroup v1 时代遗留控制器其用法文档位于仓库的 Documentation/admin-guide/cgroup-v1/ 目录。1.1 单一属性文件模型net_cls.classidnet_cls 控制器的完整接口只有一个属性文件创建任意 net_cls cgroup 实例时内核会自动生成一个net_cls.classid文件其初始值为 0。这个文件由 net/core/netclassid_cgroup.c 中的ss_files[]数组注册static struct cftype ss_files[] { { .name classid, .read_u64 read_classid, .write_u64 write_classid, }, { } /* terminate */ };控制器为每个 cgroup 保存的内部状态只有两个字段见 include/net/cls_cgroup.hstruct cgroup_cls_state { struct cgroup_subsys_state css; u32 classid; };1.2 classid 的编码格式0xAAAABBBBnet_cls.classid接受十六进制写入格式固定为0xAAAABBBBAAAA高 16 位major handle number主句柄号BBBB低 16 位minor handle number次句柄号。它本质是一个 32 位无符号整数classid (AAAA 16) | BBBB。写入时内核直接把值截断保存为u32cs-classid (u32)value;而read_u64回调read_classid返回的正是这个整数因此读取net_cls.classid得到的是十进制结果——例如写入0x100001读出来是1048577。从源码结构看这一 AAAA:BBBB 的划分并非任意约定而是为了让 classid 能直接与 tc 的 class 句柄体系handle 形如10:1一一对应方便后续在 tc 侧用handle 1: cgroup分类器读取。二、让 classid 在进程树中传递继承与自动传播net_cls 在 cgroup 生命周期上的行为有一个很实用的设计——classid 沿父子 cgroup 自动继承见cgrp_css_online()net/core/netclassid_cgroup.cstatic int cgrp_css_online(struct cgroup_subsys_state *css) { struct cgroup_cls_state *cs css_cls_state(css); struct cgroup_cls_state *parent css_cls_state(css-parent); if (parent) cs-classid parent-classid; return 0; }也就是说新建子 cgroup 时会拷贝父 cgroup 当前的 classid之后父、子各自独立写值互不影响。另一个关键机制在attach任务移入/移出 cgroup与 classid 写入时内核不仅更新 cgroup 内部状态还会即时地遍历该 cgroup 中所有任务已打开的 socket 文件描述符把新 classid 写到每个 socket 上sock_cgroup_set_classid。相关实现在cgrp_attach()与write_classid()net/core/netclassid_cgroup.c底层逐 fd 扫描逻辑为update_classid_task()static void update_classid_task(struct task_struct *p, u32 classid) { ... /* Only update the leader task, when many threads in this task, * so it can avoid the useless traversal. */ if (!thread_group_leader(p)) return; do { task_lock(p); fd iterate_fd(p-files, fd, update_classid_sock, ctx); task_unlock(p); cond_resched(); } while (fd); }其中有两处值得留意的工程细节每个线程组只处理线程组 leaderthread_group_leader避免对海量线程做无谓遍历为防遍历大量 fd 导致持锁阻塞update_classid_sock()每处理 1000 个描述符UPDATE_CLASSID_BATCH就释放一次file_lock并cond_resched()让出 CPUnet/core/netclassid_cgroup.c。因此若某线程同时打开了大量 socketclassid 的刷新是分批次渐进完成的之后新创建的 socket 则会直接带上最新 classid。结论echo写入 classid 后进程组中已经建立的 socket 也会被同步打标无需重启进程或重建连接超大批量 fd 场景可能需稍等片刻完成全部刷新。三、报文在数据路径上的取值task 与 socket 两级兜底发送路径上报文该用哪个 classid核心逻辑集中在 include/net/cls_cgroup.h新建 socket 时sock_update_classid取当前任务所属 net_cls cgroup 的 classid写入 socket 的sk_cgrp_data。发包取标签时task_get_classid正常情况下直接读取current任务的 classid但如果当前处于softirq 上下文softirq_count()非零访问current会得到错误结果此时退而使用 skb 对应 socket 上已经保存好的 classidstatic inline u32 task_get_classid(const struct sk_buff *skb) { u32 classid __task_get_classid(current); if (softirq_count()) { struct sock *sk skb_to_full_sk(skb); /* If there is an sock_cgroup_classid well use that. */ if (!sk || !sk_fullsock(sk)) return 0; classid sock_cgroup_classid(sk-sk_cgrp_data); } return classid; }这正是前面「把 classid 刷到 socket 上」这一步的价值所在——它保证即使报文最终由软中断如 TCP 重传、backlog 处理发出仍然能拿到正确的标签若 socket 不存在或不是 full socket则返回 0。这套接口同时也是 tccgroup分类器读取 classid 的底层依据。四、实战第一步挂载 net_cls 并配置 classid4.1 前提内核开启 CONFIG_CGROUP_NET_CLASSID运行环境的内核需要启用CONFIG_CGROUP_NET_CLASSID。若未开启即使挂载成功net_cls.classid相关逻辑也不会生效include/net/cls_cgroup.h 中的桩函数直接返回 0。启用后内核将自动注册net_cls子系统可通过cat /proc/cgroup或ls /sys/fs/cgroup确认。4.2 创建并挂载与挂载其他 cgroup v1 控制器类似核心操作如下文档示例见 Documentation/admin-guide/cgroup-v1/net_cls.rst# 挂载 net_cls 控制器无层级时net_cls 通常挂到 cgroup v1 根下 mkdir /sys/fs/cgroup/net_cls mount -t cgroup -onet_cls net_cls /sys/fs/cgroup/net_cls # 创建子 cgroup 实例例如名为 0 mkdir /sys/fs/cgroup/net_cls/0 # 写入 classid 0x100001对应句柄 10:1 echo 0x100001 /sys/fs/cgroup/net_cls/0/net_cls.classid几点说明若系统同时挂载了 cgroup v1 的多个控制器如-o net_cls,net_prio注意不同控制器层级分离的规则在标准 v1 用法中通常单独挂载 net_cls。子 cgroup 刚创建时其net_cls.classid会继承父 cgroup 的值初始为 0。若环境中同时存在 cgroup v2统一层级net_cls 这类 v1 控制器通常需要挂载到独立层级或不可与 v2 混用请以发行版实际配置为准。4.3 验证读取十六进制写、十进制读cat /sys/fs/cgroup/net_cls/0/net_cls.classid # 10485770x100001 1048576 1 1048577即 major0x10(10)minor0x1(1)对应10:1句柄。文档明确说明写入格式为十六进制 0xAAAABBBB读取结果为十进制这一点与源码中read_u64/write_u64的实现完全吻合。把目标进程加入该 cgroup即可让该进程新建/已建 socket 自动获得10:1标签echo pid /sys/fs/cgroup/net_cls/0/tasks五、实战第二步用 tc 按 classid 做分层流量整形classid 最常见的消费方是内核的流量控制框架。cgroup分类器filter会提取每个报文对应的 classid并将报文导向 tc 中句柄匹配的 class。以下是一个完整的 HTB 示例沿用文档命令Documentation/admin-guide/cgroup-v1/net_cls.rst# 1) 在 eth0 上建立根队列句柄为 10: tc qdisc add dev eth0 root handle 10: htb # 2) 在根下创建类 10:1限速 40Mbit —— 与 net_cls 中的 0x100001 对应 tc class add dev eth0 parent 10: classid 10:1 htb rate 40mbit # 3) 挂 cgroup 分类器把带 cgroup classid 的报文交给 tc 匹配 tc filter add dev eth0 parent 10: protocol ip prio 10 handle 1: cgroup工作流程拆解内核为来自 net_cls cgroup 的 socket 报文打上 classid如10:1报文进入 eth0 的根 qdischandle10:后依次经过 filter 匹配handle 1: cgroup分类器读取报文 classid命中10:1类从而应用htb rate 40mbit的整形策略。这样不同 cgroup 的进程流量便可在同一网卡上被分流到不同的 HTB 类分别获得独立带宽/优先级——常见的「按用户/业务分组限速」场景即可由此搭建。更细的速率层次如再分10:10只需追加tc class与子过滤器即可。六、实战第三步用 iptables 的 cgroup 匹配做过滤除了 tcclassid 还可供 Netfilter 消费。iptables 的cgroup匹配扩展可按报文 classid 决定放行或丢弃。文档给出的基础示例是iptables -A OUTPUT -m cgroup ! --cgroup 0x100001 -j DROP含义在 OUTPUT 链上对 classid不等于0x100001 的报文一律 DROP——即只允许来自 classid 为 0x10000110:1对应上面 net_cls cgroup0组的进程发包实现「按 cgroup 白名单出网」。若去掉!则变成对该 classid 的报文执行动作。注意iptablescgroup匹配与--cgroup后的取值同样沿用0xAAAABBBB十六进制格式与写入net_cls.classid的格式一致避免在 tc 与 iptables 之间出现不一致。七、使用限制与注意事项属于 cgroup v1 遗留控制器net_cls 只在 cgroup v1 层级模型中生效。使用 systemd 或纯 cgroup v2 的现代系统中该控制器通常不可直接挂载需要单独的 v1 层级相关机制与新版 cgroup 的关系请结合发行版与内核文档判断。classid 初始与缺省值为 0新创建的 cgroup 和无法匹配到 socket 的软中断发包场景标签取 0tc/iptables 规则需自行考虑如何对待 classid 0 的流量。写入为十六进制、读出为十进制跨脚本读写时若直接比较字符串容易踩坑建议在脚本中用数值比较或统一格式转换。作用对象为出站路径的 socket 报文net_cls 标签通过 socket 附着主要影响本机发起的出站流量分类入向流量的分类需配合 tc ingress、mirred 等其他机制。八、总结net_cls cgroup 用最轻量的方式打通了「进程分组」与「网络分类」一个0xAAAABBBB的 classid、一个net_cls.classid属性文件配合继承与 socket 级自动刷新即可让 tc 和 iptables 以统一标签识别来自特定 cgroup 的流量。核心状态与行为分别在 net/core/netclassid_cgroup.ccgroup 接口继承、attach、读写与 include/net/cls_cgroup.hsocket 打标、softirq 兜底取 classid中实现原始操作指引可随时查阅 Documentation/admin-guide/cgroup-v1/net_cls.rst。需要按业务/用户隔离带宽或管控出网流量时net_cls tc/iptables 的组合依然是一个值得优先考虑的内核原生方案。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考