第九章:内存管理
第九章:内存管理
理解 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 对比
| 特性 | mmap | malloc (大块) | malloc (小块) |
|---|---|---|---|
| 底层机制 | 直接系统调用 | 通常 mmap | brk/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 扩展阅读
man 2 mmap、man 2 mprotect、man 2 mlock、man 2 madvise- APUE 第 14-15 章:Advanced I/O, IPC
- TLPI 第 49-52 章:Memory Mappings, Virtual Memory
- 《Understanding the Linux Kernel》 第 9 章:Process Address Space
- jemalloc / tcmalloc:高性能内存分配器
9.12 本章小结
| 要点 | 说明 |
|---|---|
| 虚拟地址空间 | 代码段、数据段、堆、栈、内核空间 |
| mmap() | 内存映射:文件映射、匿名映射、共享映射 |
| mprotect() | 修改页面保护属性(读/写/执行) |
| mlock()/munlock() | 锁定内存到物理 RAM(实时/安全场景) |
| madvise() | 向内核提供内存使用建议 |
| brk()/sbrk() | 调整堆大小(底层接口,不推荐直接使用) |
| MAP_ANONYMOUS | 不关联文件的内存分配 |