鸿蒙高级——内存专用之linux-reserve-memory 方案简介

引言

最近因为工作需求涉及到一些AI端侧推理加速的需求,于是新学习了一个新的知识点。

一、linux-reserve-memory 方案

linux-reserve-memory 是 Linux 内核中用于预留内存机制。预留内存(Reserved Memory) 是一种机制,用于预留特定物理内存区域,使其不被内核的常规内存管理分配使用。这种内存通常用于专用硬件(如GPU、DMA控制器)或需要连续物理内存的关键任务。在某些场景下,系统需要预先保留一部分内存,以供特定的设备、驱动程序或者内核功能使用,避免这部分内存被常规的内存分配器(如 kmalloc、vmalloc 等)分配出去,linux-reserve-memory 机制就是为了满足这种需求而设计的。

1、 使用场景

  • 设备特定需求:一些硬件设备(如摄像头、硬件加速器)可能需要固定的、连续的物理内存区域来进行 DMA(直接内存访问)操作。例如图形处理单元(GPU)、高速网络接口卡等,它们在工作时需要直接访问内存,如果内存不连续或者被其他程序占用,可能会导致性能下降或者出现错误。
  • 内核功能需求:某些内核功能可能需要预先分配一块内存来存储关键数据或者执行特定的操作。例如,内核日志缓冲区、内核调试信息存储区等。
  • 避免内存碎片:为需要大块连续内存的设备(如DMA)预留区域。
  • 安全/实时性需求:确保关键数据不被换出或覆盖(如实时系统或安全模块)。
  • 启动阶段保留:在系统初始化时,通过内核参数或设备树静态保留内存。

2、配置linux-reserve-memory

在 Linux 内核中,预留内存通常通过设备树(Device Tree)或者内核命令行参数来配置。

2.1、设备树方式

设备树是一种描述硬件设备信息的数据结构,在设备树中可以定义预留内存节点,在设备树(.dts 或 .dtsi 文件)中定义 reserved-memory 节点。

复制代码
/reserved-memory {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;

    my-reserved-memory: my-memory@10000000 {
        reg = <0x10000000 0x1000000>; /* 起始地址为 0x10000000,大小为 16MB */
        no-map;
    };
};

启动后查看 /proc/iomem,保留区域会标记为 reserved

  • no-map:内核不会将此区域映射到虚拟内存,需驱动手动处理。
  • reusable:内存可被系统临时使用,但需要时能释放(如CMA)。

以上定义了一个名为 my-reserved-memory 的预留内存节点,起始地址为 0x10000000,大小为 0x1000000(即 16MB)。no-map 属性表示这部分内存不会被内核映射到虚拟地址空间,除非有特定的驱动程序进行映射。除了上面这种方式还可以结合CMA

连续内存分配器(CMA, Contiguous Memory Allocator)

CMA是一种动态保留机制,内存平时可被普通进程使用,但需要时能回收供设备使用。

复制代码
reserved-memory {
    linux,cma {
        compatible = "shared-dma-pool";
        reusable;
        size = <0x4000000>; // 64MB
        alignment = <0x2000>;
        linux,cma-default; // 设为默认CMA区域
    };
};

使用CMA

驱动通过 dma_alloc_coherent() 或 dmam_alloc_coherent() 分配内存。

2.2、内核命令行参数方式(内核启动参数Boot Parameters)

除了设备树,还可以通过内核命令行参数来预留内存,语法:memmap=< size >@< start_address >。例如,在 /boot/grub/grub.cfg 中添加以下参数

复制代码
mem=1G memmap=16M$1G

这个参数表示系统总内存为 1GB,从 1GB 地址处开始预留 16MB 的内存。

3、内核代码中的使用预留内存步骤

在 Linux 内核代码中,可以通过以下步骤来使用预留内存,保留内存需要驱动程序访问:

3.1、查找预留内存区域

使用 of_reserved_mem_device_init 函数来初始化设备的预留内存区域。例如:

c 复制代码
#include <linux/of_reserved_mem.h>

static int my_driver_probe(struct platform_device *pdev)
{
    struct reserved_mem *rmem;
    int ret;

    ret = of_reserved_mem_device_init(&pdev->dev);
    if (ret)
        return ret;

    rmem = of_reserved_mem_lookup(&pdev->dev);
    if (!rmem) {
        dev_err(&pdev->dev, "No reserved memory found\n");
        return -ENODEV;
    }

    // 使用 rmem 进行后续操作
    // ...

    return 0;
}

3.2、物理地址直接映射

使用 ioremap() 或 memremap() 将物理地址映射到内核虚拟地址。如果需要将预留的物理内存映射到虚拟地址空间,可以使用 ioremap 函数。

c 复制代码
#include <linux/io.h>

void *virt_addr;
phys_addr_t phys_addr = rmem->base;
size_t size = rmem->size;

virt_addr = ioremap(phys_addr, size);
if (!virt_addr) {
    dev_err(&pdev->dev, "Failed to map reserved memory\n");
    return -ENOMEM;
}

3.3、DMA API

通过 dma_alloc_coherent() 从CMA区域分配内存。

4、验证保留内存

  • 查看 cat /proc/iomem 文件,保留区域会显示为 reserved 或指定名称。

    10000000-101fffff : my-reserved-memory

  • 内核日志:dmesg 会输出内存保留信息。

  • CMA状态:查看 cat /proc/meminfo 中的 CmaTotal 和 CmaFree。

5、注意事项

  • 内存管理:预留内存不会被内核的常规内存分配器管理,因此需要驱动程序自己负责这部分内存的分配和释放。
  • 兼容性:不同的硬件平台和内核版本可能对预留内存的配置和使用方式有所不同,需要根据具体情况进行调整。
  • 错误处理:在使用预留内存时,需要进行充分的错误处理,避免因内存分配失败或者映射失败导致系统崩溃。
  • 地址冲突:确保保留区域不与现有内存或硬件地址重叠。
  • 大小对齐:物理地址需按页对齐(通常 4KB),某些硬件可能需要更大对齐(如2MB)。
  • 性能影响:过度保留内存可能减少系统可用内存,影响性能。
  • 动态预留:CMA适合需要灵活分配的场景,而静态保留适合硬件强制要求固定地址的情况。

7、普通进程访问保留内存的方法

在Linux系统中,普通应用程序(用户空间进程)不能直接访问保留内存,因为保留内存通常由内核或专用硬件管理,且用户空间与内核空间存在隔离。但通过以下方法,可以间接或受限地访问保留内存:

7.1、内核空间访问(推荐场景:驱动/内核模块)

有以下几种方式:

7.1.1、映射物理内存到内核虚拟地址

如果预留内存通过设备树标记为 no-map(即内核不自动映射),需通过 ioremap 或 memremap 手动映射。

c 复制代码
// 内核模块示例
#include <linux/io.h>
#include <linux/module.h>

static void __iomem *reserved_mem;

static int __init my_module_init(void)
{
    phys_addr_t phys_addr = 0x80000000; // 预留内存的物理地址
    size_t size = 0x10000000;          // 预留内存大小(256MB)

    // 映射物理内存到内核虚拟地址
    reserved_mem = ioremap(phys_addr, size);
    if (!reserved_mem) {
        pr_err("Failed to ioremap reserved memory\n");
        return -ENOMEM;
    }

    // 写入/读取示例
    writel(0x12345678, reserved_mem); // 写入32位数据
    pr_info("Read value: 0x%x\n", readl(reserved_mem));

    return 0;
}

static void __exit my_module_exit(void)
{
    iounmap(reserved_mem);
}

module_init(my_module_init);
module_exit(my_module_exit);
7.1.2、直接物理地址访问(DMA场景)

如果内存用于 DMA 操作(如硬件加速卡),可直接将物理地址传递给设备:

复制代码
// 在设备驱动中设置 DMA 地址
dma_addr_t dma_handle = phys_to_dma(dev, 0x80000000);
dma_engine_start_transfer(dev, dma_handle, buffer_size);

高度依赖,需要加速卡驱动程序配合

7.1.3、使用 CMA(Contiguous Memory Allocator)

如果预留内存标记为 shared-dma-pool(动态分配连续内存):

c 复制代码
// 从 CMA 区域分配内存
struct page *cma_pages;
cma_pages = dma_alloc_from_contiguous(dev, size, order);

// 转换为虚拟地址
void *virt_addr = page_to_virt(cma_pages);

若保留内存是 CMA 区域,可通过 DMA 接口分配内存并共享到用户空间,步骤:

  • 内核驱动分配 CMA 内存
c 复制代码
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
  • 用户空间映射,通过 mmap 或 ioctl 将 DMA 缓冲区暴露给应用。

7.2、 用户空间访问(需谨慎控制权限)

有两种

7.2.1、使用 /dev/mem 直接访问物理内存

Linux 提供了 /dev/mem 设备文件,允许有权限的进程直接读写物理内存,但是有风险。

首先启用 /dev/mem:
  • 内核配置:确保 CONFIG_STRICT_DEVMEM 未启用(或通过 CAP_SYS_RAWIO 权限绕过限制)。
  • 内核需启用 CONFIG_DEVMEM 和 CONFIG_STRICT_DEVMEM(默认启用,但可能限制访问)。
  • 需要 root 权限或 CAP_SYS_RAWIO 能力。
  • 权限控制:限制 /dev/mem 的访问权限(chmod 600 /dev/mem)。
  • 直接访问物理内存可能引发缓存一致性问题(需使用 O_SYNC 标志)。
再映射物理地址
复制代码
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, phys_addr);

# 查看物理内存布局(确认保留地址)
sudo cat /proc/iomem
# 使用 dd 读取物理内存(示例地址 0x10000000)
sudo dd if=/dev/mem bs=1 count=4 skip=$((0x10000000)) 2>/dev/null | hexdump -C

一个简单的例子:

c 复制代码
#include <sys/mman.h>
#include <fcntl.h>
#define RESERVED_PHYS_ADDR 0x80000000
#define RESERVED_SIZE      0x10000000

int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd == -1) {
        perror("open /dev/mem failed");
        return -1;
    }
    // 映射物理内存到用户空间
    void *mem = mmap(NULL, RESERVED_SIZE, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, RESERVED_PHYS_ADDR);
    if (mem == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return -1;
    }
    // 读写操作
    *((volatile uint32_t*)mem) = 0xdeadbeef;
    printf("Read value: 0x%x\n", *((volatile uint32_t*)mem));

    munmap(mem, RESERVED_SIZE);
    close(fd);
    return 0;
}

也存在一些风险隐患:

  • 系统稳定性:误操作可能导致内核崩溃。
  • 安全漏洞:恶意程序可能通过 /dev/mem 窃取敏感数据。
  • 内核配置限制:CONFIG_STRICT_DEVMEM 默认阻止访问关键内存区域。
7.2.2、通过 UIO(Userspace I/O)框架

Linux UIO(Userspace I/O)框架 是一种允许在用户空间(Userspace)中实现设备驱动程序的机制。它通过将部分设备驱动逻辑(如内存映射和中断处理)从内核空间(Kernelspace)转移到用户空间,简化了硬件访问流程,同时减少了对内核模块开发的依赖。UIO 特别适合需要快速原型开发、实时性要求高或需要隔离用户空间与内核空间的场景,更安全的用户空间访问方案(需内核驱动支持)UIO 框架将设备驱动分为两部分:

  1. 内核模块:仅负责硬件的基本初始化、中断处理和物理内存映射。
  2. 用户空间程序:实现设备控制逻辑(如读写寄存器、数据处理等)。
    这种分离使得开发者无需编写复杂的内核代码即可操作硬件,典型场景:
7.2.2.1、UIO 的主要功能
内存映射
  • 将设备的物理内存映射到用户空间虚拟地址,用户程序可直接访问硬件寄存器或内存区域。
  • 内核通过 uio_map 接口暴露内存地址,用户程序通过 mmap 映射。
中断处理
  • 内核侧:捕获硬件中断并通知用户空间。
  • 用户侧:通过 read 或 poll 监听中断事件(如读取 /dev/uioX 设备文件)。
设备发现

通过 sysfs 文件系统(/sys/class/uio/uioX)提供设备信息(如内存区域、中断号等)

7.2.2.2、UIO 的使用步骤
内核配置
  • 启用 UIO 支持

    CONFIG_UIO=y # 基础 UIO 框架
    CONFIG_UIO_PDRV_GENIRQ=y # 通用 UIO 平台驱动(适用于设备树配置)

设备树配置
复制代码
通过设备树定义 UIO 设备(以 FPGA 为例):
/ {
    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        fpga_region: fpga@80000000 {
            compatible = "shared-dma-pool";
            reg = <0x80000000 0x1000000>;  // 16MB 保留内存
            no-map;
        };
    };

    uio_fpga {
        compatible = "generic-uio";
        memory-region = <&fpga_region>;     // 绑定到预留内存
        interrupts = <0 123 4>;             // 中断号(示例)
    };
};
加载 UIO 驱动,加载通用 UIO 驱动或自定义内核模块
复制代码
# 加载通用 UIO 平台驱动
sudo modprobe uio_pdrv_genirq
用户空间通过UIO访问
7.2.3、通过内核驱动程序映射

这是最安全和标准的方法,需编写内核模块将保留内存暴露给用户空间

7.2.3.1、编写内核驱动

1.在驱动中申请保留内存(如通过设备树预留的内存区域)。

2.创建字符设备,并实现 mmap 操作,将保留内存映射到用户空间。

c 复制代码
// 设备树中定义的保留内存区域
static struct resource *reserved_mem;
static void __iomem *mem_base;

static int mydev_mmap(struct file *filp, struct vm_area_struct *vma) {
    // 将物理内存映射到用户空间
    return remap_pfn_range(vma, vma->vm_start, 
                           PFN_DOWN(reserved_mem->start),
                           vma->vm_end - vma->vm_start, 
                           vma->vm_page_prot);
}

static struct file_operations fops = {
    .mmap = mydev_mmap,
};

// 初始化模块时获取保留内存资源
static int __init mydev_init(void) {
    reserved_mem = request_mem_region(0x10000000, 0x200000, "my_reserved_mem");
    mem_base = ioremap(reserved_mem->start, resource_size(reserved_mem));
    // 注册字符设备...
    return 0;
}
7.2.3.2、用户空间访问

1.应用程序通过 open() 打开设备文件。

2.使用 mmap() 系统调用将设备内存映射到进程的虚拟地址空间。

复制代码
#include <fcntl.h>
#include <sys/mman.h>

int main() {
    int fd = open("/dev/mydevice", O_RDWR);
    void *addr = mmap(NULL, 0x200000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // 通过 addr 访问保留内存
    munmap(addr, 0x200000);
    close(fd);
    return 0;
}

小结

  • 生产环境:使用内核驱动程序通过 mmap 安全地暴露保留内存。
  • 临时调试:谨慎使用 /dev/mem(需关闭 CONFIG_STRICT_DEVMEM)。
  • 硬件开发:结合 UIO 框架实现用户空间驱动。
相关推荐
Sammyyyyy1 小时前
jev-ultrafast 实战:浏览器操作代理加本地 AI 网关,把云控制台配置做成自动化流水线
运维·人工智能·自动化·ai编程·servbay
灯澜忆梦1 小时前
【Linux】#5 | 文件与目录基础操作命令
java·linux·运维
逐梦之程1 小时前
EXCEL与DBC格式互转软件
运维·服务器·网络
yychen_java1 小时前
第十二篇:完整项目实战——构建可验证的能源智能运维 Agent 中枢
运维·人工智能·能源
9624561 小时前
餐饮 SaaS 优惠券系统架构演进(七)营销定价引擎设计:活动、会员价、优惠券、买赠如何在同一 PriceQuote 中可解释地组合
java·运维·算法
重生之高冷汉堡包1 小时前
什么是数据中心代理?原理拆解、类型区别、应用场景与选型指南
服务器·网络·数据库·安全·ip
Frank_refuel2 小时前
分布式IoT数据采集与存储系统(项目复盘)
运维·服务器
wuminyu2 小时前
stackChunkOop内部结构与GC屏障的配合工作原理剖析
java·linux·c语言·jvm·c++
青衫客362 小时前
从网卡到 Socket:再探 Linux 网络协议栈与 iptables 数据包处理流程
linux·网络·网络协议