Linux dma 需要物理地址的解决方案

背景

在linux中开发userspace驱动时,发现hdma使用只能传入物理地址,那就需要想办法将虚拟地址转为物理地址。但自己没有实践,不知道GPT和网上给出的转物理地址方式是否正确,花了一天时间来验证转为的物理地址读出来的数据是否与虚拟地址一致。
补充:
过了一段时间,我自己都没看懂为啥要这么复杂。为了便于理解,梳理一下:
1 最开始只知道/dev/mem 是通过mmap的方式进行读写的。
2 基于第1点,就一直在寻找 mmap 失败的原因,尝试解决。但是因为是系统进程的物理地址空间,始终不能mmap
3 找到了另一种访问 /dev/mem 的方式 --pread
4 基于第3点,要想使用pread访问系统进程的物理地址空间,需要修改内核的配置 CONFIG_STRICT_DEVMEM=n。因此需要根据内核源码重新编译一个kernel。
5 基于第4点,为了测试稳妥使用虚拟机内切换内核。
6 本wiki的目的是验证通过 /proc/self/pagemap 方式获取的物理地址是否正确。结论是正确的,物理地址可以使用。

环境

Host: Ubuntu20 linux 5.4.0-147-genreic
VM: Ubuntu18 更换内核后linux 5-13-10
为了支持获取CAP_SYS_RAWIO权限,安装的工具
sudo apt-get install libcap-dev

基础知识

/dev/mem

/dev/mem设备文件是一个特殊的文件,它代表了整个系统内存(包括物理内存、BIOS ROM、PCI配置空间等)。当操作系统需要使用某些硬件或进行调试时,需要读取或修改这些地址空间。而/dev/mem文件则提供了一种方便的方式来实现这一目的。

但/dev/mem的访问需要root权限,因为它可以直接读写系统内存,如果其他非特权用户也可以访问/dev/mem,那么就可能被用于恶意行为,比如读取敏感信息或者篡改系统内存中的数据。所以,CONFIG_DEVMEM选项控制是否允许非特权用户访问/dev/mem文件,从而保证系统的安全性和稳定性。
然而,访问物理内存也是很危险的,因为它可以绕过操作系统的访问控制,导致安全漏洞。因此,CONFIG_STRICT_DEVMEM可以提供一定的保护机制,避免未经授权的访问。

具体而言,当CONFIG_STRICT_DEVMEM被设置为n时,只有拥有CAP_SYS_RAWIO权限(具有root特权的用户或进程)才能够访问/dev/mem 实现时鸡肋,还是要root执行可执行文件,没有深究。普通用户或进程将无法访问该设备文件,从而保证系统的安全性。

/proc/self/pagemap

/proc/self/pagemap 是一个特殊的文件,它包含了当前进程的所有虚拟内存页面的映射信息。每个进程都有一个 /proc/<pid>/pagemap 文件,其中 <pid> 是进程的标识符。

在Linux系统中,它提供了一个进程空间的内存映射信息,包含每个虚拟内存页面(page)的相关信息。该文件的每一行都代表着一个虚拟内存页面,其中包含了一个64位的数字,其每个位都有不同的含义。

Present bit (bit 63):表示该页面是否存在于物理内存中,如果为1,则存在。

Swap bit (bit 62):表示该页面是否被交换出去到硬盘上,如果为1,则已经交换出去。

Page frame number (PFN, bits 0-54):表示该页面在物理内存中的位置,由55个bit组成,因为64GB的内存地址可以用55位来表示。如果Present bit为1,那么这些位将给出该页面在物理内存中的帧号码(frame number)。

Soft dirty bit (bit 55):表示该页面是否被软件修改过,如果为1,则已经被修改。

Exclusive bit (bit 56):表示该页面是否正在使用,如果为1,则正在使用。

File page (bit 61):表示该页面是否属于文件映射,如果为1,则属于文件映射。

Reserved bits (bits 57-60):保留未使用。

其中,PFN 是物理页帧号,指的是与该虚拟页相关联的物理页的编号。 如果这个位于虚拟地址空间的页面当前没有被加载到内存中,那么这个页面将不会有一个相关联的物理页,也就不会有一个有效的 PFN。

/proc/self/pagemap 可以用于诊断和调试进程的内存使用情况,以及对虚拟地址和物理地址之间的映射关系进行查询分析。例如,它可以帮助分析某些性能问题,例如页面错误(page faults)率、缓存命中率等。

mmap /dev/mem 的原因以及失败的原因

这点存疑,没有深究,是查的GPT,有点放屁文学。

要访问/dev/mem内容首先打开/dev/mem设备文件,并使用mmap函数将物理地址映射到虚拟地址。然后,它计算出了虚拟地址,并读取该地址中的数据。

当无法使用mmap()函数映射/dev/mem文件时,可能是由于以下原因之一:

缺少足够的权限:需要root或具有相应特权的用户才能使用mmap()函数映射/dev/mem文件。

内核设置问题:操作系统内核可能不允许使用mmap()函数映射/dev/mem文件。要解决此问题,您需要更改内核参数,并重新编译内核。

硬件兼容性问题:如果硬件不支持/dev/mem文件,则无法使用mmap()函数映射该文件。在这种情况下,您可以使用其他方法来访问物理内存。

文件损坏:如果/dev/mem文件已经损坏,那么无法使用mmap()函数映射该文件。解决此问题最简单的方法是重新安装操作系统。

建议您先检查是否具有足够的权限,如果权限正确,则可以尝试更改内核设置或检查硬件兼容性问题。如果所有这些都没用,那么您可以考虑重新安装操作系统
没深究mmap /dev/mem是需要哪些config配置,很鸡肋吧,也用不上了,有需要的可以去深究一下

代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/user.h>

#include <sys/capability.h>
#include <sys/prctl.h>

#define ALLOCATE_SIZE 4096
#define PHY_MAP 0
#define ALLIGN_PHY 0
/* summary:
	一 虚拟地址转物理地址: 核心是获取当前进程的映射页
		1 获取虚拟地址中所在的内存页--通过除内存每一页的size
		2 获取当前页在映射表中的偏移地址 -- 每一页都有一个8字节的映射项
		3 打开当前进程的页表映射文件
		4 读取当前页表项,提取页表项中包含的当前虚拟地址页对应的物理页数 -- 前提是判断当前虚拟地址是映射有对应物理地址
		5 得到物理地址 --物理地址页数 * 每一页的size + 虚拟地址在自己页中的偏移值(地址组成中这个是一致的)
	二 读取物理地址内的数据: 核心是开启内核中 CONFIG_STRICT_DEVMEM才能读出正确值
		1 打开/dev/mem
		2 pread读取偏移地址的值 -- 试过对齐页去读取,或直接物理地址偏移读取都是正确的
		3 gpt说应该将物理地址mmap之后才能访问是错误的, 不管是否开启STRICT,都不能mmap物理地址。
	三 目前测试申请地址是否锁定地址到物理地址都有效
		*/

void* create_buffer(void) {
	void *buffer = NULL;
	// Allocate some memory to manipulate
	buffer = malloc(ALLOCATE_SIZE);
	if(buffer == NULL) {
		fprintf(stderr, "Failed to allocate memory for buffer\n");
		exit(1);
   }
 
	// Lock the page in memory
	// Do this before writing data to the buffer so that any copy-on-write
	// mechanisms will give us our own page locked in memory
	if(mlock(buffer, ALLOCATE_SIZE) == -1) {
		fprintf(stderr, "Failed to lock page in memory: %s\n", strerror(errno));
		exit(1);
	}

	// Add some data to the memory
	strncpy(buffer, "hello! everone", 15*sizeof(char));
 
	return buffer;
}

unsigned long long get_phy_address(void *v_addr) {
	int i = 0;
	unsigned long long phy_address;
	unsigned long virt_address = (unsigned long)v_addr;
	off_t offset;
	unsigned long virt_pfn;
	uint64_t page_frame_number;
	uint64_t pagemap_entry;
	int is_present = 0;
	// Open the pagemap file for the current process
	int page_map_fd = open("/proc/self/pagemap", O_RDONLY);
 
	// Seek to the page that the buffer is on
	virt_pfn = virt_address >> PAGE_SHIFT;
	offset = virt_pfn * (sizeof(uint64_t));	//每一项页表的大小
	printf("在pagemap中的偏移值:%lx\n", offset);

	if (pread(page_map_fd, &pagemap_entry, sizeof(pagemap_entry), offset) != sizeof(pagemap_entry)) {
		perror("pread");
		exit(1);
	}
	page_frame_number = pagemap_entry & ((1ULL << 55) - 1);
	is_present = (pagemap_entry >> 63);              // 判断页面是否映射到物理内存中

	if (!is_present) {
		printf("虚拟地址没有对应的物理地址\n");
		exit(1);
	}
	phy_address = page_frame_number << PAGE_SHIFT;
	phy_address += (virt_address & ( (1ULL << PAGE_SHIFT) - 1));
 
	close(page_map_fd);
 
	return phy_address;
}

int uptoroot_and_capsys(void)
{
	cap_t caps;
	cap_value_t cap_list[] = {CAP_SYS_RAWIO};
	if (setuid(0) != 0) {
		perror("Failed to set uid to root");
		return 1;
	}

	// 获取CAP_SYS_RAWIO权限
	caps = cap_get_proc();
	if (caps == NULL) {
		perror("Failed to get process capabilities");
		return 1;
	}

	cap_set_flag(caps, CAP_EFFECTIVE, 1, cap_list, CAP_SET);

	if (cap_set_proc(caps) != 0) {
		perror("Failed to set process capabilities");
		return 1;
	}
	printf("set SYS_CAP_RAWIO successful\n");
	return 0;
}

void mmap_data_from_phy(unsigned long phy)
{
	void *map_base, *virt_addr;
	int page_size = 0;
	unsigned long page_base = 0;
	int mem_fd = 0;
	
	page_size = 1 << PAGE_SHIFT;
	page_base = phy & ~(page_size -1);

	mem_fd = open("/dev/mem", O_RDONLY);
	if (mem_fd < 0) {
		perror("打开/dev/mem失败");
		exit(EXIT_FAILURE);
	}
	map_base = mmap(NULL, ALLOCATE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, page_base);
	if (map_base == (void *)-1) {
		printf("无法映射地址\n");
		close(mem_fd);
		exit(EXIT_FAILURE);
	}
	virt_addr = map_base + (phy % page_size);
	printf("物理地址获取结果为: %s\n", (char*)virt_addr);

	if (munmap(map_base, ALLOCATE_SIZE) == -1) {
		printf("取消映射失败\n");
		close(mem_fd);
		exit(EXIT_FAILURE);
	}

	/* 关闭文件 */
	close(mem_fd);
}

int main(void) {
	unsigned long virt_addr = 0, page_start_addr;
	int i = 0;
	unsigned long long cal_phy_address = 0;
	int mem_fd = 0;
	int page_size = 0;
	off_t len = 0;
	char data[ALLOCATE_SIZE*2] = {0};
	
	// 分配10个整数大小的内存空间,并返回其首地址
	char* ptr = NULL;
	ptr = (char*)create_buffer();

	printf("动态分配内存的起始地址:%p\n", ptr);


	cal_phy_address = get_phy_address(ptr);

	printf("物理地址为: 0x%llx\n", cal_phy_address);

	// 计算物理地址并将文件指针移动到该位置
	page_start_addr = cal_phy_address;
	uptoroot_and_capsys();

#if PHY_MAP
	mmap_data_from_phy(page_start_addr);
#else
	// 打开/dev/mem设备文件
	mem_fd = open("/dev/mem", O_RDONLY);
	if (mem_fd < 0) {
		perror("打开/dev/mem失败");
		return 1;
	}
	
#if ALLIGN_PHY
	// 读取数据并输出
	page_size = 1 << PAGE_SHIFT;
	len =  ALLOCATE_SIZE + (page_start_addr & (page_size - 1));
	page_start_addr &= ~(page_size - 1);
	pread(mem_fd, data, len, page_start_addr);
	printf("物理地址 %lx 中的数据为 %s\n", page_start_addr, &data[len - ALLOCATE_SIZE]);
#else
	len =  ALLOCATE_SIZE;
	pread(mem_fd, data, len, page_start_addr);
	printf("物理地址 %lx 中的数据为 %s\n", page_start_addr, data);
#endif
	printf("虚拟地址数据: %s\n", ((char*)ptr));
#endif

	// 释放内存并关闭文件
	free(ptr);
#if !PHY_MAP
	close(mem_fd);
#endif

	return 0;
}

修改CONFIG_STRICT_DEVMEM=n后替换虚拟机内核执行结果

执行结果

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_三剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值