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

序言

对于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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Apache Flink】基于时间和窗口的算子-配置时间特性

文章目录 前言配置时间特性将时间特性设置为事件时间时间戳分配器周期性水位线分配器创建一个实现AssignerWithPeriodicWatermarks接口的类,目的是为了周期性生成watermark 定点水位线分配器示例 参考文档 前言 Apache Flink 它提供了多种类型的时间和窗口概念&…

JDK项目分析的经验分享

基本类型的包装类(Character放在最后) String、StringBuffer、StringBuilder、StringJoiner、StringTokenizer(补充正则表达式的知识) CharacterIterator、StringCharacterIterator、CharsetProvider、CharsetEncoder、CharsetDecoder(较难) java.util.function下的函数表…

​测绘人注意,你可能会改变历史!

你也许想不到,曾经有一个测绘人员在进行实地测量作业时,在地图上就这么随手一标注,却让这个地方成为了如今的网红打卡地。 这个地方就是外地游客慕名而来的“宽窄巷子”,如果连这个地方都不知道的成都人,就应该不能算…

python的pytorch和torchvision利用wheel文件安装

python的pytorch和torchvision利用wheel文件安装 在做人工智能的时候,我们需要下载pytorch和torchvision,那么如何下载呢。利用wheel文件pip安装 下载 首先要看你的python版本,打开命令行,输入: python -V就可以看…

华为云资源搭建过程

网络搭建 EIP: 弹性EIP,支持IPv4和IPv6。 弹性公网IP(Elastic IP)提供独立的公网IP资源,包括公网IP地址与公网出口带宽服务。可以与弹性云服务器、裸金属服务器、虚拟IP、弹性负载均衡、NAT网关等资源灵活地绑定及解绑…

纯css实现手机端loading

纯css实现手机端loading <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"wid…

PHP连接SQLServer echo输出中文汉字显示乱码解决方法

1、查询结果有中文会显示乱码。 解决方法一&#xff08;较简单&#xff0c;建议使用&#xff09;&#xff1a; 在php文件最开头写上&#xff1a; header(Content-type: text/html; charsetUTF8); // UTF8不行改成GBK试试&#xff0c;与你保存的格式匹配 <?php header(&q…

Django实战项目-学习任务系统-任务管理

接着上期代码框架&#xff0c;开发第3个功能&#xff0c;任务管理&#xff0c;再增加一个学习任务表&#xff0c;用来记录发布的学习任务的标题和内容&#xff0c;预计完成天数&#xff0c;奖励积分和任务状态等信息。 第一步&#xff1a;编写第三个功能-任务管理 1&#xff0…

P3983 赛斯石(赛后强化版),背包

题目背景 白露横江&#xff0c;水光接天&#xff0c;纵一苇之所如&#xff0c;凌万顷之茫然。——苏轼真程海洋近来需要进购大批赛斯石&#xff0c;你或许会问&#xff0c;什么是赛斯石&#xff1f; 首先我们来了解一下赛斯&#xff0c;赛斯是一个重量单位&#xff0c;我们用…

Wi-Fi 6和5G 在应用场景上的区别

在工作领域&#xff0c;我们经常会面临两个选择&#xff0c;场景的解决方案是要用5G还是WiFi 6&#xff0c;其实判断并不困难&#xff0c;但我们仍然还是从理论概念上区分一下。 文章目录 什么是Wi-Fi 6什么是5GWi-Fi 6和5G 的区别区别一&#xff1a;覆盖范围区别二&#xff1…

腾讯云轻量服务器“镜像类型”以及“镜像”选择方法

腾讯云轻量应用服务器镜像类型分为应用镜像、系统镜像、Docker基础镜像、自定义镜像和共享镜像&#xff0c;腾讯云百科txybk.com来详细说下不同镜像类型说明和详细介绍&#xff1a; 轻量应用服务器镜像类型说明 腾讯云轻量应用服务器 应用镜像&#xff1a;独有的应用镜像除了包…

氮化镓功率HEMT的表征与建模

来源&#xff1a;Characterization and Modeling of a Gallium Nitride Power HEMT(IEEE TRANSACTIONS ON INDUSTRY APPLICATIONS) GaN high-electron-mobility transistor (HEMT) GaN高电子迁移率晶体管   该模型包括一个电压相关电流源Ids、两个电压相关电容Cgd和Cds、一…

NLP之LSTM原理剖析

文章目录 背景simpleRNN的局限性 LSTM手写一下sigmoid例子支持长记忆的神经网络解读3重门 背景 SimpleRNN有一定局限性&#xff0c; 图片上的文字内容: 图片标题提到“SimpleRNN是一种基础模型。它用于解决序列型问题&#xff0c;其中的每一步的输出会影响到下一步的结果。图…

Yolo-Z:改进的YOLOv5用于小目标检测

目录 一、前言 二、背景 三、新思路 四、实验分析 论文地址&#xff1a;2112.11798.pdf (arxiv.org) 一、前言 随着自动驾驶汽车和自动驾驶赛车越来越受欢迎&#xff0c;对更快、更准确的检测器的需求也在增加。 虽然我们的肉眼几乎可以立即提取上下文信息&#xff0c;即…

JVM虚拟机:堆结构的逻辑分区

堆内存的逻辑分区 堆内存的逻辑分区如下所示: 堆内存中分为新生代和老年代,二者空间大小1:3。在新生代里面分为两类区域(eden、survivor),三个区域(eden、survivor、survivor),三个区大小比例为8:1:1。 对象存放的位置 栈 当我们new一个对象的时候,首先会将对象…

Wpf 使用 Prism 实战开发Day03

一.实现左侧菜单绑定 效果图: 1.首先需要在项目中创建 mvvm 的架构模式 创建 Models &#xff0c;放置实体类。 实体类需要继承自Prism 框架的 BindableBase&#xff0c;目的是让实体类支持数据的动态变更! 例如: 系统导航菜单实体类 / <summary>/// 系统导航菜单实体类…

vcomp140.dll丢失是什么意思,vcomp140.dll丢失这几个方法都能修复好

vcomp140.dll是什么&#xff1f; vcomp140.dll是一个动态链接库&#xff08;Dynamic Link Library&#xff09;&#xff0c;它主要用于支持Microsoft Visual C 2015编程语言的运行。这个文件包含了编译器相关的函数和资源&#xff0c;对于使用Visual C 2015开发的程序和游戏来…

mac vscode 使用 clangd

C 的智能提示 IntelliSense 非常不准&#xff0c;我们可以使用 clangd clangd 缺点就是配置繁琐&#xff0c;优点就是跳转和提示代码精准 开启 clangd 之后会提示你关闭 IntelliSense 1、安装插件 clangd 搜索第一个下载多的就是 2、配置 clangd 可执行程序路径 clangd 插…

【数据结构】数组和字符串(五):特殊矩阵的压缩存储:稀疏矩阵——压缩稀疏行(CSR)

文章目录 4.2.1 矩阵的数组表示4.2.2 特殊矩阵的压缩存储a. 对角矩阵的压缩存储b~c. 三角、对称矩阵的压缩存储d. 稀疏矩阵的压缩存储——三元组表e. 压缩稀疏行&#xff08;Compressed Sparse Row&#xff0c;CSR&#xff09;矩阵结构体创建CSR矩阵元素设置初始化打印矩阵销毁…

VPS是什么?详解亚马逊云科技Amazon Lightsail(VPS)虚拟专用服务器

2006年&#xff0c;南非开普敦&#xff0c;亚马逊推出了WBS&#xff0c;以网络服务的形式向企业提供基础的IT服务。亚马逊云科技的一小步&#xff0c;在无数技术更迭&#xff0c;天才设计师和程序员的努力与基础设施建设的完善之下成为了人类科技进展的一大步。 亚马逊云科技可…