Linux系统 mmap 存储映射

本文详细探讨了Linux系统中的mmap函数,包括设备缓存与设备内存的区别,mmap函数原型,映射属性,配置以及devmem2工具的使用。文章还深入讨论了PAT(Page Attribute Table)配置,映射的物理内存属性,以及如何调试和管理内存映射。通过对mmap的各种用例分析,揭示了内核中内存映射的工作原理和管理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设备缓存与设备内存

设备缓存位于主内存中,是驱动程序管理的一段内存区域,比如framebuffer, driver工作需要的scratch pad buffer等等。

而设备内存是位于设备上的,和设备是分不开的,比如某些设备上的FIFO,SRAM,或者显卡上的DOORBELL区等,都属于设备内存。

不同的内存映射给用户态或者内核态的时候,需要使用不同的API。驱动开发者需要保证在正确的状态下使用正确的接口将虚拟地址映射到正确的存储类型上,不能混用。

mmap函数原型: 

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
addr:推荐映射定义,如果为NULL,内核自动分配,如果flags设置了MAP_FIXED且addr合法,则映射addr开始的地址并返回。
length: length是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起,一般为PAGE整数倍,不足一内存页按一内存页处理.
prot:映射读写属性等等,期望的内存保护标志,不能与文件的打开模式冲突.
flags:私有,共享,匿名等等
    MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
    MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
    MAP_FIXED:映射addr开始的地址,如果事先存在VMA,则删除原来的在做映射
fd:对于file backend映射,需要指定文件fd,如果为匿名映射,则为-1.
offset:文件内字节从偏移处开始映射

根据内核代码,offset参数的单位是字节,内核在调用ksys_mmap_pgoff前会将其单位转换为page。内核中mmap函数的vm_pgoff来源于off >> PAGE_SHIFT,在mmap实现中可以根据此映射fd文件的不同区域。

映射属性:

映射建立后,会在进程的地址空间中创建一个struct vm_area_struct对象,来描述虚拟地址空间的信息,信息包括起始地址,区域大小,映射属性FLAG等等,FLAG包括:

VM_IO: Memory Maped I/O or similary.(MMIO).

VM_DONTCOPY: 在fork子进程的时候不要拷贝这个部分。

VM_DONTEXPAND: 不能用mremap 函数扩展映射区域。

VM_DONTDUMP:在coredump时不保存这个部分。

VM_PFNMAP: 这个比较重要,它表示这个映射针对的是PURE PFN,纯物理空间,没有struct page 对其进行管理。所以有些情况下,使用受到限制,通常设备的IO映射以及PCI BAR空间的映射,都会带有这个标志。比如AMD KFD GPU驱动中在IO映射时就设置了这个标志:

/dev/mem设备节点可以将物理内存全部映射到用户态,这里实践一把。

配置:

为了启用/dev/mem设备节点,并且BYPASS调如下的检查逻辑,需要设置内核如下的配置单,CONFIG_STRICT_DEVMEM主要是对映射范围进行安全性检查,避免将一些限制区域映射到用户态。

CONFIG_DEVMEM=y
# CONFIG_STRICT_DEVMEM is not set
# CONFIG_X86_PAT is not set

来自于LDD3的测试代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <limits.h>

int main(int argc, char **argv)
{
    char *fname;
    FILE *f;
    unsigned long offset, len;
    void *address;

    if (argc !=4
       || sscanf(argv[2],"%li", &offset) != 1
       || sscanf(argv[3],"%li", &len) != 1) {
        fprintf(stderr, "%s: Usage \"%s <file> <offset> <len>\"\n", argv[0],
                argv[0]);
        exit(1);
    }
    /* the offset might be big (e.g., PCI devices), but conversion trims it */
    if (offset == INT_MAX) {
	if (argv[2][1]=='x')
            sscanf(argv[2]+2, "%lx", &offset);
        else
            sscanf(argv[2], "%lu", &offset);
    }

    fname=argv[1];

    if (!(f=fopen(fname,"r"))) {
        fprintf(stderr, "%s: %s: %s\n", argv[0], fname, strerror(errno));
        exit(1);
    }

    address=mmap(0, len, PROT_READ, MAP_FILE | MAP_PRIVATE, fileno(f), offset);

    if (address == (void *)-1) {
        fprintf(stderr,"%s: mmap(): %s\n",argv[0],strerror(errno));
        exit(1);
    }
    fclose(f);
    fprintf(stderr, "mapped \"%s\" from %lu (0x%08lx) to %lu (0x%08lx)\n",
            fname, offset, offset, offset+len, offset+len);

    fwrite(address, 1, len, stdout);
    return 0;
}
$ sudo ./a.out /dev/mem  0x10000 0x1000|od -Ax -t x1

这段程序非常好用,基本上可以映射/dev/mem背后的所有物理内存,上面的例子不够特殊,我们以物理地址0内存为例,首先看到物理内存确实是从0开始,0物理地址映射的确实是RAM,通过内核模块获取page_address(pfn_to_page(0))地址,打印其中内容:

然后xxd直接获取设备内容如下:

ldd程序获取内容如下:

可见三种方法得到的数据完全一致。

devmem2

开源工具devmem2是一个LINUX命令行工具,用于读写物理内存地址,通过将/dev/mem映射到进程地址空间,devmem2允许用户直接访问物理内存。项目源码在如下地址:

GitHub - radii/devmem2: devmem2 - simple program to read/write from/to any location in memory.

CONFIG_X86_PAT

根据menuconfig的解释,PAT是页面属性表的简称(Page Attribute Table),功能是传统的MTRR的(Memory Type Range Register)扩展和增强,目的是对页面的CACHE访问属性进行配置,比如可用于为不同的用途(例如,可缓存,不可更改,写合并等)指定物理地址空间的不同部分。

PAT(Page Attribute Table) 是在线性地址空间中对映射的物理内存的一种缓存策略的描述,物理内存中的一个物理页框采用什么样的缓存策略,是由两方面决定的,一个是在物理地址空间进行描述的MTRR,一个是线性空间的PAT。

Write Combine 和 Cache 是什么关系 - 知乎

配置CONFIG_X86_PAT的目的是DISABLE如下的检查。

remap_pfn_range->track_pfn_remap(vma, &prot, remap_pfn, addr, PAGE_ALIGN(size));

如果映射区有设置CACHE 属性,则调用lookup_memtyp寻找:

设置文件在arch/x86/mm/pat.c这里。

PCIE 空间的配置方式:

PCIe架构定义了4种地址空间:配置空间、Memory空间、IO空间和message空间:

每个PCIe Function都有4KB的配置空间(Configuration Space)。前256 Bytes 是和PCI 兼容的配置空间(64B是HEADER),剩余的是PCIe扩展配置空间(Extended Configuration Space)。

PCIe设备声明的两种不同类型的MMIO:可预取MMIO(Prefetchable MMIO,P-MMIO)和不可预取MMIO(Non-Prefetchable MMIO,NP-MMIO)。可预取空间有两个意义十分明确的属性:

  • 读操作不存在副作用。(Reads do not have side effects)(读操作不会出发设备逻辑操作)
  • 允许写合并(Write merging is allowed)(Write Conbined.)

X86采用独立编址的方式,将memory操作与外设IO操作分开了,才有了memory空间和IO空间的区分。ARM采用统一编码的方式。

登记物理内存的CACHE属性

包括系统内存和设备内存在内,按照归属分成不同的区段,内核驱动模块可以调用reserve_memtype,传入物理地址和SIZE大小,将期望的CACH属性传递进去,函数会维护建立i一个红黑树,维护所有这些区段的CACHE属性,当驱动进行实际的映射时,会调用track_pfn_remap->lookup_memtype寻找对应的属性,将其设置在MMU的PTE页表中生效,运行机制如此。

调试PAT属性

debugfs中存在pat_memtype_list文件节点,可以调试系统中记录的物理内存区的CACHE属性:,使用如下方式查看

cat /sys/kernel/debug/x86/pat_memtype_list

可以看到,显存的CACHE属性为WC,表示可以将零散数据BUST起来,增加传输效率。

以我的集成显卡为例,BAR地址是0xb0000000,属性也是WC:

独立显卡,显存32M

对于X86架构来说,PAGE的访问属性是物理决定的层决定的,而其它架构下,比如ARM,可以自定义设置PTE的属性。

callstack

编译内核

配置结束后,参照如下博客进行编译升级

Ubuntu18.04 Linux内核编译升级_papaofdoudou的博客-优快云博客_ubuntu18.04升级内核

/dev/mem

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

papaofdoudou

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

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

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

打赏作者

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

抵扣说明:

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

余额充值