task_struct进程链表遍历:用户态程序问题排查与结束判断优化
优雅解决用户态遍历进程链表的终止问题
嘿,我来帮你搞定这个问题!首先得搞清楚内核里for_each_process的核心逻辑:它依赖的是循环链表的特性——当遍历指针回到链表起点(也就是init_task的地址)时,就代表整个链表遍历完成了。这才是最可靠、最优雅的终止条件,完全不需要依赖mmap失败这种不可靠的判断(毕竟mmap失败可能有各种其他原因,不一定是遍历到了尽头)。
内核里的终止逻辑参考
内核的进程链表是双向循环链表,for_each_process的本质实现大概是这样的:
#define for_each_process(p) \ for (p = &init_task ; (p = next_task(p)) != &init_task ; )
看到没?当指针回到&init_task就终止循环,这才是最准确的方式,我们用户态程序直接照搬这个逻辑就行。
对你的代码的优化建议
你最后版本的代码已经尝试加入了ret!= 0xc1a8cac0的判断,但还有几个可以优化的点,同时修正一些潜在问题:
1. 用「回到init_task地址」作为唯一终止条件
把这个作为循环的终止判断,彻底抛弃依赖mmap失败或者返回0的逻辑,和内核保持一致,稳定性拉满。
2. 减少不必要的系统调用
你的readtaskstruct和readval每次都打开关闭/dev/mem,这会带来额外的系统开销,建议只打开一次,用完再关闭。
3. 别硬编码偏移量!
你代码里的620、996这些是特定内核版本的task_struct成员偏移,换个内核版本就失效了。可以用pahole工具(来自dwarves包)获取当前内核的准确偏移:
pahole -C task_struct /usr/lib/debug/lib/modules/$(uname -r)/vmlinux | grep -E "(tasks|comm)"
这样能避免内核版本变动导致程序崩溃。
4. 修复内存泄漏
你的readval里用calloc分配了内存,但从来没释放,运行久了会内存泄漏,记得用完后free。
优化后的最终代码
我给你重构了代码,解决了上面的问题:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> // 替换成你用pahole获取的当前内核的准确偏移 #define OFFSET_TASKS_NEXT 620 #define OFFSET_COMM 996 // 替换成你系统里init_task的实际地址 #define INIT_TASK_ADDR 0xc1a8cac0 static int mem_fd = -1; // 初始化/dev/mem文件描述符,只打开一次 int init_mem_fd() { if (mem_fd == -1) { // 只读权限足够,不需要写权限 mem_fd = open("/dev/mem", O_RDONLY | O_SYNC); if (mem_fd == -1) { perror("Failed to open /dev/mem"); return -1; } } return 0; } // 读取指定虚拟地址的long值 long read_long(long virt_addr) { void *map; long page_size = getpagesize(); // 取页起始地址 long page_virt = virt_addr & ~(page_size - 1); // 页内偏移 long offset = virt_addr & (page_size - 1); long val = 0; // 内核虚拟地址转物理地址(32位内核直接减去0xc0000000,64位需要调整) long phys_addr = page_virt - 0xc0000000; map = mmap(NULL, page_size, PROT_READ, MAP_SHARED, mem_fd, phys_addr); if (map == MAP_FAILED) { perror("mmap failed"); return 0; } memcpy(&val, map + offset, sizeof(long)); munmap(map, page_size); return val; } // 读取进程名,返回的字符串需要调用者free char* read_process_comm(long task_virt_addr) { void *map; long page_size = getpagesize(); long comm_virt = task_virt_addr + OFFSET_COMM; long page_virt = comm_virt & ~(page_size - 1); long offset = comm_virt & (page_size - 1); // comm字段长度是16字节,不用分配100字节 char* comm = calloc(1, 17); if (!comm) return NULL; long phys_addr = page_virt - 0xc0000000; map = mmap(NULL, page_size, PROT_READ, MAP_SHARED, mem_fd, phys_addr); if (map == MAP_FAILED) { perror("mmap failed"); free(comm); return NULL; } memcpy(comm, map + offset, 16); munmap(map, page_size); return comm; } int main(void) { if (init_mem_fd() == -1) { return 1; } long current_task = INIT_TASK_ADDR; // 循环遍历,回到init_task就停止,和内核逻辑一致 do { char* comm = read_process_comm(current_task); if (comm) { printf("Process: %s (Task addr: 0x%lx)\n", comm, current_task); free(comm); } // 读取tasks.next指针,转换为task_struct的起始地址 long tasks_next = read_long(current_task + OFFSET_TASKS_NEXT); current_task = tasks_next - OFFSET_TASKS_NEXT; } while (current_task != INIT_TASK_ADDR); close(mem_fd); return 0; }
额外注意事项
- 必须以root权限运行这个程序,普通用户没法打开
/dev/mem。 - 如果是64位内核,虚拟地址转物理地址的逻辑会不一样,不能直接减
0xc0000000,需要参考你内核的内存布局。 - 每次内核版本更新后,都要重新用
pahole获取task_struct的成员偏移,硬编码的偏移很容易失效。
内容的提问来源于stack exchange,提问作者dev
相关产品推荐
相关产品推荐

