使用pagemap工具查看某个进程虚拟地址实际映射的物理内存大小

背景

项目中出现物理内存用完导致OOM。通过查看主进程的虚拟内存,发现创建了很多大数组,使用了大量虚拟地址空间。
但我们想查看这些虚拟地址是否全部映射到了物理内存。

源码地址

github-pagemap

介绍

将源码下载后,得到如下文件
请添加图片描述

如果是在64位机器(如虚拟机)运行,则直接make即可。
其中,pagemap.c需要传入pid,起始虚拟地址,结束虚拟地址;得出一部分的映射情况,而pagemap2.c只需要传入pid,即可得出此进程的所有虚拟地址映射情况。

但我要在32位ARM开发板运行,需要用到交叉编译工具链,所以需要改一点源码和Makefile

文件名不太直观,我将pagemap.c改成pagemap_address.c,将pagemap2.c改成pagemap_all.c

因为pagemap_all.c已经包含pagemap_address.c的部分,所以直接用pagemap_all.c即可。

修改后的源码-pagemap_all.c

#define _POSIX_C_SOURCE 200809L
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h> 
#define PAGE_SIZE 0x1000

#define FIND_LIB_NAME

static void print_page(uint64_t address, uint64_t data,
    const char *lib_name) {

    printf("0x%-16llx : pfn %-16llx soft-dirty %ld file/shared %lld "
        "swapped %lld present %ld library %s\n",
        address,
        data & 0x7fffffffffffff,
        (data >> 55) & 1,
        (data >> 61) & 1,
        (data >> 62) & 1,
        (data >> 63) & 1,
        lib_name);
}

void handle_virtual_range(int pagemap, uint64_t start_address,
    uint64_t end_address, const char *lib_name) {

    for(uint64_t i = start_address; i < end_address; i += 0x1000) {
        uint64_t data;
        uint64_t index = (i / PAGE_SIZE) * sizeof(data);
        if(pread(pagemap, &data, sizeof(data), index) != sizeof(data)) {
            if(errno) perror("pread");
            break;
        }

        print_page(i, data, lib_name);
    }
}

void parse_maps(const char *maps_file, const char *pagemap_file) {
    int maps = open(maps_file, O_RDONLY);
    if(maps < 0) return;

    int pagemap = open(pagemap_file, O_RDONLY);
    if(pagemap < 0) {
        close(maps);
        return;
    }

    char buffer[BUFSIZ];
    int offset = 0;

    for(;;) {
        ssize_t length = read(maps, buffer + offset, sizeof buffer - offset);
        if(length <= 0) break;

        length += offset;

        for(size_t i = offset; i < (size_t)length; i ++) {
            uint64_t low = 0, high = 0;
            if(buffer[i] == '\n' && i) {
                size_t x = i - 1;
                while(x && buffer[x] != '\n') x --;
                if(buffer[x] == '\n') x ++;
                size_t beginning = x;

                while(buffer[x] != '-' && x+1 < sizeof buffer) {
                    char c = buffer[x ++];
                    low *= 16;
                    if(c >= '0' && c <= '9') {
                        low += c - '0';
                    }
                    else if(c >= 'a' && c <= 'f') {
                        low += c - 'a' + 10;
                    }
                    else break;
                }

                while(buffer[x] != '-' && x+1 < sizeof buffer) x ++;
                if(buffer[x] == '-') x ++;

                while(buffer[x] != ' ' && x+1 < sizeof buffer) {
                    char c = buffer[x ++];
                    high *= 16;
                    if(c >= '0' && c <= '9') {
                        high += c - '0';
                    }
                    else if(c >= 'a' && c <= 'f') {
                        high += c - 'a' + 10;
                    }
                    else break;
                }

                const char *lib_name = 0;
#ifdef FIND_LIB_NAME
                for(int field = 0; field < 4; field ++) {
                    x ++;  // skip space
                    while(buffer[x] != ' ' && x+1 < sizeof buffer) x ++;
                }
                while(buffer[x] == ' ' && x+1 < sizeof buffer) x ++;

                size_t y = x;
                while(buffer[y] != '\n' && y+1 < sizeof buffer) y ++;
                buffer[y] = 0;

                lib_name = buffer + x;
#endif

                handle_virtual_range(pagemap, low, high, lib_name);

#ifdef FIND_LIB_NAME
                buffer[y] = '\n';
#endif
            }
        }
    }

    close(maps);
    close(pagemap);
}


void process_pid(pid_t pid) {
    char maps_file[BUFSIZ];
    char pagemap_file[BUFSIZ];
    snprintf(maps_file, sizeof(maps_file),
        "/proc/%lu/maps", pid);
    snprintf(pagemap_file, sizeof(pagemap_file),
        "/proc/%lu/pagemap", pid);

    parse_maps(maps_file, pagemap_file);
}

int main(int argc, char *argv[]) {
    if(argc < 2) {
        printf("Usage: %s pid1 [pid2...]\n", argv[0]);
        return 1;
    }

    for(int i = 1; i < argc; i ++) {
        pid_t pid = (pid_t)strtoul(argv[i], NULL, 0);

        printf("=== Maps for pid %d\n", (int)pid);
        process_pid(pid);
    }

    return 0;
}

makefile

# Makefile for pagemap

CROSS_COMPILE = arm-linux-gnueabihf-

STRIP = $(CROSS_COMPILE)strip
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
CFLAGS = -std=c99

.PHONY: all
all: pagemap_address pagemap_all

pagemap_address: pagemap_address.c
	$(CC) $(CFLAGS) $^ -o $@
pagemap_all: pagemap_all.c
	$(CC) $(CFLAGS) $^ -o $@

.PHONY: clean
clean:
	-rm pagemap_address pagemap_all

执行make后,生成可执行文件pagemap_all,将其拷贝至开发板。

测试

此测试代码创建了4个线程,每个线程分配不同的栈空间,内部创建不同大小的数组。
先注释了memset函数,验证默认情况下创建的数组未使用的情况下是否会占用物理内存。

测试代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define NUM_THREADS 4

void* grow_stack(void* arg)
{
    int thread_num = *((int*)arg);
    int size = (thread_num + 1) * 1024 * 1024; // 1MB, 2MB, 3MB, 4MB
    char big_array[size];
    //memset(big_array, 0, size * sizeof(char));
    while (1)
    {
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_nums[NUM_THREADS];

    // Stack sizes in MB
    int stack_sizes[NUM_THREADS] = {3, 4, 6, 8};

    for (int i = 0; i < NUM_THREADS; i++)
    {
        pthread_attr_t attr;
        size_t stack_size = stack_sizes[i] * 1024 * 1024;
        thread_nums[i] = i;

        pthread_attr_init(&attr);
        pthread_attr_setstacksize(&attr, stack_size);

        pthread_create(&threads[i], &attr, grow_stack, &thread_nums[i]);
        pthread_attr_destroy(&attr);
    }
    for (int i = 0; i < NUM_THREADS; i++)
    {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

Makefile

TARGET := memset_test

CROSS_COMPILE = arm-linux-gnueabihf-

STRIP = $(CROSS_COMPILE)strip
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar

CFLAGS += -fPIC -Wall -std=gnu99
CFLAGS += -DLinux -D_GNU_SOURCE
CFLAGS += -g -rdynamic -funwind-tables -ffunction-sections

LDFLAGS := -lpthread
OBJS += main.o

all: $(TARGET)
$(TARGET):$(OBJS)
	$(CC) -o $@ $^ $(LDFLAGS)
    
clean:
	-rm $(OBJS) $(TARGET) -f

执行make后,生成可执行文件memset_test,将其拷贝至开发板。

运行

后台运行memset_test,输入ps得到它的pid后,再运行pagemap_all获取内存映射情况。

# ./memset_test &
# ps
	199 ttymxc0  00:00:00 memset_test
# ./pagemap_all 199 > 1.txt

分析结果

打开刚才生成的文件1.txt,内容如下
在这里插入图片描述

其中,第一列代表虚拟地址,第二列代表映射的物理地址。每一行为一个内存页,也就是4k大小。
其中,pfn 0 代表未映射,也就是未占用物理内存的虚拟地址空间。

通过使用Notepad++的计数功能分别搜索pfnpfn 0,如下
在这里插入图片描述

得到如下结果:未使用memset

total:5790
free:5564
use:5790-5564=226 -> 904k

从以上结果可以得出此进程实际占用了904k的空间。
但我们测试程序中4个线程分别创建了1M,2M,3M,4M的数组。明显未实际占用到物理内存中。

修改测试代码,将数组memset为0

将之前注释的此行放开,重新编译运行

memset(big_array, 0, size * sizeof(char));

重复以上测试步骤,重新计算
得到如下结果:使用memset

total:5790
free:3008
use:2782 -> 11128k

和各线程创建的数组大小差不多,说明创建的数组已经全部实际占用到物理内存中。

总结

使用pagemap工具不仅仅是看某个进程占用的物理内存,还可以搭配/proc/pid/maps更详细得看到某块虚拟地址空间的映射情况,还可以提供有关每个虚拟内存页的其他信息。

参考链接

github-pagemap
揭示/proc/pid/pagemap的力量:在Linux中将虚拟地址映射到物理地址

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个简单的C++代码示例,用于将/proc/pid/pagemap映射到内存中并获取一个地址以判断其是否为物理地址或虚拟地址: ```c++ #include <iostream> #include <fstream> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> using namespace std; int main() { // 获取进程ID pid_t pid = getpid(); // 构建/proc/pid/pagemap文件的路径 char path[100]; sprintf(path, "/proc/%d/pagemap", pid); // 打开/proc/pid/pagemap文件 int fd = open(path, O_RDONLY); if (fd == -1) { cout << "无法打开/proc/pid/pagemap文件" << endl; return -1; } // 获取一个地址,这里以0地址为例 void *addr = (void*)0; // 计算地址在pagemap中的索引 off_t offset = (off_t)addr / getpagesize() * sizeof(uint64_t); // 将pagemap映射到内存中 uint64_t *pagemap = (uint64_t*)mmap(NULL, sizeof(uint64_t), PROT_READ, MAP_PRIVATE, fd, offset); if (pagemap == MAP_FAILED) { cout << "无法映射/proc/pid/pagemap文件" << endl; return -1; } // 判断是否为物理地址 if (*pagemap & (1ULL<<63)) { cout << "物理地址" << endl; } else { cout << "虚拟地址" << endl; } // 解除pagemap映射 munmap(pagemap, sizeof(uint64_t)); // 关闭/proc/pid/pagemap文件 close(fd); return 0; } ``` 在上面的代码中,我们首先获取当前进程的ID,然后构建/proc/pid/pagemap文件的路径。接着打开该文件,并获取一个地址(这里以0地址为例)。然后我们计算该地址在pagemap中的索引,并将pagemap映射到内存中。我们可以通过判断pagemap中的第63位来确定该地址是物理地址还是虚拟地址。最后我们需要解除pagemap映射并关闭/proc/pid/pagemap文件。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值