好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mmap
函数,它提供了一种将文件或设备映射到进程内存地址空间的方法,允许程序像访问普通内存一样直接读写文件内容,这在处理大文件或需要频繁随机访问文件时非常高效。
1. 函数介绍 链接到标题
mmap
(memory map) 是一个强大的 Linux 系统调用,用于在调用进程的虚拟地址空间中创建一个内存映射区域 (memory mapping)。这个映射可以关联到一个文件、一个设备,或者是一块匿名内存。
简单来说,mmap
可以让你把一个文件的内容“映射”到你的程序内存里的一块区域。一旦映射成功,你就可以直接通过读写这块内存来访问或修改文件内容,而无需使用 read
和 write
系统调用。内核会负责在后台处理实际的文件 I/O。
这就像把一本书(文件)的一页(部分内容)贴在你的书桌上(内存),你可以直接在桌子上阅读和修改,而不需要每次都去书架上翻找(调用 read
/write
)。
2. 函数原型 链接到标题
#include <sys/mman.h> // 必需
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
3. 功能 链接到标题
- 创建映射: 请求内核在调用进程的地址空间中分配一段大小为
length
字节的连续虚拟内存区域。 - 建立关联: 将这段内存区域与文件描述符
fd
指向的文件(或设备)从offset
开始的部分关联起来。文件内容会按需(通常是按页)从磁盘加载到物理内存,并通过虚拟内存系统映射到进程的地址空间。 - 返回地址: 如果成功,返回指向映射区域起始地址的指针。如果失败,返回
MAP_FAILED
(即(void *) -1
)。 - 访问数据: 程序可以直接通过返回的指针读写内存,就像操作普通数组或变量一样。内核会处理内存和文件之间的数据同步(根据映射类型)。
4. 参数 链接到标题
void *addr
: 指定映射区域在进程地址空间中的期望起始地址。- 通常设置为
NULL
。这告诉内核:“我不关心具体地址,你帮我选一个合适的地方。” 这是最常见和推荐的做法。 - 如果设置为非
NULL
值,内核会尝试在该地址附近创建映射,但这可能失败(如果该地址已被占用或不满足对齐要求)。除非有特殊需求,否则不建议指定具体地址。
- 通常设置为
size_t length
: 映射区域的长度(以字节为单位)。这是你希望映射的文件部分的大小。int prot
: 指定映射区域的保护模式 (protection),即进程可以对该区域进行何种操作。这是一个位掩码,由以下值按位或组合:PROT_READ
: 页面可读。PROT_WRITE
: 页面可写。PROT_EXEC
: 页面可执行。PROT_NONE
: 页面不可访问。 例如,PROT_READ | PROT_WRITE
表示映射区域可读可写。
int flags
: 指定映射的类型和行为。必须包含以下互斥值之一:MAP_SHARED
: 创建一个共享映射。对映射区域的修改会反映到文件本身,并且对映射同一文件的其他进程可见。这是实现进程间共享内存的一种方式。MAP_PRIVATE
: 创建一个私有映射。对映射区域的修改会产生一个副本(写时复制 Copy-On-Write),不会影响原始文件,且对其他进程不可见。 还可以按位或上其他标志来修改行为,常见的包括:MAP_ANONYMOUS
或MAP_ANON
: 创建一个匿名映射,不与任何文件关联。映射区域的内容被初始化为零。常用于分配大块内存。MAP_FIXED
: 强制内核在addr
指定的确切地址创建映射,如果该地址已被占用则会覆盖。使用需非常谨慎,因为它可能破坏现有映射。
int fd
: 这是与映射相关的文件描述符。它必须是通过open
等系统调用成功打开的。- 如果
flags
包含MAP_ANONYMOUS
,则fd
参数会被忽略(通常传 0 或 -1)。
- 如果
off_t offset
: 文件映射的起始偏移量。它必须是系统页大小(通常为 4KB,在 Linux 上可以用getpagesize()
获取)的整数倍。指定从文件的哪个字节开始映射。
5. 返回值 链接到标题
- 成功时: 返回指向映射区域起始地址的指针。这个指针可以像普通内存指针一样使用。
- 失败时: 返回
MAP_FAILED
(即(void *) -1
),并设置全局变量errno
来指示具体的错误原因(例如EINVAL
参数无效、EACCES
权限不足、ENOMEM
内存不足等)。
6. 相似函数,或关联函数 链接到标题
munmap
: 用于解除之前通过mmap
创建的内存映射,释放映射区域。mprotect
: 修改已存在映射区域的保护模式(例如,将只读区域改为可读可写)。msync
: 将映射区域的修改同步(刷新)到关联的文件中。对于MAP_SHARED
映射,在某些情况下是必要的,以确保数据写入磁盘。read
,write
: 传统的文件 I/O 方式,与mmap
提供的内存映射方式相对。open
,close
: 通常在使用mmap
映射文件时,需要先open
文件获取fd
,映射后通常可以close
掉fd
(映射仍然有效),但最好在munmap
之后再close
。getpagesize
: 获取系统的内存页大小,这对于设置offset
和length
很有用。
7. 示例代码 链接到标题
示例 1:使用 mmap
读取文件内容
链接到标题
这个例子演示如何将一个文件映射到内存中并读取其内容。
#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h> // open, O_RDONLY
#include <unistd.h> // close, fstat, getpagesize
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // strchr
int main(int argc, char *argv[]) {
int fd;
struct stat sb;
char *mapped;
size_t length;
off_t offset = 0;
int pagesize;
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 打开文件
fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 获取文件大小
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
length = sb.st_size;
if (length == 0) {
printf("File is empty.\n");
close(fd);
exit(EXIT_SUCCESS);
}
// 3. 获取系统页大小 (虽然在这个例子中不严格需要,但了解它很重要)
pagesize = getpagesize();
printf("System page size: %d bytes\n", pagesize);
// 4. 创建内存映射
// addr=NULL: 让内核选择地址
// length: 文件大小
// prot=PROT_READ: 只读映射
// flags=MAP_PRIVATE: 私有映射,修改不影响文件
// fd: 文件描述符
// offset=0: 从文件开头映射
mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("File '%s' mapped successfully. Size: %zu bytes\n", argv[1], length);
printf("Mapped memory starts at: %p\n", (void*)mapped);
// 5. 直接访问映射的内存,就像访问字符数组一样
printf("First 100 characters (or less) of the file:\n");
size_t to_print = (length < 100) ? length : 100;
for (size_t i = 0; i < to_print; ++i) {
// 简单打印,不处理换行符等
putchar(mapped[i]);
}
printf("\n--- End of first 100 chars ---\n");
// 查找第一个换行符
char *newline = strchr(mapped, '\n');
if (newline != NULL) {
size_t first_line_len = newline - mapped;
printf("Length of first line: %zu characters\n", first_line_len);
printf("First line: ");
fwrite(mapped, 1, first_line_len, stdout);
printf("\n");
} else {
printf("No newline found in the mapped region.\n");
}
// 6. 解除映射
if (munmap(mapped, length) == -1) {
perror("munmap");
// 即使 munmap 失败,也应尝试关闭 fd
}
// 7. 关闭文件描述符
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE); // 如果 close 失败,通常是很严重的问题
}
printf("File unmapped and closed.\n");
return 0;
}
代码解释:
- 检查命令行参数。
- 使用
open
以只读模式打开文件。 - 使用
fstat
获取文件大小,因为mmap
需要知道要映射多少字节。 - 调用
getpagesize()
获取系统页大小(虽然此例未严格使用,但了解很重要)。 - 调用
mmap
:addr=NULL
: 让内核选择地址。length=sb.st_size
: 映射整个文件。prot=PROT_READ
: 只读。flags=MAP_PRIVATE
: 私有映射。fd
: 文件描述符。offset=0
: 从文件开头开始映射。
- 检查
mmap
是否成功(返回值不为MAP_FAILED
)。 - 直接通过
mapped
指针访问文件内容,就像操作字符数组一样。示例打印了前 100 个字符和第一行。 - 使用
munmap
解除映射,释放资源。 - 使用
close
关闭文件描述符。
示例 2:使用 mmap
修改文件内容(MAP_SHARED)
链接到标题
这个例子演示如何创建一个共享映射来修改文件内容。
#include <sys/mman.h> // mmap, munmap, msync
#include <sys/stat.h> // fstat
#include <fcntl.h> // open
#include <unistd.h> // close, fstat
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // strlen
int main(int argc, char *argv[]) {
int fd;
struct stat sb;
char *mapped;
size_t length;
const char *new_content = "Content written via mmap!\n";
size_t content_len = strlen(new_content);
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 以读写模式打开文件
fd = open(argv[1], O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 获取文件大小
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
length = sb.st_size;
// 如果文件为空或太小,调整映射长度为要写入的内容长度
// (或者可以先 truncate 文件)
if (length < content_len) {
printf("File is smaller than content to write. Mapping %zu bytes.\n", content_len);
length = content_len;
// 可选:扩展文件大小
// if (ftruncate(fd, length) == -1) { perror("ftruncate"); close(fd); exit(EXIT_FAILURE); }
}
// 3. 创建共享内存映射 (可读可写)
// prot=PROT_READ | PROT_WRITE: 可读可写
// flags=MAP_SHARED: 共享映射,修改会写回文件
mapped = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("File '%s' mapped for read/write. Size: %zu bytes\n", argv[1], length);
// 4. 修改映射内存中的内容
// 这里简单地将新内容复制到映射区域的开头
// 注意:确保 mapped 区域足够大
for (size_t i = 0; i < content_len && i < length; ++i) {
mapped[i] = new_content[i];
}
// 或者使用 memcpy(mapped, new_content, content_len); // 需 #include <string.h>
printf("Wrote new content to mapped memory.\n");
// 5. (可选但推荐) 强制将修改同步到文件
// MS_SYNC: 同步等待 I/O 完成
if (msync(mapped, content_len, MS_SYNC) == -1) {
perror("msync");
// 即使 msync 失败,也继续清理
} else {
printf("Changes synced to file.\n");
}
// 6. 解除映射
if (munmap(mapped, length) == -1) {
perror("munmap");
}
// 7. 关闭文件描述符
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
printf("File unmapped, synced, and closed.\n");
printf("Check the file content now.\n");
return 0;
}
代码解释:
- 以读写模式 (
O_RDWR
) 打开文件。 - 获取文件大小。
- 调用
mmap
:prot=PROT_READ | PROT_WRITE
: 可读可写。flags=MAP_SHARED
: 共享映射。这是关键,它使得对内存的修改会反映到文件中。
- 直接修改
mapped
指针指向的内存区域。 - 调用
msync
将修改强制刷新到磁盘,确保数据持久化。MS_SYNC
会等待写入完成。 - 使用
munmap
解除映射。 - 使用
close
关闭文件描述符。
示例 3:使用 mmap
分配匿名内存
链接到标题
这个例子演示如何使用 mmap
分配一块不与任何文件关联的内存(匿名映射)。
#include <sys/mman.h> // mmap, munmap
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <string.h> // strcpy
int main() {
size_t length = 1024; // 分配 1KB
char *anonymous_mem;
// 1. 创建匿名内存映射
// addr=NULL: 让内核选择地址
// length: 分配大小
// prot=PROT_READ | PROT_WRITE: 可读可写
// flags=MAP_PRIVATE | MAP_ANONYMOUS: 私有匿名映射
// fd=-1: 对于匿名映射,fd 被忽略
// offset=0: 对于匿名映射,offset 被忽略
anonymous_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (anonymous_mem == MAP_FAILED) {
perror("mmap anonymous");
exit(EXIT_FAILURE);
}
printf("Allocated %zu bytes of anonymous memory at %p\n", length, (void*)anonymous_mem);
// 2. 像使用普通 malloc 分配的内存一样使用它
strcpy(anonymous_mem, "Hello from anonymous mmap!");
printf("Content written: %s\n", anonymous_mem);
// 修改内存
anonymous_mem[17] = 'M';
anonymous_mem[18] = 'M';
anonymous_mem[19] = 'A';
anonymous_mem[20] = 'P';
printf("Content modified: %s\n", anonymous_mem);
// 3. 解除映射 (释放内存)
if (munmap(anonymous_mem, length) == -1) {
perror("munmap anonymous");
exit(EXIT_FAILURE);
}
printf("Anonymous memory unmapped.\n");
return 0;
}
代码解释:
- 调用
mmap
分配内存:flags=MAP_PRIVATE | MAP_ANONYMOUS
: 指定为私有匿名映射。fd=-1
和offset=0
: 对于匿名映射,这两个参数通常被忽略。
- 成功后,
anonymous_mem
指向一块大小为length
的可读写内存。 - 可以像使用
malloc
分配的内存一样使用它(但需要munmap
来释放,而不是free
)。 - 使用
munmap
释放这块内存。
mmap
是一个功能强大且高效的工具,特别适用于大文件处理、内存数据库、进程间共享内存等场景。理解其参数和不同标志的含义对于有效利用它至关重要。