You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何保护mmap()返回内存页中指定大小的随机内存块?

页内内存块防重复分配的可行方案

你遇到的问题是因为mprotect()仅支持页级别的内存保护,没法直接作用于页内的中间地址。针对你的需求——避免后续allocate()调用覆盖已分配块,这里有几个实用的解决思路:

方法一:用元数据链表管理空闲/已分配块

这是用户态内存分配器(比如标准malloc)的核心实现思路,完全不需要依赖系统级内存保护,纯用户态就能解决问题:

  1. 定义一个元数据结构体,用来记录每个内存块的状态:
typedef struct BlockMeta {
    size_t size;          // 当前块的总大小(含元数据)
    int is_allocated;     // 标记块是否已被分配
    struct BlockMeta* next; // 链表指针,串联所有块
} BlockMeta;
  1. 初始化页时,把整个4KB页初始化为一个大的空闲块:
// 假设page已经通过mmap获取
BlockMeta* initial_block = (BlockMeta*)page;
initial_block->size = PAGESIZE - sizeof(BlockMeta);
initial_block->is_allocated = 0;
initial_block->next = NULL;
  1. 在allocate()函数里,遍历这个链表寻找符合大小的空闲块:
void* allocate(int size) {
    // 对齐size(可选,减少内存碎片)
    size = (size + sizeof(size_t) - 1) & ~(sizeof(size_t) - 1);
    
    BlockMeta* current = (BlockMeta*)page;
    while (current != NULL) {
        if (!current->is_allocated && current->size >= size) {
            // 如果块剩余空间足够拆分,生成新的空闲块
            if (current->size - size > sizeof(BlockMeta)) {
                BlockMeta* split_block = (BlockMeta*)((char*)current + sizeof(BlockMeta) + size);
                split_block->size = current->size - size - sizeof(BlockMeta);
                split_block->is_allocated = 0;
                split_block->next = current->next;
                current->next = split_block;
            }
            // 标记当前块为已分配
            current->is_allocated = 1;
            current->size = size;
            // 返回元数据后的内存地址给用户
            return (char*)current + sizeof(BlockMeta);
        }
        current = current->next;
    }
    // 没有足够空闲空间,返回NULL
    return NULL;
}

这种方式下,后续allocate()会自动跳过已标记为is_allocated的块,完全不会出现覆盖问题,同时还能高效管理内存碎片。

方法二:子页粒度映射(适合需要严格内存写保护的场景)

如果你的需求不仅是防止allocate()重复分配,还要防止用户程序越界写入已分配块,可以把4KB页拆分为多个更小的、独立映射的子页:

  1. 提前按固定粒度(比如256B、512B)拆分4KB页,每个子页单独用mmap映射(用MAP_FIXED确保地址在原页范围内):
#define SUB_PAGE_SIZE 256
#define SUB_PAGE_COUNT (PAGESIZE / SUB_PAGE_SIZE)

void* sub_pages[SUB_PAGE_COUNT];
int sub_page_allocated[SUB_PAGE_COUNT] = {0}; // 标记子页是否已分配

// 初始化子页映射
void init_sub_pages() {
    for (int i = 0; i < SUB_PAGE_COUNT; i++) {
        uintptr_t sub_page_addr = (uintptr_t)page + i * SUB_PAGE_SIZE;
        sub_pages[i] = mmap((void*)sub_page_addr, SUB_PAGE_SIZE, 
                           PROT_READ | PROT_WRITE,
                           MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    }
}
  1. 分配时,计算需要的子页数量,把对应子页标记为已分配,并可以用mprotect()将未分配的子页设为PROT_NONE(防止误写):
void* allocate(int size) {
    int required_sub_pages = (size + SUB_PAGE_SIZE - 1) / SUB_PAGE_SIZE;
    // 寻找连续的空闲子页
    int start_idx = -1;
    int count = 0;
    for (int i = 0; i < SUB_PAGE_COUNT; i++) {
        if (!sub_page_allocated[i]) {
            if (start_idx == -1) start_idx = i;
            count++;
            if (count == required_sub_pages) break;
        } else {
            start_idx = -1;
            count = 0;
        }
    }
    if (start_idx == -1) return NULL;
    
    // 标记子页为已分配,并配置内存权限
    for (int i = 0; i < SUB_PAGE_COUNT; i++) {
        if (i >= start_idx && i < start_idx + required_sub_pages) {
            sub_page_allocated[i] = 1;
            mprotect(sub_pages[i], SUB_PAGE_SIZE, PROT_READ | PROT_WRITE);
        } else if (!sub_page_allocated[i]) {
            mprotect(sub_pages[i], SUB_PAGE_SIZE, PROT_NONE);
        }
    }
    
    return sub_pages[start_idx];
}

这种方法的缺点是会增加系统调用的开销,因为每个子页都需要单独映射和保护,适合对内存安全性要求极高的场景。

总结

如果只是想避免allocate()自身覆盖已分配块,方法一的元数据链表方案是最优选择,它是所有主流内存分配器的核心实现方式,高效且灵活;如果需要防止用户程序越界写入,再考虑方法二的子页保护方案。

内容的提问来源于stack exchange,提问作者hxdshell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 07:32:15