
Linux 内核 Taskstats基于 Netlink 的每任务/每进程统计接口全解析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxTaskstats 是 Linux 内核通过 GENERIC Netlink 将每任务per-task与每进程per-process统计数据导出到用户空间的统一接口在资源管理、性能调优和计费审计等场景中用于追踪任务生命周期内的 CPU/内存/IO 消耗。读完本文你能掌握 taskstats 的两种使用模式查询命令与退出事件监听的完整报文格式与属性含义、struct taskstats的版本化字段语义、内核侧从do_exit()到taskstats_exit()的实现链路以及高退出速率下的流控方法从而自行编写监听用户态统计数据的工具。一、设计目标与术语约定Taskstats 是一个基于 Netlink 的接口负责把内核中采集的 per-task 与 per-process 统计发送到用户空间。它的原始设计目标有三点在任务生命周期内以及任务退出时都能高效地提供统计数据为多个核算accounting子系统提供统一接口为未来新增的核算补丁保留可扩展性。理解 taskstats 前必须先区分两组术语这是接口数据语义的基础pid、tid 与 task三者可互换使用指代标准 Linux 任务即struct task_struct定义的实体。因此 per-pid 统计就是 per-task 统计。tgid、process 与 thread group三者可互换使用指代共享同一个mm_struct的一组任务即传统 Unix 意义上的进程。注意尽管使用了 tgid 这一术语线程组 leader 并不享有特殊待遇——只要进程中还有任意一个存活任务该进程就被视为活着。二、两种使用模式2.1 生命周期内查询主动查询用户空间打开一个单播 Netlink socket属于NETLINK_GENERIC家族发送携带 pid 或 tgid 的命令指定pid内核返回该单个任务的统计指定tgid内核返回该进程内所有任务的统计之和。对应的内核命令处理入口是 kernel/taskstats.c 中的taskstats_user_cmd()它根据携带的属性TASKSTATS_CMD_ATTR_PID/TASKSTATS_CMD_ATTR_TGID/ 注册、注销 cpumask分派到cmd_attr_pid()、cmd_attr_tgid()或cmd_attr_cpumask()。2.2 退出事件监听被动接收要获取正在退出的任务的统计用户空间监听者需发送注册命令并指定一个cpumask。之后每当 cpumask 覆盖的某个 CPU 上有任务退出该任务的 per-pid 统计就会被发送到已注册的监听者。使用 cpumask 的价值在于它限制单个监听者收到的数据量从而辅助 Netlink 接口的流控详见第七节。补充规则若退出的是线程组的最后一个线程内核会额外发送一条携带 per-tgid 统计的记录。per-tgid 统计包含该线程组内所有线程历史已退出 当前存活的 per-pid 统计总和。内核侧的触发点在 kernel/exit.cdo_exit()流程中在tsk-exit_code code;之后调用taskstats_exit(tsk, group_dead)由group_dead线程组是否全部退出决定是否附带 per-tgid 数据。2.3 示例工具 getdelays.c内核仓库自带 tools/accounting/getdelays.c这是一个演示 taskstats 接口用法的简单工具主要用于报告 delay accounting 统计。它覆盖了本文涉及的全部典型操作注册 cpumask、发送命令并处理响应、监听 per-tid/per-tgid 退出数据、把数据落盘以及通过增大接收缓冲区做基础流控。编写自己的监听工具时建议直接以它为模板参考。三、Netlink 报文格式与消息类型用户态—内核态接口封装在头文件中用户空间 ABI 定义见 include/uapi/linux/taskstats.h内核侧声明见 include/linux/taskstats_kern.h。官方文档为避免随接口演进而过时只给出当前版本的轮廓——taskstats.h 始终优先于文档描述。struct taskstats是 per-pid 与 per-tgid 数据共用的核算结构体。它是带版本号的可随内核新增核算子系统而扩展字段语义以 taskstats.h 为准。用户态与内核态交换的数据是NETLINK_GENERIC家族的 Netlink 报文采用 Netlink 属性attribute接口。报文整体布局为----------- - --------------------------------- | nlmsghdr | Pad | genlmsghdr | taskstats payload | ----------- - ---------------------------------家族与版本常量TASKSTATS_GENL_NAME TASKSTATS、TASKSTATS_GENL_VERSION 0x1见 include/uapi/linux/taskstats.h。taskstats 载荷共有三种类型3.1 命令用户 → 内核查询 pid/tgid 的命令只携带一个属性类型为TASKSTATS_CMD_ATTR_PID/TASKSTATS_CMD_ATTR_TGID属性载荷为 u32 类型的 pid 或 tgid即用户空间想要获取统计的目标任务/进程。注册/注销退出数据监听只携带一个属性类型为TASKSTATS_CMD_ATTR_REGISTER_CPUMASK/TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK载荷为cpumask 的 ASCII 字符串格式为逗号分隔的 CPU 区间。例如要监听 CPU 1、2、3、5、7、8 上的退出数据字符串应为1-3,5,7-8。若用户空间在关闭监听 socket 前忘记注销 cpumask内核会在一段时间后自动清理其监听项但出于效率考虑建议显式注销。从源码可以确认几条重要的实现约束cpumask 字符串长度上限为TASKSTATS_CPUMASK_MAXLEN (100 6 * NR_CPUS)超长返回-E2BIG见 kernel/taskstats.c 与parse()第 358–375 行内部使用cpulist_parse()解析只有init user namespace 且 init pid namespace中的进程可以注册/注销监听add_del_listener()中对current_user_ns()与task_active_pid_ns(current)的检查见 kernel/taskstats.c非特权容器内的监听请求会被拒绝-EINVAL。命令枚举未版本化新命令只能追加在__TASKSTATS_CMD_MAX之前enum { TASKSTATS_CMD_UNSPEC 0, /* 保留 */ TASKSTATS_CMD_GET, /* 用户-内核 请求/取回响应 */ TASKSTATS_CMD_NEW, /* 内核-用户 事件 */ __TASKSTATS_CMD_MAX, };属性类型枚举enum { TASKSTATS_TYPE_UNSPEC 0, /* 保留 */ TASKSTATS_TYPE_PID, /* 进程 id */ TASKSTATS_TYPE_TGID, /* 线程组 id */ TASKSTATS_TYPE_STATS, /* taskstats 结构体 */ TASKSTATS_TYPE_AGGR_PID, /* 内含 pid stats */ TASKSTATS_TYPE_AGGR_TGID, /* 内含 tgid stats */ TASKSTATS_TYPE_NULL, /* 不含数据 */ __TASKSTATS_TYPE_MAX, };3.2 命令响应内核 → 用户响应的载荷是由三个属性组成的序列顺序属性类型含义aTASKSTATS_TYPE_AGGR_PID/TASKSTATS_TYPE_AGGR_TGID无载荷仅表示其后将跟随一个 pid/tgid 及其统计bTASKSTATS_TYPE_PID/TASKSTATS_TYPE_TGID载荷为正在返回统计的 pid/tgid 数值cTASKSTATS_TYPE_STATS载荷为struct taskstatsper-pid 与 per-tgid 统计使用同一结构内核组装响应的代码在mk_reply()kernel/taskstats.c先nla_nest_start嵌套AGGR_*属性再放入PID/TGID最后nla_reserve_64bit预留 64 位对齐的TASKSTATS_TYPE_STATS区域不足部分以TASKSTATS_TYPE_NULL填充对齐。3.3 退出事件新消息内核主动推送每当有任务退出时内核推送的消息载荷是由以下属性序列组成顺序属性类型含义aTASKSTATS_TYPE_AGGR_PID表示接下来的两个属性为 pid 统计bTASKSTATS_TYPE_PID包含退出任务的 pidcTASKSTATS_TYPE_STATS包含该任务的 per-pid 统计dTASKSTATS_TYPE_AGGR_TGID表示接下来两个属性为 tgid 统计仅组死亡时eTASKSTATS_TYPE_TGID包含退出任务所属进程的 tgidfTASKSTATS_TYPE_STATS包含该进程的 per-tgid 统计taskstats_exit()kernel/taskstats.c实现了这条推送链路先检查当前 CPU 是否有监听者listener_arrayper-CPU 链表无监听者直接返回按 pid 组一条PIDSTATS若group_dead则对stats-ac_flag置上AGROUP标志若是线程组taskstats_tgid_alloc()返回非空且组已死亡再附加一条TGIDSTATS最后通过send_cpu_listeners()单播给该 CPU 上所有已注册监听 socket对返回-ECONNREFUSED的失效监听者做惰性删除。四、struct taskstats 字段语义当前版本为TASKSTATS_VERSION 17include/uapi/linux/taskstats.h。字段的完整逐组解释见配套文档 Documentation/accounting/taskstats-struct.rst核心分组如下1通用与基础核算字段CONFIG_TASKSTATS开启即在任务do_exit()时采集__u16 version; /* 恒等于 TASKSTATS_VERSION */ __u32 ac_exitcode; /* 退出状态 */ __u8 ac_flag; /* AFORK/ASU/ACOMPAT/ACORE/AXSIG/AGROUP 等核算标志 */ __u8 ac_nice; /* task_nice */ char ac_comm[TS_COMM_LEN]; /* 命令名TS_COMM_LEN 32 */ __u8 ac_sched; /* 调度策略 */ __u32 ac_uid, ac_gid, ac_pid, ac_ppid; __u32 ac_btime; /* 开始时间1970 起秒数32 位上限到 2106 年 */ __u64 ac_etime; /* 存活时长 usec */ __u64 ac_utime, ac_stime; /* 用户/内核 CPU 时间 usec */ __u64 ac_minflt, ac_majflt; /* 缺页计数 */2延迟核算字段CONFIG_TASK_DELAY_ACCT开启时采集cpu_count/cpu_delay_total可运行态等待 CPU 的延迟count 与 total 不原子更新、blkio_count/blkio_delay_total同步块 I/O 完成等待不计 I/O 提交延迟、swapin_count/swapin_delay_total换页 I/O 等待、cpu_run_real_total墙钟运行时间部分架构会扣除虚拟化偷走的时间、cpu_run_virtual_total内核视角的运行时间。v16 起为每类延迟追加了*_delay_max/min极值v17 再追加极值发生的时间戳cpu_delay_max_ts等 8 个__kernel_timespec字段。注意xxx_delay_total溢出时回绕为 0而xxx_count不受回绕影响。3扩展核算字段CONFIG_TASK_XACCT开启时采集coremem/virtmem累计 RSS/VM 用量单位 MB-usec每次 tick 计入系统时间时累加可据此计算单位系统时间内的平均内存占用、hiwater_rss/hiwater_vm高水位KB、read_char/write_char/read_syscalls/write_syscalls字符级 I/O 统计。4上下文切换计数nvcsw自愿切换、nivcsw非自愿切换直接取自tsk-nvcsw/nivcsw见 kernel/taskstats.c。5SMT 机器时间核算ac_utimescaled、ac_stimescaled、cpu_scaled_run_real_total按频率等缩放的 CPU 时间。6存储 I/O 与内存回收延迟read_bytes/write_bytes/cancelled_write_bytesCONFIG_TASK_IO_ACCOUNTING、freepages_count/freepages_delay_total等待内存回收、thrashing_*颠簸页延迟、compact_*内存整理延迟v11、wpcopy_*写保护拷贝延迟v13、irq_*IRQ/SOFTIRQ 等待v14。v12 新增ac_tgid、ac_tgetime线程组墙钟时间组死亡时为进程总墙钟、ac_exe_dev/ac_exe_inode可执行文件的设备号与 inode等价于/proc/self/exe在最后一次exec()后的值内核线程全为 0——填充逻辑见 kernel/taskstats.c 的exe_add_tsk()。4.1 统计填充的实现链路fill_stats()kernel/taskstats.c展示了 taskstats 作为统一接口的聚合方式——各核算子系统各自实现per-task-foo(stats, tsk)回调依次把数据写入同一结构static void fill_stats(struct user_namespace *user_ns, struct pid_namespace *pid_ns, struct task_struct *tsk, struct taskstats *stats) { memset(stats, 0, sizeof(*stats)); delayacct_add_tsk(stats, tsk); /* 延迟核算 */ stats-version TASKSTATS_VERSION; stats-nvcsw tsk-nvcsw; stats-nivcsw tsk-nivcsw; bacct_add_tsk(user_ns, pid_ns, stats, tsk); /* 基础核算 */ xacct_add_tsk(stats, tsk); /* 扩展核算 */ exe_add_tsk(stats, tsk); /* 可执行文件信息 */ }查询路径上fill_stats_for_pid()通过find_get_task_by_vpid()定位任务后填充fill_stats_for_tgid()kernel/taskstats.c则先memcpy已累积的tsk-signal-stats再在lock_task_sighand()保护下遍历存活线程累加tgid_stats_add_task()逐项累加ac_etime、ac_utime、ac_stime、切换计数这与第五节描述的 per-tgid 语义一一对应。五、per-tgid 统计的累积机制Taskstats 之所以在 per-task 之外额外提供 per-process 统计是因为资源管理通常以进程粒度进行而纯粹在用户空间聚合任务统计既低效又可能不准缺乏原子性。但同时在内核中维护 per-process 与 per-task 两套统计会增加空间和时间开销。taskstats 的解法是把每个退出任务的统计累积进一个进程级数据结构当进程的最后一个任务退出时累积的进程级数据会随 per-task 数据一并发送到用户空间。当用户查询 per-tgid 数据时内核会把组内所有其他存活线程的统计求和加上此前已退出线程累积的总量。源码证据taskstats_tgid_alloc()kernel/taskstats.c进程级统计挂在signal_struct.stats上采用懒分配 smp_store_release发布——单线程进程thread_group_empty()不分配该结构多线程序列的第一个线程触发分配并用 siglock 双重检查防止重复分配从而把开销只花在确实多进程语义上需要的场景。组死亡时fill_tgid_exit()第 283–297 行在siglock保护下把最后退出线程的数据累入signal-stats结构体在signal_struct释放时经 include/linux/taskstats_kern.h 的taskstats_tgid_free()归还专用 slab 缓存taskstats_cache由taskstats_init_early()创建。六、扩展 taskstats 的两种方式当未来有新的统计采集补丁合入内核、需要导出更多 per-task/process 统计时有两种扩展方式在现有struct taskstats末尾追加字段。版本兼容性由结构体内部的版本号保证用户空间只使用与其所用版本对应的字段。头文件注释明确规定了新增字段的操作规程a) 提升TASKSTATS_VERSIONb) 在结构体尾部添加注明新版本号的注释c) 新字段加在版本注释之后并保持 64 位对齐。Documentation/accounting/taskstats-struct.rst 也强调未来扩展只允许追加到结构体末尾不得改变各字段的相对位置。定义独立的统计结构体并通过 Netlink 属性接口返回。由于用户空间对每个 Netlink 属性独立处理它总能忽略类型不认识的属性即旧版接口使用者自动忽略新属性。权衡若只需增加少量字段方式 1 更优——内核与用户空间都无需处理新 Netlink 属性的开销若新字段使既有结构体膨胀得过大导致形形色色的用户空间核算工具被迫接收大量与其无关的大结构体那么方式 2 就更值得采用。七、流控Flow Control当任务退出速率很高时监听者可能跟不上内核推送 per-tid/per-tgid 退出数据的速率导致数据丢失当 taskstats 结构体变大、CPU 数量增多时这一问题会被放大。为避免统计丢失用户空间应采取以下一项或多项措施增大监听 socket 的接收缓冲区增加监听者数量减少每个监听者监听的 CPU 数量。极端情况下每个 CPU 一个监听者。还可考虑把监听者的 CPU 亲和性设置为其监听的 CPU 子集上特别是只监听单个 CPU 时。即便采取了上述措施若用户空间收到ENOBUFS错误表示接收缓冲区溢出仍需采取措施处理数据丢失。八、内核配置与源码地图taskstats 相关选项均定义在 init/Kconfig依赖关系如下选项依赖作用CONFIG_TASKSTATSNET、MULTIUSER默认 N启用 Netlink 统计导出接口本身。与 BSD 进程核算的区别统计在任务生命周期内可响应命令查询退出时同样推送到用户空间CONFIG_TASK_DELAY_ACCTTASKSTATS并select SCHED_INFO采集任务等待 CPU、同步块 I/O 完成、换页等资源的延迟CONFIG_TASK_XACCTTASKSTATS采集扩展核算数据coremem/virtmem/hiwater 等CONFIG_TASK_IO_ACCOUNTINGTASK_XACCT采集任务造成的存储 I/O 字节数read_bytes/write_bytes 等CONFIG_TASKSTATS关闭时include/linux/taskstats_kern.h 将taskstats_exit()等全部降级为空 inline 函数do_exit()路径零开销。主要源码文件速查文件职责include/uapi/linux/taskstats.h用户空间 ABIstruct taskstats、命令/属性枚举、GENL 家族名与版本include/linux/taskstats_kern.h内核声明与CONFIG_TASKSTATS关闭时的空实现kernel/taskstats.c全部实现命令分派taskstats_user_cmd、统计填充fill_stats/fill_stats_for_pid/fill_stats_for_tgid、cpumask 监听注册add_del_listenerper-CPUlistener_array rwsem、退出推送taskstats_exit/send_cpu_listeners、genl 家族注册late_initcall注册成功后打印 registered taskstats version 1kernel/exit.cdo_exit()中调用taskstats_exit(tsk, group_dead)的触发点tools/accounting/getdelays.c用法演示工具Documentation/accounting/taskstats-struct.rststruct taskstats逐字段语义文档九、实践要点小结打开NETLINK_GENERIC单播 socket定位名为TASKSTATS的 genl 家族版本 0x1所有操作都围绕TASKSTATS_CMD_GET命令 属性组合展开查询单个任务发TASKSTATS_CMD_ATTR_PID查询进程发TASKSTATS_CMD_ATTR_TGID返回全线程之和注册退出监听发TASKSTATS_CMD_ATTR_REGISTER_CPUMASK如1-3,5,7-8关闭前显式发DEREGISTER解析响应时按AGGR_PID → PID → STATS的嵌套序列提取数据组死亡事件额外跟一段AGGR_TGID → TGID → STATSac_flag的AGROUP位标识线程组死亡解析struct taskstats时以version字段为准裁剪字段范围当前为 17未知尾部数据不要越界读取高退出速率场景优先加大接收缓冲区并拆分监听者出现ENOBUFS时按数据丢失处理而不是无限重压编写工具前直接阅读 tools/accounting/getdelays.c它完整演示了注册、命令、响应解析与落盘全流程。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考