内存DMA及设备内存控制详解

news2024/11/20 10:23:25

序言

对于PCIe 设备(PCIe Endpoint)来说,其和CPU CORE、DRAM 的交互,主要涉及两种类型的内存访问:

  1. 设备内存访问:PCIe 设备的 Device Memory(设备内存)的访问,例如CPU 需要读写配置 PCIe 网卡或显卡的 寄存器(设备内存)。发起者是CPU,响应者是设备。
  2. DMA内存访问:PCIe 设备需要 DMA 读写 主机的 DRAM 内存,例如 网卡收到数据报文后,需要将其上报主机,则通过 PCIe Endpoint 中的 DMA 控制器 进行 DMA 写 主机的 DRAM 内存。发起者是设备DMA控制器,响应者主机内存DRAM,不需要CPU的参与。

以CPU CORE 发起设备内存读访问为例。从上图的左侧开始,CPU CORE 执行程序指令时,发出内存读写访问的地址是虚拟地址,接着MMU(内存管理单元或称为地址翻译单元更好) 将该虚拟地址转换为物理地址。如果物理地址访问的数据不在Cache中,则通过 Root Complex 中的 host bridge,将物理地址转换为总线地址,最后基于总线地址访问设备内存空间(Device Memory)。本文将基于上图的计算机系统架构,追本溯源,详细的解释设备内存 和 DMA内存 的访问细节。

重要概念

对于本文架构图中涉及的主要概念解释如下,如果不想被概念搞晕,建议暂时滤过,等不清楚时再回查即可。

System Bus:系统总线,用于CPU Package 内部子系统之间的连接,例如连接L3 Cache 和 Root Complex。

Cache:X86 的 Cache 通常分为三级,各个CPU CORE 有自己的 L1和L2 Cache,所有CPU CORE 共享 L3 Cache。Intel I7 L3 为 8MB,L2 256KB,L1 共64KB,所以Cache Size从 L1 到L3是递增。对于程序指令或数据都可以加载到Cache,加载的单位是 Cache Line,通常为 64B。

Root Complex:其中包括了两个重要组件,一个是 PCIe Root Bridge,通常称为 Host Bridge,其 Bus:Device. Function 为 00:00.0;另一个是 iommu,其用于将设备(如PCIe Endpoint 网卡)发起DMA读写DRAM使用的总线地址,转换为DRAM内存的 物理地址。

Memory Controller:DRMA 内存控制器,通常包含在CPU package 中,用于控制对内存的访问。

PCIe bus:PCIe 总线,用于连接 Host Bridge 到 PCIe Switch,PCIe Bridge 以及 PCIe Endpoint。

DMA Controller:DMA 控制器,其通常位于 PCIe Endpoint 内,用于设备对 DRAM 中数据进行 ”DMA 读”或“DMA 写”。

Device Memory:设备内存,用于对设备进行控制,如配置设备、获取设备状态等;在系统对PCIe 总线进行深度优先扫描时,会根据设备的 Bar 空间大小(Device Memory Size,如下面的网卡例如,Bar 0,2,4 Size 分别为64K,1M,8K),分配 iomem 中映射的物理地址,下文会提到 iomem 的细节。

内存访问

如前面所所述,计算机内部主要涉及两种内存访问。一种是 物理内存DRAM,另一种是 设备内存(如网卡,显卡等PCIe设备)。

内存访问方向

以上两种类型内存的访问,主要包括上图中三个方向:

  1. CPU -> DRAM: 从CPU 读写物理内存中的数据或程序指令;
  2. Device DMA -> DRAM: 设备的DMA 控制器发出针对DRAM的 DMA 读写,例如网卡从物理内存中DMA读取数据包进行发包。
  3. CPU -> Device Memory: 主机CPU 发出针对设备的读写,例如主机需要初始化设置显卡寄存器,让其开始工作。

内存访问过程

A. CPU访问物理内存(CPU -> DRAM)

  1. CPU使用虚拟地址发出内存读写请求
  2. MMU将虚拟地址转为DRAM的物理地址
  3. 如果访问数据的物理地址在Cache 有缓存数据,则直接从Cache 读取即可
  4. 如果Cache 中没有缓存,则通过内存控制器,访问DRMA(当然这涉及到页表的管理,不在本文的讨论范围),如果有兴趣,强烈建议深入阅读《深入理解计算机系统》

B. 设备访问物理内存(Device DMA -> DRAM)

  1. 外设使用总线地址,发起针对DRAM的DMA读写访问
  2. IOMMU负责将总线地址转为物理地址
  3. 通过内存控制器,访问DRMA

C. 设备内存访问(CPU -> Device Memory)

  1. CPU使用虚拟地址发出内存读写请求
  2. MMU将虚拟地址转为设备内存的物理地址(通过 cat /proc/iomem 可以查看到)
  3. Host Bridge 将物理地址转为总线地址
  4. 通过PCIe总线寻址到设备,并进行设备内存读写

总结:访问物理内存时,可以认为 虚拟地址 和 总线地址 都是Virtual的地址,都需要通过一个地址转换硬件(CPU侧是MMU,device侧是IOMMU),将虚拟地址转换为物理内存DRAM的实际物理地址。而访问设备内存时,通过Host Bridge将物理地址转换为设备内存的总线地址。

地址空间

虚拟地址:CPU Virtual Address Space(VA)

虚拟地址 被主机CPU用于访问物理内存,或者设备(PCIe EP Device)的 Bar 空间的内存(寄存器)。每个进程都有相同的虚拟地址空间(例如32位Linux系统,最大支持4G的地址空间,但只有高地址3GB用于进程的虚拟地址空间,低地址或顶端的1G给内核使用)。CPU执行指令访问内存使用的地址是虚拟地址,然后通过MMU、TLB 及 Page Tables 将虚拟地址转换为内存的物理地址。

物理地址:CPU Physical Address Space(PA)

我们可以认为物理地址空间,就是所有硬件的内存映射空间(MMIO- memory map I/O)。可将物理内存RAM看成一种特殊的MMIO空间。OS在给硬件设备编址时,其会看到物理内存 以及设备内存(通过PCIe Bar 空间映射的设备寄存器),所以进行了统一的物理地址的编址。如下,低地址给RAM使用,而PCI 设备内存通常位于高地址。

物理地址空间的编址 可以通过 cat /proc/iomem 查看:

[root@node2 ~]# cat /proc/iomem
00000000-00000fff : Reserved
00001000-0005cfff : System RAM   // System RAM:物理内存
0005d000-0005dfff : Reserved
0005e000-0009ffff : System RAM
......
00100000-3fffffff : System RAM         // Kernel Space
  01000000-01c00ea0 : Kernel code
  01e00000-02214fff : Kernel rodata
  02400000-026294bf : Kernel data
  02893000-02bfffff : Kernel bss
......
8f800000-dfffffff : PCI Bus 0000:00   // PCI Root Bridge Bus 0 下挂PCIe 设备内存的总大小
  8f800000-8f9fffff : PCI Bus 0000:02
  8fa00000-8fbfffff : PCI Bus 0000:02
  8fc00000-8fdfffff : PCI Bus 0000:04
  8fe00000-8fffffff : PCI Bus 0000:04
  90000000-9fffffff : 0000:00:02.0
    90000000-902fffff : BOOTFB
  a0000000-a02fffff : PCI Bus 0000:01  // PCIe bus 01 下挂PCIe 设备内存的总大小
    a0000000-a00fffff : 0000:01:00.1   // PCIe 网卡 Bar 2 空间(设备内存)映射的物理内存
      a0000000-a00fffff : bnxt_en
    a0100000-a01fffff : 0000:01:00.0
      a0100000-a01fffff : bnxt_en
    a0200000-a020ffff : 0000:01:00.1  // PCIe 网卡 Bar 0 空间(设备内存)映射的物理内存
      a0200000-a020ffff : bnxt_en
    a0210000-a021ffff : 0000:01:00.0
      a0210000-a021ffff : bnxt_en
    a0220000-a0221fff : 0000:01:00.1  // PCIe 网卡 Bar 1 空间(设备内存)映射的物理内存
      a0220000-a0221fff : bnxt_en
    a0222000-a0223fff : 0000:01:00.0
      a0222000-a0223fff : bnxt_en
    a0224000-a0243fff : 0000:01:00.1
    a0244000-a0263fff : 0000:01:00.1

在作者使用这台主机上,物理地址范围 a0000000-a02fffff 对应: PCI Bus 0000:01。该Bus位于PCI Bridge 和 PCI Endpoint之间(可以回到文章开始的图查看),所以可以通过命令 ( lspci -s 00:01.0 -vvv )查看到 Bus 上一级 Bridge 的信息,得到Bridge下挂设备的总地址空间,即下图中可以被CPU访问预取的 Memory 范围:00000000a0000000-00000000a02fffff

而在该PCI Bridge下,下挂PCIe Endpoint的其中一个Function(01:00.1),其 Bar2 对应物理地址范围:a0000000-a00fffff : 0000:01:00.1(Bus:Device.Function),即为 PCIe 网卡 Bar 2(下图Region 2)空间(设备内存)映射的物理地址空间。

总线地址:Bus Address Space(BA)

总线地址是为 终端设备读写DMA 内存 或者 主机读写设备配置空间(PCIe Bar)使用的总线地址空间的地址(PCI 总线)。

我们在调用【dma = dma_map_single(device, buf, size, DMA_TO_DEVICE)】,将数据 buf(虚拟地址) 建立 dma地址映射给device 访问时,返回的类型为 dma_addr_t 的 dma 地址,就是 bus address。该地址可以传给设备用于DMA的物理内存数据读取。

在一些系统中,总线地址和物理地址是一样的(我们可以调用 phy = virt_to_phys(buf) 得到虚拟地址buf 对应的物理地址 phy, 确认 phy 和 dma 地址是否相同)。Host Bridge和 IOMMU可以实现 物理地址和总线地址的任意映射。

注意:从设备的角度,不管是访问设备内存,或者设备发起DMA读写,都是使用的总线地址。

访问过程详解

CPU 读取设备内存

首先,CPU需要通过ioremap,将虚拟地址A映射到物理地址空间 MMIO 中物理机地址B;

然后,CPU发起 ioread、iowrite 请求,发起时用的虚拟地址C,接着MMU转换为物理地址B;

最后,PCIe Host Bridge 将物理地址B转换为总线地址A,通过总线地址A对Bar空间进行读写。

注意:对Bar Register 的读写基于 PCIe的 Configuration TLP 操作。

代码示例:

/* CPU ioremap */
struct pci_dev *pdev;
u8 __iomem *hw_addr = ioremap(pci_resource_start(pdev, 0),
			      pci_resource_len(pdev, 0));

u32 val = 10;
u32 reg_offset = 0;
/* 将 10 写入hw_addr[reg_offset] */
writel(val, &hw_addr[reg_offset]);
value = readl(&hw_addr[reg]);

Device DAM 访问物理内存

首先,CPU基于虚拟机地址X,通过MMU,将数据写入物理地址Y对应的 物理内存中;

然后,CPU 调用如dma_map_single 这样的API,将总线地址Z 映射到 虚拟地址X和物理地址Y。

最后,Device 通过总线地址Z,发出DMA读请求(PCIe Memeory Read),接着IOMMU将总线地址翻译为物理地址Y 去 读写物理内存 DMA Buffer.

编程示例:

struct device *dev;		/* device for DMA mapping */
struct sk_buff *skb;
dma = dma_map_single(dev, skb->data, size, DMA_TO_DEVICE);
/* tx_desc 是网卡发包时用到的描述符,
  * 硬件设备可以通过DMA 访问到描述符绑定的dma地址,然后硬件可以基于该DMA地址发起内存访问
  */
tx_desc->read.buffer_addr = cpu_to_le64(dma);

DMA映射编程

常用的DMA映射有两种类型,一种是一致性DMA映射(consistent DMA mapping);另一种是流式DMA映射(streaming DMA mapping)。理解这两种常用的 DMA映射类型,是编程的基础。

一致性DMA映射

一致性DMA 映射关闭了 L1/L2/L3 Cache。首先,当 CPU 写入数据时,则会直接放入内存,而不会在Cache进行缓存,所以设备可以立即DMA读取到CPU写入的数据;其次,当设备DMA写入数据到内存后,则CPU可以立即读取到该变化的数据,而不会读取Cache中的脏数据,因为Cache关闭了。

Consistent DMA mapping的常用场景:

  1. 网卡驱动程序 和 网卡DMA控制器往往是通过内存中的一些描述符(形成环)进行交互(描述符中包括收发数据包用到的大块DMA内存地址),这些保存描述符的memory,需要被主机CPU和网卡DMA控制器频繁的读写,并且在任何一端写,都需要另一端立即可以访问到,所以通常采用Consistent DMA mapping比较方便。

2. SCSI硬件适配器上的DMA 与 主存中的一些数据结构(mailbox command)进行交互,这些保存mailbox command的memory一般采用Consistent DMA mapping。

代码示例

如下调用 dma_alloc_coherent 分配一块一致性DMA内存(取自intel ixgbe驱动 linux-4.18.0-348\drivers\net\ethernet\intel\ixgbe\ixgbe.h & linux-4.18.0-348\drivers\net\ethernet\intel\ixgbe\ixgbe_main.c):

struct ixgbe_ring {
	struct device *dev;		/* device for DMA mapping */
	void *desc;			/* descriptor ring memory */
	dma_addr_t dma;			/* phys. address of descriptor ring */
	unsigned int size;		/* length in bytes */
	u16 count;			/* amount of descriptors */
}
/**
 * ixgbe_setup_tx_resources - allocate Tx resources (Descriptors)
 * @tx_ring:    tx descriptor ring (for a specific queue) to setup
 *
 * Return 0 on success, negative on failure
 **/
int ixgbe_setup_tx_resources(struct ixgbe_ring *tx_ring)
{
       struct device *dev = tx_ring->dev;
        // 返回是 DMA地址 对应的虚拟地址 (tx_ring->desc)
 	tx_ring->desc = dma_alloc_coherent(dev,  // 进行DMA映射的PCI设备对应的device
					   tx_ring->size,   // 描述符环对应的总大小(bytes)
					   &tx_ring->dma,  // 输出:DMA 地址
					   GFP_KERNEL);
...
}

参考内核 Document/core-api/dma-api.rst

void *
dma_alloc_coherent(struct device *dev, size_t size,
                   dma_addr_t *dma_handle, gfp_t flag)

Consistent memory is memory for which a write by either the device or 
the processor can immediately be read by the processor or device
without having to worry about caching effects.

流式DMA映射

因为一致性DMA关闭了Cache,虽然使用带来了方便,但是会牺牲数据读写的性能。例如Intel当前的CPU package,支持在 PCIe Endpoint DMA 访问内存时,将数据放入到L3 Cache,然后DMA 控制器从L3 Cache 读取数据(这是对我们DMA控制器直接访问DRAM内存常识的挑战)。而这些硬件的优化,在使用流式DMA影射时,可以充分发挥性能的优势。

streaming DMA mapping的常用场景:

  1. 网卡进行数据传输使用的DMA buffer,发送数据是,主机准备好DMA Buffer,由硬件DMA读取;接受数据时,也是主机准备好DMA Buffer,由硬件DMA 写入接受到的数据。
  2. 文件系统中的各种数据buffer,这些buffer中的数据最终要被读写到SCSI设备上去

代码示例1

如下dma_map_single 将 虚拟地址 skb->data 指向的内存,映射到dma 总线地址上,然后将dma 地址写入描述符给硬件DMA读取 skb->data 指向的内存数据。

需要注意的是,skb->data 指向的数据区域需要是连续物理内存。内核采用带k字的分配函数(如kmalloc)即得到连续的物理内存,而使用v开头的分配函数,如vmalloc,则不能保证。

struct device *dev;		/* device for DMA mapping */
struct sk_buff *skb;
dma = dma_map_single(dev, skb->data, size, DMA_TO_DEVICE);
/* tx_desc 是网卡发包时用到的描述符,
  * 硬件设备可以通过DMA 访问到描述符绑定的dma地址,然后硬件可以基于该DMA地址发起内存访问
  */
tx_desc->read.buffer_addr = cpu_to_le64(dma);

代码示例2

如下调用dev_alloc_pages 分配一个page,再调用 dma_map_page_attrs 将该页映射dma 地址,最后调用dma_sync_single_range_for_device 将页面的数据同步给设备访问:

	struct page *page;
	dma_addr_t dma;
        struct device *dev;		/* device for DMA mapping */
        int page_size = PAGE_SIZE;
	/* alloc new page for storage */
	page = dev_alloc_pages(0);

	/* map page for use */
	dma = dma_map_page_attrs(dev, page, 0,
				 page_size,
				 DMA_FROM_DEVICE,
				 DMA_ATTR_SKIP_CPU_SYNC);

	/* sync the buffer for use by the device */
	dma_sync_single_range_for_device(dev, dma,
					 0, page_size,
					 DMA_FROM_DEVICE);
 

总结

本文结合计算机系统的架构,我们从内存访问的角度,介绍了各种地址空间(虚拟、物理、总线)的概念。以及物理内存和设备内存访问三个方向(CPU->DRAM, CPU -> Device Memory, Device DMA -> DRAM)。最后介绍了DMA 映射编程常用的方法,如果觉得不过瘾,建议继续研究作者参考的内核 Document。

参考

1,linux-4.18.0-348/Document/core-api/dma-api-howto.rst

2,linux-4.18.0-348/Document/core-api/dma-api.rst

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1154849.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java.net.URISyntaxException: Illegal character in query at index

现象 现象调用httpGet请求时,报错,如下: 原因 因为调用的url里有特殊字符 如单引号,双引号,等号,& | 等 解决方案 使用url带参构造方法,会对url里面的特殊字符进行转义处理 URL url n…

Python-常用的量化交易代码片段

算法交易正在彻底改变金融世界。通过基于预定义标准的自动化交易,交易者可以以闪电般的速度和比以往更精确的方式执行订单。如果您热衷于深入了解算法交易的世界,本指南提供了帮助您入门的基本代码片段。从获取股票数据到回溯测试策略,我们都能满足您的需求! 1. 使用 YFina…

k8s从私有仓库拉取镜像

从私有仓库拉取镜像 | Kubernetes 准备开始 你必须拥有一个 Kubernetes 的集群,同时你必须配置 kubectl 命令行工具与你的集群通信。 建议在至少有两个不作为控制平面主机的节点的集群上运行本教程。可以通过 Minikube 构建一个你自己的集群,或者你可以…

网管的利器之NMap

在进行网络管理过程中,网管会借助很多的工具比如付费的一些产品,比如漏洞扫描、安全隐患发现、网络设备管理、上网行为管理等。 更多的情况下,网管员使用一些DOS命令或者免费的工具进行,比如前面介绍过的PingInfoView.exe、WinMTR…

机器学习(六)构建机器学习模型

1.9构建机器学习模型 我们使用机器学习预测模型的工作流程讲解机器学习系统整套处理过程。 整个过程包括了数据预处理、模型学习、模型验证及模型预测。其中数据预处理包含了对数据的基本处理,包括特征抽取及缩放、特征选择、特征降维和特征抽样;我们将…

lambda表达式 - c++11

文章目录: lambda表达式概念lambda表达式语法函数对象与lambda表达式 lambda表达式概念 lambda 表达式是 c11 中引入的一种匿名函数,它可以在需要函数对象的地方使用,可以用作函数参数或返回值。lambda 表达式可以看作是一种局部定义的函数对…

mysql之用户管理、权限管理、密码管理

用户管理 创建用户create user 杨20.0.0.13 identified by 123; 用户重命名rename user 杨20.0.0.13 to yang20.0.0.13; 删除用户drop user 杨20.0.0.13; 权限管理 查看用户权限show grants for 杨20.0.0.13; 赋予用户权限grant all privileges on *.* to 杨localhost id…

文章导读助你高效成长

文章目录 Java基础篇MySQL数据库篇Redis缓存篇 📕我是廖志伟,一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作者、产品软文创造者、技术文章评审老师、问卷调查设计师、个人社区创始人、开…

超低直流电阻测试仪

KDZD5510半导体体积电阻率测试仪是一款针对超低直流电阻测试专门设计开发的一款高精度测试仪,界面清爽、操作便捷;量程范围为:0.01uΩ~10MΩ;显示位数为五位半;自动双向电流测试, 同时脉冲式的测试方式避免…

医院室内地图导航技术分析与作用

随着科技的不断发展,医疗行业的服务水平也在逐步提高。为了方便患者和医务人员,医院室内地图导航技术应运而生。这种技术运用了多种元素,包括模型地图、室内3D电子地图、路线指引、对接医院系统、位置分享和寻车导航等,为医院提供…

Three.js 开发引擎的特点

Three.js 是一个流行的开源 3D 游戏和图形引擎,用于在 Web 浏览器中创建高质量的三维图形和互动内容。以下是 Three.js 的主要特点和适用场合,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎交流合作…

Python3,区区5行代码,制作期待的图表,这技能值得拥有(二)。

1、引言 小屌丝:鱼哥,这次按脚还不错? 小鱼:你说呢~ 小屌丝:那seabornde还记得? 小鱼:昂, 有印象 小屌丝:那咱开始整? 小鱼:这个… 行吧 小屌丝&…

ctfshow-web入门37-52

include($c);表达式包含并运行指定文件。 使用data伪协议 ?cdata://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg 是<?php system(cat flag.php);?> base64加密 源代码查看得到flag 38 多禁用了ph…

订水商城实战教程-06店铺信息

目录 1 创建数据源2 生成管理后台3 创建腾讯地图API4 配置小程序5 地址组件配置地图API6 显示店铺名称总结 上一篇我们介绍了权限控制&#xff0c;本篇我们就开始首页开发了。首页先需要显示店铺的名称&#xff0c;我们需要将店铺的信息存入数据源中。 1 创建数据源 打开控制台…

计组之存储系统

存储器概述 分类 1.按在计算机中的作用&#xff08;层次&#xff09;分类 主存储器。CPU可以直接随机地对其进行访问&#xff0c;也可以和高速缓冲存储器&#xff08;Cache)及辅助存储器交换数据。辅助存储器。辅存的内容需要调入主存后才能被CPU访问。高速缓冲存储器。位于…

电脑办公最佳拍档 夸克网盘升级低耗能备份、PDF阅读器等功能

临近年终&#xff0c;上班族不仅要总结过去一年的成绩还要开始制定新规划&#xff0c;在这个过程中整理资料是必不可少的环节。对于经常需要使用文件备份和PDF的用户&#xff0c;推荐大家试一下夸克网盘电脑端&#xff0c;升级后的“低耗能备份”和“PDF阅读器”让备份体验更丝…

Python 算法高级篇:最短路径算法的优化

Python 算法高级篇&#xff1a;最短路径算法的优化 引言 1. Dijkstra 算法2. Bellman-Ford 算法3. SPFA 算法4. 优化与比较5. 案例分析&#xff1a;地理导航6. 总结 引言 最短路径算法是图算法中的一个重要领域&#xff0c;它用于查找从一个起始节点到目标节点的最短路径。在这…

2.1 点纹理背景

快速复制——设置背景纹理 然后填充为淡蓝色&#xff0c;无轮廓&#xff0c;纹理背景就做好了

阿里云2023年双11活动,云服务器价格出炉,2核2G云服务器99元/年!

阿里云2023年双11期间推出了金秋云创季活动&#xff0c;新老用户均可领取上云满减券礼包&#xff0c;单笔订单最高减2400元&#xff0c;还有多款爆品超低折扣&#xff0c;2核2G云服务器99元/年&#xff0c;续费不涨价&#xff0c;新老用户同享&#xff01; 一、阿里云双11活动地…