Skip to content
← 返回C列表
开发实践

C 定长分配器完整实践:为 PMR 提供存储 ​

先掌握指针、基本所有权和分离编译;C++部分另需RAII与标准容器。示例未执行;本文仅接受文本、JSON、哈希及Markdown模板验证,没有执行原生工具链。

专题入口:C 定长分配器:容量、对齐与空闲链 · C 定长分配器:归还、复用与销毁契约 · C 定长分配器完整实践:为 PMR 提供存储 · C 定长分配器排错:溢出、悬空与容量

C分配器完整项目的输入输出是什么 ​

C分配器完整项目的输入输出是什么:config.hpp 固定块宽256、8块和整数1到6;C++客户端通过C库支持的resource创建PMR list,预期输出 sum=21 live=0 并退出0。节点大小依实现变化,因此不能从六个元素推断六次池命中;合计值和销毁后计数才是跨实现预期。

C分配器完整项目怎样保持同源 ​

C分配器完整项目怎样保持同源:slab.c 是唯一C实现,slab_resource.hpp 包装相同句柄;两个语言的实践页引用所有项目文件,不复制可漂移的实现。头文件通过extern C保留C链接,CMake把slab编译为C11静态库,并让C++20客户端与测试链接它;构建配置不是已编译证据。

C分配器项目怎样手动构建和测试 ​

C分配器项目怎样手动构建和测试:先在独立目录备好 CMake >=3.20、C11和C++20标准库。README列出configure、build和ctest步骤,多配置生成器使用Debug配置;Windows的exe后缀和产物目录与单配置系统不同。本轮仅保留这些可复现说明,没有执行命令、测试或样例;不能把模板渲染当作C语法检查。

C分配器测试应该覆盖哪些失败 ​

C分配器测试应该覆盖哪些失败:test_slab.c 覆盖配置溢出、池空、错误对齐、内部偏移、重复归还、busy销毁和LIFO复用;CHECK宏不受NDEBUG关闭。TEST-SPEC 区分已有完整测试源码和待扩展malloc故障注入,后者当前只列规格。全部测试示例未执行,没有Sanitizer、性能、跨平台或运行正确性结论。

CMakeLists.txt(同源文件,示例未执行)
text
cmake_minimum_required(VERSION 3.20)
project(p2_allocators LANGUAGES C CXX)
add_library(slab STATIC slab.c)
target_include_directories(slab PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(slab PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED YES C_EXTENSIONS NO)
add_executable(allocator_demo main.cpp)
target_link_libraries(allocator_demo PRIVATE slab)
target_compile_features(allocator_demo PRIVATE cxx_std_20)
add_executable(test_slab test_slab.c)
target_link_libraries(test_slab PRIVATE slab)
add_executable(test_resource test_resource.cpp)
target_link_libraries(test_resource PRIVATE slab)
target_compile_features(test_resource PRIVATE cxx_std_20)
enable_testing()
add_test(NAME slab_contract COMMAND test_slab)
add_test(NAME resource_contract COMMAND test_resource)
config.hpp(同源文件,示例未执行)
cpp
#pragma once
#include <cstddef>
inline constexpr std::size_t block_bytes = 256;
inline constexpr std::size_t block_count = 8;
inline constexpr int item_count = 6;
main.cpp(同源文件,示例未执行)
cpp
#include "config.hpp"
#include "slab_resource.hpp"
#include <iostream>
#include <list>
int main() {
    try {
        slab_resource resource(block_bytes, block_count);
        int sum = 0;
        {
            std::pmr::list<int> items(&resource);
            for (int i = 1; i <= item_count; ++i) items.push_back(i);
            for (int value : items) sum += value;
        }
        std::cout << "sum=" << sum << " live=" << resource.live() << '\n';
        return sum == 21 && resource.live() == 0 ? 0 : 1;
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n'; return 1;
    }
}
README.md(同源文件,示例未执行)
md
# Fixed slab / PMR integrated teaching project

示例未执行。C11 slab 库被 C++20 memory_resource 适配器、PMR list 客户端和两个测试入口共同使用,无第三方依赖。单线程;不是通用 malloc 替代品。

输入在 config.hpp:256 字节、8 块、整数1到6;预期 allocator_demo 输出 `sum=21 live=0` 后换行,退出0。list 节点布局依标准库变化,不能承诺6次都命中池;上游保证支持更大节点。默认 new_delete_resource 允许溢出,因此整个适配器不是有界内存;用 null_memory_resource 才禁止上游成功分配。

手动步骤仅供读者,本文未执行:Windows PowerShell、Linux Bash、macOS Zsh 均在本目录执行 `cmake -S . -B build`,`cmake --build build --config Debug`,`ctest --test-dir build -C Debug --output-on-failure`。单配置产物在 build,多配置通常在 build/Debug;执行对应 allocator_demo 或 allocator_demo.exe。需预先具备 CMake >=3.20、兼容C11/C++20工具链及PMR标准库,不在站点安装或运行。

slab_create 两次 malloc;初始化失败不发布句柄。池空与不支持的大小/对齐走上游,释放按池块起点等值归属决定路径,不能仅根据大小重算,因为池空时小请求也会走上游。分配与释放的 bytes/alignment 必须相同,指针必须来自当前等价资源且尚未释放。资源先构造、容器后构造,容器先销毁。析构检查未归还计数并 terminate 是教学诊断,不代替调用契约。

所有 C 句柄禁止按值复制、并发访问、外部修改元数据。malloc backing 按 max_align_t 对齐,stride向上取整;不承诺扩展对齐。free list 索引存外部元数据,位图记录当前借出;重复归还只在未重新分配期间可检测。无法判断伪造、悬空指针或复用后旧别名;调用者必须保证传入的指针值可合法求值。slab_give 不销毁C++对象,容器负责先析构元素再释放存储。

测试是完整源码,TEST-SPEC 列出预期和未实现故障注入。没有声称 native AST、编译、CMake、测试、性能或三平台运行成功。
slab.c(同源文件,示例未执行)
c
#include "slab.h"
#include <stdint.h>
#include <stdlib.h>
struct slab {
    unsigned char *storage;
    size_t width, stride, count, head, live;
    size_t next[64];
    uint64_t used;
};
size_t slab_alignment(void) { return _Alignof(max_align_t); }
slab *slab_create(size_t width, size_t count) {
    size_t a = slab_alignment(), stride;
    slab *s;
    if (!width || !count || count > 64 || width > SIZE_MAX - (a - 1)) return NULL;
    stride = ((width + a - 1) / a) * a;
    if (stride > SIZE_MAX / count) return NULL;
    s = malloc(sizeof *s);
    if (!s) return NULL;
    s->storage = malloc(stride * count);
    if (!s->storage) { free(s); return NULL; }
    s->width = width; s->stride = stride; s->count = count;
    s->head = 0; s->live = 0; s->used = 0;
    for (size_t i = 0; i < count; ++i) s->next[i] = i + 1;
    return s;
}
void *slab_take(slab *s, size_t bytes, size_t a) {
    size_t i;
    if (!s || !bytes || !a || (a & (a - 1)) ||
        bytes > s->width || slab_alignment() % a || s->head == s->count) return NULL;
    i = s->head; s->head = s->next[i];
    s->used |= UINT64_C(1) << i; ++s->live;
    return s->storage + i * s->stride;
}
static size_t locate(const slab *s, const void *q) {
    /* Equality only; no subtraction/order comparison with unrelated pointers. */
    for (size_t i = 0; i < s->count; ++i)
        if (q == (const void *)(s->storage + i * s->stride)) return i;
    return s->count;
}
int slab_owns(const slab *s, const void *q) {
    return s && q && locate(s, q) != s->count;
}
int slab_give(slab *s, void *q) {
    size_t i;
    if (!s || !q) return -1;
    i = locate(s, q);
    if (i == s->count || !(s->used & (UINT64_C(1) << i))) return -1;
    s->used &= ~(UINT64_C(1) << i);
    s->next[i] = s->head; s->head = i; --s->live;
    return 0;
}
size_t slab_live(const slab *s) { return s ? s->live : 0; }
int slab_destroy(slab *s) {
    if (!s) return 0;
    if (s->live) return -1;
    free(s->storage); free(s); return 0;
}
slab.h(同源文件,示例未执行)
c
#ifndef TEACHING_SLAB_H
#define TEACHING_SLAB_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct slab slab;
/* Single-thread only. count in [1,64], block_bytes > 0.
   Owns malloc storage; supports fundamental alignment only. */
slab *slab_create(size_t block_bytes, size_t count);
/* Returns NULL for unsupported request or exhaustion; never changes state on failure. */
void *slab_take(slab *, size_t bytes, size_t alignment);
/* q must be NULL or a determinate pointer value to still-live storage.
   Rejects non-block starts and duplicate returns while the slab exists.
   Does NOT validate dangling/forged pointers or detect stale aliases after reuse. */
int slab_give(slab *, void *q);
int slab_owns(const slab *, const void *q);
size_t slab_live(const slab *);
size_t slab_alignment(void);
/* Busy returns -1 without freeing. NULL is accepted. */
int slab_destroy(slab *);
#ifdef __cplusplus
}
#endif
#endif
slab_resource.hpp(同源文件,示例未执行)
cpp
#pragma once
#include "slab.h"
#include <cstddef>
#include <exception>
#include <memory_resource>
#include <new>
#include <stdexcept>
class slab_resource final : public std::pmr::memory_resource {
    slab *pool_;
    std::pmr::memory_resource *upstream_;
    std::size_t upstream_live_ = 0;
    void *do_allocate(std::size_t bytes, std::size_t alignment) override {
        if (void *p = slab_take(pool_, bytes, alignment)) return p;
        void *p = upstream_->allocate(bytes, alignment);
        ++upstream_live_; // counts successful upstream allocations only
        return p;
    }
    void do_deallocate(void *p, std::size_t bytes, std::size_t alignment) override {
        // Preconditions: original pointer, size and alignment; not arbitrary pointer validation.
        if (slab_owns(pool_, p)) {
            if (slab_give(pool_, p) != 0) std::terminate();
        } else {
            upstream_->deallocate(p, bytes, alignment);
            --upstream_live_;
        }
    }
    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return this == &other;
    }
public:
    explicit slab_resource(std::size_t width, std::size_t count,
                           std::pmr::memory_resource *upstream = std::pmr::new_delete_resource())
        : pool_(nullptr), upstream_(upstream) {
        if (!upstream) throw std::invalid_argument("null upstream");
        pool_ = slab_create(width, count);
        if (!pool_) throw std::bad_alloc();
    }
    slab_resource(const slab_resource&) = delete;
    slab_resource& operator=(const slab_resource&) = delete;
    ~slab_resource() override {
        if (upstream_live_ || slab_destroy(pool_) != 0) std::terminate();
    }
    std::size_t live() const noexcept { return slab_live(pool_) + upstream_live_; }
};
TEST-SPEC.md(同源文件,示例未执行)
md
# Pending execution / expectations

示例未执行。test_slab 和 test_resource 是可执行测试源码,CHECK 不受 NDEBUG 影响,失败返回源码行号(进程退出码截断可因平台不同)。

- C:0/65块与SIZE_MAX溢出配置拒绝;不支持请求不改状态;两块耗尽;busy destroy 保持句柄有效;外部活对象和内部非起点拒绝;重复归还拒绝;释放后LIFO复用;全部归还可销毁。
- C++:池满的小请求、超大请求、扩展对齐转上游;释放路由正确;不同池即使同上游也不相等;显式allocator复制选择目标资源;null上游确定抛bad_alloc且状态不变;所有容器先销毁,再检查计数归零。
- 待扩展故障注入:包装malloc使第二次分配失败,记录第一次释放;多次上游抛错;元素构造抛错后节点归还。现有源码没有这些注入,不把人工推演说成测试覆盖。
- 禁止把无效pmr deallocate、释放后访问或异源swap当作可恢复测试;它们先违反接口前提。ASan/UBSan若读者另行执行只能提供部分证据;复用后旧指针可能仍指向有效backing,需代际句柄设计才能拒绝。
test_resource.cpp(同源文件,示例未执行)
cpp
#include "slab_resource.hpp"
#include <list>
#include <type_traits>
#define CHECK(x) do { if (!(x)) return __LINE__; } while (0)
class tracking final : public std::pmr::memory_resource {
    void *do_allocate(std::size_t n, std::size_t a) override {
        void *p = std::pmr::new_delete_resource()->allocate(n, a); ++live; return p;
    }
    void do_deallocate(void *p, std::size_t n, std::size_t a) override {
        std::pmr::new_delete_resource()->deallocate(p, n, a); --live;
    }
    bool do_is_equal(const std::pmr::memory_resource& r) const noexcept override { return this == &r; }
public: int live = 0;
};
int main() {
    tracking upstream;
    slab_resource r(256, 1, &upstream), other(256, 1, &upstream);
    CHECK(r.is_equal(r) && !r.is_equal(other));
    void *p = r.allocate(16, 1), *q = r.allocate(16, 1);
    CHECK(upstream.live == 1); // exhaustion forwards even supported request
    r.deallocate(q, 16, 1); r.deallocate(p, 16, 1);
    void *large = r.allocate(1024, 1);
    CHECK(upstream.live == 1); r.deallocate(large, 1024, 1);
    void *over = r.allocate(32, slab_alignment() * 2);
    CHECK(upstream.live == 1); r.deallocate(over, 32, slab_alignment() * 2);
    {
        std::pmr::list<int> a(&r); a.push_back(7);
        std::pmr::list<int> explicit_copy(a, &other);
        CHECK(explicit_copy.get_allocator().resource() == &other);
        CHECK(explicit_copy.front() == 7);
    }
    slab_resource bounded(16, 1, std::pmr::null_memory_resource());
    void *only = bounded.allocate(1, 1);
    bool failed = false;
    try { (void)bounded.allocate(1, 1); } catch (const std::bad_alloc&) { failed = true; }
    CHECK(failed && bounded.live() == 1); bounded.deallocate(only, 1, 1);
    CHECK(upstream.live == 0 && r.live() == 0 && other.live() == 0);
    static_assert(!std::allocator_traits<std::pmr::polymorphic_allocator<int>>::propagate_on_container_move_assignment::value);
    return 0;
}
test_slab.c(同源文件,示例未执行)
c
#include "slab.h"
#include <stdint.h>
#include <stdio.h>
#define CHECK(x) do { if (!(x)) return __LINE__; } while (0)
int main(void) {
    slab *s = slab_create(17, 2);
    void *a, *b; int foreign = 0;
    CHECK(s);
    CHECK(!slab_create(0, 2)); CHECK(!slab_create(1, 0));
    CHECK(!slab_take(s, 0, 1)); CHECK(!slab_take(s, 1, 0));
    CHECK(!slab_create(SIZE_MAX, 2));
    CHECK(!slab_create(1, 65));
    CHECK(!slab_take(s, 18, 1)); CHECK(!slab_take(s, 1, 3));
    a = slab_take(s, 17, slab_alignment());
    b = slab_take(s, 1, 1);
    CHECK(a && b && a != b && slab_live(s) == 2);
    CHECK(!slab_take(s, 1, 1)); CHECK(slab_destroy(s) == -1);
    CHECK(slab_give(s, &foreign) == -1);
    CHECK(slab_give(s, (unsigned char *)a + 1) == -1);
    CHECK(slab_give(s, a) == 0); CHECK(slab_give(s, a) == -1);
    CHECK(slab_take(s, 1, 1) == a); // stale alias cannot be detected after reuse
    CHECK(slab_give(s, a) == 0); CHECK(slab_give(s, b) == 0);
    CHECK(slab_destroy(s) == 0);
    puts("slab contracts: expected pass"); return 0;
}

独立练习 ​

加入零字节和alignment=0两个C请求,要求如何验收?

参考答案(先独立推演)

均应返回NULL,前后live和下次有效take的地址顺序保持相同;将其写入test_slab,不调用未定义的PMR对齐请求。现有 test_slab.c 已覆盖这两个请求返回NULL;前后live和下一次有效地址顺序保持不变的断言仍可补充。

一手资料与边界 ​

2026-10-17核对 WG14 C11 N1570 6.2.8、6.5.6、6.5.8与7.22.3;C++资源协议工作草案用于溯源,滚动草案的新接口不自动纳入C++20;Allocator要求用于核对传播边界。国内资料核读 Microsoft中文memory_resource概览,平台实现说明不推广为所有标准库。项目未编译、未执行,普通静态模板通过不能证明内存安全。

继续阅读