【Linux进阶】mmap实战:文件映射、进程通信与LRU缓存
mmap(内存映射)是Linux系统中高效的I/O技术,它将文件或设备直接映射到进程虚拟地址空间,无需通过
read/write系统调用拷贝数据,大幅提升读写性能。除了基础文件操作,mmap还广泛应用于进程间通信、内存分配、缓存设计等场景。
一、mmap基础:原理与核心用法
1.1 什么是mmap?
mmap通过内核将文件/设备的部分或全部内容映射到进程虚拟地址空间,进程直接操作这段内存即可完成对文件的读写。其核心优势在于:
- 减少数据拷贝:跳过内核缓冲区与用户缓冲区的拷贝过程。
- 统一接口:用内存操作(指针读写)替代文件I/O调用。
- 支持共享:可通过共享映射实现进程间数据共享。
1.2 核心接口与参数解析
cpp
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
关键参数说明
| 参数 | 作用 |
|---|---|
addr |
期望的映射起始地址,传NULL让内核自动分配 |
length |
映射长度(必须是系统页大小的整数倍,默认4KB) |
prot |
内存保护属性:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) |
flags |
映射类型:MAP_SHARED(共享映射,修改同步到文件)、MAP_PRIVATE(私有映射,写时拷贝) |
fd |
待映射文件的文件描述符(匿名映射传-1) |
offset |
文件起始偏移量(必须是页大小整数倍) |
返回值
- 成功:返回映射区域的虚拟地址指针。
- 失败:返回
MAP_FAILED(即(void*)-1),并设置errno。
1.3 基础实战:文件映射读写
1.3.1 写入映射(修改文件内容)
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
#define SIZE 4096 // 4KB,页大小整数倍
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
return 1;
}
std::string filename = argv[1];
// 以读写模式打开文件(必须支持写才能同步修改)
int fd = ::open(filename.c_str(), O_CREAT | O_RDWR, 0666);
if (fd < 0) {
perror("open");
return 2;
}
// 调整文件大小(默认文件大小为0,无法映射)
::ftruncate(fd, SIZE);
// 创建共享映射
char *mmap_addr = (char*)::mmap(nullptr, SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (mmap_addr == MAP_FAILED) {
perror("mmap");
return 3;
}
// 直接操作内存,同步修改文件
for (int i = 0; i < SIZE; i++) {
mmap_addr[i] = 'a' + i % 26; // 填充a-z循环
}
// 取消映射(修改已同步到文件,无需显式write)
::munmap(mmap_addr, SIZE);
::close(fd);
std::cout << "文件映射写入完成" << std::endl;
return 0;
}
1.3.2 读取映射(高效读取文件)
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
return 1;
}
std::string filename = argv[1];
int fd = ::open(filename.c_str(), O_RDONLY);
if (fd < 0) {
perror("open");
return 2;
}
// 获取文件实际大小
struct stat st;
::fstat(fd, &st);
// 创建只读映射
char *mmap_addr = (char*)::mmap(nullptr, st.st_size, PROT_READ,
MAP_SHARED, fd, 0);
if (mmap_addr == MAP_FAILED) {
perror("mmap");
return 3;
}
// 直接读取内存(无需read调用)
std::cout << "文件内容:" << std::endl;
std::cout << mmap_addr << std::endl;
::munmap(mmap_addr, st.st_size);
::close(fd);
return 0;
}
1.4 进阶:用mmap模拟malloc内存分配
mmap支持匿名映射(MAP_ANONYMOUS),无需关联文件,可直接分配内存,实现简易版malloc:
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
// 自定义malloc:基于匿名映射
void* my_malloc(size_t size) {
// MAP_PRIVATE:私有映射,进程间不可见
// MAP_ANONYMOUS:匿名映射,无关联文件
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
return ptr;
}
// 自定义free:取消映射
void my_free(void* ptr, size_t size) {
if (munmap(ptr, size) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}
}
int main() {
size_t size = 1024; // 分配1KB内存
char* ptr = (char*)my_malloc(size);
printf("分配内存地址:%p\n", ptr);
memset(ptr, 'A', size); // 填充内存
// 打印内存内容(每1秒输出一个字符)
for (int i = 0; i < size; i++) {
printf("%c ", ptr[i]);
fflush(stdout);
sleep(1);
}
my_free(ptr, size);
printf("\n内存释放完成\n");
return 0;
}
编译运行:
bash
g++ -o mymalloc mymalloc.cpp -std=c++11
./mymalloc
调试验证 :用gdb查看内存映射:
gdb
(gdb) info proc mapping
# 可看到匿名映射的内存区域(无关联objfile)
二、mmap进程间通信:共享内存+同步机制
mmap的共享映射(MAP_SHARED)可实现进程间数据共享,结合互斥锁和条件变量,能实现安全的进程间通信(IPC)。
2.1 设计思路
- 共享内存载体 :用
shm_open创建POSIX共享内存对象,作为mmap的映射源。 - 同步机制 :在共享内存中嵌入互斥锁(
pthread_mutex_t)和条件变量(pthread_cond_t),保证进程间互斥访问。 - 数据传输:共享内存中预留缓冲区,用于存储进程间通信的数据。
2.2 核心封装:共享内存对象
cpp
// SharedMem.hpp
#pragma once
#include <iostream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#define SIZE 4096 // 缓冲区大小
#define SHARED_MEMORY_FILE "/shm" // 共享内存对象名称(必须以/开头)
#define SHARED_MEMORY_SIZE sizeof(SafeObj)
// 带同步机制的共享对象
class SafeObj {
public:
void InitObj() {
// 初始化进程间共享的互斥锁
pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
// 设置锁为进程间共享(默认仅线程间共享)
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&lock, &mattr);
// 初始化进程间共享的条件变量
pthread_condattr_t cattr;
pthread_condattr_init(&cattr);
pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
pthread_cond_init(&cond, &cattr);
// 清空缓冲区
memset(buffer, 0, sizeof(buffer));
}
void CleanupObj() {
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
// 加锁/解锁/等待/通知接口
void LockObj() { pthread_mutex_lock(&lock); }
void UnlockObj() { pthread_mutex_unlock(&lock); }
void Wait() { pthread_cond_wait(&cond, &lock); }
void Signal() { pthread_cond_signal(&cond); }
void BroadCast() {
int n = pthread_cond_broadcast(&cond);
std::cout << (n == 0 ? "广播成功" : "广播失败") << std::endl;
}
// 数据读写接口
void GetContent(std::string *out) { *out = buffer; }
void SetContent(const std::string &in) {
memset(buffer, 0, sizeof(buffer));
strncpy(buffer, in.c_str(), in.size());
}
private:
pthread_mutex_t lock; // 进程间互斥锁
pthread_cond_t cond; // 进程间条件变量
char buffer[SIZE]; // 共享缓冲区
};
// 共享内存管理基类
class MmapMemory {
public:
MmapMemory(const std::string &file, int size)
: _file(file), _size(size), _fd(-1), _mmap_addr(nullptr) {}
~MmapMemory() {
if (_fd > 0) close(_fd);
if (_mmap_addr != MAP_FAILED) {
munmap(_mmap_addr, _size);
std::cout << "munmap完成" << std::endl;
}
}
// 打开共享内存对象
void OpenFile() {
_fd = shm_open(_file.c_str(), O_CREAT | O_RDWR, 0666);
if (_fd < 0) {
perror("shm_open");
exit(1);
}
}
// 调整共享内存大小
void TruncSharedMemory() {
if (ftruncate(_fd, _size) < 0) {
perror("ftruncate");
exit(2);
}
}
// 执行mmap映射
void *Mmap() {
_mmap_addr = mmap(nullptr, _size, PROT_READ | PROT_WRITE,
MAP_SHARED, _fd, 0);
if (_mmap_addr == MAP_FAILED) {
perror("mmap");
exit(3);
}
return _mmap_addr;
}
// 删除共享内存对象(仅服务端调用)
void RemoveFile() {
if (shm_unlink(_file.c_str()) < 0) {
perror("shm_unlink");
exit(4);
}
}
void *MmapAddr() { return _mmap_addr; }
private:
int _fd;
int _size;
std::string _file;
void *_mmap_addr;
};
// 服务端:创建共享内存,等待客户端消息
class MmapMemoryServer : public MmapMemory {
public:
MmapMemoryServer() : MmapMemory(SHARED_MEMORY_FILE, SHARED_MEMORY_SIZE) {
OpenFile();
TruncSharedMemory();
Mmap();
obj = static_cast<SafeObj *>(MmapAddr());
obj->InitObj();
}
~MmapMemoryServer() {
obj->CleanupObj();
RemoveFile();
}
// 接收客户端消息
void RecvMessage(std::string *out) {
obj->LockObj();
obj->Wait(); // 等待客户端通知
obj->GetContent(out);
obj->UnlockObj();
}
private:
SafeObj *obj;
};
// 客户端:连接共享内存,发送消息
class MmapMemoryClient : public MmapMemory {
public:
MmapMemoryClient() : MmapMemory(SHARED_MEMORY_FILE, SHARED_MEMORY_SIZE) {
OpenFile();
Mmap();
obj = static_cast<SafeObj *>(MmapAddr());
}
// 发送消息给服务端
void SendMessage(const std::string &in) {
obj->LockObj();
obj->SetContent(in);
obj->BroadCast(); // 通知所有等待的服务端进程
obj->UnlockObj();
}
private:
SafeObj *obj;
};
2.3 服务端实现(多进程等待)
cpp
// Server.cc
#include "SharedMem.hpp"
#include <sys/wait.h>
// 子进程逻辑:等待并处理消息
void Active(MmapMemoryServer &svr, std::string processname) {
std::cout << "进程启动:" << processname << std::endl;
std::string who;
while (true) {
svr.RecvMessage(&who);
// 处理目标进程消息或广播消息
if (who == processname || who == "all") {
std::cout << processname << " 被激活!" << std::endl;
}
// 退出指令
if (who == "end") {
std::cout << processname << " 退出!" << std::endl;
break;
}
}
}
int main() {
MmapMemoryServer svr;
// 创建10个子进程
for (int i = 0; i < 10; i++) {
pid_t id = fork();
if (id == 0) {
std::string name = "process-" + std::to_string(i);
Active(svr, name);
exit(0);
}
}
// 主进程也参与等待
Active(svr, "process-main");
// 等待所有子进程退出
for (int i = 0; i < 10; i++) {
wait(nullptr);
}
return 0;
}
2.4 客户端实现(发送控制消息)
cpp
// Client.cc
#include "SharedMem.hpp"
#include <string>
#include <iostream>
int main() {
MmapMemoryClient cli;
std::string who;
while (true) {
std::cout << "请输入目标进程(process-0~9/all/end):";
std::getline(std::cin, who);
cli.SendMessage(who);
if (who == "end") {
break;
}
}
return 0;
}
2.5 编译运行与效果
编译脚本(Makefile)
makefile
.PHONY: all clean
all: server client
server: Server.cc
g++ -o $@ $^ -lpthread -lrt -std=c++11 -g
client: Client.cc
g++ -o $@ $^ -lpthread -lrt -std=c++11 -g
clean:
rm -f server client
运行步骤
- 启动服务端:
./server - 启动客户端(新终端):
./client - 客户端输入指令:
- 输入
process-3:仅process-3被激活 - 输入
all:所有进程被激活 - 输入
end:所有进程退出
- 输入
运行效果:
# 服务端输出
进程启动:process-0
进程启动:process-1
...
process-3 被激活!
所有进程 被激活!
process-0 退出!
process-1 退出!
...
三、mmap高级应用:实现大文件LRU缓存
对于GB级大文件,直接加载到内存不现实。利用mmap映射文件块,结合LRU(最近最少使用)算法,可实现高效的文件缓存,提升随机访问性能。
3.1 设计思路
- 数据结构 :
- 双向链表(
std::list):存储缓存的文件块,按访问顺序排序(最近访问的在头部)。 - 哈希表(
std::unordered_map):快速查找文件块是否在缓存中,映射偏移量到链表节点。
- 双向链表(
- 缓存单元:每个缓存单元对应一个4KB的文件块,包含偏移量、映射地址、状态等信息。
- LRU策略:缓存满时,淘汰链表尾部(最久未使用)的文件块,取消其mmap映射。
3.2 核心封装:LRU缓存实现
cpp
// LRUCache.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <list>
#include <memory>
#include <unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
namespace LRUCache {
// 4KB地址对齐(清除低12位)
#define BLOCK_ADDR_ALIGN(off) (off & ~(0xFFF))
// 块状态标志
#define NORMAL (1 << 0) // 普通状态
#define NEW (1 << 1) // 新加入缓存
#define VISIT (1 << 2) // 被访问
#define DELETE (1 << 3) // 待删除
const int gblocksize = 4096; // 缓存块大小(4KB)
const int gcapacity = 3; // 最大缓存块数量(可调整)
const int gdefaultfd = -1;
// 文件块缓存单元
class DataBlock {
public:
DataBlock(off_t off, off_t size)
: _off(off), _size(size), _addr(nullptr), _status(NEW) {}
// 映射文件块到内存
bool DoMap(int fd) {
_addr = mmap(nullptr, _size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, _off);
if (_addr == MAP_FAILED) {
perror("mmap");
return false;
}
std::cout << "mmap成功:偏移量" << _off << ",地址" << _addr << std::endl;
return true;
}
// 取消映射
bool DoUnmap() {
if (munmap(_addr, _size) < 0) {
perror("munmap");
return false;
}
std::cout << "munmap成功:偏移量" << _off << std::endl;
return true;
}
// 状态操作
void Status2Normal() { _status = NORMAL; }
void Status2Visit() { _status = VISIT; }
bool IsNew() { return _status & NEW; }
bool IsVisit() { return _status & VISIT; }
// 获取属性
off_t Off() { return _off; }
void *Addr() { return _addr; }
off_t Size() { return _size; }
// 调试打印
void DebugPrint() {
std::cout << "偏移量:" << _off
<< ",大小:" << _size
<< ",地址:" << _addr
<< ",状态:" << (_status & NEW ? "NEW " : "")
<< (_status & VISIT ? "VISIT " : "")
<< (_status & NORMAL ? "NORMAL" : "") << std::endl;
}
private:
off_t _off; // 文件块起始偏移量(4KB对齐)
off_t _size; // 文件块大小
void *_addr; // 映射后的虚拟地址
unsigned _status;// 块状态
};
// LRU文件缓存主类
class FileCache {
public:
FileCache(const std::string &file)
: _file(file), _fd(gdefaultfd), _total(0), _cachemaxnum(gcapacity) {
// 打开文件(必须存在)
_fd = open(_file.c_str(), O_RDWR);
if (_fd < 0) {
perror("open");
return;
}
// 获取文件总大小
struct stat st;
if (fstat(_fd, &st) < 0) {
perror("fstat");
return;
}
_total = st.st_size;
std::cout << "文件大小:" << _total << "字节" << std::endl;
}
~FileCache() {
if (_fd != gdefaultfd) {
close(_fd);
}
// 释放所有缓存块的映射
for (auto &block : _cache) {
block->DoUnmap();
}
}
// 获取指定偏移量的文件块(核心接口)
std::shared_ptr<DataBlock> GetBlock(off_t off) {
// 1. 检查偏移量合法性
if (!IsOffLegal(off)) {
std::cerr << "偏移量非法:" << off << std::endl;
return nullptr;
}
// 2. 4KB对齐偏移量(确保块起始地址正确)
off = BLOCK_ADDR_ALIGN(off);
// 3. 检查缓存是否命中
if (IsCached(off)) {
// 命中:标记为已访问,触发LRU调整
_hash[off]->Status2Visit();
} else {
// 未命中:加载文件块到缓存
DoCache(off);
}
// 4. 执行LRU策略(调整顺序或淘汰)
DoLRU(off);
return _hash[off];
}
// 打印缓存内容
void PrintCache() {
std::cout << "\n---------缓存内容---------" << std::endl;
for (auto &block : _cache) {
block->DebugPrint();
}
std::cout << "--------------------------\n" << std::endl;
}
private:
// 检查偏移量是否在文件范围内
bool IsOffLegal(off_t off) { return off < _total; }
// 检查块是否已缓存
bool IsCached(off_t off) { return _hash.find(off) != _hash.end(); }
// 检查缓存是否已满
bool IsCacheFull() { return _cache.size() > _cachemaxnum; }
// 根据偏移量计算块大小(最后一块可能不足4KB)
off_t GetSizeFromOff(off_t off) {
if (off + gblocksize > _total) {
return _total - off; // 剩余字节数
}
return gblocksize;
}
// 加载文件块到缓存
void DoCache(off_t off) {
off_t blocksize = GetSizeFromOff(off);
// 创建文件块并映射到内存
auto block = std::make_shared<DataBlock>(off, blocksize);
if (!block->DoMap(_fd)) {
return;
}
// 添加到哈希表和链表头部(新块优先级最高)
_hash[off] = block;
_cache.push_front(block);
}
// 执行LRU策略
void DoLRU(off_t off) {
auto block = _hash[off];
if (!block) return;
if (block->IsNew()) {
// 新块:标记为普通状态,缓存满则淘汰尾部
block->Status2Normal();
if (IsCacheFull()) {
// 淘汰最久未使用的块(链表尾部)
auto &last = _cache.back();
std::cout << "缓存满,淘汰块:" << last->Off() << std::endl;
last->DoUnmap();
_hash.erase(last->Off());
_cache.pop_back();
}
} else if (block->IsVisit()) {
// 已访问块:移动到链表头部(更新访问顺序)
block->Status2Normal();
_cache.remove(block);
_cache.push_front(block);
std::cout << "块" << off << "移动到缓存头部" << std::endl;
}
}
private:
std::string _file; // 文件名
int _fd; // 文件描述符
off_t _total; // 文件总大小
std::list<std::shared_ptr<DataBlock>> _cache; // 缓存链表(LRU顺序)
std::unordered_map<off_t, std::shared_ptr<DataBlock>> _hash; // 哈希表(快速查找)
int _cachemaxnum; // 最大缓存块数
};
}
3.3 测试代码
cpp
// Main.cc
#include "LRUCache.hpp"
#include <iostream>
using namespace LRUCache;
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
return 1;
}
// 1. 创建测试大文件(可选:dd if=/dev/zero of=log.txt bs=4096 count=10)
std::string filename = argv[1];
FileCache fc(filename);
// 2. 测试加载10个不同的块(缓存最大3个,触发淘汰)
int count = 0;
while (count < 10) {
off_t off = count * gblocksize;
std::cout << "\n加载块:" << off << std::endl;
fc.GetBlock(off);
fc.PrintCache();
count++;
sleep(1);
}
// 3. 测试访问已缓存的块(触发LRU调整)
while (true) {
off_t off;
std::cout << "请输入要访问的偏移量(4KB倍数):";
std::cin >> off;
auto block = fc.GetBlock(off);
if (block) {
std::cout << "访问成功,块地址:" << block->Addr() << std::endl;
}
fc.PrintCache();
}
return 0;
}
3.4 编译运行与效果
编译脚本(Makefile)
makefile
lrucache: Main.cc
g++ -o $@ $^ -std=c++17 -g
.PHONY: clean
clean:
rm -f lrucache
运行步骤
-
创建测试大文件(10个4KB块,共40KB):
bashdd if=/dev/zero of=log.txt bs=4096 count=10 -
运行缓存程序:
bash./lrucache log.txt
模拟运行效果:
文件大小:40960字节
加载块:0
mmap成功:偏移量0,地址0x7ffff7ffb000
---------缓存内容---------
偏移量:0,大小:4096,地址:0x7ffff7ffb000,状态:NORMAL
--------------------------
加载块:4096
mmap成功:偏移量4096,地址0x7ffff7fca000
---------缓存内容---------
偏移量:4096,大小:4096,地址:0x7ffff7fca000,状态:NORMAL
偏移量:0,大小:4096,地址:0x7ffff7ffb000,状态:NORMAL
--------------------------
# 缓存满(3个块),加载第4个块时淘汰最久未使用的块0
加载块:12288
mmap成功:偏移量12288,地址0x7ffff7fc9000
缓存满,淘汰块:0
munmap成功:偏移量0
---------缓存内容---------
偏移量:12288,大小:4096,地址:0x7ffff7fc9000,状态:NORMAL
偏移量:8192,大小:4096,地址:0x7ffff7fcb000,状态:NORMAL
偏移量:4096,大小:4096,地址:0x7ffff7fca000,状态:NORMAL
--------------------------
四、总结与进阶方向
mmap作为Linux系统的核心技术,其应用场景覆盖文件I/O、进程通信、内存管理、缓存设计等多个领域。本文通过三个实战案例,从基础到高级,完整展现了mmap的核心用法:
- 基础用法 :文件映射读写,替代传统
read/write,提升I/O效率。 - 进程通信:结合共享内存和同步机制,实现高效的IPC通信。
- 高级应用:大文件LRU缓存,解决大文件随机访问性能问题。
关键注意事项
- 映射长度和偏移量必须是系统页大小(默认4KB)的整数倍。
- 共享映射(
MAP_SHARED)需确保文件以可写模式打开,否则修改无法同步。 - 进程间共享锁/条件变量时,必须设置
PTHREAD_PROCESS_SHARED属性。 - 用完映射后需调用
munmap释放,否则会造成内存泄漏。
进阶学习方向
- 性能优化 :结合
msync控制共享映射的同步时机,平衡性能与数据一致性。 - 异常处理:处理信号中断、文件截断、映射区域越界等异常场景。
- 扩展应用:实现共享内存池、零拷贝网络传输、内存映射数据库等高级场景。
- 跨平台兼容 :研究Windows系统的
CreateFileMapping,实现跨平台的内存映射方案。