简介
mem_stress.c — 内存加压
按入参大小mmap 一块匿名私有页 (MAP_PRIVATE|MAP_ANONYMOUS),逐页写入触页提交物理内存,形成真实内存压力。
- 默认每页写 1 字节提交;
-f整块 memset -t sec保持时长(默认直到 SIGINT/SIGTERM)-k sec周期重触,对抗 XNU compressor / 鸿蒙 zram 压缩回收- iOS/macOS 额外打印
phys_footprint(jetsam 判定口径)
mem_bench.c — mmap/memset 耗时统计
按入参大小分轮申请匿名页,分别计时 mmap / 填充 / munmap,输出 avg/min/max 与吞吐。-m memset(默认):整块填充 + MiB/s 吞吐-m touch:每页写 1 字节,输出 ns/page 首触缺页成本-m none:仅 mmap/munmap(映射建立成本)- 每轮换填充值 0x11/0x22… 并读回校验,防零页/去重优化
设计要点:纯 POSIX C、单文件、无第三方依赖,仅 RSS 读取按平台分支(Apple 走
task_vm_info,
Linux/鸿蒙走 /proc/self/statm)。大小参数支持 512M / 2G / 100K / KiB 二进制单位。
参数为手写解析(BSD/musl 的 getopt 不做 GNU 重排,"size 在前选项在后"会解析失败,已规避)。
用法与实测输出
mem_stress — 内存加压
# 提交 512M 匿名页, 保持 2s 后释放; 长时间加压建议加 -k 5 对抗压缩
./mem_stress 512M -t 2
./mem_stress 1500M -t 60 -k 5
macOS (Apple Silicon, 16K 页) 实测:
alloc : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mmap 0.040 ms touch : 10% ( 51.20 MiB) elapsed 0.01 s ... touch : 99% ( 506.88 MiB) elapsed 0.09 s touch : touched 32768 pages (512.00 MiB) in 0.092 s [5580.1 MiB/s, 357.1K pages/s] rss : resident 229.52 MiB, phys_footprint 513.22 MiB hold : 2.000 s release: munmap ok, total 2.120 s, peak rss 233.16 MiB
注意上面
rss 229 MiB < phys_footprint 513 MiB —— 触页完成后压缩器已吃掉约一半驻留页,
而 phys_footprint 把已压缩脏页一并计入,才是 jetsam 眼中的真实压力。加压实验请看 footprint。
mem_bench — mmap / memset 耗时统计
# 256M 重复 3 轮, 统计 mmap/memset/unmap 耗时; -m touch 看单缺页成本
./mem_bench 256M -r 3
./mem_bench 256M -m touch
macOS 实测(memset 模式):
target : 268435456 bytes (256.00 MiB), 16384 page(s) x 16384 B, mode=memset, runs=3 run 1/3: mmap= 1.0 us | fill(memset)= 65.330 ms ( 3918.6 MiB/s) | unmap= 3.712 ms | rss= 256.56 MiB run 2/3: mmap= 10.0 us | fill(memset)= 33.109 ms ( 7732.0 MiB/s) | unmap= 6.100 ms | rss= 256.80 MiB run 3/3: mmap= 10.0 us | fill(memset)= 35.269 ms ( 7258.5 MiB/s) | unmap= 3.870 ms | rss= 256.80 MiB ---- summary (3 runs, 256.00 MiB, memset mode) ---- mmap : avg 7.000 us | min 0.999 us | max 10.001 us fill : avg 44.569 ms | min 33.109 ms | max 65.330 ms unmap : avg 4.561 ms | min 3.712 ms | max 6.100 ms memset throughput: avg 6303.0 MiB/s over 3 run(s) peak rss: 256.80 MiB
首触缺页成本对比(-m touch):
macOS (16K 页) : first-touch fault: avg 1492.7 ns/page over 3 run(s) Linux musl (4K页): first-touch fault: avg 1103.2 ns/page over 3 run(s)
mmap 本身只是建立映射(µs 级),真正的成本在填充阶段的缺页 + 写入 —— 两者分开计时,恰好回答"申请"与"提交"各自的耗时。
编译与部署
| 平台 | 命令 | 产物 |
|---|---|---|
| 本机 (macOS/Linux) | make | mem_stress / mem_bench |
| iOS (越狱) | make ios | ios/*.arm64 Mach-O |
| HarmonyOS | make ohos(需 OHOS NDK) | ohos/*.musl ELF |
iOS — 编译 + 越狱设备部署
make ios # 等价: xcrun -sdk iphoneos clang -arch arm64 -O2 -miphoneos-version-min=12.0 -o ios/mem_stress mem_stress.c # 部署 (iproxy 2222 → 设备 22, root/alpine) scp -P 2222 ios/mem_stress root@localhost:/var/mobile/ios_memslice/ ssh -p 2222 root@localhost \ '/usr/bin/ldid -S /var/mobile/ios_memslice/mem_stress && chmod +x /var/mobile/ios_memslice/mem_stress' ssh -p 2222 root@localhost '/var/mobile/ios_memslice/mem_stress 200M -t 10'
- 必须设备端
ldid -S签名:Mac 侧签完传上去会被 SIGKILL (rc=137) - 覆盖二进制先
rm再传,直接覆盖同样触发 rc=137 - sftp/scp 上传后必须
chmod +x;工具仅需普通 mmap,无需 entitlements - 非越狱 iOS 无法直接跑 CLI 二进制,同一份源码可嵌进 App target 编译
HarmonyOS — OHOS NDK 编译 + hdc 部署
make ohos # 等价 (OHOS_NATIVE 按实际安装路径调整): OHOS_NATIVE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native $OHOS_NATIVE/llvm/bin/clang --target=aarch64-linux-ohos \ --sysroot=$OHOS_NATIVE/sysroot -O2 -o ohos/mem_stress mem_stress.c # 部署 (设备需开发者模式) hdc file send ohos/mem_stress /data/local/tmp/mem_stress hdc shell "chmod +x /data/local/tmp/mem_stress" hdc shell "/data/local/tmp/mem_stress 200M -t 10"
- 必须用 OHOS NDK 的 clang(链接 musl,动态链接器
/lib/ld-musl-aarch64.so.1);glibc 交叉链产物设备上无法运行 - 模拟器 (x86_64):
make ohos OHOS_TRIPLE=x86_64-linux-ohos - 鸿蒙存在 zram 压缩回收,长时间加压建议
-k 5重触
验证矩阵(2026-10 实测)
| 平台 | 编译 | 运行 |
|---|---|---|
| macOS (Apple Silicon, 16K 页) | ✅ -Wall -Wextra 零警告 | ✅ 全模式 + SIGTERM 干净退出 + 压缩行为观察 |
iOS arm64 (xcrun -sdk iphoneos clang) | ✅ 零警告, 纯 libSystem 符号 | 待越狱设备部署(命令如上) |
| Linux aarch64 glibc | ✅ 零警告 | ✅ 全模式(4K 页, /proc RSS) |
| Linux aarch64 musl(鸿蒙同族 libc) | ✅ 零警告 | ✅ 全模式 |
| HarmonyOS 真机 | 命令已给出(本机无 NDK/hdc) | 待设备 |
平台须知
- 单位均为二进制:K=1024、M=1024²、G=1024³、T=1024⁴;大小自动向上取整到页
- iOS 单进程匿名保留上限 ~4GiB(14.8 实测:mmap 4G 成功 / 8G 失败),超限 mmap 直接报错;更常见的是压力过大先被 jetsam SIGKILL(属预期行为)
- iOS 极限加压(free 页 < 500)有内核 panic 风险,真机测试请留余量
- 信号处理:SIGINT/SIGTERM 干净退出并输出峰值 RSS;触页过程中断即时生效(
-f整块 memset 除外) - README 里有完整的坑清单与样例输出
源码
以下为完整源码(与服务端文件一致),右上角可复制 / 下载原始文件:
/*
* mem_stress.c — 匿名页内存加压工具 (iOS / HarmonyOS / macOS / Linux 通用)
*
* 功能:
* 按入参大小 mmap 一块匿名私有内存 (MAP_PRIVATE|MAP_ANONYMOUS), 并逐页写入
* 触页 (touch) 提交物理页, 形成真实内存压力; 之后按参数保持一段时间
* (或直到收到 SIGINT/SIGTERM) 再释放。
*
* 可选 -k 周期性重触: 每个周期向所有页写入新值, 迫使被 XNU compressor /
* Linux zram 压缩回收的页重新解压驻留, 并把干净页重新弄脏, 维持压力。
*
* 用法:
* mem_stress [-f] [-k sec] [-t sec] [-qv] <size>[K|M|G|T]
*
* 构建:
* host : cc -O2 -o mem_stress mem_stress.c
* iOS : xcrun -sdk iphoneos clang -arch arm64 -O2 \
* -miphoneos-version-min=12.0 -o mem_stress mem_stress.c
* HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
* --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_stress mem_stress.c
*
* 说明:
* - 纯 POSIX C, 无第三方依赖; 仅 RSS/footprint 读取按平台分支。
* - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
* - iOS 上单进程匿名保留有 ~4GiB 上限 (14.8 实测), 超限 mmap 直接失败;
* 更常见的是压力过大先被 jetsam 以 SIGKILL 终止 —— 属预期行为。
*/
#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#endif
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
static volatile sig_atomic_t g_stop = 0;
static size_t g_page = 4096;
/* ---------------- 公共小工具 ---------------- */
static void on_signal(int sig)
{
(void)sig;
g_stop = 1;
}
static double now_sec(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static double to_mib(unsigned long long bytes)
{
return (double)bytes / 1048576.0;
}
/*
* 解析大小参数: "1024" "100K" "512M" "2G" "1T", 兼容 B/iB 后缀与大小写。
* 二进制单位 (K=1024)。成功返回 0, 失败返回 -1。
*/
static int parse_size(const char *s, unsigned long long *out)
{
if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
return -1;
errno = 0;
char *end = NULL;
unsigned long long v = strtoull(s, &end, 10);
if (errno != 0 || end == s)
return -1;
const char *u = end;
unsigned long long mult = 1ULL;
if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
*u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
switch (*u | 0x20) {
case 'k': mult = 1ULL << 10; break;
case 'm': mult = 1ULL << 20; break;
case 'g': mult = 1ULL << 30; break;
case 't': mult = 1ULL << 40; break;
}
u++;
if (*u == 'i') /* KiB / MiB / GiB ... */
u++;
if (*u == 'B' || *u == 'b')
u++;
} else if (*u == 'B' || *u == 'b') {
u++; /* "100B" = 100 字节 */
}
if (*u != '\0')
return -1;
if (v == 0)
return -1;
if (v > ULLONG_MAX / mult)
return -1;
*out = v * mult;
return 0;
}
/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.resident_size;
#else
/* Linux / OpenHarmony(musl): /proc/self/statm 第二列 = 驻留页数 */
FILE *f = fopen("/proc/self/statm", "r");
if (f == NULL)
return 0;
unsigned long long total = 0, resident = 0;
int n = fscanf(f, "%llu %llu", &total, &resident);
fclose(f);
if (n != 2)
return 0;
return resident * (unsigned long long)g_page;
#endif
}
#if defined(__APPLE__)
/* iOS jetsam 的判定口径: phys_footprint = 驻留脏页 + 已压缩脏页 + IOKit 映射 */
static unsigned long long get_footprint_bytes(void)
{
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.phys_footprint;
}
#endif
/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) != 0)
return 0;
#if defined(__APPLE__)
return (unsigned long long)ru.ru_maxrss;
#else
return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}
/*
* 解析秒数参数 (strtod 严格校验, 拒绝垃圾输入)。成功返回 0。
* 注: 不用 getopt —— BSD/musl 的 getopt 不重排 argv, "size 在前、选项在后"
* 的写法会解析失败; 手写解析保证各平台行为一致。
*/
static int parse_seconds(const char *s, double *out)
{
if (s == NULL || *s == '\0')
return -1;
errno = 0;
char *end = NULL;
double v = strtod(s, &end);
if (errno != 0 || end == s || *end != '\0' || v < 0.0)
return -1;
*out = v;
return 0;
}
static void usage(FILE *out, const char *prog)
{
fprintf(out,
"mem_stress - anonymous memory pressure tool (iOS/HarmonyOS/macOS/Linux)\n"
"\n"
"usage: %s [-f] [-k sec] [-t sec] [-qv] <size>\n"
"\n"
" <size> bytes of anonymous memory to commit\n"
" e.g. 268435456 | 100K | 512M | 1500M | 2G | 1T\n"
" (binary units: K=1024, M=1024^2, G=1024^3; B/iB suffix ok)\n"
" -f full fill: memset the whole region (default: 1 byte per page)\n"
" -k sec re-touch all pages every <sec> while holding, to defeat\n"
" compressor/zram reclaim (0 = off, default off)\n"
" -t sec hold duration after touch, then release\n"
" (default: hold until SIGINT/SIGTERM)\n"
" -q quiet (suppress progress lines)\n"
" -v verbose progress\n"
"\n"
"examples:\n"
" %s 512M -t 30 commit 512M, hold 30s, release\n"
" %s 1500M -t 60 -k 5 hold 60s, re-touch every 5s against compression\n",
prog, prog, prog);
}
int main(int argc, char **argv)
{
int full_fill = 0, verbose = 0, quiet = 0;
double keep_interval = 0.0;
double hold_secs = -1.0; /* -1 = 一直持有直到信号 */
const char *size_arg = NULL;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-f") == 0) {
full_fill = 1;
} else if (strcmp(a, "-q") == 0) {
quiet = 1;
} else if (strcmp(a, "-v") == 0) {
verbose = 1;
} else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(stdout, argv[0]);
return 0;
} else if (strcmp(a, "-k") == 0 || strcmp(a, "-t") == 0) {
double v = 0.0;
if (i + 1 >= argc || parse_seconds(argv[i + 1], &v) != 0) {
fprintf(stderr, "error: %s expects a non-negative number of seconds\n", a);
return 2;
}
i++;
if (a[1] == 'k')
keep_interval = v;
else
hold_secs = v;
} else if (a[0] == '-' && a[1] != '\0') {
fprintf(stderr, "error: unknown option '%s'\n", a);
usage(stderr, argv[0]);
return 2;
} else {
if (size_arg != NULL) {
fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
size_arg, a);
return 2;
}
size_arg = a;
}
}
if (size_arg == NULL) {
usage(stderr, argv[0]);
return 2;
}
unsigned long long req = 0;
if (parse_size(size_arg, &req) != 0) {
fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
size_arg);
return 2;
}
long ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
g_page = (size_t)ps;
if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
req);
return 2;
}
size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
unsigned long long pages = (unsigned long long)(size / g_page);
setvbuf(stdout, NULL, _IOLBF, 0); /* 管道/ssh 下进度实时可见 */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* 不重启: nanosleep 被信号打断立刻返回 */
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
double t_start = now_sec();
/* ---- 申请匿名区 ---- */
double t0 = now_sec();
char *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
double map_s = now_sec() - t0;
if (mem == MAP_FAILED) {
fprintf(stderr, "error: mmap %llu bytes failed: %s\n",
(unsigned long long)size, strerror(errno));
#if defined(__APPLE__)
fprintf(stderr, "hint: iOS caps a single process's anonymous reservation "
"(~4GiB observed on 14.8); try a smaller size.\n");
#endif
return 1;
}
if (!quiet)
printf("alloc : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
"mmap %.3f ms\n",
(unsigned long long)size, to_mib(size), pages, g_page,
map_s * 1e3);
/* ---- 触页提交物理内存 ---- */
unsigned long long done = 0; /* 已触页数 (page-touch 模式) */
t0 = now_sec();
if (full_fill) {
memset(mem, 0xA5, size); /* 整块填充 (不可中途打断) */
} else {
/* 每页首字节写一次即触发缺页提交; 默认每 10% 汇报, -v 每 1% */
unsigned long long report_every = pages / (verbose ? 100 : 10);
if (report_every == 0)
report_every = 1;
for (size_t off = 0; off < size; off += g_page) {
if (g_stop)
break;
mem[off] = (char)0xA5;
done++;
if (!quiet && (done % report_every) == 0)
printf("touch : %3llu%% (%8.2f MiB) elapsed %.2f s\n",
done * 100ULL / pages,
to_mib(done * (unsigned long long)g_page),
now_sec() - t0);
}
}
double touch_s = now_sec() - t0;
if (g_stop && !full_fill) {
printf("touch : interrupted at %.2f MiB (%llu/%llu pages)\n",
to_mib(done * (unsigned long long)g_page), done, pages);
} else {
printf("touch : %s %llu pages (%.2f MiB) in %.3f s",
full_fill ? "filled" : "touched", pages, to_mib(size), touch_s);
if (touch_s > 0.0)
printf(" [%.1f MiB/s, %.1fK pages/s]",
to_mib(size) / touch_s,
(double)pages / touch_s / 1000.0);
printf("\n");
}
{
unsigned long long rss = get_rss_bytes();
#if defined(__APPLE__)
unsigned long long fp = get_footprint_bytes();
if (rss || fp)
printf("rss : resident %.2f MiB, phys_footprint %.2f MiB\n",
to_mib(rss), to_mib(fp));
#else
if (rss)
printf("rss : resident %.2f MiB\n", to_mib(rss));
#endif
}
/* ---- 保持 (可选周期重触) ---- */
if (!g_stop) {
if (hold_secs >= 0.0) {
if (!quiet)
printf("hold : %.3f s%s\n", hold_secs,
keep_interval > 0.0 ? ", re-touch on" : "");
} else if (!quiet) {
printf("hold : until SIGINT/SIGTERM (press Ctrl-C to release)\n");
}
double hold0 = now_sec();
double deadline = (hold_secs >= 0.0) ? hold0 + hold_secs : -1.0;
double next_cycle = (keep_interval > 0.0) ? hold0 + keep_interval : -1.0;
unsigned cycle = 0;
while (!g_stop) {
double now = now_sec();
if (deadline > 0.0 && now >= deadline)
break;
if (next_cycle > 0.0 && now >= next_cycle) {
cycle++;
/* 写入与上一周期不同的值: 强制压缩页解压回 RAM、干净页重新变脏 */
unsigned char pat = (unsigned char)(0xA0 | (cycle & 0x0F));
if (full_fill) {
memset(mem, pat, size);
} else {
for (size_t off = 0; off < size && !g_stop; off += g_page)
mem[off] = (char)pat;
}
if (!quiet) {
printf("cycle : %u re-touched %.2f MiB", cycle, to_mib(size));
unsigned long long r2 = get_rss_bytes();
if (r2)
printf(", rss %.2f MiB", to_mib(r2));
printf("\n");
}
next_cycle += keep_interval;
continue;
}
struct timespec ts = { 0, 20 * 1000 * 1000 }; /* 20ms 轮询 */
nanosleep(&ts, NULL);
}
}
/* ---- 释放 ---- */
munmap(mem, size);
printf("release: munmap ok, total %.3f s, peak rss %.2f MiB\n",
now_sec() - t_start, to_mib(get_peak_rss_bytes()));
return 0;
}
/*
* mem_bench.c — 匿名页申请耗时基准 (iOS / HarmonyOS / macOS / Linux 通用)
*
* 功能:
* 按入参大小反复 mmap 一块匿名私有内存, 分别统计:
* - mmap 耗时 (建立映射本身, 通常只有 µs 级)
* - fill 耗时 (memset 整块填充 —— 真正触发缺页 + 写入的主成本;
* 或 touch 模式: 每页写 1 字节, 专门测首次触页缺页成本)
* - unmap 耗时 (munmap 释放)
* 每轮结束后做轻量读回校验 (防止填充被优化掉, 并确认内存真实可读),
* 最后输出 avg/min/max 汇总与吞吐率。
*
* 用法:
* mem_bench [-r n] [-m memset|touch|none] [-q] <size>[K|M|G|T]
*
* 构建:
* host : cc -O2 -o mem_bench mem_bench.c
* iOS : xcrun -sdk iphoneos clang -arch arm64 -O2 \
* -miphoneos-version-min=12.0 -o mem_bench mem_bench.c
* HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
* --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_bench mem_bench.c
*
* 说明:
* - 纯 POSIX C, 无第三方依赖; 仅 RSS 读取按平台分支。
* - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
* - 每轮 memset 使用不同填充值 (0x11/0x22/...), 规避任何零页/去重优化。
* - iOS 为 16K 页, Linux/鸿蒙通常为 4K 页; touch 模式的 ns/page 指标
* 可直接对比两平台的单缺页成本。
*/
#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#endif
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
enum fill_mode { FILL_MEMSET, FILL_TOUCH, FILL_NONE };
static volatile sig_atomic_t g_stop = 0;
static volatile uint64_t g_sink = 0; /* 读回校验累加器, volatile 防优化 */
static size_t g_page = 4096;
/* ---------------- 公共小工具 ---------------- */
static void on_signal(int sig)
{
(void)sig;
g_stop = 1;
}
static double now_sec(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static double to_mib(unsigned long long bytes)
{
return (double)bytes / 1048576.0;
}
/* 解析大小参数 (与 mem_stress 相同: 二进制单位, 兼容 B/iB 后缀) */
static int parse_size(const char *s, unsigned long long *out)
{
if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
return -1;
errno = 0;
char *end = NULL;
unsigned long long v = strtoull(s, &end, 10);
if (errno != 0 || end == s)
return -1;
const char *u = end;
unsigned long long mult = 1ULL;
if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
*u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
switch (*u | 0x20) {
case 'k': mult = 1ULL << 10; break;
case 'm': mult = 1ULL << 20; break;
case 'g': mult = 1ULL << 30; break;
case 't': mult = 1ULL << 40; break;
}
u++;
if (*u == 'i')
u++;
if (*u == 'B' || *u == 'b')
u++;
} else if (*u == 'B' || *u == 'b') {
u++;
}
if (*u != '\0')
return -1;
if (v == 0)
return -1;
if (v > ULLONG_MAX / mult)
return -1;
*out = v * mult;
return 0;
}
/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.resident_size;
#else
FILE *f = fopen("/proc/self/statm", "r");
if (f == NULL)
return 0;
unsigned long long total = 0, resident = 0;
int n = fscanf(f, "%llu %llu", &total, &resident);
fclose(f);
if (n != 2)
return 0;
return resident * (unsigned long long)g_page;
#endif
}
/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) != 0)
return 0;
#if defined(__APPLE__)
return (unsigned long long)ru.ru_maxrss;
#else
return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}
/*
* 严格的整数解析 (拒绝垃圾输入)。注: 不用 getopt —— BSD/musl 的 getopt
* 不重排 argv, "size 在前、选项在后"的写法会解析失败; 手写解析保证
* 各平台行为一致。
*/
static int parse_int(const char *s, long *out)
{
if (s == NULL || *s == '\0')
return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno != 0 || end == s || *end != '\0')
return -1;
*out = v;
return 0;
}
static const char *fill_mode_name(enum fill_mode m)
{
switch (m) {
case FILL_MEMSET: return "memset";
case FILL_TOUCH: return "touch ";
case FILL_NONE: return "none ";
}
return "?";
}
static void usage(FILE *out, const char *prog)
{
fprintf(out,
"mem_bench - anonymous mmap/memset timing benchmark (iOS/HarmonyOS/macOS/Linux)\n"
"\n"
"usage: %s [-r n] [-m mode] [-q] <size>\n"
"\n"
" <size> bytes of anonymous memory per run\n"
" e.g. 268435456 | 100K | 512M | 2G (binary units, B/iB suffix ok)\n"
" -r n repeat runs (default 3)\n"
" -m mode fill mode after mmap (default memset):\n"
" memset memset the whole region (faults + bandwidth)\n"
" touch write 1 byte per page (first-touch fault cost, ns/page)\n"
" none mmap only (mapping setup/teardown cost)\n"
" -q quiet: per-run lines suppressed\n"
"\n"
"examples:\n"
" %s 512M -r 5 time mmap + memset of 512M, 5 runs\n"
" %s 512M -m touch time first-touch page faults (ns/page)\n",
prog, prog, prog);
}
/* 汇总打印一行: name avg/min/max (value 数组单位为秒, 乘 scale 换显示单位) */
static void print_stats(const char *name, const double *v, int n,
double scale, const char *unit)
{
double sum = 0.0, mn = v[0], mx = v[0];
for (int i = 0; i < n; i++) {
sum += v[i];
if (v[i] < mn) mn = v[i];
if (v[i] > mx) mx = v[i];
}
printf("%-7s: avg %10.3f %s | min %10.3f %s | max %10.3f %s\n",
name, sum / (double)n * scale, unit,
mn * scale, unit, mx * scale, unit);
}
int main(int argc, char **argv)
{
int repeats = 3;
int quiet = 0;
enum fill_mode mode = FILL_MEMSET;
const char *size_arg = NULL;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-q") == 0) {
quiet = 1;
} else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(stdout, argv[0]);
return 0;
} else if (strcmp(a, "-r") == 0) {
long v = 0;
if (i + 1 >= argc || parse_int(argv[i + 1], &v) != 0 ||
v < 1 || v > 10000) {
fprintf(stderr, "error: -r expects an integer in 1..10000\n");
return 2;
}
i++;
repeats = (int)v;
} else if (strcmp(a, "-m") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "error: -m expects a mode (memset|touch|none)\n");
return 2;
}
i++;
if (strcmp(argv[i], "memset") == 0) mode = FILL_MEMSET;
else if (strcmp(argv[i], "touch") == 0) mode = FILL_TOUCH;
else if (strcmp(argv[i], "none") == 0) mode = FILL_NONE;
else {
fprintf(stderr, "error: unknown mode '%s' (memset|touch|none)\n",
argv[i]);
return 2;
}
} else if (a[0] == '-' && a[1] != '\0') {
fprintf(stderr, "error: unknown option '%s'\n", a);
usage(stderr, argv[0]);
return 2;
} else {
if (size_arg != NULL) {
fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
size_arg, a);
return 2;
}
size_arg = a;
}
}
if (size_arg == NULL) {
usage(stderr, argv[0]);
return 2;
}
unsigned long long req = 0;
if (parse_size(size_arg, &req) != 0) {
fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
size_arg);
return 2;
}
long ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
g_page = (size_t)ps;
if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
req);
return 2;
}
size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
unsigned long long pages = (unsigned long long)(size / g_page);
setvbuf(stdout, NULL, _IOLBF, 0);
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
double *t_mmap = malloc((size_t)repeats * sizeof(double));
double *t_fill = malloc((size_t)repeats * sizeof(double));
double *t_unmap = malloc((size_t)repeats * sizeof(double));
if (t_mmap == NULL || t_fill == NULL || t_unmap == NULL) {
fprintf(stderr, "error: out of memory\n");
return 1;
}
printf("target : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
"mode=%s, runs=%d\n",
(unsigned long long)size, to_mib(size), pages, g_page,
fill_mode_name(mode), repeats);
int completed = 0;
for (int i = 0; i < repeats && !g_stop; i++) {
/* --- mmap --- */
double t0 = now_sec();
char *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
double t1 = now_sec();
if (p == MAP_FAILED) {
fprintf(stderr, "error: mmap %llu bytes failed on run %d: %s\n",
(unsigned long long)size, i + 1, strerror(errno));
#if defined(__APPLE__)
fprintf(stderr, "hint: iOS caps a single process's anonymous "
"reservation (~4GiB observed on 14.8); "
"try a smaller size.\n");
#endif
break;
}
t_mmap[i] = t1 - t0;
/* --- fill --- */
unsigned char pat = (unsigned char)(0x11 * (unsigned)(i + 1)); /* 0x11,0x22,... */
t0 = now_sec();
if (mode == FILL_MEMSET) {
memset(p, pat, size);
} else if (mode == FILL_TOUCH) {
for (size_t off = 0; off < size && !g_stop; off += g_page)
p[off] = (char)pat;
}
t1 = now_sec();
t_fill[i] = t1 - t0;
/* 轻量读回校验: 每页读首字节累加, 防止填充/映射被优化掉 */
if (mode != FILL_NONE) {
uint64_t sum = 0;
for (size_t off = 0; off < size; off += g_page)
sum += (uint64_t)(unsigned char)p[off];
g_sink = sum;
}
unsigned long long rss = get_rss_bytes();
/* --- unmap --- */
t0 = now_sec();
munmap(p, size);
t_unmap[i] = now_sec() - t0;
completed = i + 1;
if (!quiet) {
printf("run %d/%d: mmap=%9.1f us | fill(%s)=%10.3f ms",
i + 1, repeats, t_mmap[i] * 1e6,
mode == FILL_MEMSET ? "memset" :
mode == FILL_TOUCH ? "touch " : "none ",
t_fill[i] * 1e3);
if (mode == FILL_MEMSET && t_fill[i] > 0.0)
printf(" (%8.1f MiB/s)", to_mib(size) / t_fill[i]);
if (mode == FILL_TOUCH && t_fill[i] > 0.0 && pages > 0)
printf(" (%8.1f ns/page)",
t_fill[i] * 1e9 / (double)pages);
printf(" | unmap=%8.3f ms", t_unmap[i] * 1e3);
if (rss)
printf(" | rss=%8.2f MiB", to_mib(rss));
printf("\n");
}
if (i + 1 < repeats) {
struct timespec ts = { 0, 100 * 1000 * 1000 }; /* 100ms 间隔 */
nanosleep(&ts, NULL);
}
}
if (completed == 0) {
fprintf(stderr, "error: no run completed\n");
free(t_mmap); free(t_fill); free(t_unmap);
return 1;
}
/* ---- 汇总 ---- */
printf("---- summary (%d run%s, %.2f MiB, %s mode) ----\n",
completed, completed == 1 ? "" : "s", to_mib(size),
mode == FILL_MEMSET ? "memset" :
mode == FILL_TOUCH ? "touch" : "none");
print_stats("mmap", t_mmap, completed, 1e6, "us");
if (mode != FILL_NONE)
print_stats("fill", t_fill, completed, 1e3, "ms");
print_stats("unmap", t_unmap, completed, 1e3, "ms");
if (mode == FILL_MEMSET) {
double tp_sum = 0.0;
int tp_n = 0;
for (int i = 0; i < completed; i++) {
if (t_fill[i] > 0.0) {
tp_sum += to_mib(size) / t_fill[i];
tp_n++;
}
}
if (tp_n > 0)
printf("memset throughput: avg %.1f MiB/s over %d run(s)\n",
tp_sum / (double)tp_n, tp_n);
}
if (mode == FILL_TOUCH && pages > 0) {
double fp_sum = 0.0;
int fp_n = 0;
for (int i = 0; i < completed; i++) {
if (t_fill[i] > 0.0) {
fp_sum += t_fill[i] * 1e9 / (double)pages;
fp_n++;
}
}
if (fp_n > 0)
printf("first-touch fault: avg %.1f ns/page over %d run(s)\n",
fp_sum / (double)fp_n, fp_n);
}
printf("peak rss: %.2f MiB\n", to_mib(get_peak_rss_bytes()));
(void)g_sink;
free(t_mmap); free(t_fill); free(t_unmap);
return 0;
}
# mem_alloc_bench — 跨平台构建
#
# make 本机 (macOS/Linux) 编译 mem_stress + mem_bench
# make ios 交叉编译 iOS arm64 二进制 (输出到 ios/)
# make ohos 交叉编译 HarmonyOS arm64 二进制 (输出到 ohos/, 需 OHOS NDK)
# make clean
#
# OHOS NDK 路径按实际安装位置覆盖, 例如:
# make ohos OHOS_NATIVE=/path/to/openharmony/native
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra -std=c11
BINS = mem_stress mem_bench
# ---- iOS (Xcode 自带工具链) ----
IOS_CC := xcrun -sdk iphoneos clang
IOS_CFLAGS := -arch arm64 -O2 -Wall -Wextra -std=c11 -miphoneos-version-min=12.0
# ---- HarmonyOS (DevEco Studio 的 OHOS NDK, musl libc) ----
OHOS_NATIVE ?= /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
OHOS_CC := $(OHOS_NATIVE)/llvm/bin/clang
# 真机 arm64 用 aarch64-linux-ohos; 模拟器改 x86_64-linux-ohos
OHOS_TRIPLE ?= aarch64-linux-ohos
OHOS_CFLAGS := --target=$(OHOS_TRIPLE) --sysroot=$(OHOS_NATIVE)/sysroot \
-O2 -Wall -Wextra -std=c11
all: $(BINS)
mem_stress: mem_stress.c
$(CC) $(CFLAGS) -o $@ $<
mem_bench: mem_bench.c
$(CC) $(CFLAGS) -o $@ $<
ios: ios/mem_stress ios/mem_bench
ios/%: %.c
@mkdir -p ios
$(IOS_CC) $(IOS_CFLAGS) -o $@ $<
ohos: ohos/mem_stress ohos/mem_bench
ohos/%: %.c
@mkdir -p ohos
$(OHOS_CC) $(OHOS_CFLAGS) -o $@ $<
clean:
rm -rf $(BINS) ios ohos
.PHONY: all ios ohos clean
# mem_alloc_bench
两个纯 POSIX C 的匿名内存工具, 单文件、零依赖, 同一份源码可在
**iOS**(越狱设备) / **HarmonyOS** / macOS / Linux 上编译运行:
| 源码 | 功能 |
|---|---|
| `mem_stress.c` | 按入参大小 mmap 匿名页并逐页触页提交, 进行**内存加压**; 可选周期性重触对抗压缩回收, 可选保持时长后释放 |
| `mem_bench.c` | 按入参大小申请匿名页, 分轮统计 **mmap / 填充(memset) / munmap** 耗时, 输出 avg/min/max 与吞吐 |
## 用法
### mem_stress — 内存加压
```
usage: mem_stress [-f] [-k sec] [-t sec] [-qv] <size>
<size> bytes of anonymous memory to commit
e.g. 268435456 | 100K | 512M | 1500M | 2G | 1T
(binary units: K=1024, M=1024^2, G=1024^3; B/iB suffix ok)
-f full fill: memset the whole region (default: 1 byte per page)
-k sec re-touch all pages every <sec> while holding, to defeat
compressor/zram reclaim (0 = off, default off)
-t sec hold duration after touch, then release
(default: hold until SIGINT/SIGTERM)
-q quiet (suppress progress lines)
-v verbose progress
```
```console
$ ./mem_stress 512M -t 2
alloc : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mmap 0.040 ms
touch : 10% ( 51.20 MiB) elapsed 0.01 s
...
touch : 99% ( 506.88 MiB) elapsed 0.09 s
touch : touched 32768 pages (512.00 MiB) in 0.092 s [5580.1 MiB/s, 357.1K pages/s]
rss : resident 229.52 MiB, phys_footprint 513.22 MiB
hold : 2.000 s
release: munmap ok, total 2.120 s, peak rss 233.16 MiB
```
要点:
- 默认**每页写 1 字节**触发缺页提交物理内存 (加压的标准姿势); `-f` 改为整块 memset。
- 默认**一直持有**直到 `Ctrl-C` / SIGTERM; `-t` 指定保持秒数。
- `-k sec` 周期性向所有页写入新值: 迫使被 XNU compressor / Linux zram 压缩的页
解压回 RAM、干净页重新变脏, 用于长时间维持真实压力。
- iOS/macOS 上额外打印 `phys_footprint` (jetsam 的判定口径, = 驻留脏页 + 已压缩脏页)。
上面真实样例里 RSS 229 MiB < footprint 513 MiB, 就是压缩器已经吃掉一部分页的
直接证据 —— 加压时看 footprint 才是真实压力。
### mem_bench — mmap / memset 耗时统计
```
usage: mem_bench [-r n] [-m mode] [-q] <size>
<size> bytes of anonymous memory per run
e.g. 268435456 | 100K | 512M | 2G (binary units, B/iB suffix ok)
-r n repeat runs (default 3)
-m mode fill mode after mmap (default memset):
memset memset the whole region (faults + bandwidth)
touch write 1 byte per page (first-touch fault cost, ns/page)
none mmap only (mapping setup/teardown cost)
-q quiet: per-run lines suppressed
```
```console
$ ./mem_bench 512M -r 3
target : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mode=memset, runs=3
run 1/3: mmap= 3.0 us | fill(memset)= 82.261 ms ( 6221.3 MiB/s) | unmap= 8.328 ms | rss= 513.44 MiB
run 2/3: mmap= 5.0 us | fill(memset)= 55.465 ms ( 9231.9 MiB/s) | unmap= 5.724 ms | rss= 512.94 MiB
run 3/3: mmap= 12.0 us | fill(memset)= 42.753 ms ( 11975.5 MiB/s) | unmap= 5.822 ms | rss= 512.94 MiB
---- summary (3 runs, 512.00 MiB, memset mode) ----
mmap : avg 7.667 us | min 3.000 us | max 12.000 us
fill : avg 60.160 ms | min 42.753 ms | max 82.261 ms
unmap : avg 6.658 ms | min 5.724 ms | max 8.328 ms
memset throughput: avg 9143.6 MiB/s over 3 run(s)
peak rss: 512.78 MiB
```
要点:
- mmap 本身只是建立映射 (µs 级); 真正的成本在填充阶段 (缺页 + 写入),
两者分开计时, 恰好回答"申请"与"提交"各自的耗时。
- `-m touch` 每页只写 1 字节, 输出 **ns/page** —— 首次触页缺页的单页成本,
可直接对比 iOS (16K 页) 与鸿蒙 (4K 页) 平台。
- 每轮 memset 使用不同填充值 (0x11/0x22/…), 并做读回校验, 防止零页/去重类优化。
## 编译
### 本机 (macOS / Linux)
```console
$ make # 或 cc -O2 -o mem_stress mem_stress.c
$ ./mem_stress 512M -t 5
$ ./mem_bench 256M
```
### iOS (越狱设备, 无需 Xcode 工程)
```console
$ make ios # 产物在 ios/, arm64 Mach-O
# 等价命令:
$ xcrun -sdk iphoneos clang -arch arm64 -O2 -miphoneos-version-min=12.0 \
-o ios/mem_stress mem_stress.c
```
部署到设备 (iproxy 2222 → 设备 22, root/alpine):
```console
$ scp -P 2222 ios/mem_stress ios/mem_bench root@localhost:/var/mobile/ios_memslice/
$ ssh -p 2222 root@localhost \
'/usr/bin/ldid -S /var/mobile/ios_memslice/mem_stress && chmod +x /var/mobile/ios_memslice/mem_stress'
$ ssh -p 2222 root@localhost '/var/mobile/ios_memslice/mem_stress 200M -t 10'
```
注意 (2026-09 越狱设备实测的坑):
- **必须在设备端 `ldid -S` 签名**; Mac 侧 ldid 签完传上去会被 SIGKILL (rc=137)。
- **覆盖已有二进制必须先 `rm` 再传**, 直接覆盖同样触发 rc=137。
- `scp`/sftp 上传后必须 `chmod +x`。
- 本工具只需普通 mmap, 无需任何 entitlements。
- 非越狱 iOS 无法直接运行 CLI 二进制; 同一份源码可嵌进 App target 编译执行。
### HarmonyOS (DevEco Studio 的 OHOS NDK, musl libc)
```console
$ make ohos # 产物在 ohos/
# 等价命令 (OHOS_NATIVE 按实际安装路径调整):
$ OHOS_NATIVE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
$ $OHOS_NATIVE/llvm/bin/clang --target=aarch64-linux-ohos \
--sysroot=$OHOS_NATIVE/sysroot -O2 -o ohos/mem_stress mem_stress.c
```
部署到设备 (hdc, 设备需处于开发者模式):
```console
$ hdc file send ohos/mem_stress /data/local/tmp/mem_stress
$ hdc file send ohos/mem_bench /data/local/tmp/mem_bench
$ hdc shell "chmod +x /data/local/tmp/mem_stress /data/local/tmp/mem_bench"
$ hdc shell "/data/local/tmp/mem_stress 200M -t 10"
$ hdc shell "/data/local/tmp/mem_bench 256M -r 5"
```
注意:
- 必须用 **OHOS NDK 的 clang** (链接 musl, 动态链接器
`/lib/ld-musl-aarch64.so.1`); 普通 glibc 交叉链产物在设备上无法运行。
- 模拟器 (x86_64) 把 target 改为 `x86_64-linux-ohos`:
`make ohos OHOS_TRIPLE=x86_64-linux-ohos`。
- 鸿蒙内核同样存在 zram 压缩回收, 长时间加压建议加 `-k 5` 之类的重触选项。
## 验证状态 (2026-10 实测)
| 平台 | 编译 | 运行 |
|---|---|---|
| macOS (Apple Silicon, 16K 页) | ✅ `-Wall -Wextra` 零警告 | ✅ 全模式 + SIGTERM 干净退出 |
| iOS arm64 (`xcrun -sdk iphoneos clang`) | ✅ 零警告, 纯 libSystem 符号, 无需 entitlements | 待设备 (需越狱部署, 见上) |
| Linux aarch64 glibc (VM) | ✅ 零警告 | ✅ 全模式 (4K 页, /proc RSS) |
| Linux aarch64 **musl** (VM, 鸿蒙同族 libc) | ✅ 零警告 | ✅ 全模式 |
| HarmonyOS 真机 (OHOS NDK) | 命令已给出, 本机无 NDK/hdc | 待设备 |
## 已知平台行为
- **单位均为二进制**: K=1024, M=1024², G=1024³, T=1024⁴; 大小自动向上取整到页。
- **iOS 单进程匿名保留上限 ~4GiB** (iOS 14.8 实测: mmap 4G 成功 / 8G 失败),
超限 mmap 直接报错; 更常见的是压力过大先被 **jetsam SIGKILL** —— 属预期行为。
- iOS 极限加压 (free 页 < 500) 有内核 panic 风险, 手机上测试请留余量。
- macOS/Linux 上两者同样可用 (`make` 即可), 便于先在本机验证行为再到手机上跑。