利用pagemap获取虚拟地址对应物理地址
利用linux虚拟文件系统获取虚拟地址对应物理地址,同时,借由fork()机制和全局局部变量在虚拟内存中的位置分析写时拷贝技术。
一、pagemap相关知识
Linux文件目录中的/proc记录着当前进程的信息,称其为虚拟文件系统。在/proc下有一个链接目录名为self,这意味着哪一个进程打开了它,self中存储的信息就是所链接进程的。self中有一个名为pagemap的文件,专门用来记录所链接进程的物理页号信息。这样通过/proc/pid/pagemap文件,允许一个用户态的进程查看到每个虚拟页映射到的物理页。
/proc/pid/pagemap中的每一项都包含了一个64位的值,这个值内容如下所示。每一项的映射方式不同于真正的虚拟地址映射,其文件中遵循独立的对应关系,即虚拟地址相对于0x0经过的页面数是对应项在文件中的偏移量
* /proc/pid/pagemap. This file lets a userspace process find out which
physical frame each virtual page is mapped to. It contains one 64-bit
value for each virtual page, containing the following data (from
fs/proc/task_mmu.c, above pagemap_read):
* Bits 0-54 page frame number (PFN) if present//present为1时,bit0-54表示物理页号
* Bits 0-4 swap type if swapped
* Bits 5-54 swap offset if swapped
* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
* Bit 56 page exclusively mapped (since 4.2)
* Bits 57-60 zero
* Bit 61 page is file-page or shared-anon (since 3.5)
* Bit 62 page swapped
* Bit 63 page present//如果为1,表示当前物理页在内存中;为0,表示当前物理页不在内存中
根据上表,我们首先启动某个进程,获取该进程的进程号pid,然后打开/proc/pid/pagemap这个文件,读取其内容(这个文件比较大,正常在命令行中无法打开,因为每一个虚拟地址都有一条类似上表的记录)
/*
operating environment:
Ubuntu 18.04.2 LTS
gcc version 7.5.0
程序功能:输入进程pid和虚拟地址,计算对应物理地址
本程序提供自身pid号 一个全局变量a 和一个局部变量b ,可以输入本程序自身pid和相应虚拟地址进行查询
也可以使用其他程序的pid和虚拟地址
程序必须在root权限下运行,否则读取到的pagemap数据为全0
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
//计算虚拟地址对应的地址,传入虚拟地址vaddr,通过paddr传出物理地址
int mem_addr(pid_t pid, unsigned long vaddr, unsigned long *paddr)
{
int pageSize = getpagesize(); //调用此函数获取系统设定的页面大小,通常为4k
unsigned long v_pageIndex = vaddr / pageSize; //计算此虚拟地址相对于0x0的经过的页面数
unsigned long v_offset = v_pageIndex * sizeof(uint64_t); //计算在/proc/pid/pagemap文件中的偏移量
unsigned long page_offset = vaddr % pageSize; //计算虚拟地址在页面中的偏移量
uint64_t item = 0; //存储对应项的pagemap值
unsigned long total_page = 64 * 1024 * 1024 * 1024;
int present_page = 0;
char path [0x100] = {};
sprintf(path, "/proc/%u/pagemap", pid); //生成进程pid对应pagemap的路径
int fd = open(path, O_RDONLY); //以只读方式打开/proc/pid/pagemap
if(fd == -1){ //判断是否打开失败
printf("open /proc/self/pagemap error\n");
return -1;
}
if(lseek(fd, v_offset, SEEK_SET) == -1){ //移动到pagemap相应的位置,即存储虚拟页对应的信息位置
printf("lseek error\n");
return -2;
}
if(read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t)){ //读取对应项的值,并存入item中,且判断读取数据位数是否正确
printf("read item error\n");
return -3;
}
if((((uint64_t)1 << 63) & item) == 0){ //判断present是否为0,即该页是否被换出内存
printf("page present is 0\n");
return -4;
}
uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item; //计算物理页号,即取item的bit0-54
*paddr = (phy_pageIndex * pageSize) + page_offset; //再加上页内偏移量就得到了物理地址
printf("pid virtual addr v page index page offset pagemap item phy page index physical addr\n");
printf("%-10d%-18lx%-16lx%-18lx%-24lx%-20lx%-20lx\n", pid, vaddr, v_pageIndex, page_offset, item, phy_pageIndex, *paddr);
return 0;
}
// global variable a
int a = 1;
int main()
{
printf("Please run in admin mode\n");
printf("Use CTRL+C to exit\n\n");
// local variable a
int b = 1;
printf("my pid = %d\nglobal var 'a' virtual addr = 0x%lx\nlocal var 'b' virtual addr = 0x%lx\n",getpid(),&a,&b);
pid_t pid = -1; //pid
unsigned long vaddr = 0; //虚拟地址
unsigned long phy = 0; //物理地址
while(1){
printf("\ninput the pid: ");
scanf("%d",&pid);
printf("input the vitual addr: ");
scanf("%lx", &vaddr);
if(mem_addr(pid, vaddr, &phy) != 0) {
printf("v_addr to phy_addr failure\n");
return -1;
}
}
return 0;
}
程序主要声明了一个全局变量和一个局部变量,用以返回出其虚拟地址以供在pagemap文件中查找(虚拟地址相当于数组下标,对应值即是pagemap项),然后如果该地址在物理地址中存在即present位1,0~53位即是该虚拟地址对应的物理页号,结合物理页号和虚拟地址的偏移量(低12位,即4k)即可得到虚拟地址对应的物理地址。
二、文件读写函数
为了找出该进程实际用了多少内存空间(实际在物理内存中存在),即上述pagemap单项中present值为1的项有多少。最容易想到的自然是遍历(下一节会解释该方法的不可行性,或者不建议这么做的原因)。
在源代码中重新打开pagemap文件,循环遍历,初始化总项数为1024 * 1024(4g/4k,一共这么多个虚拟页表)。写出如下代码,该代码错误之处很多,下面解释。
int fd = open(path, O_RDONLY); //以只读方式打开/proc/pid/pagemap
if(fd == -1){ //判断是否打开失败
printf("open /proc/self/pagemap error\n");
return -1;
}
while (total_page--)
{
if(lseek(fd, sizeof(uint64_t), SEEK_SET) == -1){ //移动到pagemap相应的位置,即存储虚拟页对应的信息位置
printf("lseek error\n");
return -2;
}
if(read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t)){ //读取对应项的值,并存入item中,且判断读取数据位数是否正确
printf("read item error\n");
return -3;
}
if((((uint64_t)1 << 63) & item) == 1){ //判断present是否为0,即该页是否被换出内存
printf("page present is 1, count++\n");
present_page++;
}
printf("left %d pages to read\n", total_page);
printf("current item %-24lx\n", item);
printf("\n");
}
首先是lseek函数,该函数用来调整当前文件读写位置,参数意义如下:
fd:文件描述符
offset:偏移量
whence:位置
SEEK_SET:The offset is set to offset bytes. offset为0时表示文件开始位置。
SEEK_CUR:The offset is set to its current location plus offset bytes. offset为0时表示当前位置。
SEEK_END:The offset is set to the size of the file plus offset bytes. offset为0时表示结尾位置
因此上述代码错误主要在于第三个参数的设置,应该设为SEEK_CUR,每次从上次读的文件末去读。
除此之外,read函数也会改变当前文件读写位置,因此如果上述代码只改lseek函数就相当于读一个item跳一个item。总结一下,如果想要连续读item,其实只需要一直使用read函数即可,每次调用read函数之后会自动将文件读写位置指向本次读的文件位置末。也因此在利用read,write函数进行读写操作时,如果先写入数据,然后立即读,此时是读不到写入数据的,因为读位置在上一次写位置的末位置,一般在读之前利用lseek函数调整当前文件读写位置至合适。
三、64位机地址空间
解决完了一些函数使用上的错误后,重新执行代码,结果present的总虚拟页数还是0,继续找原因,稍微计算一下1024 x 1024的值明显比下图中虚拟页表值要小的多。
按4g虚拟内存来算,每个页表4k,应该是有1024 x 1024项,但是现在不匹配,在算一下4g的虚拟内存是 2^32 即32位,换算成16进制也是8位,而图中的虚拟地址却是48位。然后就想到了4g虚拟内存是对于32位机来说的,对于64位机,实际用了48位,也就是2^48寻址空间,也就是256T的虚拟内存空间,页表单位仍是4k,除下来2^48 / 2^12 = 36位,换算成16进制是9位,与图中相符。关于64位机的地址空间其实也不是都有用,详情可以查阅相关资料,这里不再深究。
然后我们再算一下,2^36 = 1024 * 1024 * 1024 * 3,中间我跑了一下1024 * 1024 * 3的遍历,用了约18分钟。然后再乘上1024,大约需要几天才能遍历完,这也是之前提到的遍历这种策略不可行的原因(其实还可以根据/proc/pid/maps文件找出实际使用的地址空间,在其中遍历,能够大大缩小遍历范围,但是不再考虑,下面来看最终的实现方法)
四、/proc/pid/status文件
status文件的意义如下
"VmPeak:\t%8lu kB\n"------------------------------------虚拟内存使用量的峰值,取mm->total_vm和mm->hiwater_vm的大值。
"VmSize:\t%8lu kB\n"------------------------------------当前虚拟内存的实际使用量。
"VmLck:\t%8lu kB\n"-------------------------------------PG_mlocked属性的页面总量,常被mlock()置位。
"VmPin:\t%8lu kB\n"-------------------------------------不可被移动的Pined Memory内存大小。
"VmHWM:\t%8lu kB\n"-------------------------------------HWM是High Water Mark的意思,表示rss的峰值。
"VmRSS:\t%8lu kB\n"-------------------------------------应用程序实际占用的物理内存大小,这里和VmSize有区别。VmRss要小于等于VmSize。
"RssAnon:\t%8lu kB\n"-----------------------------------匿名RSS内存大小。
"RssFile:\t%8lu kB\n"-----------------------------------文件RSS内存大小。
"RssShmem:\t%8lu kB\n"----------------------------------共享内存RSS内存大小。
"VmData:\t%8lu kB\n"------------------------------------程序数据段的所占虚拟内存大小,存放了初始化了的数据。
"VmStk:\t%8lu kB\n"-------------------------------------进程在用户态的栈大小。
"VmExe:\t%8lu kB\n"-------------------------------------进程主程序代码段内存使用量,即text段大小。
"VmLib:\t%8lu kB\n"-------------------------------------进程共享库内存使用量。
"VmPTE:\t%8lu kB\n"-------------------------------------进程页表项Page Table Entries内存使用量。
"VmPMD:\t%8lu kB\n"-------------------------------------进程PMD内存使用量。
"VmSwap:\t%8lu kB\n",-----------------------------------进程swap使用量。
下面程序实现对status文件的解析,打印输入pid程序的虚拟内存,实际物理内存占用:
int get_memory_by_pid(pid_t pid)
{
FILE *fd;
char file[64] = {0};
char line_buff[256] = {0};
char name_VmRSS[32];
char name_VmSize[32];
char name_VmSwap[32];
int i, VmRSS, VmSize, VmSwap = 0;
sprintf(file, "/proc/%d/status", pid);
// 以R读的方式打开文件再赋给指针fd
fd = fopen(file, "r");
if(fd==NULL)
{
return -1;
}
// 读取VmRSS这一行的数据
for (int i = 0; i < 40; i++)
{
if (fgets(line_buff, sizeof(line_buff), fd) == NULL)
{
printf("read error!");
break;
}
if (strstr(line_buff, "VmRSS") != NULL)
{
sscanf(line_buff, "%s %d", name_VmRSS, &VmRSS);
continue;
}
if (strstr(line_buff, "VmSize") != NULL)
{
sscanf(line_buff, "%s %d", name_VmSize, &VmSize);
continue;;
}
if (strstr(line_buff, "VmSwap") != NULL)
{
sscanf(line_buff, "%s %d", name_VmSwap, &VmSwap);
continue;;
}
}
printf("当前使用的物理内存:\n");
fprintf (stderr, "====%s: %d kB ====\n", name_VmRSS, VmRSS);
printf("当前使用的虚拟内存:\n");
fprintf (stderr, "====%s: %d kB====\n", name_VmSize, VmSize);
printf("进程swap使用量:\n");
fprintf (stderr, "====%s: %d kB====\n", name_VmSwap, VmSwap);
fclose(fd);
return 0;
}
参考:https://blog.csdn.net/kongkongkkk/article/details/74366200