ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux No-MMU 内存映射支持完全指南:uClinux 环境下的 mmap 行为、约束与驱动实现

Linux No-MMU 内存映射支持完全指南:uClinux 环境下的 mmap 行为、约束与驱动实现 Linux No-MMU 内存映射支持完全指南uClinux 环境下的 mmap 行为、约束与驱动实现【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读在缺少 MMUMemory Management Unit的嵌入式环境中即 uClinux 所代表的系统Linux 内核只能提供受限的内存映射支持。本文以内核文档 Documentation/admin-guide/mm/nommu-mmap.rst 为骨架结合内核源码逐项剖析 no-MMU 下 mmap()、shmat()、execve()、fork()/clone() 的行为差异详解各类映射匿名映射、文件映射、共享映射在 MMU 与 no-MMU 两种模式下的异同并给出为字符设备、内存后端文件、块设备提供可共享映射支持的驱动实现路径以及vm.nr_trim_pages页修剪行为的调优方法。读完本文你将掌握在 no-MMU 内核上正确使用与实现内存映射的完整知识体系。1. 背景no-MMU 环境下的内存映射全景内核在 no-MMU 条件下如 uClinux 环境对内存映射只提供有限支持。从用户空间角度看内存映射主要与三类系统调用相关mmap()创建内存映射shmat()SYSV 共享内存附着execve()加载可执行程序。从内核角度看execve() 的映射实际由 binfmt二进制格式驱动完成这些驱动会回调 mmap() 例程来执行实际工作。内存映射行为还牵涉 fork()、vfork()、clone() 与 ptrace() 的工作方式。在 uClinux 下没有 fork()clone() 必须携带CLONE_VM标志才能工作——因为 no-MMU 下无法通过页表复制实现地址空间隔离。MMU 与 no-MMU 的映射行为相似但不相同且后者受到的约束严格得多。下文逐一对比各类映射在两种模式下的表现。2. 各类映射在 MMU 与 no-MMU 下的行为对比2.1 匿名映射MAP_PRIVATE模式行为MMUVM 区域由任意页virtual pages支撑fork 时写时复制copy-on-writeno-MMUVM 区域由任意一段连续的物理页支撑contiguous run of pages这是 no-MMU 最本质的差异由于没有页表做虚拟到物理的间接映射匿名私有映射必须一次性分配一整段物理连续的内存。2.2 匿名映射MAP_SHARED在 MMU 模式下共享匿名映射的行为非常类似私有映射区别在于 fork() 或未带 CLONE_VM 的 clone() 之后它是进程间共享的。而 no-MMU不支持共享匿名映射因此其行为与 MAP_PRIVATE 完全相同。2.3 文件映射MAP_PRIVATEPROT_READ / PROT_EXEC不带 PROT_WRITEMMU 模式VM 区域由从文件读入的页支撑底层文件的修改会反映到映射中fork 时复制。no-MMU 模式按优先级依次尝试复用已有映射如果存在对同一文件同一段的已有映射且权限兼容内核会直接复用——即使该映射是由另一个进程创建的直接映射到后备设备如果后备设备具有NOMMU_MAP_DIRECT能力且具备相应的映射保护能力文件映射将直接建立在后备设备上。ramfs、romfs、cramfs 和 mtd 都可能允许这种直接映射复制到内存如果后备设备不允许直接共享但具有NOMMU_MAP_COPY能力内核会把文件相应部分读入一段连续内存并将 EOF 之外的额外空间清零。无论哪种路径对文件的写操作不会影响映射对映射的写操作虽然不应该发生会因 no-MMU 缺乏保护而可见于其他进程。2.4 文件映射MAP_PRIVATEPROT_READ / PROT_EXEC 加 PROT_WRITEMMU 模式与非 PROT_WRITE 情形类似但相关页在真正写入前会先被复制copy-before-write。此后该页之下的底层文件修改不再反映到映射的支撑页中该页改由 swap 支撑。no-MMU 模式工作方式与非 PROT_WRITE 情形大体相同但总是复制、绝不共享a copy is always taken and never shared。2.5 普通文件 / 块设备MAP_SHAREDPROT_READ / PROT_EXEC / PROT_WRITEMMU 模式VM 区域由从文件读入的页支撑对页的修改回写到文件对文件的修改反映到映射页fork 后共享。no-MMU 模式不支持not supported。也就是说在 no-MMU 下对普通文件和块设备发起可写共享映射会直接失败。2.6 内存后端文件memory-backed regular fileMAP_SHARED可读写执行MMU 模式与普通文件一致。no-MMU 模式提供内存后端文件的文件系统如 ramfs 或 tmpfs可以选择通过 open → truncate → mmap 序列提供一段连续页用于映射。此时共享可写映射成为可能行为与 MMU 情形一致若文件系统不提供此类支持映射请求将被拒绝。这一机制正是 POSIX 共享内存的实现基础详见第 5 节。2.7 内存后端块设备MAP_SHARED可读写执行MMU 模式与普通文件一致。no-MMU 模式与内存后端文件类似但块设备必须能在不调用 truncate 的情况下直接提供一段连续页。例如 ramdisk 驱动若在初始化时一次性把所有内存分配为连续数组即可满足此要求。2.8 内存后端字符设备MAP_SHARED可读写执行MMU 模式与普通文件一致。no-MMU 模式字符设备驱动可以选择通过 mmap() 提供对底层设备的直接访问前提是设备具有可被直接访问的内存或类内存区域。典型例子是帧缓冲frame buffers和闪存设备flash devices。若驱动不提供此类支持映射请求将被拒绝。3. 进一步说明no-MMU mmap 的特殊细节3.1 页对齐规则私有文件映射可能返回非页对齐的缓冲区因为可能发生 XIPexecute-in-place就地执行后备存储中的数据本身可能不是页对齐的匿名映射则始终页对齐。如果可能请求大小应为 2 的幂次方否则会浪费部分空间——内核必须按 2 的幂次粒度分配而多余部分只有在配置了修剪时才被回收见第 8 节。3.2 匿名映射的清零行为根据 Linux man pages2.22 版及以后的规范匿名映射返回给用户前必须清零MMU 情形虚拟页支撑区域只有在对特定页发生写入时才映射到已清零的物理页此前读操作实际命中全局零页。这种方式把页内容初始化的开销摊薄到了映射的写入过程性能较好no-MMU 情形匿名映射由物理页支撑整个映射在分配时一次性清零。这会给用户空间的 malloc() 带来显著延迟——C 库执行匿名映射后内核会对整个映射做 memset。对于无需预清零的内存如 malloc() 返回的内存mmap() 可携带MAP_UNINITIALIZED标志告知内核不必在返回前清零。但注意必须启用CONFIG_MMAP_ALLOW_UNINITIALIZED配置否则该标志会被忽略。该配置项定义于 mm/Kconfig依赖EXPERT !MMU默认关闭default n并明确警示其存在明显安全风险应谨慎使用。从源码 mm/nommu.c 可以看到实际清零逻辑/* clear anonymous mappings that dont ask for uninitialized data */ if (!vma-vm_file (!IS_ENABLED(CONFIG_MMAP_ALLOW_UNINITIALIZED) || !(flags MAP_UNINITIALIZED))) memset((void *)region-vm_start, 0, region-vm_end - region-vm_start);即匿名映射且未请求未初始化数据时对整段区域执行 memset 清零。该特性的两个典型受益者是uClibc用它加速 malloc()ELF-FDPIC binfmt用它分配 brk 与栈区域。3.3 映射可见性/proc 接口系统上所有私有复制与匿名映射的清单可通过/proc/maps查看no-MMU 模式特有某进程使用的全部映射清单可通过/proc/pid/maps查看。3.4 不支持的标志提供MAP_FIXED或请求特定映射地址将直接返回错误。no-MMU 无法实现固定地址映射因为映射位置由物理连续内存的可用性决定。3.5 对文件 read 方法的要求被私有映射的文件通常必须由驱动或文件系统提供read 方法以便在 mmap() 选择不直接映射后备设备时能把文件内容读入已分配的内存。若缺失 read 方法则产生错误。这最常发生在字符设备文件、管道pipes、FIFO 和 socket 上。4. Futex 支持若架构支持no-MMU 模式同样支持 futex。以下情况会返回错误传入 futex 系统调用的地址位于进程映射之外该地址所在的映射不支持 futex例如 I/O 字符设备映射。原因在于 futex 依赖物理内存上的原子操作与等待队列而 no-MMU 无法对 I/O 映射或映射外地址提供此类保证。5. 进程间共享内存IPC SHM / POSIX SHMSYSV IPC SHM 共享内存与 POSIX 共享内存在 no-MMU 模式下都受支持SYSV SHM 通过常规机制shmat 等提供POSIX 共享内存通过创建在ramfs 或 tmpfs 挂载点上的文件提供——即借助第 2.6 节描述的内存后端文件共享映射机制。内核文档 Documentation/admin-guide/mm/nommu-mmap.rst 同时建议对这类空文件执行增大文件大小的 truncate 操作应视为收集足够页以兑现映射的请求——这是支持 POSIX 共享内存的必备前提。相应的此类内存后端设备由其 backing device info 中的memory_backed标志标识。6. No-MMU 下的 mremap 语义mremap() 在 no-MMU 下部分支持可以改变映射大小且在指定MREMAP_MAYMOVE且新大小超出当前 slab 对象容量或更小的 slab 对象可被利用时可以移动映射注移动能力当前未实现见文档脚注[#]MREMAP_FIXED 不受支持但若地址无变化且对象无需移动则该标志会被忽略共享映射不可移动可共享映射即便当前未共享也不可移动mremap() 的基地址与大小必须与先前映射精确匹配不允许在既有映射中打洞、移动部分映射或调整部分映射大小——它必须作用于完整的映射。7. 为设备提供可共享映射支持驱动实现指南7.1 共享字符设备支持要提供可共享的字符设备支持驱动必须实现三个关键操作file-f_op-get_unmapped_area()mmap() 例程调用它获取提议的映射地址。若映射过长、偏移怪异、标志组合不受支持等它可以返回错误拒绝backing device info 能力位驱动应提供后备设备信息设置能力位以指示该设备允许的映射类型。默认假定为可读可写、不可执行、仅可直接共享不能被复制file-f_op-mmap()实际启用映射时调用。此处仍可拒绝返回ENOSYS错误会在指定了NOMMU_MAP_COPY时触发改为复制映射的降级路径。此外vm_ops-close()例程会在 chardev 上最后一个映射被移除时调用已有映射若可能则被共享整体或部分共享无需通知驱动get_unmapped_area() 也允许返回-ENOSYS表示本驱动不想处理此请求。典型场景是 framebuffer 驱动把调用转发给设备特定驱动而后者未实现该操作。此时若未指定NOMMU_MAP_COPY映射请求被拒绝否则降级为复制映射。能力位定义位于 include/linux/fs.h/* * NOMMU_MAP_COPY: Copy can be mapped (MAP_PRIVATE) * NOMMU_MAP_DIRECT: Can be mapped directly (MAP_SHARED) * NOMMU_MAP_READ: Can be mapped for reading * NOMMU_MAP_WRITE: Can be mapped for writing * NOMMU_MAP_EXEC: Can be mapped for execution */ #define NOMMU_MAP_COPY 0x00000001 #define NOMMU_MAP_DIRECT 0x00000008 #define NOMMU_MAP_READ VM_MAYREAD #define NOMMU_MAP_WRITE VM_MAYWRITE #define NOMMU_MAP_EXEC VM_MAYEXEC #define NOMMU_VMFLAGS \ (NOMMU_MAP_READ | NOMMU_MAP_WRITE | NOMMU_MAP_EXEC)注意NOMMU_MAP_DIRECT的注释明确对应MAP_SHAREDNOMMU_MAP_COPY对应MAP_PRIVATE——这正是第 2.3 节中直接映射 vs 复制映射两条路径在内核里的落地实现。重要安全提示文档以.. important::标注某些类型的设备在不同模式下会呈现不同外观。例如闪存芯片处于编程或擦除模式时映射中看到的是状态信息而非数据。此时必须格外小心避免驱动控制设备期间用户空间通过共享或私有映射看到此类信息。尤其要记住私有可执行映射在某些情况下仍可能直接从设备映射即 XIP不能想当然认为私有就一定安全。7.2 共享内存后端文件支持提供内存后端文件的共享映射与共享字符设备类似主要区别在于提供该服务的文件系统通常会分配一段连续页并允许在此之上建立映射。推荐行为对空文件执行增大文件大小的 truncate 操作时视为收集足够页以兑现映射的请求——这是 POSIX 共享内存的必要条件见第 5 节。内存后端设备由其 backing device info 的memory_backed标志标识。7.3 共享块设备支持块设备文件上的共享映射支持与字符设备完全相同。若设备之下没有真实的物理设备驱动应分配足够的连续内存以兑现任何受支持的映射。8. 页修剪行为与 vm.nr_trim_pages 调优8.1 问题根源2 的幂次分配导致的浪费与碎片化no-MMU mmap 分配内存时自动向上取整到最近的 2 的幂次页数。由于系统分配器只能按 2^N × PAGE_SIZE 的量交付内存块实际分配往往大于所需产生两种副作用若不修剪长期映射会浪费多余空间若修剪多余页被返还分配器但在进程频繁创建/销毁大量瞬时进程时会造成额外碎片化。8.2 配置与 sysctl修剪行为是可配置的默认行为是激进修剪——把超出部分全部返还页分配器。为在碎片化上保留更细粒度的控制可以完全禁用修剪或调高触发修剪的页数水位watermark。运行时通过 sysctlvm.nr_trim_pages调节其值表示触发修剪所需的最小多余页数设为 0 表示不进行修剪。内核实现位于 mm/nommu.cstatic int sysctl_nr_trim_pages CONFIG_NOMMU_INITIAL_TRIM_EXCESS; static const struct ctl_table nommu_table[] { { .procname nr_trim_pages, .data sysctl_nr_trim_pages, .maxlen sizeof(sysctl_nr_trim_pages), .mode 0644, .proc_handler proc_dointvec_minmax, .extra1 SYSCTL_ZERO, }, };可见其默认值来自编译期配置CONFIG_NOMMU_INITIAL_TRIM_EXCESS定义于 mm/Kconfig默认1且通过proc_dointvec_minmax校验最小值不得低于 0。8.3 编译期配置 CONFIG_NOMMU_INITIAL_TRIM_EXCESS该选项在启动前设定 mmap() 多余空间修剪行为depends on !MMU默认 1在 mm/Kconfig 中详细描述了修剪的取舍开启修剪可回收多余页但可能加剧碎片化关闭修剪则长期映射会浪费空间。它给出了三种运行形态开启修剪多余空间被裁剪并返还系统分配器瞬时进程多时可能引发碎片化关闭修剪多余空间保留但不用对长期映射意味着空间浪费动态调节通过/proc/sys/vm/nr_trim_pages设定触发修剪的最小多余页数0 表示不修剪。实际修剪判定发生在 mm/nommu.c当多余页数total - point达到sysctl_nr_trim_pages阈值时执行修剪从而在回收浪费与避免碎片化之间取得平衡。9. 总结no-MMU 映射的实践要点在 uClinux 等 no-MMU 系统上设计内存映射代码时请牢记以下要点关注点no-MMU 结论匿名映射必须物理连续整段分配时清零除非 MAP_UNINITIALIZED CONFIG_MMAP_ALLOW_UNINITIALIZED文件私有映射复用已有映射 → 直接映射NOMMU_MAP_DIRECT→ 复制映射NOMMU_MAP_COPY且要求文件有 read 方法文件共享映射普通文件/块设备不支持仅内存后端文件ramfs/tmpfs、内存后端块设备、支持直接访问的字符设备可行MAP_FIXED / 指定地址直接报错进程模型无 fork()clone() 需 CLONE_VMmremap部分支持只能作用于完整映射共享/可共享映射不可移动futex架构支持即可用但 I/O 映射与映射外地址会报错碎片化控制编译期 CONFIG_NOMMU_INITIAL_TRIM_EXCESS 运行期 vm.nr_trim_pages驱动开发实现 get_unmapped_area()/mmap()/vm_ops-close() 并正确设置 NOMMU_MAP_* 能力位深入源码读者可继续参阅 mm/nommu.cno-MMU mmap 核心实现含 sysctl 表与清零/修剪逻辑、include/linux/fs.hNOMMU_MAP_* 能力位定义以及 mm/KconfigMMAP_ALLOW_UNINITIALIZED 与 NOMMU_INITIAL_TRIM_EXCESS 的完整语义。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表