C++ `mlock` 与 `mlockall`:锁定内存,防止关键数据被换出到磁盘

哈喽,各位好!今天咱们聊聊C++里两个有点“神秘”,但关键时刻能救命的函数:mlockmlockall。 它们的作用嘛,简单来说就是让你的程序“霸道”地把一些或者所有内存“锁死”在RAM里,不让操作系统随便把它扔到硬盘上睡觉。

为什么要“锁”内存?

想象一下,你正在开发一个加密软件,内存里存着用户的银行密码。如果操作系统觉得你的程序暂时用不着,就把这块内存换到硬盘上,万一硬盘被黑客攻破,密码就暴露了!

或者,你正在做一个实时交易系统,每一毫秒都至关重要。如果操作系统突然把你的关键数据换出到硬盘,再换回来,那延迟就可能让你损失惨重。

所以,对于安全性要求极高,或者对延迟极其敏感的程序,mlockmlockall 就显得尤为重要。

mlock: 精确打击,锁定指定区域

mlock 函数就像一个狙击手,允许你精确地锁定内存中的某个特定区域。它的原型是这样的:

#include <sys/mman.h>

int mlock(const void *addr, size_t len);
  • addr: 要锁定的内存区域的起始地址。
  • len: 要锁定的内存区域的长度,单位是字节。

如果成功,返回0;如果失败,返回-1,并设置 errno

示例代码:

#include <iostream>
#include <sys/mman.h>
#include <errno.h>
#include <string.h> // for strerror
#include <unistd.h> // for sysconf

int main() {
  size_t page_size = sysconf(_SC_PAGE_SIZE);
  if (page_size == -1) {
    std::cerr << "Error getting page size: " << strerror(errno) << std::endl;
    return 1;
  }

  // 1. 分配一块内存
  size_t region_size = 2 * page_size;
  void* memory = malloc(region_size);
  if (memory == nullptr) {
    std::cerr << "Failed to allocate memory: " << strerror(errno) << std::endl;
    return 1;
  }

  // 2. 使用 mlock 锁定这块内存
  if (mlock(memory, region_size) == -1) {
    std::cerr << "Failed to lock memory: " << strerror(errno) << std::endl;
    free(memory);
    return 1;
  }

  std::cout << "Memory locked successfully!" << std::endl;

  // 3. 在这里,你可以安全地使用这块内存,不用担心被换出。
  // 写入一些数据...
  memset(memory, 'A', region_size);

  // 4.  不再需要锁定时,使用 munlock 解锁
  if (munlock(memory, region_size) == -1) {
    std::cerr << "Failed to unlock memory: " << strerror(errno) << std::endl;
  }

  // 5. 释放内存
  free(memory);

  return 0;
}

代码解释:

  1. 获取页大小: sysconf(_SC_PAGE_SIZE) 获取系统的页大小。 mlockmunlock 通常需要锁定/解锁的内存区域是页大小的整数倍。

  2. 分配内存: 使用 malloc 分配一块内存区域。

  3. 锁定内存: mlock(memory, region_size) 尝试锁定从 memory 开始,长度为 region_size 的内存。

  4. 使用内存: 这里只是简单地使用 memset 填充内存,表示可以在这个区域安全地读写数据。

  5. 解锁内存: munlock(memory, region_size) 解锁之前锁定的内存区域。 非常重要: 必须使用 munlock 解锁,否则程序退出后,操作系统可能无法回收这块内存,导致资源泄漏。

  6. 释放内存: 使用 free 释放之前分配的内存。

注意事项:

  • 页对齐: addr 必须是系统页大小的整数倍。 你可以使用 sysconf(_SC_PAGE_SIZE) 获取系统页大小。
  • 长度: len 最好也是页大小的整数倍。如果不是,mlock 可能会将包含 addraddr + len - 1 的整个页锁定。
  • 权限: 你必须有足够的权限来锁定内存。 通常,你需要 CAP_IPC_LOCK capability。 可以使用 sudo setcap cap_ipc_lock=+ep <executable> 为可执行文件设置该 capability。
  • 资源限制: 操作系统对可以锁定的内存总量有限制。 可以使用 ulimit -l 查看限制。 如果你试图锁定超过限制的内存,mlock 会失败。
  • 错误处理: 务必检查 mlock 的返回值,如果失败,打印 errno 对应的错误信息。

mlockall: 全面封锁,锁定整个进程空间

mlockall 函数更加激进,它会尝试锁定整个进程的虚拟地址空间。 它的原型如下:

#include <sys/mman.h>

int mlockall(int flags);
  • flags: 标志位,用于控制锁定哪些内存区域。

常用的 flags 有:

  • MCL_CURRENT: 锁定调用 mlockall 时已经映射到进程地址空间的内存页。
  • MCL_FUTURE: 锁定将来映射到进程地址空间的内存页。 这意味着,即使你之后使用 malloc 分配新的内存,这些内存也会自动被锁定。

你可以使用 MCL_CURRENT | MCL_FUTURE 来同时锁定当前和将来的内存。

如果成功,返回0;如果失败,返回-1,并设置 errno

示例代码:

#include <iostream>
#include <sys/mman.h>
#include <errno.h>
#include <string.h> // for strerror
#include <unistd.h> // for sysconf

int main() {
  // 1. 使用 mlockall 锁定整个进程的地址空间
  if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
    std::cerr << "Failed to lock all memory: " << strerror(errno) << std::endl;
    return 1;
  }

  std::cout << "All memory locked successfully!" << std::endl;

  // 2.  现在,你可以安全地分配和使用内存,不用担心被换出。
  // 分配一些内存...
  void* memory = malloc(1024);
  if (memory == nullptr) {
    std::cerr << "Failed to allocate memory: " << strerror(errno) << std::endl;
    munlockall(); // 别忘了解锁
    return 1;
  }

  // 写入一些数据...
  memset(memory, 'B', 1024);

  // 3.  不再需要锁定时,使用 munlockall 解锁
  if (munlockall() == -1) {
    std::cerr << "Failed to unlock all memory: " << strerror(errno) << std::endl;
  }

  // 4. 释放内存
  free(memory);

  return 0;
}

代码解释:

  1. 锁定所有内存: mlockall(MCL_CURRENT | MCL_FUTURE) 尝试锁定当前和将来分配的所有内存。

  2. 分配和使用内存: 可以像往常一样分配和使用内存,因为 mlockall 已经保证了它们不会被换出。

  3. 解锁所有内存: munlockall() 解锁之前锁定的所有内存。 同样非常重要: 必须使用 munlockall 解锁,否则程序退出后,操作系统可能无法回收这些内存,导致资源泄漏。

  4. 释放内存: 使用 free 释放之前分配的内存。

注意事项:

  • 谨慎使用: mlockall 会锁定大量内存,可能影响系统的整体性能。 只有在绝对必要的情况下才使用。
  • 资源限制:mlock 类似,操作系统对可以锁定的内存总量有限制。 mlockall 也可能因为超过限制而失败。
  • 错误处理: 务必检查 mlockall 的返回值,如果失败,打印 errno 对应的错误信息。
  • 解锁: 必须使用 munlockall() 来解锁通过 mlockall 锁定的内存。

munlockmunlockall: 解锁内存,恢复自由

既然有锁,就得有钥匙。 munlockmunlockall 就是用来解锁内存的。

  • munlock(const void *addr, size_t len): 解锁由 mlock 锁定的内存区域。 参数与 mlock 相同。
  • munlockall(): 解锁由 mlockall 锁定的所有内存。 不需要参数。

重要: 如果你使用 mlockmlockall 锁定了内存,必须 使用相应的 munlockmunlockall 来解锁。 否则,程序退出后,操作系统可能无法回收这些内存,导致资源泄漏。

错误处理:errno

mlock, mlockall, munlock, 和 munlockall 在失败时都会设置 errno。 常见的错误包括:

errno 含义
EPERM 调用进程没有足够的权限来锁定内存。
ENOMEM 操作系统没有足够的可用内存来满足锁定请求。
EINVAL addr 不是页大小的整数倍,或者 len 是负数。
EAGAIN 锁定操作会超过进程的 RLIMIT_MEMLOCK 资源限制。
ENOSYS 系统不支持 mlockmlockall
ENODEV 底层文件系统不支持内存锁定。例如,在 tmpfs 中进行 mlock 可能会失败,具体取决于配置。

总结:mlock vs mlockall

特性 mlock mlockall
锁定范围 指定的内存区域 整个进程地址空间
灵活性 更灵活,可以精确控制锁定哪些内存 简单粗暴,锁定所有内存
性能影响 影响较小,只锁定必要的内存 影响较大,锁定大量内存
使用场景 需要精确控制锁定范围的场景 安全性要求极高,必须锁定所有内存的场景
解锁 munlock munlockall
风险 忘记解锁会导致部分内存泄漏 忘记解锁会导致大量内存泄漏,可能影响系统稳定性

实际应用场景:

  • 加密软件: 保护密钥和敏感数据,防止被换出到硬盘。
  • 实时交易系统: 保证关键数据始终在内存中,避免延迟。
  • 高性能数据库: 提高数据访问速度,减少磁盘IO。
  • 安全启动: 确保启动代码和数据不被篡改。

一些补充说明:

  1. 内存碎片: 频繁地 mlockmunlock 可能会导致内存碎片。 尤其是在小块内存上进行操作时,需要注意。

  2. OOM Killer: 即使使用了 mlockall, 也不意味着你的程序完全不会被 OOM Killer 杀死。 如果系统内存极度紧张,OOM Killer 仍然可能会选择杀死你的进程。 然而,锁定内存会增加你的程序的存活几率,因为 OOM Killer 通常会优先杀死没有锁定内存的进程。

  3. Capabilities: 默认情况下,普通用户没有权限使用 mlockmlockall。你需要使用 sudo setcap cap_ipc_lock=+ep <executable> 为可执行文件添加 CAP_IPC_LOCK capability。 另一种方法是以 root 用户身份运行程序,但通常不推荐这样做,因为它会带来安全风险。

  4. 其他锁定方法: 除了 mlockmlockall, 还有一些其他的内存锁定方法,例如 madviseshm_lock。 这些方法各有优缺点,适用于不同的场景。

总结:

mlockmlockall 是强大的工具,可以帮助你提高程序的安全性和性能。 但是,它们也需要谨慎使用,避免过度锁定内存,影响系统的整体性能。 记住,用完之后一定要解锁! 否则,你的程序就会变成一个“内存黑洞”,慢慢吞噬系统的资源。

希望今天的讲解对大家有所帮助! 记住,学以致用,才能真正掌握这些知识。 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注