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
英文地址,請注明出處:http://en.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…