背景
项目中出现物理内存用完导致OOM。通过查看主进程的虚拟内存,发现创建了很多大数组,使用了大量虚拟地址空间。
但我们想查看这些虚拟地址是否全部映射到了物理内存。
源码地址
介绍
将源码下载后,得到如下文件
如果是在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++的计数功能分别搜索pfn和pfn 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更详细得看到某块虚拟地址空间的映射情况,还可以提供有关每个虚拟内存页的其他信息。