You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:50:24