利用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函数之后会自动将文件读写位置指向本次读的文件位置末。也因此在利用readwrite函数进行读写操作时,如果先写入数据,然后立即读,此时是读不到写入数据的,因为读位置在上一次写位置的末位置,一般在读之前利用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