CppCon 2016 学习:Out of memory? Business as usual.

当程序因为内存耗尽而抛出 std::bad_alloc 异常时,这并不意味着程序必须崩溃或停止运行。我们应该考虑“内存不足”作为一种可能正常出现的情况(“Out of memory? Business as usual.”),并设计应用程序能优雅地处理这种异常。
具体点说:

  • 有些应用可能因为内存耗尽而进入死循环或卡住,表现为“不终止”。
  • 这是因为程序没有合理地捕获和处理 std::bad_alloc,导致无法恢复或清理资源。
  • 更健壮的设计是在内存分配失败时能捕获异常,进行资源回收、释放内存,甚至降级服务或重启部分逻辑。
  • 这对于内存受限或实时系统尤其重要。
    换句话说,这段话强调:

遇到内存分配失败时,程序不必惊慌失措,而应该做好异常处理,确保即使在极端条件下依然保持可控和可恢复状态。

这是在讨论和澄清内存不足异常(std::bad_alloc)相关的问题,具体分三点:

1. What is “out of memory” and what is “bad allocation”

  • **“Out of memory”**指的是系统无法满足程序请求的内存分配请求,因为可用内存已耗尽。
  • **std::bad_alloc**是C++标准库中在new操作符内存分配失败时抛出的异常,用于通知程序分配内存失败。
    简单来说,“out of memory”是一个系统状态,“bad allocation”是该状态在C++层面上的具体表现。

2. What do those who catch std::bad_alloc do with it

  • 捕获std::bad_alloc的程序员通常做什么?
    • 尝试释放资源或回收内存,减轻内存压力。
    • 降级功能或通知用户,比如提示“内存不足,部分功能不可用”。
    • 日志记录,以便后续分析。
    • 优雅退出,确保程序不崩溃,保存用户数据。
    • 有些系统可能重试分配或延迟操作

3. How common (or rare) are these applications

  • 捕获并处理std::bad_alloc的应用程序并不常见,特别是普通桌面或后台程序,很多程序默认遇到内存耗尽直接终止。
  • 需要高度健壮性的应用,如金融系统、嵌入式系统、服务器软件或实时系统,更可能设计成捕获并处理这类异常
  • 但总体来说,处理内存不足异常的程序属于少数,且编写难度较大。
    总结:
    这三点帮助理解内存不足的本质、异常处理实践和现实中应用程序的普遍性。它鼓励我们设计更健壮的程序,主动捕获std::bad_alloc,避免程序因内存耗尽而直接崩溃。

这段话讲的是资源(resource)和资源获取失败的情况,具体点解读如下:

1. 资源的定义和分类

  • 资源 ≈ 有限的可用量的东西(根据Wikipedia和cppreference的定义)。
  • 假定资源(Assumed resources)
    • CPU时间、CPU核心、CPU缓存
    • 网络带宽
    • 随机数发生器的熵
    • 电力供应
    • 栈内存
      这些通常被系统或平台保证或默认可用,程序不直接检测它们是否耗尽。
  • 需检测的资源(Checked resources)
    • 磁盘空间
    • 硬件设备(文件描述符、套接字描述符)
    • 线程、锁
    • 其他软件资源
    • 堆内存
      这些资源申请可能会失败,程序需要主动处理。

2. 资源申请失败时的异常类型

  • std::system_error
    • 线程相关对象(如thread、unique_lock、shared_lock)
    • 可能shared_ptr
    • 网络套接字(basic_socket)
    • 图形显示对象(display_surface)
    • 这类异常代表系统调用失败或底层操作失败。
  • std::bad_alloc
    • 主要表示内存申请失败(堆分配失败)。
    • 发生在很多标准库容器和资源分配中,如vector、list、map/set、string、function、shared_ptr等。
    • 还包括一些更高级的组件如promise、packaged_task、正则表达式等。

总结:

  • 资源不是单一的“内存”,而是多种有限系统资源。
  • 资源申请失败时,C++标准库通过不同的异常类型来通知程序员,主要是std::bad_allocstd::system_error
  • 理解这点对编写健壮、可靠的程序尤其重要,必须捕获并合理处理这些异常。

这段是在解释操作系统中“内存耗尽(Out Of Memory, OOM)”的真实含义,尤其从虚拟内存和系统管理角度来讲。我们逐条分析:

1. “Memory” means page-based virtual memory

❝ “Memory” 指的是基于分页的虚拟内存。

  • 在现代操作系统中,每个进程看到的是一个线性的虚拟地址空间(如从 0x000000000xFFFFFFFF),而这个地址空间被**按页(通常4KB)**划分。
  • 每页有不同的属性:
    • 私有 or 共享(private/shared)
    • 只读 or 写时复制写入(COW, Copy-On-Write)
    • 干净 or 脏页(clean/dirty)
    • 常驻内存 or 已被换出(paged-out)
      这意味着:你看到的“内存地址”并不代表真实物理内存位置,它是操作系统通过页表管理的抽象。

2. “Unused memory is wasted memory”

❝ 操作系统会用空闲内存做缓存或缓冲,如果程序不在用,那系统就拿去干别的。

  • 所以现代操作系统(如 Linux、Windows)通常会尽量填满物理内存,用于:
    • 文件缓存(page cache)
    • IO buffer
    • 共享库等
  • 空闲内存越少≠出问题,反而可能是系统在高效工作。

3. Commit charge

❝ Commit charge 指的是所有可写但没有文件后备的页的总和,包括:

  • 栈(stack)
  • 数据段(data)
  • 堆(heap)
  • 私有 mmap
  • 共享库的 .GOT 表(全局偏移表)等
    这些内容不是来自文件,因此一旦写入,系统必须提供**物理内存或交换空间(swap)**来存它们。

4. When commit charge > (RAM + Swap), is it OOM?

❝ 当 commit charge 超过 可用RAM + 可用swap 时,是不是一定内存溢出(OOM)?

答案是:未必!

  • 操作系统通常是**懒分配(lazy allocation)**的 —— malloc 申请内存成功 ≠ 系统马上分配物理页。
  • 真正使用(写入)时才分配页框(称为“触发页面错误”)。
  • 所以,可能程序申请了一堆内存,但还没实际用 → commit charge 没增加多少
  • 一旦你写入太多页,系统发现“没有物理内存或swap能保证这些数据”时,才触发OOM。

总结

概念说明
虚拟内存每个进程有自己的地址空间,分页管理
页面属性可读/可写、共享/私有、驻留/换出
commit charge所有需要内存支持的、可写、非文件映射页的总量
OOM条件当系统无法为潜在写入页提供内存或swap时,可能OOM
懒分配机制实际用内存(写入页)才会真正占用资源

这段是在讲 Linux 下 fork() 与大内存分配之间的经典问题,特别是在 fork/exec 模式下的大程序启动机制对内存的隐性影响。

fork/exec 问题解释

背景:

在 Unix/Linux 上,执行新程序通常分两步:

  1. fork():父进程复制自己(内存等资源)→ 得到一个子进程
  2. exec():子进程替换自己,加载新程序(二进制)
    然而,这个“复制”的动作有代价。

问题描述:

当你的进程分配了大量内存(比如 10GB 或 20GB),即使你只是想调用 exec() 启动一个新程序,在这之前你必须成功调用 fork()

❝ 但 fork() 会复制整个地址空间(虚拟地址页表) —— 这可能会因为内存不足而失败。

为什么小程序没问题?

  • Linux 的 fork() 使用了 写时复制(COW)
    • 父子共享内存页,直到某一方尝试写入。
    • 所以 fork() 通常 不立即复制全部内容,看起来是“便宜”的。
      但!

为什么 fork() 会失败?

即使使用了 COW,系统仍然必须保证“如果将来子进程写入,它能支持分离写入”,这意味着:

fork() 成功 ⟺ 系统拥有足够的 commit space 来承诺:

“即使你们都写入每一页,我也能支持”。

于是:

  • malloc(20GB) 成功,但未写入
  • fork() 失败:系统发现你请求的20GB,再加上子进程可能的20GB,系统撑不住 → 拒绝

对比 Windows:

Windows 没有 POSIX 的 fork() 模型,常用 CreateProcess()

  • 一步到位加载新进程
  • 不会复制当前进程的内存
  • 避免了 Linux 的 fork/exec 模式下的 OOM 问题

替代方案:

方案描述问题
vfork()暂时共享地址空间,不复制内存子进程不能访问内存(容易出错)
posix_spawn()内核级替代 fork/exec,效率高语法复杂,不如 fork/exec 灵活
使用线程池 + 管道父进程控制资源,避免复制不适用于 exec

总结核心点:

问题解释
fork() 复制整个虚拟内存空间即使使用了 COW,系统也要保证“万一全写了”也能撑住
大量 malloc + fork 就可能触发 “Out of Memory”尤其是在总内存 + swap 不足时
exec() 会丢掉子进程的内存但得等 fork() 成功后才能调用 exec()
Windows 不用 fork,因此没有这个问题它直接用 CreateProcess()
如果你在构建一个 高内存使用后台服务,需要频繁调用子进程,可以考虑:
  • 避免用 fork(),改用 posix_spawn() 或 worker 模型
  • 把子进程启动放在内存膨胀之前

理解 —— 这段在讨论不同操作系统如何处理**内存 overcommit(过度分配)**的问题,也即:操作系统是否允许你申请比物理内存(+swap)还多的内存,以及在不够时是否立刻失败。

什么是 Overcommit?

Overcommit:允许进程分配的虚拟内存总量超过系统实际可用物理内存 + swap。
原因是:

实际分配 ≠ 实际使用,大多数内存分配(如 malloc)最终未被访问。

两种策略的对比:

严格 commit accounting(不允许 overcommit)

  • 系统在你分配内存时就检查是否“将来”能提供这块内存
  • 如果不能保证,将立即返回 std::bad_alloc / ENOMEM
  • OS:Windows, Solaris, HP-UX 等
  • fork() 更容易失败:因为 fork() 理论上可能复制全部内存。

Overcommit + OOM killer(允许分配,必要时强制杀死进程)

  • 系统允许几乎任意分配,但一旦物理内存耗尽,会触发 OOM 杀手。
  • 被杀的进程是由内核算法决定的,不可预测。
  • OS:Linux(默认使用 heuristic 模式)

Linux 的 overcommit 策略

在 Linux 中,控制 overcommit 的 sysctl 变量是:

/proc/sys/vm/overcommit_memory

可取值:

含义
0默认:启发式。可能 overcommit,内核根据启发式算法判断
1始终允许 overcommit(不做检查,分配成功 ≠ 最终成功)
2严格限制:禁止 overcommit,分配必须有 backing store

相关机制:

  • oom_adj / oom_score_adj:影响进程被 OOM 杀手选中的概率
  • cgroups:可用来对进程群组设定内存限额,配合 OOM 控制更有效
  • rlimits:即使在 overcommit 模式下,进程资源限制仍有效(如 RLIMIT_AS

其他系统特色

系统行为说明
AIX默认严格,但可通过 SIGDANGER opt-out信号通知进程“快没内存了”
FreeBSD可系统关闭 overcommit,也可进程层面控制使用 protect(1)
Linux默认 heuristic,可调兼顾灵活性与安全性

补充细节

  • 在 Linux 上,即使启用了 overcommit,fork() 仍有可能失败,如果启用了 COW accounting(如 strict 模式 2)。
  • "never" 模式下,Linux 会预留一部分内存专门给 shell/top/kill —— 以便用户可以杀掉 OOM 进程。这是防止系统彻底卡死的一个机制。
  • "always" 模式能带来性能,但也更容易导致非确定性崩溃。

总结重点

项目内容
“Overcommit” 是什么虚拟内存分配可以超过物理内存
严格模式更安全、更可预测,但 fork() 等操作容易失败
宽松模式更灵活但不可控,可能触发 OOM 杀手
Linux 默认行为启发式(heuristic),可配置
fork 问题和 overcommit 策略强相关(特别是 fork + 大内存)

这段内容揭示了围绕内存分配失败的一些**“常见误区(myths)”**,尤其是针对 Linux 系统上的 new/malloc 行为。

核心观点:别太相信“分配总成功”的说法

Myth(误区):

“在 Linux 上,内存分配总是成功的。”

这是错误的泛化,很多人(包括流行库的作者)误认为:

  • newmalloc 永远不会失败
  • 或者,只有地址空间耗尽才会抛出 std::bad_alloc

举例

  • Herb Sutter 曾批判这类想法(见他的《To new, perchance to throw part 2》)。
  • LevelDB issue #335:也在讨论异常安全时提到:“Linux only throws std::bad_alloc when address space is exhausted”。

实际情况:Linux 并非总是 overcommit!

Linux 的默认设置是:

vm.overcommit_memory = 0

这是启发式(heuristic)模式,而不是 always-overcommit。
所以在以下情况中,你的分配 可以失败

  • 内核估算 commit charge 过高,拒绝了分配;
  • 显式切到 strict 模式(vm.overcommit_memory = 2);
  • 使用 cgroup、rlimit 等限制;
  • mmap() 的匿名映射在某些配置下会检查 backing store;
  • fork() 由于写时复制(COW)导致潜在的高 commit,系统提前拒绝。

误信 myth 的后果

如果你写的是系统级代码、高可靠服务、或者内存敏感代码(如数据库、图形引擎、浏览器):

  • 忽视 new 失败会导致非预期 crash;
  • 无法优雅处理 OOM 场景;
  • 程序可能在压力下行为不稳定、不安全。

总结

MythReality
Linux 上 new 永远成功错误。Linux 的默认行为是启发式 overcommit,有可能拒绝
只有地址空间耗尽才抛 std::bad_alloc错误。commit charge 超过估算也可能触发
不用关心 new 是否失败错误。在部分配置和工作负载下,失败是可能的而且严重的
如果你希望,我可以帮你:
  • 检查你的 Linux 当前的 overcommit 配置;
  • 写一段 C++/C 代码来实验 mallocnew 是否失败;
  • 设置或查询 overcommit 参数。

这段话阐述了一个重要安全概念:

“内存分配失败(bad allocation)不等于内存不足(OOM)”

核心观点

std::vector<int>(-1); // 这是坏分配 (bad allocation),不是 OOM
  • 这个代码试图创建一个大小为 -1vector(即 size_t(-1)),
  • 这相当于分配几乎 2^64 - 1 个元素 —— 明显是程序员逻辑错误,不是内存用光了才失败的。

为什么这很重要?

很多安全漏洞(甚至是 CVE)都源于:

  • 未经验证的输入
  • 导致了极端的内存分配
  • 最终程序崩溃或被拒服(DoS)

真实世界的例子(OOM DoS via bad allocation)

CVE漏洞组件问题描述
CVE-2016-2109OpenSSL恶意短编码引起解码器尝试分配大量内存
CVE-2016-2463Android伪造媒体文件触发异常大分配
CVE-2016-6170ISC BIND伪造 DNS UPDATE 消息引发 OOM
CVE-2015-7540samba AD-DC恶意网络包触发异常内存使用
CVE-2015-1819libxml精心构造 XML 导致 OOM
CVE-2014-3506OpenSSL伪造 DTLS 握手消息引发过度分配
CVE-2013-7447cairo特制图像数据导致 cairo 分配过大缓冲区

正确的防御措施

  • 永远不要相信外部输入(如 Content-Length)。
  • 对分配前的值进行 范围检查
  • 使用 std::vector::reserve()std::string::resize() 之前先确保大小合理。
  • 若使用 new,在分配前验证:
    if (size > MAX_SAFE_SIZE) throw std::runtime_error("Size too big");
    

总结重点

内容
bad_alloc ≠ OOM有时是由无效输入导致的逻辑错误
不信任输入任何外部数据都可能是恶意的
测试不足逻辑漏洞往往在单元测试中未暴露
CVE 案例多个成熟库都曾因此类问题中招
防护建议输入验证 + 分配前检查大小上限
如果你正在开发处理外部数据的系统(如网络、图像、音频、XML、协议栈),请始终将输入视为不可信。这种“简单的长度验证”常常是安全的第一道防线

这段内容深入探讨了 C 与 C++ 在处理内存分配失败(尤其是 malloc 失败)时的现实问题,尤其是在面对攻击者输入或内存耗尽的场景下。

核心问题:内存分配失败了怎么办?

在 C 中,如果 malloc(n_bytes) 失败,会返回 NULL
但现实是 —— 你通常:

不知道如何优雅地把这个错误往上传达(尤其是跨多层函数调用)。

C中的处理方式

以下是几种现实中看到的处理策略:

方式示例缺点
返回错误码if (!ptr) return ERR_ALLOC_FAIL;错误传播代码繁琐,容易漏
longjmp/setjmp跳出多层函数极度易错,破坏栈结构,调试困难
直接中止进程g_error("failed to allocate...")非库友好:调用方无法恢复
忽略失败直接解引用崩溃、漏洞、DoS
示例代码中 GLib 的行为:
mem = malloc(n_bytes);
if (mem)return mem;
g_error("failed to allocate ..."); //  中止程序

C++ 如何改进?

在 C++ 中:

try {_input_buffer.resize(_isize); // 内部可能 new[],如果失败会抛 bad_alloc
} catch (bad_alloc) {// 统一异常处理逻辑log_error(...);drop_connection();
}

C++ 优点:

  • 标准库 new 默认在失败时抛出 std::bad_alloc
  • 你可以 catch 到它
  • 允许你在靠近“业务逻辑”层做集中处理
  • 如果你愿意,还可以 set_new_handler() 做自定义清理或回退

但即使在 C++中,也不是银弹

  • 如果没有仔细 try-catch,程序仍会终止
  • STL 容器里隐含分配的地方多(如 vector::resize, map::insert
  • 异常传播对性能敏感路径不利(如实时系统)
  • 一些库(如 absl::btree) 可能选择不抛异常

更好的设计方式(C/C++ 通用)

  1. 验证所有外部数据 —— 特别是分配大小前:
    if (_isize > MAX_SIZE) return error();
    
  2. 封装内存分配 —— 便于统一错误处理:
    void* safe_malloc(size_t sz) {void* p = malloc(sz);if (!p) throw bad_alloc();return p;
    }
    
  3. 区分可恢复 vs 致命错误 —— 不要总是 exit()

总结对比

比较点CC++
内存失败信号NULLbad_alloc 异常
错误传播手动返回码/longjmptry/catch 或传递异常
默认行为多数库直接 abort抛出异常可捕获
可恢复性依赖设计更易于结构化恢复
安全隐患忽略返回值、崩溃忽略异常、资源泄露

建议

  • 不管是 C 还是 C++,永远不要直接相信输入可安全用于分配
  • 在分配前进行上限检查
  • 用 RAII、异常处理(在 C++),或者封装错误检查(在 C)来实现健壮行为
  • 在大型项目中统一封装内存分配接口

这部分深入分析了 C++ 中的内存分配失败(std::bad_alloc)的处理现状,并通过真实案例展示其影响力和实际处理缺失的问题。

一次“分配”摧毁整个世界:CVE-2009-1692

这个 CVE 展示的是一种 JavaScript 滥用内存 的方式,能导致:

浏览器/平台行为
IE, Firefox, Safari, Chrome, Opera内存激增后崩溃
Konqueror、Wii、PS3、iPhone整个设备挂死,需要硬重启
Chrome仅标签崩溃,稍好一些
原因:大部分 JavaScript 引擎未对异常内存使用做合理限制。例如:
let s = "A";
while (true) s += s;

或者构造巨大的数组、字符串、对象,诱使引擎分配超过系统可承受的内存。

那么 std::bad_alloc 真有人处理吗?

真实搜索:Debian Code Search

catch (std::bad_alloc) 的显式捕获进行统计:

  • 共 341 个包、3043 处代码。
  • 抓出几类处理方式:
    | 类型 | 占比 | 行为 |
    | -------------------------------- | — | ----------------------------- |
    | 忽略(“Somebody else’s problem”) | 46% | 没有做任何 meaningful 处理 |
    | 转换为错误码 | 23% | 设置 error flag / 返回错误码 |
    | 转为自定义异常 | 13% | 更高级错误管理(但仍依赖上层) |
    | 转为其他语言 OOM 异常 | 5% | Python 的 PyErr_NoMemory(),等 |
    | 原样 rethrow | 4% | 不处理,只传播 |
    结论:大多数项目并没有有效处理 bad_alloc,即使是大型项目或库。

深层次启示

为什么这么少人处理?

  1. 罕见性:现代系统 overcommit,有 swap,bad_alloc 变得少见。
  2. 难以测试:制造 OOM 非常困难,特别是在 CI 或开发机上。
  3. 恢复复杂:一旦分配失败,很多对象可能已经部分构造,状态不明。
  4. 设计缺陷:默认 C++ 构造过程不适合 rollback/恢复,RAII 有局限。

建议与启发

☑ 如果你关心健壮性或面对不受信任输入:

  • 任何外部输入都必须验证分配大小上限
  • try-catch bad_alloc 是必要但不充分条件,你需要设计出清晰的 错误传播路径
  • 封装资源敏感操作,比如:
std::optional<std::vector<char>> safe_allocate(size_t n) {try {return std::vector<char>(n);} catch (const std::bad_alloc&) {return std::nullopt;}
}
  • 在大型系统中考虑更强的 OOM 策略,如:
    • 内存池+监控机制
    • 分配失败时释放低优先级资源
    • 限制最大内存占用

总结

  • 现实中一次失控的内存分配可以导致整个设备挂死(如 CVE-2009-1692)
  • 即使在现代开源软件中,大多数 bad_alloc未被优雅处理
  • 你若做的是面向用户、网络、或嵌入设备的软件,必须认真对待 OOM
  • C++ 提供了异常处理机制,但不意味着它自动解决了恢复问题

这一页继续分析了在 C++ 中如何应对 std::bad_alloc,并结合真实代码仓库中的例子展示了多种常见的应对策略。

主题总结:C++ 库中如何“处理”分配失败

C++ 的内存分配失败默认抛出 std::bad_alloc 异常,但实际应用中,开发者的应对方式五花八门,本页主要展示了两类更“实际”的做法:

① “Convert to Error Code”(转换为错误码)——占 23%

这类库通常采用如下策略:

  • 在内部分配失败时 catch 异常
  • 然后返回某种错误码(而不是让异常继续传播)
    示例:Notepad++ / Scintilla
try {if (_pscratchTilla->execute(SCI_GETSTATUS) != SC_STATUS_OK)throw;InsertString(position, data, length);
} catch (std::bad_alloc &) {return SC_STATUS_BADALLOC;
}

优点:调用者可以统一地检查错误码并处理,不需要处理异常
缺点:你得永远记得去检查返回值,否则就白做了
来源代码:

  • Scintilla Buffer.cpp
  • Scintilla Document.cxx

② 置空指针 + 状态标志(member variable update)

一些更底层的库(如 libstdc++)不会直接抛异常,而是在失败时设置标志位或返回空指针。
示例:libstdc++ / GCC 的 ios stream 处理

try {_words = new (std::nothrow) _Words[_newsize];
} catch (const std::bad_alloc&) {_words = nullptr;
}
if (!_words)_M_streambuf_state |= badbit;

std::nothrow 会阻止抛出异常,失败时返回 nullptr
状态标志 badbit 会告诉流使用者出了问题,但不致 crash
来源代码:

  • GCC libstdc++ ios.cc

小结:这种做法的“哲学”是?

稳定优先:比起抛出异常破坏流程,不如用返回码/状态位尽量“稳住”

可恢复性强:尤其在嵌入式或交互式应用中,更倾向于用错误码回传错误

对比 catch 异常的做法:

方法表现优点缺点
抛出 bad_alloc直接跳出到上层自动传播稳定性差,易崩
返回错误码调用者自行判断更稳健容易忘记检查
设置状态位延迟处理不会中断流程易被忽视

实战建议:

  • 若你写的是“控制逻辑类库”,建议用 返回错误码
  • 若你写的是“算法或组件库”,可以保留 bad_alloc 异常,让上层处理
  • 若你是做 UI、嵌入式、或系统层调用,尽量别崩,catch bad_alloc 并恢复 是最佳实践

本页内容要点:

继续讲 C++ 实战中对 std::bad_alloc 的应对方式,尤其聚焦于:

③ Convert to custom exception(转换为自定义异常)——占 13%

示例:POCO 项目中的 PostgreSQL 模块

代码片段摘自 Poco::Data::PostgreSQL::Binder

try {if (aPosition >= _bindVector.size())_bindVector.resize(aPosition + 1);InputParameter inputParameter(aFieldType, aBufferPtr, aLength);_bindVector[aPosition] = inputParameter;
} catch (std::bad_alloc&) {throw PostgreSQLException("Memory allocation error while binding");
}

解释与动机:

这段代码的作用是:在 PostgreSQL 参数绑定时,如果分配内存失败,抛出特定的数据库异常
为什么不直接让 bad_alloc 传播?

  • std::bad_alloc 语义太笼统:只表示“内存不足”,调用者难以分辨哪里出错
  • 业务逻辑希望知道:“这是数据库层的绑定问题”,而不是其他未知的系统异常
  • 抛出更语义明确的异常 PostgreSQLException,便于上层精确捕获和处理

通用做法:

这种模式适用于 中间件、数据库接口、网络通信库 等系统:

“底层异常 + 语义变换 = 业务语境中的异常”

具体实现步骤通常是:

  1. 捕获如 std::bad_alloc 这种通用异常
  2. 转换成你的领域自定义异常(加上有意义的信息)
  3. 重新抛出给上层

自定义异常的优点:

优点说明
语义清晰可以根据上下文区分哪一层出了问题
更好调试带有错误文本,便于记录日志或提示用户
更好管理可将所有异常统一包裹成某种“领域异常”

注意事项:

  • 如果你抛出自定义异常,务必让其继承自 std::exception,否则 catch (std::exception&) 捕不到
  • 如果你使用异常做控制流,记得别在性能关键路径上滥用

总结:

这一类处理方式体现了 “异常语义升维”:把底层的问题提升成高层可以理解和应对的业务语境。

如果你在写自己的 C++ 框架,建议在模块边界用这个策略,例如:

catch (const std::bad_alloc&) {throw MyApp::DatabaseError("OOM when preparing query buffer");
}

本页内容:std::bad_alloc 的第四种处理方式

④ Rethrow as-is(原样重新抛出)——占 4%

示例代码片段:

m_allocations.push_back(ptr);
catch (const std::bad_alloc& e)throw e;            // 显式重新抛出异常
delete[] ptr;
catch (...) throw;              // 捕获所有异常并重新抛出
_allocator.deallocate(_instance, 1);
_instance = 0;
throw;

含义解释:

  • “Rethrow as-is” 指的是:捕获异常只是为了清理资源,清理完成后立即重新抛出原始异常
  • 异常不转换、不吞掉、不降级为 error code
    换句话说:

我只打扫战场,不阻止你继续报警。

为什么这么做?

在 C++ 中使用 RAII(资源获取即初始化)可以自动释放大多数资源。但 某些资源不是 RAII 管理的,比如:

  • 显式堆分配但没有用智能指针管理的资源
  • 显卡资源、文件描述符、数据库连接等外部资源
  • 全局状态(比如 _instance
    这种情况下,如果直接抛异常,你可能会资源泄漏状态混乱

实用模式:

try {ptr = new int[1024];m_allocations.push_back(ptr);
} catch (const std::bad_alloc& e) {delete[] ptr;              // 手动清理throw;                     // 原样抛出
}

或在 Singleton 模式中:

try {_instance = _allocator.allocate(1);
} catch (const std::bad_alloc&) {_instance = nullptr;throw;                     // 让调用者知道失败原因
}

注意事项:

风险点描述
throw e;这样会 slicing(异常对象复制)且重置 what() 栈信息。应使用 throw; 保持异常原样
忘记清理仅适用于你确实在 catch 中完成了非 RAII 资源的清理
复杂代码中若 catch block 很长,重新抛出前可能会混淆上下文逻辑,要留意可维护性

总结:

Rethrow-as-is 是一种 严谨负责的清理行为,适用于“我做了一点事,然后继续让上层来决定”。

优点:

  • 不吞异常、不篡改语义
  • 能清理局部非 RAII 资源
    缺点:
  • 不能完全避免资源泄漏(建议配合智能指针)
  • 不能添加业务语义(不像前面讲的“Convert to custom exception”)

本页重点:

“Cleanup and terminate” —— 占总样本 21%

主要分类:

类型比例说明代表项目
Not from main12%在非主线程(非 main 函数)捕获异常后清理并终止rethinkdb, ipopt, fluxbox, krita 等
From main8%在主线程的 main 函数里捕获异常后清理并终止tripwire, smartmontools, tango 等
Configurable handler, default abort1%配置异常处理器,默认调用 abortigraph, gdal 等
“Parachute”<1%代码中带有“安全降落伞”风格的异常处理(参考《Code Complete》)scantailor

意思是什么?

  • 捕获异常后,程序不能安全继续运行,就进行清理资源并优雅退出。
  • 不是试图“恢复”或转换异常,而是“最后保护”,保证资源正确释放,输出错误信息,然后终止程序。
  • “Not from main”通常是子线程或库内部异常,这种情况下清理后终止避免不确定状态。
  • “From main”意味着在程序入口集中处理所有异常。

“Parachute” 异常处理:

  • 来自 Steve McConnell《Code Complete》概念
  • 程序中加入异常“安全网”,防止任何异常逃逸到操作系统导致崩溃。
  • 保障程序的最小安全退出,保证日志和资源回收。

总结:

这种策略通常适用于:

  • 对内存安全和稳定性要求极高的系统(服务器、嵌入式、图形驱动)
  • 不能容忍异常破坏状态的程序
  • 只希望在异常时尽可能干净退出
    如果你要设计 Vulkan 框架底层,也应该考虑在顶层捕获异常,做最后清理和终止,避免崩溃时资源泄漏或硬件状态异常。
    需要我帮你写一个 C++ 程序主入口的异常安全模板吗?包括统一异常捕获,日志记录,资源释放,安全退出。

核心要点补充:

  • “Cleanup” 通常是指:
    • 删除临时文件、锁文件,释放有限资源,避免“僵尸”或资源泄漏。
    • 例如 unlink_ofile(oname); 删除临时输出文件。
  • 异常捕获后立即清理并退出:
    • 例子中捕获 std::bad_alloc,打印错误信息,退出程序。
  • 库内部直接崩溃(CRASH())示例:
    • fastMalloc 里,如果 malloc 返回空指针,直接调用崩溃函数。
    • 这种做法代价高,但防止异常传播导致状态不确定。

设计启示

  • 在 Vulkan 或高性能计算框架中,底层内存分配失败可考虑:
    • 统一捕获 bad_alloc,安全清理后终止,防止状态崩溃。
    • 对临时资源(缓冲区、锁文件等)要有明确的释放策略。
    • 对于无法恢复的分配失败,宁可安全崩溃,避免隐蔽错误。

这部分意思是:

面对 std::bad_alloc,一些程序选择 “继续向前走”,采取不同的策略,而不是直接终止:

  1. 尝试分配更少的内存(3%)
    • 例如用更小的缓冲区,或改为即时计算,减少内存需求。
    • 例子:Audacity、Eigen3、LibreOffice 等。
  2. 在析构函数中吞掉 bad_alloc(3%)
    • 为了缓存或延迟操作,在析构时悄悄处理内存不足。
    • 例子:LibreOffice、OpenCV 等。
  3. 使用替代算法(2%)
    • 比如改用“就地”算法避免额外内存分配。
    • 例子:VTK、Krita、Octave 等。
  4. 释放内存(清理缓存、腾出空闲链表)(2%)
    • 尝试通过释放缓存或内部资源来恢复内存。
    • 例子:libstdc++、Sonic Visualizer。
  5. 重试分配(1%)
    • 直接尝试重新申请内存,期望短期内资源释放后成功。
    • 例子:GNU Radio。

设计启示

  • Vulkan框架设计时,可以提供多级降级策略,面对OOM:
    • 优先减小数据量(如分块计算)
    • 使用更节省内存的算法路径
    • 动态清理缓存
    • 甚至尝试重试
    • 只有最后才彻底失败或终止
  • 这样能增强系统健壮性和灵活性,提升用户体验。

这段代码示范了遇到内存分配失败(std::bad_alloc)时,优雅切换到“就地”算法(in-place algorithm),以节省内存,保证程序继续运行。

代码逻辑解析:

bool inline inplace_transpose(arma::Mat<eT>& X)
{try {X = arma::trans(X);  // 先尝试直接转置(需要额外内存)return false;        // 成功,返回false表示没有用“就地”方法}catch (std::bad_alloc&) {
#if (ARMA_VERSION_MAJOR >= 4) || ((ARMA_VERSION_MAJOR == 3) && (ARMA_VERSION_MINOR >= 930))arma::inplace_trans(X, "lowmem");  // 失败后调用就地转置,节省内存return true;         // 表示用了“就地”转置
#endif}
}
  • 先尝试普通转置,失败时抛出 bad_alloc
  • 捕获异常后,切换到就地转置,避免开辟新的内存空间
  • 返回值表示是否用就地方法

启示:

  • 设计高性能或内存敏感框架时,预设“备选算法”非常重要。
  • 在OOM时,尽量避免直接崩溃,切换到内存占用更小的“降级方案”。
  • 对外接口上,告知调用方是否使用了备选算法,有利于调优或告警。

这部分讲的是面对 std::bad_alloc(内存分配失败),软件通常采取“回滚并做别的事情”的策略,避免程序崩溃,提升用户体验和系统稳定性。具体做法分成几类:

主要做法:

  1. 交互式应用拒绝用户操作(约12%)
    • 比如打开文件失败,弹错误框,提醒用户。
    • 例子:Notepad++, LibreOffice, Inkscape, TeXstudio 等。
  2. 服务器拒绝服务请求(约5%)
    • 服务器因资源不足,直接丢弃新请求,保证现有服务稳定。
    • 包括网络服务器(ntopng, apt-cacher-ng等)和数据库(scylladb, tarantool等)。
  3. 预设降级或替代方案(约1%)
    • 使用默认资源代替失败的加载,比如错误纹理、无声驱动等。
    • 例子:游戏0ad、模拟器desmume。

设计启发

  • 在 Vulkan 或其它底层框架设计中,应允许调用者检测内存失败并优雅“退回”,比如拒绝提交任务、通知用户、加载备用资源。
  • 设计接口时,支持错误返回或异常捕获,允许“放弃当前操作但继续运行”。
  • 对于服务器或批处理任务,提供负载调节或请求拒绝机制,避免崩溃。

这段内容细化了“回滚并做别的事情”里交互式应用的典型处理:

  • 最常见场景是文件太大,无法加载,比如 Poppler::Document::load(fileName) 失败时抛 std::bad_alloc,然后返回错误状态(PopplerErrorBadAlloc),并返回一个空的智能指针,避免程序崩溃。
  • 另一个例子是 vtkstd::bad_alloc 抛出时,用自定义异常 IRISException 包装,再通过 UI 弹窗警告用户“生成网格时内存不足”,这样用户能感知错误且程序保持稳定。
    这体现了:
  • 优雅地捕获内存分配失败异常,避免直接崩溃
  • 返回错误状态或抛出自定义异常
  • 及时向用户反馈错误信息
  • 保证程序其余部分继续稳定运行

这里讲的是服务器端遇到 std::bad_alloc 时的典型处理:

  • 服务器通常会捕获异常后放弃当前请求,避免整个服务崩溃或卡死。
  • 具体例子里,processPacket(...) 可能抛出 bad_alloc,catch 捕获后打印日志(或其他轻量级处理),然后直接跳过这条请求,继续处理后续数据包。
    这体现了服务器稳定性和容错性的设计思路:
  • 服务器优先保证持续服务能力
  • 某些请求内存不足导致失败时,宁愿放弃该请求,也不影响整体系统正常运行
  • 记录日志帮助排查和监控

这部分讲的是单元测试中对 std::bad_alloc 异常的捕获和验证:

  • 单元测试(Unit Tests)会故意触发 bad_alloc,检查代码在内存分配失败时的表现是否符合预期。
  • 通过宏或断言(如 ASSERT_THROWS_IN_TEST)确认特定操作会抛出 std::bad_alloc
  • 这样可以确保程序在异常情况下依然稳定或做出正确反应。
    举例来自 TBB 的测试代码,模拟内存压力,确认容器操作抛出异常,保证健壮性。

这一段给出了几个领域(办公软件、代码编辑器、浏览器、数据库)在遇到C++内存耗尽(OOM)时的典型表现和应对状况总结,体现了现实世界中std::bad_alloc处理的复杂性和挑战:

  • 办公软件中,很多程序在大文件或大粘贴操作时崩溃或异常退出,有些能存活但表现不稳定,极少数能优雅处理。
  • 代码编辑器/IDE也类似,有的优雅拒绝操作,有的直接崩溃。
  • 浏览器表现尤为复杂,JavaScript运行时OOM导致标签页崩溃,或者浏览器崩溃,部分能阻止脚本但仍会逐渐崩溃。
  • 数据库有些能存活(ScyllaDB、Tarantool),大多数会崩溃,且社区呼声强烈希望数据库能更优雅地处理OOM,如取消导致OOM的查询并释放内存,而不是直接崩溃服务。
    最后提到OOM处理的核心要点:
  • 用户确实需要OOM保护,尤其是避免数据丢失或服务中断时
  • 一致的RAII风格编程是基础,避免析构时抛出异常
  • 多种策略并存,极端情况可预分配全部资源
  • 库的角色极其重要,有的库会使用户陷入困境,有的库(如Scintilla)则帮用户优雅处理OOM
    整体来说,这是一段现实案例的总结和建议,告诉我们OOM是非常棘手的问题,必须从设计、代码、库、架构多方面入手,结合实际场景选择合适策略。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/87293.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/87293.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

庙算兵棋推演AI开发初探(8-神经网络模型接智能体进行游戏)

前言の碎碎念 由于我做的模仿学习&#xff0c;可能由于没有完全模仿&#xff0c;可以说效果很烂……后来用强化学习优化&#xff0c;这个倒是不用自己做数据集了&#xff0c;为方便大家只搞代码&#xff0c;这里只说这部分的经历和方法。 实践基础介绍 1-动作 先介绍一个强化…

Uart_Prj02 Windows 窗口版串口_Step1

完成上位机控制台串口后&#xff0c;接下来想用C#做一个Windows 窗口版的串口。上位机编程不是很熟练&#xff0c;每天学一点做一点。 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.…

自动驾驶系统研发—从工程视角看纯视觉自动驾驶的安全挑战与应对策略

🌟🌟 欢迎来到我的技术小筑,一个专为技术探索者打造的交流空间。在这里,我们不仅分享代码的智慧,还探讨技术的深度与广度。无论您是资深开发者还是技术新手,这里都有一片属于您的天空。让我们在知识的海洋中一起航行,共同成长,探索技术的无限可能。 🚀 探索专栏:学…

PostgreSQL认证怎么选?PGCP中级认证、PGCM高级认证

上图是2025年6月份最新的db-engines上的数据库排名情况&#xff0c;可以看出PostgreSQL数据库仍然呈上升趋势&#xff0c;跟排名第三的"Microsoft SQL Server"起来越接近&#xff0c;国内亦是如此&#xff0c;PostgreSQL的热潮依在&#xff0c;可见学习PostgreSQL数据…

Hive 3.x数据静态脱敏与加密

引言 在大数据时代&#xff0c;数据已成为企业和组织的核心资产。作为数据处理的重要平台&#xff0c;Hive 3.x存储着大量敏感信息&#xff0c;如用户个人身份、财务数据、商业机密等。如何确保这些数据在存储和处理过程中的安全性&#xff0c;成为数据从业者关注的焦点。数据…

CppCon 2016 学习:Lightweight Object Persistence With Modern C++

你给出的这段文字是某个演讲、论文或者技术文档的概要&#xff08;Overview&#xff09;部分&#xff0c;内容主要是关于内存分配器&#xff08;allocator&#xff09;设计以及**对象持久化&#xff08;object persistence&#xff09;**的一些思路。让我帮你逐条解析和理解&am…

IPv6中的ARP“NDP协议详解“

一、概述 在IPv4网络环境当中,我们想要与对端进行网络通信时,首先需要去解析对方的MAC地址这样我们才能封装二层数据帧,就算访问不同网络时也需要解析网关的MAC,这些都是需要我们的ARP协议来进行操作完成的,但是在我们的IPv6网络环境当中并没有ARP协议,而是通过NDP协议来完成类…

TortoiseSVN迁移到本地git

将项目从Subversion&#xff08;SVN&#xff09;迁移到Git是许多开发团队的需求&#xff0c;因为Git提供了更多的功能和灵活性。本文将详细介绍如何使用TortoiseSVN将项目迁移到本地Git仓库。 一、准备工作 安装Git&#xff1a;确保在本地机器上安装了Git。可以通过以下命令检…

高性能 Web 服务器之Tengine

一、概述 Tengine 是一个由淘宝网发起的 Web 服务器项目。它基于 Nginx 然后针对大访问量网站的需求&#xff0c;添加了很多高级功能和特性&#xff0c;从 2011 年 12 月开始&#xff0c;Tengine 正式开源。Tengine 的性能和稳定性已经100多家大型网站如淘宝网&#xff0c;天猫…

简单实现HTML在线编辑器

我们继续来看一下如何开发一个简单的html在线编辑器&#xff0c;要求很简单 能够同时编辑html&#xff0c;css&#xff0c;js代码&#xff0c;并且运行之后可以同时预览效果 一&#xff1a;前置知识 在H5中设置了一个新的标签&#xff0c;<iframe>&#xff0c; 用于在当前…

【Bluedroid】蓝牙启动之核心模块(startProfiles )初始化与功能源码解析

本文深入解析Android蓝牙协议栈中 start_profiles 函数及其调用的核心模块初始化逻辑,涵盖 BNEP、PAN、A2DP、AVRC、HID Host、BTA_AR 等关键配置文件和应用层模块。通过代码分析与流程梳理,阐述各模块如何通过全局控制块、状态机、回调机制实现功能初始化、连接管理及数据交…

RK3576 Android14 DMIC调制

一、背景 近期项目中有个DMIC调试的需求&#xff0c;搁置了较长时间&#xff0c;现今着手调试&#xff0c;遂作记录。 二、开发环境 OS&#xff1a;Android14 Platform&#xff1a;RK3576 Linux Version&#xff1a;6.1.99 SDK Version&#xff1a;android-14.0-mid-rkr6 …

使用 Prometheus 监控 Spring Boot 应用

SpringBoot+Prometheus+Grafana实现监控 逻辑如图 应用程序在生产环境中运行时,监控其运行状况是非常必要的。通过实时了解应用程序的运行状况,才能在问题出现之前得到警告,也可以通监控应用系统的运行状况,优化性能,提高运行效率。 一、监控 Spring Boot 应用 下面我们…

简易计算器 Python 实现

目录 一、代码逐步分析&#xff08;适合刚入门的朋友看&#xff09; 1.定义了一个名为simple_calculator的函数&#xff0c;封装了整个计算器的逻辑。 二、深入分析代码块&#xff0c;用更加官方的语词来说&#xff08;适合想要深入学习的朋友&#xff09; 主循环结构 退出…

开源编译器介绍

文章目录 基本构成传统编译器编译器的发展历史&#xff08;History of Compiler&#xff09;GCC 编译过程与原理&#xff08;GCC Process and Principle&#xff09;LLVM/Clang 编译过程与原理&#xff08;LLVM/Clang Process and Principle&#xff09;GCC与与 LLVM/Clang 的对…

C++ String知识点

当然可以&#xff01;下面我将以系统全面、通俗易懂、深入浅出的方式&#xff0c;为你讲解 C 中非常核心但也容易被低估的内容 —— std::string。 &#x1f31f; C std::string 全面详解 &#x1f4cc; 一、string 是什么&#xff1f; C 的 std::string 是 C 标准库中封装好…

全新NVIDIA Llama Nemotron Nano视觉语言模型在OCR基准测试中准确率夺冠

全新NVIDIA Llama Nemotron Nano视觉语言模型在OCR基准测试中准确率夺冠 PDF、图表、图形和仪表板等文档是丰富的数据源&#xff0c;当这些数据被提取和整理后&#xff0c;能够为决策制定提供有价值的洞察。从自动化财务报表处理到改进商业智能工作流程&#xff0c;智能文档处…

gradle的 build时kaptDebugKotlin 处理数据库模块

gradle的 build时输出&#xff1a; Task :app:kaptDebugKotlin 注: Processing class HDCoinBean 注: Processing class HDCurrencyBean 注: Processing class HDSelfAddCoin 注: Processing class MN 注: Creating DefaultRealmModule <—> 80% EXECUTING [7m 56s] IDLE…

二叉树的节点操作算法

235. 二叉搜索树的最近公共祖先 力扣题目链接(opens new window) 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大…

【ubuntu驱动安装】安装nvidia驱动和cuda环境

1、安装驱动 首先查看环境和显卡&#xff1a; 更新apt 查看nouveau是否禁用 如果有返回值禁用nouveau(nouveau是通用的驱动程序)&#xff08;必须&#xff09;&#xff0c;两种文件&#xff0c;22.04是下面那个 添加如下&#xff1a; 终端输入后更新 重启电脑sudo reboo…