如何使用bcc捕获指定用户进程触发的块设备写操作相关信息
解决方案
一、关联回写IO与原始进程的实现思路
异步缓存回写场景下IO由内核线程触发,无法直接在block_rq_complete中拿到原始进程PID,可以通过两级追踪实现关联:
- 在内核将页标记为脏的节点(比如
account_page_dirtied函数)插入探测点,记录<struct page *地址, 进程PID/进程名>的映射关系存入BPF哈希表 - 在
block_rq_complete触发时,遍历请求对应的bio中的page,查表获取写入该页的原始进程信息,即可将swapper/flush线程执行的回写IO关联到原始发起进程
二、无关事件过滤方案
可以在eBPF代码侧提前做三层过滤,大幅减少无效事件上报:
- 设备号过滤:
block_rq_completetracepoint自带dev参数,可提前获取目标SSD的主从设备号(通过lsblk命令查询),仅处理对应设备的IO - 操作类型过滤:通过
args->cmd_flags判断请求类型,仅上报写操作,丢弃读、discard等无关请求 - 进程过滤:关联到原始PID后,仅上报你指定关注的进程ID/进程名对应的IO,其余直接丢弃
三、修正后的示例代码
注意:该代码需要内核支持读取bio中的page结构,测试前请确认目标内核版本兼容,部分内核版本写请求宏需要替换为
REQ_OP_WRITE
from bcc import BPF import sys # 传入要监控的块设备号(比如8:0对应sda,设备号为0x800)和目标进程PID(可选) TARGET_DEV = int(sys.argv[1], 16) if len(sys.argv) >1 else 0x800 TARGET_PID = int(sys.argv[2]) if len(sys.argv) >2 else 0 PROG = f""" #include <linux/blkdev.h> #include <linux/sched.h> #include <linux/bio.h> struct data_t {{ u32 original_pid; u64 ts; u64 sector_start; u64 nr_sectors; char comm[TASK_COMM_LEN]; char original_comm[TASK_COMM_LEN]; }}; BPF_PERF_OUTPUT(events); // 哈希表存储脏页对应的原始进程信息 BPF_HASH(page_pid_map, struct page *, u32); BPF_HASH(page_comm_map, struct page *, char[TASK_COMM_LEN]); // 探测脏页标记函数,记录页对应的进程信息 int trace_account_page_dirtied(struct pt_regs *ctx, struct page *page) {{ u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; char comm[TASK_COMM_LEN] = {{0}}; bpf_get_current_comm(comm, sizeof(comm)); page_pid_map.update(&page, &pid); page_comm_map.update(&page, comm); return 0; }} TRACEPOINT_PROBE(block, block_rq_complete) {{ // 第一层过滤:只处理目标设备的请求 if (args->dev != {TARGET_DEV}) {{ return 0; }} // 第二层过滤:只处理写请求 if (!(args->cmd_flags & REQ_WRITE)) {{ return 0; }} struct data_t data = {{}}; u32 *original_pid; char (*original_comm)[TASK_COMM_LEN]; data.ts = bpf_ktime_get_ns(); data.sector_start = args->sector; data.nr_sectors = args->nr_sector; bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 遍历bio获取page查原始进程 struct request *rq = (struct request *)args->rq; struct bio *bio = rq->bio; while (bio != NULL) {{ struct bio_vec bvec; struct bvec_iter iter; bio_for_each_segment(bvec, bio, iter) {{ struct page *page = bvec.bv_page; original_pid = page_pid_map.lookup(&page); if (original_pid != NULL) {{ data.original_pid = *original_pid; original_comm = page_comm_map.lookup(&page); if (original_comm != NULL) {{ __builtin_memcpy(&data.original_comm, original_comm, sizeof(data.original_comm)); }} // 第三层过滤:如果指定了目标PID,只上报对应进程的IO #if {TARGET_PID} != 0 if (data.original_pid != {TARGET_PID}) {{ goto next; }} #endif events.perf_submit(args, &data, sizeof(data)); // 一个请求只要匹配到目标进程就上报,避免重复提交 goto end; }} }} next: bio = bio->bi_next; }} end: return 0; }} """ def print_event(cpu, data, size): event = b["events"].event(data) print(f"原始进程:{event.original_comm.decode()}({event.original_pid}) 执行进程:{event.comm.decode()} 时间戳:{event.ts} 起始扇区:{event.sector_start} 扇区数:{event.nr_sectors}") b = BPF(text=PROG) # 挂载脏页追踪点 b.attach_kprobe(event="account_page_dirtied", fn_name="trace_account_page_dirtied") b["events"].open_perf_buffer(print_event) print(f"监控设备号0x{TARGET_DEV:x} 目标PID:{TARGET_PID if TARGET_PID !=0 else '全部'},开始采集...") while True: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()
四、使用说明
- 先通过
lsblk -o NAME,MAJ:MIN查询目标SSD的主从设备号,比如sda的MAJ:MIN是8:0,转换为16进制设备号就是0x800 - 启动脚本时传入设备号和可选的目标PID即可,比如监控sda设备上PID为1234的进程的写IO:
python3 script.py 0x800 1234
内容的提问来源于stack exchange,提问作者Tez_Nikka
相关产品推荐
相关产品推荐

