好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 madvise
函数,它允许进程向内核提供关于其内存访问模式的建议(advice),从而帮助内核优化内存管理策略,提高应用程序的性能。
1. 函数介绍 Link to heading
madvise
(Memory Advice) 是一个 Linux 系统调用,它允许应用程序向操作系统内核传达其对某块内存区域未来访问模式的预期或意图。这些建议并非强制性的指令,而是一种提示,内核可以利用这些信息来优化内存管理,例如:
- 预读取 (Prefetching): 如果应用程序表明它将顺序访问大量数据,内核可以提前将后续页面加载到内存中,减少等待时间。
- 页面回收 (Page Reclaim): 如果应用程序表明某些数据不再需要,内核可以更积极地将这些页面换出到交换空间或直接丢弃(对于匿名映射),从而释放物理内存给其他更需要的进程。
- 延迟分配 (Lazy Allocation): 对于某些类型的大内存区域,内核可以根据建议推迟实际的物理内存分配,直到真正需要为止。
你可以把 madvise
想象成你对图书管理员说的话:“我接下来会按顺序看这几章书,麻烦你先把它们都准备好” 或 “这几页我看过了,暂时用不到了,你可以先收起来”。
2. 函数原型 Link to heading
#include <sys/mman.h> // 必需
int madvise(void *addr, size_t length, int advice);
3. 功能 Link to heading
- 传达意图: 向内核传达从地址
addr
开始、长度为length
字节的内存区域的访问意图。 - 影响策略: 内核根据收到的
advice
来调整其对该内存区域的管理策略(如预读、换出、合并等)。 - 范围操作: 可以对内存映射的任意子区域提供建议。
4. 参数 Link to heading
void *addr
: 指向要提供建议的内存区域的起始虚拟地址。- 重要: 虽然 POSIX 允许
addr
不是页对齐的,但为了可移植性和确保建议应用于完整的页面,强烈建议addr
是系统页大小的整数倍。可以使用getpagesize()
获取页大小。
- 重要: 虽然 POSIX 允许
size_t length
: 要提供建议的内存区域的长度(以字节为单位)。- 同样,为了确保建议应用于完整的页面,
length
最好也是页大小的整数倍。如果不足一页,内核通常会向上舍入到最近的页边界。
- 同样,为了确保建议应用于完整的页面,
int advice
: 指定要提供的具体建议。这是一个整数常量,定义在<sys/mman.h>
中。常见的包括:MADV_NORMAL
: 普通访问。这是默认行为,内核使用标准的页面读取和换出策略。MADV_SEQUENTIAL
: 顺序访问。应用程序预计会以向前的顺序访问这段内存。内核可能会增加预读的激进程度。MADV_RANDOM
: 随机访问。应用程序预计会以随机的顺序访问这段内存。内核可能会减少或禁用预读。MADV_WILLNEED
: 将会需要。应用程序预计很快就会访问这段内存。内核可能会提前将相关页面加载到物理内存中。MADV_DONTNEED
: 不需要。应用程序目前不需要这段内存中的数据(对于文件映射)或表明这些页面可以被丢弃(对于匿名映射,如MAP_ANONYMOUS
)。注意: 对于私有匿名映射 (MAP_PRIVATE | MAP_ANONYMOUS
),这通常会导致页面内容被丢弃(下次访问会得到零页)。对于共享映射或文件映射,行为可能不同,主要是提示内核可以更积极地换出这些页面。MADV_FREE
(Linux 4.5+): 告诉内核这段内存可以被释放。与MADV_DONTNEED
类似,但对于私有匿名页,它更明确地表示页面内容可以丢弃。如果之后又访问了这些页面,会得到零页。在某些内核版本中,MADV_FREE
的效果可能比MADV_DONTNEED
更延迟,即内核可能不会立即释放物理页。MADV_REMOVE
: 尝试移除指定范围内的页面。对于支持的文件系统,这可能导致文件中对应部分被 deallocate(释放空间,类似fallocate
的FALLOC_FL_PUNCH_HOLE
)。对于匿名映射,行为类似于MADV_DONTNEED
。MADV_DONTFORK
: 在fork()
时,不要将此范围的页面复制到子进程中。子进程访问这些页面会导致缺页。MADV_DOFORK
: 取消MADV_DONTFORK
的效果。MADV_MERGEABLE
/MADV_UNMERGEABLE
: 允许/禁止内核使用 KSM (Kernel Samepage Merging) 来合并内容相同的页面。常用于虚拟机环境。MADV_HUGEPAGE
/MADV_NOHUGEPAGE
: 鼓励/禁止内核为此区域使用大页 (Huge Pages)。MADV_POPULATE_READ
/MADV_POPULATE_WRITE
(Linux 5.14+): 立即为区域触发读/写访问的缺页,将页面加载到内存。
5. 返回值 Link to heading
- 成功时: 返回 0。
- 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EINVAL
advice
无效或addr
/length
无效,ENOMEM
地址范围包含未映射区域等)。
6. 相似函数,或关联函数 Link to heading
mmap
:madvise
通常作用于通过mmap
创建的内存区域。mincore
: 用于查询页面的当前驻留状态,可以与madvise
结合使用来观察建议的效果。mprotect
: 用于修改内存区域的访问权限(读/写/执行)。posix_madvise
: POSIX 标准定义的函数,功能与madvise
类似,但可移植性更好。在 Linux 上,它通常是madvise
的一个薄包装。fadvise64
/posix_fadvise
: 与madvise
类似,但作用于文件描述符和文件偏移量范围,用于向内核提供文件访问模式的建议,优化文件 I/O。
7. 示例代码 Link to heading
示例 1:使用 MADV_SEQUENTIAL
和 MADV_WILLNEED
Link to heading
这个例子演示了如何为顺序处理的大数据块提供 MADV_SEQUENTIAL
建议,并为即将访问的区域提供 MADV_WILLNEED
建议。
#define _GNU_SOURCE // 可能需要
#include <sys/mman.h> // mmap, munmap, madvise
#include <sys/stat.h> // fstat
#include <fcntl.h> // open
#include <unistd.h> // close, fstat, getpagesize
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <string.h> // memset
#include <time.h> // clock
// 模拟处理数据的函数
void process_data_chunk(char *data, size_t size) {
volatile unsigned long sum = 0; // volatile 防止编译器优化
for (size_t i = 0; i < size; ++i) {
sum += data[i];
}
// 简单使用 sum 防止编译器完全优化掉循环
if (sum % 2 == 0) { /* do nothing */ }
}
int main() {
const char *filename = "/tmp/large_data_file.bin";
int fd;
struct stat sb;
char *mapped;
size_t length;
long pagesize;
size_t chunk_size;
clock_t start, end;
double cpu_time_used;
pagesize = getpagesize();
chunk_size = 10 * pagesize; // 每次处理 10 页
// 1. 创建一个较大的测试文件
printf("Creating a large test file...\n");
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open for creation");
exit(EXIT_FAILURE);
}
// 写入大约 1MB 的随机数据
size_t total_size = 1 * 1024 * 1024; // 1 MB
char *buffer = malloc(pagesize);
if (!buffer) {
perror("malloc buffer");
close(fd);
exit(EXIT_FAILURE);
}
for (size_t written = 0; written < total_size; written += pagesize) {
// 填充缓冲区
for (int i = 0; i < pagesize; ++i) {
buffer[i] = (char)(rand() % 256);
}
if (write(fd, buffer, pagesize) != pagesize) {
perror("write");
free(buffer);
close(fd);
exit(EXIT_FAILURE);
}
}
free(buffer);
if (close(fd) == -1) {
perror("close after creation");
exit(EXIT_FAILURE);
}
printf("Created test file '%s' of size %zu bytes.\n", filename, total_size);
// 2. 打开并映射文件
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open for reading");
exit(EXIT_FAILURE);
}
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
length = sb.st_size;
mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("File mapped successfully. Address: %p, Size: %zu bytes\n", (void*)mapped, length);
// 3. 提供 MADV_SEQUENTIAL 建议 (对整个区域)
printf("\nProviding MADV_SEQUENTIAL advice for the entire mapped region...\n");
if (madvise(mapped, length, MADV_SEQUENTIAL) == -1) {
perror("madvise MADV_SEQUENTIAL");
// 不是致命错误,继续执行
} else {
printf("MADV_SEQUENTIAL advice given successfully.\n");
}
// 4. 顺序处理数据块
printf("Starting sequential processing...\n");
start = clock();
for (size_t offset = 0; offset < length; offset += chunk_size) {
size_t this_chunk_size = (offset + chunk_size > length) ? (length - offset) : chunk_size;
// 为下一个即将处理的块提供 MADV_WILLNEED 建议
if (offset + chunk_size < length) {
size_t next_offset = offset + chunk_size;
size_t next_chunk_size = (next_offset + chunk_size > length) ? (length - next_offset) : chunk_size;
if (madvise(mapped + next_offset, next_chunk_size, MADV_WILLNEED) == -1) {
perror("madvise MADV_WILLNEED");
// 不是致命错误,继续执行
}
}
// 处理当前块
process_data_chunk(mapped + offset, this_chunk_size);
// printf("Processed chunk at offset %zu, size %zu\n", offset, this_chunk_size); // 可选打印
}
end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("Sequential processing completed.\n");
printf("CPU time used: %f seconds\n", cpu_time_used);
// 5. 清理
if (munmap(mapped, length) == -1) {
perror("munmap");
}
if (close(fd) == -1) {
perror("close");
}
// 可选:删除测试文件
// unlink(filename);
printf("Test completed.\n");
return 0;
}
代码解释:
- 创建一个 1MB 的大型测试文件。
- 以只读方式打开并映射该文件。
- 调用
madvise(mapped, length, MADV_SEQUENTIAL)
,告诉内核整个映射区域将被顺序访问。这鼓励内核进行更激进的预读。 - 进入一个循环,按固定大小(
chunk_size
)顺序处理数据。 - 关键: 在处理当前块之前,为下一个即将处理的块调用
madvise(..., MADV_WILLNEED)
。这提示内核提前将下一个块的页面加载到内存中,以减少处理下一个块时的等待时间。 - 调用
process_data_chunk
模拟对数据的实际处理。 - 记录并打印处理总耗时。
- 最后进行清理。
示例 2:使用 MADV_DONTNEED
释放不需要的内存
Link to heading
这个例子演示了如何使用 MADV_DONTNEED
来告知内核某块内存区域的数据不再需要,可以被丢弃或换出。
#define _GNU_SOURCE
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
long pagesize = getpagesize();
size_t size = 5 * pagesize; // 5 页
char *mapped_memory;
unsigned char *vec; // 用于 mincore 检查
// 1. 分配用于 mincore 的向量
vec = malloc(5 * sizeof(unsigned char));
if (vec == NULL) {
perror("malloc vec");
exit(EXIT_FAILURE);
}
// 2. 创建匿名私有映射
mapped_memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
free(vec);
exit(EXIT_FAILURE);
}
printf("Anonymous memory mapped at: %p (Size: %zu bytes, %ld pages)\n",
(void*)mapped_memory, size, size / pagesize);
// 3. 在映射区域中放入一些数据
printf("Filling memory with data...\n");
for (size_t i = 0; i < size; ++i) {
mapped_memory[i] = (char)(i % 256);
}
// 4. 检查初始页面状态 (应该都在内存中,因为已访问)
printf("\n--- Page residency BEFORE MADV_DONTNEED ---\n");
if (mincore(mapped_memory, size, vec) != -1) {
for (int i = 0; i < 5; ++i) {
printf(" Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
}
} else {
perror("mincore before");
}
// 5. 访问数据以验证其内容
printf("\nVerifying data in first few bytes: ");
for (int i = 0; i < 10; ++i) {
printf("%02x ", (unsigned char)mapped_memory[i]);
}
printf("\n");
// 6. 提供 MADV_DONTNEED 建议 (针对前 3 页)
printf("\nProviding MADV_DONTNEED advice for the first 3 pages...\n");
if (madvise(mapped_memory, 3 * pagesize, MADV_DONTNEED) == -1) {
perror("madvise MADV_DONTNEED");
} else {
printf("MADV_DONTNEED advice given for first 3 pages.\n");
}
// 7. 再次检查页面状态
printf("\n--- Page residency AFTER MADV_DONTNEED ---\n");
if (mincore(mapped_memory, size, vec) != -1) {
for (int i = 0; i < 5; ++i) {
printf(" Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
}
} else {
perror("mincore after");
}
// 8. 尝试访问被标记为 DONTNEED 的页面的数据
printf("\nAccessing data in the first page (marked DONTNEED)...\n");
// 对于 MAP_PRIVATE | MAP_ANONYMOUS, MADV_DONTNEED 通常会导致内容被丢弃
printf("First few bytes of first page: ");
for (int i = 0; i < 10; ++i) {
// 访问后,页面会被重新分配并填充零
printf("%02x ", (unsigned char)mapped_memory[i]);
}
printf("\n(Notice: Data might be zeros if page was discarded)\n");
// 9. 访问未被标记为 DONTNEED 的页面 (第 4, 5 页)
printf("\nAccessing data in the fourth page (NOT marked DONTNEED)...\n");
printf("First few bytes of fourth page: ");
for (int i = 0; i < 10; ++i) {
printf("%02x ", (unsigned char)mapped_memory[3 * pagesize + i]);
}
printf("\n(Notice: Data should still be original values)\n");
// 10. 清理
if (munmap(mapped_memory, size) == -1) {
perror("munmap");
}
free(vec);
printf("\nTest completed.\n");
return 0;
}
代码解释:
- 创建一个 5 页大小的匿名、私有内存映射。
- 向整个映射区域写入一些数据。
- 使用
mincore
检查初始页面状态,确认所有页面都在内存中。 - 打印部分数据以验证其内容。
- 调用
madvise(mapped_memory, 3 * pagesize, MADV_DONTNEED)
,告诉内核前 3 页的数据不再需要。 - 再次使用
mincore
检查页面状态。可能显示前 3 页已不在内存中(或内核已将其标记为可丢弃)。 - 关键: 尝试访问第一个页面(已标记
DONTNEED
)的数据。对于MAP_PRIVATE | MAP_ANONYMOUS
映射,MADV_DONTNEED
通常会导致该页面内容被丢弃。因此,再次访问时,可能会得到零填充的页面。 - 访问第 4 页和第 5 页(未标记
DONTNEED
)的数据,确认其内容仍然保留。 - 最后进行清理。
重要提示与注意事项:
- 建议,不是命令:
madvise
的advice
只是建议,内核可以忽略。实际行为取决于内核版本、配置和当前系统负载。 - 效果延迟: 内核可能不会立即执行建议的操作(如立即加载页面或丢弃页面),而是在后续的内存管理周期中处理。
- 平台差异: 不同的操作系统和内核版本对
madvise
的支持和具体行为可能有所不同。 - 结合使用:
madvise
经常与mmap
,mincore
结合使用,以实现更精细的内存控制和性能优化。 - 性能影响: 正确使用
madvise
可以显著提升性能,尤其是在处理大文件或大数据集时。错误的建议(例如,对随机访问的数据使用MADV_SEQUENTIAL
)可能会适得其反。
总结:
madvise
是一个强大的工具,允许应用程序与操作系统内核协作,优化内存管理。通过提供关于内存访问模式的建议,程序可以引导内核采取更有效的预读、换出等策略,从而提高整体性能。理解各种 advice
选项的含义和适用场景是有效利用 madvise
的关键。