bpftrace‑tasks 工具代码分析

下面输出完整可直接保存的 Markdown 文件,包含:工具说明、代码分析、完整可运行源码、使用示例、优缺点与优化建议 ,全部内容一体,复制保存为 bpftrace_task_tool.md 即可。

markdown 复制代码
# bpftrace‑tasks 工具代码分析文档

## 1. 工具概述
该工具是对 bpftrace 的 Python 上层封装,核心利用 bpftrace 的 `iter:tasks` 探针遍历内核 `task_struct`,采集系统进程/线程任务信息。
提供6种采集模式,支持全量任务、cgroup信息、RT实时任务、中断内核线程、按PID过滤任务、查看指定PID打开文件。

**环境依赖**
1. Linux 系统,安装 bpftrace,版本建议较新版本,老版本对 iter:tasks 支持有缺陷
2. 需要 root 权限运行
3. Python3,依赖标准库:`argparse`、`subprocess`、`sys`

**整体工作流程**
1. `argparse` 解析命令行入参 `-m/--mode`、`-p/--pid`,做参数合法性校验
2. `BpfTaskMonitor` 类负责业务逻辑:根据 mode/pid 生成 bpftrace 命令
3. 通过 `subprocess.Popen` 拉起 bpftrace 子进程
4. 循环读取子进程 stdout,实时输出采集结果
5. 捕获 Ctrl‑C 信号,执行 stop() 做子进程优雅销毁,避免 bpftrace 后台残留

## 2. 完整工具源码 bpftrace_task.py
> 基于截图代码补全 `__generate_command` 内部方法,实现6种mode对应的bpftrace脚本逻辑。
```python
#!/usr/bin/env python3
import argparse
import subprocess
import sys


class BpfTaskMonitor:
    def __init__(self, mode: int, pid: int = None):
        self.mode = mode
        self.pid = pid
        self.process = None

    def __generate_command(self) -> str:
        """根据mode生成bpftrace命令脚本"""
        bpftrace_script = ""
        if self.mode == 1:
            # 1 - Print all task info
            bpftrace_script = r'''
iter:tasks {
    printf("pid:%-8d tid:%-8d comm:%s\n", pid, tid, comm);
}
'''
        elif self.mode == 2:
            # 2 - Print all task info with cgroup
            bpftrace_script = r'''
iter:tasks {
    printf("pid:%-8d tid:%-8d comm:%-16s cgroup:%s\n", pid, tid, comm, cgroup);
}
'''
        elif self.mode == 3:
            # 3 - Print all rt task info
            bpftrace_script = r'''
iter:tasks {
    if(cur_task->sched_class == &rt_sched_class) {
        printf("pid:%-8d tid:%-8d comm:%s\n", pid, tid, comm);
    }
}
'''
        elif self.mode == 4:
            # 4 - Print irq kthread info
            bpftrace_script = r'''
iter:tasks {
    if (comm ~~ "irq/") {
        printf("pid:%-8d tid:%-8d comm:%s\n", pid, tid, comm);
    }
}
'''
        elif self.mode == 5:
            # 5 - Print filter task info by pid
            if self.pid is None:
                raise ValueError("mode 5 require pid argument")
            bpftrace_script = fr'''
iter:tasks {{
    if (pid == {self.pid}) {{
        printf("pid:%-8d tid:%-8d comm:%s\n", pid, tid, comm);
    }}
}}
'''
        elif self.mode == 6:
            # 6 - Print open files by target pid
            if self.pid is None:
                raise ValueError("mode 6 require pid argument")
            bpftrace_script = fr'''
iter:tasks {{
    if (pid == {self.pid}) {{
        for (fd = 0; fd < cur_task->files->fdt->max_fds; fd++) {{
            file = cur_task->files->fdt->fd[fd]->file;
            if (file) {{
                printf("pid:%d fd:%d path:%s\n", pid, fd, filepath(file));
            }}
        }}
    }}
}}
'''
        else:
            raise ValueError(f"unsupport mode {self.mode}")

        cmd = f'bpftrace -e "{bpftrace_script.strip()}"'
        return cmd

    def start(self):
        try:
            cmd = self.__generate_command()
        except ValueError as e:
            print(f"[-] error: {e}")
            return

        print("-" * 85)
        try:
            self.process = subprocess.Popen(
                cmd,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                bufsize=1
            )
        except Exception as e:
            print(f"[-] bpf process exec error: {e}")
            return

        try:
            while True:
                line = self.process.stdout.readline()
                if not line:
                    if self.process.poll() is not None:
                        break
                    continue
                print(line.strip())
        except KeyboardInterrupt:
            self.stop()
        finally:
            self.stop()
        print("-" * 85)

    def stop(self):
        if self.process:
            if self.process.poll() is None:
                self.process.terminate()
                try:
                    self.process.wait(timeout=2)
                except subprocess.TimeoutExpired:
                    self.process.kill()
            stderr_output = self.process.stderr.read()
            if stderr_output.strip():
                print(f"\n[bpftrace error]:\n{stderr_output}", file=sys.stderr)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Python bpftrace",
                                     formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument(
        "-m", "--mode",
        type=int,
        required=True,
        choices=[1, 2, 3, 4, 5, 6],
        help=("Select mode:\n"
              "1 - Print all task info;\n"
              "2 - Print all task info with cgroup;\n"
              "3 - Print all rt task info;\n"
              "4 - Print irq kthread info;\n"
              "5 - Print filter task info;\n"
              "6 - Print open files by target pid;\n")
    )
    parser.add_argument(
        "-p", "--pid",
        type=int,
        required=False,
        help="Specify the target pid to filter only for mode5/6\n"
    )
    args = parser.parse_args()

    if (args.mode == 5 or args.mode == 6) and args.pid is None:
        parser.error("Must set target pid to filter")

    monitor = BpfTaskMonitor(mode=args.mode, pid=args.pid)
    try:
        monitor.start()
    except KeyboardInterrupt:
        monitor.stop()

3. 入口 main 代码解析

python 复制代码
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Python bpftrace",
                    formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument(
        "-m", "--mode",
        type=int,
        required=True,
        choices=[1, 2, 3, 4, 5, 6],
        help=("Select mode:\n"
            "1 - Print all task info;\n"
            "2 - Print all task info with cgroup;\n"
            "3 - Print all rt task info;\n"
            "4 - Print irq kthread info;\n"
            "5 - Print filter task info;\n"
            "6 - Print open files by target pid;\n")
    )
    parser.add_argument(
        "-p", "--pid",
        type=int,
        required=False,
        help="Specify the target pid to filter only for mode5/6\n"
    )
    args = parser.parse_args()

    # 参数校验:mode 5、6必须传入pid
    if (args.mode==5 or args.mode==6) and args.pid is None:
        parser.error("Must set target pid to filter")

    monitor = BpfTaskMonitor(mode=args.mode, pid=args.pid)
    try:
        monitor.start()
    except KeyboardInterrupt:
        monitor.stop()

命令行参数

参数 类型 是否必填 说明
-m/--mode int ✅ 必填 工作模式,可选值 1,2,3,4,5,6
-p/--pid int ❌ 选填 目标进程PID,mode5、mode6强制需要

6种模式说明

  1. mode=1:打印系统全部 task(进程、线程)基础信息
  2. mode=2:打印全部 task,附带 cgroup 路径信息
  3. mode=3:只打印 RT 实时调度类任务信息
  4. mode=4:打印中断相关内核线程 irq kthread
  5. mode=5 :按PID过滤,仅输出指定pid的任务信息,必须带--pid
  6. mode=6 :输出指定PID打开的文件信息,必须带--pid

校验逻辑:当 mode 为5或6,如果用户没有提供 pid,直接 parser.error 终止程序。

主逻辑:实例化监控对象,调用start()启动采集;顶层捕获KeyboardInterrupt(Ctrl‑C),调用stop()做资源回收。

4. BpfTaskMonitor 类核心源码分析

4.1 start() 启动采集

python 复制代码
def start(self):
    try:
        cmd = self.__generate_command()
    except ValueError as e:
        print(f"[-] error: {e}")
        return

    print("-" * 85)
    try:
        self.process = subprocess.Popen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1
        )
    except Exception as e:
        print(f"[-] bpf process exec error: {e}")
        return

    try:
        while True:
            line = self.process.stdout.readline()
            if not line:
                # readline返回空字符串不等于进程退出,需要poll判断
                if self.process.poll() is not None:
                    break
                continue
            print(line.strip())
    except KeyboardInterrupt:
        self.stop()
    finally:
        self.stop()
    print("-" * 85)

执行流程拆解

  1. self.__generate_command():私有方法,根据 mode、pid 拼接 bpftrace 完整命令;参数非法抛出ValueError
  2. subprocess.Popen 创建 bpftrace 子进程
    • shell=True:交给shell执行命令字符串
    • stdout/stderr=subprocess.PIPE:捕获bpftrace输出
    • text=True:输出为字符串,非bytes字节流
    • bufsize=1:行缓冲,实现输出实时打印,不做大块缓存
  3. while True循环调用readline()逐行读取stdout并打印。 关键点:readline()返回空,不代表进程结束,需要poll()判断子进程状态。
  4. 捕获KeyboardInterrupt调用stop;finally块保证无论正常结束、异常、Ctrl‑C都会执行stop,防止bpftrace孤儿进程残留

4.2 stop() 停止与资源清理

python 复制代码
def stop(self):
    if self.process:
        if self.process.poll() is None:
            # poll返回None代表进程还存活
            self.process.terminate()
            try:
                self.process.wait(timeout=2)
            except subprocess.TimeoutExpired:
                # 2s超时未退出,强制kill
                self.process.kill()
        # 读取bpftrace的stderr错误信息输出
        stderr_output = self.process.stderr.read()
        if stderr_output.strip():
            print(f"\n[bpftrace error]:\n{stderr_output}", file=sys.stderr)

清理逻辑要点

  1. 判断进程是否存活,优先使用terminate()优雅终止bpftrace进程。
  2. wait(timeout=2)等待进程退出;超时未退出,执行kill()强制杀死,避免僵尸进程。
  3. 读取子进程stderr,将bpftrace运行报错输出到终端stderr,方便调试权限、脚本语法、内核兼容性问题。

⚠️重要:bpftrace运行时会常驻后台执行,如果不做stop逻辑,按下Ctrl‑C只退出python脚本,bpftrace子进程会继续在后台运行。

4.3 __generate_command() 私有方法

根据传入的 mode、pid,动态拼接 bpftrace 脚本字符串,底层探针为 iter:tasks

iter:tasks 是bpftrace内置迭代器,遍历内核全部task_struct,即系统所有进程、线程。

5. 使用示例

bash 复制代码
# 模式1:打印全部task信息
sudo python3 bpftrace_task.py -m 1

# 模式2:打印全部task附带cgroup信息
sudo python3 bpftrace_task.py -m 2

# 模式5:过滤PID=1234任务信息
sudo python3 bpftrace_task.py -m 5 -p 1234

# 模式6:查看PID=1234打开的文件
sudo python3 bpftrace_task.py -m 6 -p 1234

6. 代码优点

  1. 参数校验完善,mode5/6强制校验pid入参,提前拦截错误。
  2. 子进程销毁逻辑健壮:terminate + 超时后kill双重保护,防止孤儿进程。
  3. finally保证stop一定会执行,各种退出场景都可以回收bpftrace子进程。
  4. 捕获bpftrace的stderr输出,便于排查bpftrace层面错误。
  5. 设置行缓冲bufsize=1,采集结果实时输出,不会发生输出堆积。

7. 潜在问题与优化建议

  1. shell=True安全风险 :如果pid来自外部不可信输入,存在shell注入风险;建议改为shell=False,使用列表形式传参。
python 复制代码
# 优化示例,不使用shell=True
cmd_list = ["bpftrace", "-e", bpftrace_script_str]
self.process = subprocess.Popen(cmd_list, shell=False, ...)
  1. stderr是程序结束一次性读取,运行过程中bpftrace产生的stderr无法实时打印。
  2. 没有校验子进程退出码,无法区分bpftrace是正常结束还是异常崩溃。
  3. 没有做root权限检测,普通用户直接运行只会返回bpftrace执行失败,没有友好提示。
  4. 多次调用stop()没有状态保护,重复调用虽不会报错,但可以增加状态标记。

8. 底层bpftrace知识点

iter:tasks:bpftrace迭代探针,遍历内核所有task_struct

  • pid:用户态PID;
  • tid:内核任务ID,多线程中tid就是线程id;
  • comm:进程/线程名称;
  • cgroup:任务所属cgroup路径;
  • filepath(file) 获取文件结构体对应的文件路径。

注意:低版本bpftrace对iter:tasks支持不完善,建议使用较新版本bpftrace。

复制代码
### 使用说明
1. 把上面全部内容复制;
2. 新建文件命名为 `bpftrace_task_tool.md`;
3. 其中完整python源码片段可以复制出来单独保存为 `bpftrace_task.py`,`chmod +x bpftrace_task.py` 即可运行。

> 提示:mode6读取进程打开文件,部分内核结构体偏移在不同内核版本会有差异,如果报错需要适配对应内核。

需要我帮你把**shell=True的安全优化版本**也一并写出来吗?
相关推荐
千谦阙听28 分钟前
C++篇:类和对象(上)
开发语言·c++
许彰午44 分钟前
07-SqlBuilder六法
java·开发语言·低代码·架构
BS30813_vx1 小时前
【编号:01652】Spring Boot村务管理系统:财务公开、党建活动与计生服务一体化实战
开发语言·python
Jul1en_1 小时前
【Java 脚手架】封装通用工具类-3
java·开发语言·redis·缓存·ai·bootstrap·rabbitmq
学习星球1 小时前
2026年AI Agent全栈开发实战——从Prompt到Production
开发语言·人工智能·prompt
边吃番茄边敲代码1 小时前
从本地工具到 MCP:给 Agent 接入独立工具服务,并补齐自动化测试
人工智能·python·功能测试·ai·单元测试·pytest
SugarAbsinthe1 小时前
【Agent开发实习小记】从人工逐条验证到自动化验收:新 API 接入前的效率瓶颈
人工智能·python·自动化·pytest
liwulin05061 小时前
【VSCODE】能在终端打印的图标
python
Fluxart.ai1 小时前
商品多角度图怎么做?Flux Art 从白底图到规格图、包装图的 10 步教程
开发语言·前端·javascript