强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

POSIX 标准详解教程 / 第九章:内存管理

第九章:内存管理

理解 POSIX 内存管理:mmap、brk/sbrk、虚拟内存、共享内存映射、内存保护。


9.1 进程内存布局

9.1.1 虚拟地址空间

每个进程拥有独立的虚拟地址空间(64位系统上为 48 位/256TB):

高地址 (0x7FFF...)
┌──────────────────────┐
│     内核空间          │  ← 进程不可访问
├──────────────────────┤
│     栈 (Stack)        │  ← 局部变量、函数调用帧
│     ↓ 向低地址增长    │
│                      │
│  ... 未使用空间 ...   │
│                      │
│     ↑ 向高地址增长    │
│     堆 (Heap)        │  ← malloc/free 管理
├──────────────────────┤
│     BSS 段           │  ← 未初始化全局变量
├──────────────────────┤
│     数据段 (.data)    │  ← 已初始化全局变量
├──────────────────────┤
│     代码段 (.text)    │  ← 可执行代码
└──────────────────────┘
低地址 (0x0000...)

9.1.2 内存区域对比

区域分配方式管理增长方向
栈 (Stack)自动(编译器)LIFO
堆 (Heap)手动(malloc/free)程序员
数据段静态(编译时)编译器
mmap 区域mmap()程序员/内核

9.2 mmap():内存映射

9.2.1 mmap() 接口

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明
addr映射地址建议(通常为 NULL,由内核选择)
length映射长度(字节)
prot保护标志(PROT_READ/WRITE/EXEC/NONE)
flags映射标志(MAP_PRIVATE/SHARED/ANONYMOUS)
fd文件描述符(MAP_ANONYMOUS 时为 -1)
offset文件偏移(必须页对齐)

9.2.2 mmap 标志

标志说明
MAP_PRIVATE私有映射,写入时拷贝(Copy-on-Write)
MAP_SHARED共享映射,修改反映到文件
MAP_ANONYMOUS匿名映射,无文件后端
MAP_FIXED强制使用指定地址(危险,覆盖已有映射)
MAP_NORESERVE不预留交换空间
MAP_POPULATE预填充页表(减少后续 page fault)

9.2.3 基本 mmap 用法

/*
 * mmap_basic.c - mmap 基础用法:文件映射
 * 编译: gcc -Wall -o mmap_basic mmap_basic.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>

int main(void)
{
    const char *path = "/tmp/mmap_test.txt";

    /* 创建测试文件 */
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    const char *data = "Hello, mmap! This is memory-mapped file I/O.\n";
    write(fd, data, strlen(data));

    /* 获取文件大小 */
    struct stat st;
    fstat(fd, &st);
    printf("文件大小: %ld 字节\n", (long)st.st_size);

    /* 映射文件 */
    char *mapped = mmap(NULL, st.st_size,
                         PROT_READ | PROT_WRITE,
                         MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    /* 直接访问映射内存(像普通数组一样) */
    printf("映射内容: %.*s", (int)st.st_size, mapped);

    /* 修改映射内容(自动反映到文件) */
    mapped[0] = 'h';  /* H → h */
    printf("修改后: %.*s", (int)st.st_size, mapped);

    /* 确保写入文件 */
    msync(mapped, st.st_size, MS_SYNC);

    /* 解除映射 */
    munmap(mapped, st.st_size);
    close(fd);

    /* 验证文件已修改 */
    FILE *f = fopen(path, "r");
    char buf[256];
    if (f && fgets(buf, sizeof(buf), f))
        printf("文件内容: %s", buf);
    if (f) fclose(f);

    unlink(path);
    return 0;
}

9.2.4 匿名映射(大块内存分配)

/*
 * mmap_anon.c - 使用匿名映射分配大块内存
 * 编译: gcc -Wall -o mmap_anon mmap_anon.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(void)
{
    size_t size = 1024 * 1024;  /* 1MB */

    /* 匿名映射:不关联文件 */
    char *buf = mmap(NULL, size,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (buf == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    printf("匿名映射分配 %zu 字节\n", size);
    printf("地址: %p\n", (void *)buf);

    /* 使用内存 */
    memset(buf, 'A', size);
    printf("前 64 字节: %.64s\n", buf);
    printf("最后 64 字节: %.64s\n", buf + size - 64);

    /* 释放 */
    munmap(buf, size);
    printf("内存已释放\n");

    return 0;
}

9.3 brk()/sbrk():堆管理

9.3.1 原理

brk()sbrk() 通过调整进程的"断点"(program break)来管理堆内存:

程序代码段 → 数据段 → 堆 → [断点] → ... → 栈
                                    ↑
                               brk/sbrk 移动此位置

注意brk()/sbrk() 是低级接口,不推荐直接使用。malloc() 底层使用它们(或 mmap)分配内存。

9.3.2 sbrk 示例

/*
 * sbrk_demo.c - sbrk 堆管理演示
 * 编译: gcc -Wall -o sbrk_demo sbrk_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    /* 获取当前断点位置 */
    void *current = sbrk(0);
    printf("当前断点: %p\n", current);

    /* 增加 1024 字节堆空间 */
    void *new_mem = sbrk(1024);
    if (new_mem == (void *)-1) {
        perror("sbrk");
        return 1;
    }
    printf("新分配内存起始地址: %p\n", new_mem);
    printf("新断点: %p\n", sbrk(0));
    printf("分配大小: %ld 字节\n",
           (long)((char *)sbrk(0) - (char *)new_mem));

    /* 使用分配的内存 */
    char *p = (char *)new_mem;
    p[0] = 'H';
    p[1] = 'i';
    p[2] = '\0';
    printf("内容: %s\n", p);

    /* 注意:sbrk 不能精确释放部分内存 */
    /* 生产环境应使用 mmap 或 malloc */

    return 0;
}

9.4 内存保护

9.4.1 mprotect():修改页面保护

/*
 * mprotect_demo.c - 使用 mprotect 设置内存保护
 * 编译: gcc -Wall -o mprotect_demo mprotect_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>

static void segv_handler(int sig, siginfo_t *info, void *ctx)
{
    (void)sig;
    (void)ctx;
    const char msg[] = "捕获 SIGSEGV: 尝试写入只读内存!\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);

    char buf[128];
    int len = snprintf(buf, sizeof(buf),
        "  尝试访问地址: %p\n", info->si_addr);
    write(STDERR_FILENO, buf, len);

    _exit(1);
}

int main(void)
{
    /* 注册 SIGSEGV 处理器 */
    struct sigaction sa = {
        .sa_sigaction = segv_handler,
        .sa_flags = SA_SIGINFO,
    };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);

    /* 分配一页内存 */
    size_t pagesize = sysconf(_SC_PAGESIZE);
    char *mem = mmap(NULL, pagesize,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mem == MAP_FAILED) { perror("mmap"); return 1; }

    /* 写入数据 */
    strcpy(mem, "Hello, mprotect!");
    printf("写入: %s\n", mem);

    /* 设置为只读 */
    if (mprotect(mem, pagesize, PROT_READ) == -1) {
        perror("mprotect");
        return 1;
    }
    printf("已设为只读\n");

    /* 尝试写入 → 触发 SIGSEGV */
    printf("尝试写入...\n");
    mem[0] = 'X';  /* 这将触发 SIGSEGV */

    /* 不会执行到这里 */
    munmap(mem, pagesize);
    return 0;
}
$ ./mprotect_demo
写入: Hello, mprotect!
已设为只读
尝试写入...
捕获 SIGSEGV: 尝试写入只读内存!
  尝试访问地址: 0x7f8a0c000000

9.5 mlock()/munlock():锁定内存

/*
 * mlock_demo.c - 锁定内存防止换出(用于实时/安全场景)
 * 编译: gcc -Wall -o mlock_demo mlock_demo.c
 * 注意: 可能需要 root 权限或 CAP_IPC_LOCK
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main(void)
{
    char secret[4096];  /* 一个页面 */

    strcpy(secret, "sensitive-password-12345");

    /* 锁定内存:防止被换出到磁盘 */
    if (mlock(secret, sizeof(secret)) == -1) {
        perror("mlock");
        /* 可能需要调整 ulimit 或以 root 运行 */
        return 1;
    }

    printf("内存已锁定到物理 RAM(不会被换出)\n");
    printf("内容: %s\n", secret);

    /* 安全清理(锁定的内存保证 memset 会实际写入物理页) */
    /* volatile 防止编译器优化掉 memset */
    volatile char *p = secret;
    for (size_t i = 0; i < sizeof(secret); i++)
        p[i] = 0;

    printf("敏感数据已安全清除\n");

    munlock(secret, sizeof(secret));
    return 0;
}

9.6 mmap vs malloc 对比

特性mmapmalloc (大块)malloc (小块)
底层机制直接系统调用通常 mmapbrk/sbrk
最小分配1 页 (4KB)
释放munmap()(立即)free()(可能缓存)free()(归还堆)
碎片无(页对齐)较少可能碎片化
适用场景大块、共享内存、文件映射大块动态分配小块动态分配

glibc malloc 规则:分配大于 128KB(MMAP_THRESHOLD)时,malloc 默认使用 mmap。小块使用 brk 管理的堆。


9.7 MADVISE:内存使用建议

/*
 * madvise_demo.c - 使用 madvise 优化内存使用
 * 编译: gcc -Wall -o madvise_demo madvise_demo.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>

int main(void)
{
    size_t size = 4 * 1024 * 1024;  /* 4MB */

    char *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mem == MAP_FAILED) { perror("mmap"); return 1; }

    /* 建议内核预分配页面 */
    madvise(mem, size, MADV_WILLNEED);

    /* 使用内存 */
    memset(mem, 0, size);

    /* 建议内核可以回收这些页面 */
    madvise(mem, size, MADV_DONTNEED);

    /* 提示顺序访问模式 */
    madvise(mem, size, MADV_SEQUENTIAL);

    /* 提示随机访问模式 */
    madvise(mem, size, MADV_RANDOM);

    printf("madvise 建议已设置\n");

    munmap(mem, size);
    return 0;
}
madvise 标志说明
MADV_NORMAL默认行为
MADV_RANDOM随机访问(禁用预读)
MADV_SEQUENTIAL顺序访问(激进预读)
MADV_WILLNEED提示即将访问(预读页面)
MADV_DONTNEED不再需要(可回收页面)
MADV_FREE页面可丢弃(Linux 4.5+)

9.8 虚拟内存与页面管理

9.8.1 页面大小查询

/* 获取系统页面大小 */
#include <unistd.h>
long pagesize = sysconf(_SC_PAGESIZE);  /* 通常 4096 */

/* 获取物理内存总量 */
long total_mem = sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE);

/* 获取可用内存 */
long avail_mem = sysconf(_SC_AVPHYS_PAGES) * sysconf(_SC_PAGESIZE);

9.8.2 查看进程内存信息

# Linux: 查看进程内存映射
$ cat /proc/self/maps

# 查看进程内存统计
$ cat /proc/self/status | grep Vm

# pmap 命令
$ pmap -x <PID>

9.9 业务场景:共享内存缓存

/*
 * shm_cache.c - 基于共享内存的简单缓存
 * 编译: gcc -Wall -o shm_cache shm_cache.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define CACHE_NAME "/posix_cache"
#define MAX_ENTRIES 64
#define KEY_SIZE 32
#define VALUE_SIZE 128

typedef struct {
    char key[KEY_SIZE];
    char value[VALUE_SIZE];
    int valid;
} cache_entry_t;

typedef struct {
    int count;
    cache_entry_t entries[MAX_ENTRIES];
} cache_t;

static cache_t *cache_open(void)
{
    int fd = shm_open(CACHE_NAME, O_CREAT | O_RDWR, 0644);
    if (fd == -1) return NULL;
    ftruncate(fd, sizeof(cache_t));

    cache_t *c = mmap(NULL, sizeof(cache_t),
                       PROT_READ | PROT_WRITE,
                       MAP_SHARED, fd, 0);
    close(fd);
    return (c == MAP_FAILED) ? NULL : c;
}

static int cache_set(cache_t *c, const char *key, const char *value)
{
    /* 查找已有键或空位 */
    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (c->entries[i].valid &&
            strncmp(c->entries[i].key, key, KEY_SIZE) == 0) {
            strncpy(c->entries[i].value, value, VALUE_SIZE - 1);
            return 0;
        }
    }
    /* 新条目 */
    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (!c->entries[i].valid) {
            strncpy(c->entries[i].key, key, KEY_SIZE - 1);
            strncpy(c->entries[i].value, value, VALUE_SIZE - 1);
            c->entries[i].valid = 1;
            c->count++;
            return 0;
        }
    }
    return -1;  /* 缓存满 */
}

static const char *cache_get(cache_t *c, const char *key)
{
    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (c->entries[i].valid &&
            strncmp(c->entries[i].key, key, KEY_SIZE) == 0)
            return c->entries[i].value;
    }
    return NULL;
}

int main(void)
{
    shm_unlink(CACHE_NAME);  /* 清理旧缓存 */

    cache_t *cache = cache_open();
    if (!cache) { perror("cache_open"); return 1; }
    memset(cache, 0, sizeof(cache_t));

    /* 写入缓存 */
    cache_set(cache, "server_port", "8080");
    cache_set(cache, "db_host", "localhost");
    cache_set(cache, "log_level", "INFO");

    /* 读取缓存 */
    const char *keys[] = {"server_port", "db_host", "log_level", "missing"};
    for (int i = 0; i < 4; i++) {
        const char *val = cache_get(cache, keys[i]);
        printf("%s = %s\n", keys[i], val ? val : "(未找到)");
    }
    printf("缓存条目数: %d\n", cache->count);

    /* 清理 */
    munmap(cache, sizeof(cache_t));
    shm_unlink(CACHE_NAME);
    printf("缓存已清理\n");

    return 0;
}

9.10 注意事项

⚠️ 内存泄漏:mmap 分配的内存必须用 munmap() 释放,malloc 分配的必须用 free() 释放。使用 valgrind 检测泄漏。

⚠️ mmap 地址对齐:mmap 的 offset 参数必须页对齐。offset % sysconf(_SC_PAGESIZE) == 0

⚠️ SIGSEGV 处理:对未映射或只保护的内存访问会触发 SIGSEGV。在信号处理函数中不要再次触发段错误。

⚠️ 多线程内存安全:malloc/free 不是线程安全的信号处理函数中使用(见第五章)。多线程中 malloc 内部有锁保护。

⚠️ madvise(MADV_DONTNEED):对 MAP_PRIVATE 映射使用会导致数据丢失;对 MAP_SHARED 映射会写回磁盘后丢弃。


9.11 扩展阅读

  1. man 2 mmapman 2 mprotectman 2 mlockman 2 madvise
  2. APUE 第 14-15 章:Advanced I/O, IPC
  3. TLPI 第 49-52 章:Memory Mappings, Virtual Memory
  4. 《Understanding the Linux Kernel》 第 9 章:Process Address Space
  5. jemalloc / tcmalloc:高性能内存分配器

9.12 本章小结

要点说明
虚拟地址空间代码段、数据段、堆、栈、内核空间
mmap()内存映射:文件映射、匿名映射、共享映射
mprotect()修改页面保护属性(读/写/执行)
mlock()/munlock()锁定内存到物理 RAM(实时/安全场景)
madvise()向内核提供内存使用建议
brk()/sbrk()调整堆大小(底层接口,不推荐直接使用)
MAP_ANONYMOUS不关联文件的内存分配