隨著人工智能,云計算等技術的迅猛發展,讓Python,go等新興語言流行了起來,很多人以為C++可能已經過時了,確實,C++編程語言走到今天已經有將近40年的歷史了,但它依然是當今的主流語言,我們可以看一下世界權威編程語言排行榜,C++依然是屬于第一梯隊,C++在金融交易系統,游戲,數據庫,編譯器,大型桌面程序,高性能服務器,瀏覽器,各類編程比賽(ACM-ICPC,Topcoder,Codeforces,Google Code Jam)等領域任然是主力軍。
在各個大廠情況,C++也是很多大廠主力編程語言,國外google和微軟大部分核心產品都是基于C++開發的;鵝廠編程語言TOP5,C++排第一:
C++的高抽象層次,又兼具高性能,是其他語言所無法替代的,C++標準保持穩定發展,更加現代化,更加強大,更加易用,熟練的 C++ 工程師自然也獲得了“高水平、高薪資”的名聲,但在各種活躍編程語言中,C++門檻依然很高,尤其C++的內存問題(內存泄露,內存溢出,內存宕機,堆棧破壞等問題),需要理解C++標準對象模型,C++標準庫,標準C庫,操作系統等內存設計,才能更加深入理解C++內存管理,這是跨越C++三座大山之一,我們必須拿下它。
Content
環境:
uname -a Linux alexfeng 3.19.0-15-generic #15-Ubuntu SMP Thu Apr 16 2337 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux cat /proc/cpuinfo bugs : bogomips : 4800.52 clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual cat /proc/meminfo MemTotal: 4041548 kB(4G) MemFree: 216304 kB MemAvailable: 2870340 kB Buffers: 983360 kB Cached: 1184008 kB SwapCached: 54528 kB GNU gdb (Ubuntu 7.9-1ubuntu1) 7.9 g++ (Ubuntu 4.9.2-10ubuntu13) 4.9.2
一 C++內存模型
C++11在標準庫中引入了memory model,這應該是C++11最重要的特性之一了。C++11引入memory model的意義在于我們可以在high level language層面實現對在多處理器中多線程共享內存交互的控制。我們可以在語言層面忽略compiler,CPU arch的不同對多線程編程的影響了。我們的多線程可以跨平臺。
內存模型
為 C++ 定義計算機內存存儲的語義。可用于 C++ 程序的內存是一或多個相接的字節序列。內存中的每個字節擁有唯一的地址。
字節
字節是最小的可尋址內存單元。它被定義為相接的位序列,大到足以保有任何UTF-8編碼單元( 256 個相異值)和(C++14 起)基本執行字符集(要求為單字節的 96 個字符)的任何成員。類似 C , C++ 支持 8 位或更大的字節。char 、 unsigned char 和 signed char 類型把一個字節用于存儲和值表示。
字節中的位數可作為 CHAR_BIT 或 std::numeric_limits
內存位置
內存位置是
一個標量類型(算術類型、指針類型、枚舉類型或 std::nullptr_t )對象
或非零長位域的最大相接序列
注意:各種語言特性,例如引用和虛函數,可能涉及到程序不可訪問,但為實現所管理的額外內存位置。
線程與數據競爭
執行線程是程序中的控制流,它始于 std::thread 、 std::async 或以其他方式所做的頂層函數調用。
任何線程都能潛在地訪問程序中的任何對象(擁有自動或線程局域存儲期的對象仍可為另一線程通過指針或引用訪問)。
始終允許不同的執行線程同時訪問(讀和寫)不同的內存位置,而無沖突或同步要求。
一個表達式的求值寫入內存位置,而另一求值讀或寫同一內存位置時,稱這些表達式沖突。擁有二個沖突求值的程序有數據競爭,除非
兩個求值都在同一線程上,或同一信號處理函數中執行,或
兩個沖突求值都是原子操作(見std::atomic),或
一個沖突求值先發生于( happens-before )另一個(見內存順序--std::memory_order)
若出現數據競爭,則程序的行為未定義。
內存順序(std::memory_order)
如果不使用任何同步機制(例如 mutex 或 atomic),在多線程中讀寫同一個變量,那么程序的結果是難以預料的。簡單來說,編譯器以及 CPU 的一些行為,會影響到C++程序的執行結果
即使是簡單的語句,C++ 也不保證是原子操作。
CPU 可能會調整指令的執行順序。
在 CPU cache 的影響下,一個 CPU 執行了某個指令,不會立即被其它 CPU 看見。
Intel x86, x86-64等屬于強排序CPU,x86-64的強內存模型總能保證按順序執行,遵從數據依賴順序,但PowerPC和ARM是弱排序CPU,有時需要依賴內存柵欄指令。
多線程讀寫同一變量需要使用同步機制,最常見的同步機制就是std::mutex和std::atomic。然而從性能角度看,通常使用std::atomic會獲得更好的性能.
C++11 提供6 種可以應用于原子變量的內存次序:
momory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
雖然共有 6 個選項,但它們表示的是四種內存模型:
Relaxed ordering
Release-Acquire ordering
Release-Consume ordering
Sequentially-consistent ordering
順序一致次序(sequential consisten ordering)
對應memory_order_seq_cst. SC作為默認的內存序,是因為它意味著將程序看做是一個簡單的序列。如果對于一個原子變量的操作都是順序一致的,那么多線程程序的行為就像是這些操作都以一種特定順序被單線程程序執行。從同的角度來看,一個順序一致的 store 操作 synchroniezd-with 一個順序一致的需要讀取相同的變量的 load 操作。除此以外,順序模型還保證了在 load 之后執行的順序一致原子操作都得表現得在 store 之后完成。非順序一致內存次序(non-sequentially consistency memory ordering)強調對同一事件(代碼),不同線程可以以不同順序去執行,不僅是因為編譯器可以進行指令重排,也因為不同的 CPU cache 及內部緩存的狀態可以影響這些指令的執行。但所有線程仍需要對某個變量的連續修改達成順序一致。
松弛次序(relaxed ordering)
在這種模型下,std::atomic的load()和store()都要帶上memory_order_relaxed參數。Relaxed ordering 僅僅保證load()和store()是原子操作,除此之外,不提供任何跨線程的同步。
獲取-釋放次序(acquire-release ordering)
在這種模型下,store()使用memory_order_release,而load()使用memory_order_acquire。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
在store()之前的所有讀寫操作,不允許被移動到這個store()的后面。
在load()之后的所有讀寫操作,不允許被移動到這個load()的前面。
數據依賴(Release-Consume ordering)
memory_order_consume 是 acquire-release 順序模型中的一種,但它比較特殊,它為 inter-thread happens-before 引入了數據依賴關系:dependency-ordered-before ,一個使用memory_order_consume的操作具有消費語義(consume semantics)。我們稱這個操作為消費操作(consume operations),對于memory_order_consume最的價值的觀察結果就是總是可以安全的將它替換成memory_order_acquire,消費和獲取都為了同一個目的:幫助非原子信息在線程間安全的傳遞。就像獲取操作一樣,消費操作必須與另一個線程的釋放操作一起使用。它們之間主要的區別在于消費操作可以正確起作用的案例更少。相對于它的使用不便,反過來也就意味著消費操作在某些平臺使用更有效。
默認情況下,std::atomic使用的是 Sequentially-consistent ordering。但在某些場景下,合理使用其它三種 ordering,可以讓編譯器優化生成的代碼,從而提高性能。
思考問題:
1 C++正常程序可以訪問到哪些內存和不能訪問到哪些內存(這些內存屬于該程序)?
2 內存對程序并發執行有什么影響?
3 std::memory_order 的作用是什么?
二 C++對象內存模型
1 空類對象(一般作為模板的tag來使用)
classA{};sizeof(A)=1C++標準要求C++的對象大小不能為0,C++對象必須在內存里面有唯一的地址,但又不想浪費太多內存空間,所以標準規定為1byte,
2非空類
classA { public: inta; }; sizeof(A)=8,align=8
3 非空虛基類classA { public: inta; virtualvoidv(); }; sizeof(A)=16,align=8
4 單繼承
classA{ public: inta; virtualvoidv(); }; classB:publicA{ public: intb; }; sizeof(B)=16,align=8
5 簡單多繼承
classA{ public: inta; virtualvoidv(); }; classB{ public: intb; virtualvoidw(); }; classC:publicA,publicB{ public: intc; }; sizeof(C)=32,align=8
6 簡單多繼承-2
classA{ public: inta; virtualvoidv(); }; classB{ public: intb; virtualvoidw(); }; classC:publicA,publicB{ public: intc; voidw(); }; sizeof(C)=32,align=8
7The Diamond:多重繼承 (沒有虛繼承)classA{ public: inta; virtualvoidv(); }; classB:publicA{ public: intb; virtualvoidw(); }; classC:publicA{ public: intc; virtualvoidx(); }; classD:publicB,publicC{ public: intd; virtualvoidy(); }; sizeof(D)=40align=8
注意點:此種繼承存在兩份基類成員,使用時候需要指定路徑,不方便,易出錯。
8The Diamond: 鉆石類虛繼承
解決上面的問題,讓基類只有存在一份,共享基類;
classA{ public: inta; virtualvoidv(); }; classB:publicvirtualA{ public: intb; virtualvoidw(); }; classC:publicvirtualA{ public: intc; virtualvoidx(); }; classD:publicB,publicC{ public: intd; virtualvoidy(); }; sizeof(D)=48,align=8
注意點:1.top_offset表示this指針對子類的偏移,用于子類和繼承類之間dynamic_cast轉換(還需要typeinfo數據),實現多態,vbase_offset 表示this指針對基類的偏移,用于共享基類;2.gcc為了每一個類生成一個vtable虛函數表,放在程序的.rodata段,其他編譯器(平臺)比如vs,實現不太一樣.3.gcc還有VTT表,里面存放了各個基類之間虛函數表的關系,最大化利用基類的虛函數表,專門用來為構建最終類vtable;4.在構造函數里面設置對象的vtptr指針。5.虛函數表地址的前面設置了一個指向type_info的指針,RTTI(Run Time Type Identification)運行時類型識別是有編譯器在編譯器生成的特殊類型信息,包括對象繼承關系,對象本身的描述,RTTI是為多態而生成的信息,所以只有具有虛函數的對象在會生成。6.在C++類中有兩種成員數據:static、nonstatic;三種成員函數:static、nonstatic、virtual。
C++成員非靜態數據需要占用動態內存,棧或者堆中,其他static數據存在全局變量區(數據段),編譯時候確定。虛函數會增加用虛函數表大小,也是存儲在數據區的.rodada段,編譯時確定,其他函數不占空間。
7.G++選項-fdump-class-hierarchy 可以生成C++類層結構,虛函數表結構,VTT表結構。
8.GDB調試選項:
set p obj
set p pertty
思考問題:
1Why don't we havevirtualconstructors?
From Bjarne Stroustrup's C++ Style and Technique FAQ
A virtual call is a mechanism to get work done given partial information. In particular, "virtual" allows us to call a function knowing only any interfaces and not the exact type of the object. To create an object you need complete information. In particular, you need to know the exact type of what you want to create. Consequently, a "call to a constructor" cannot be virtual.
2 為什么不要在構造函數或者析構函數中調用虛函數?
對于構造函數:此時子類的對象還沒有完全構造,編譯器會去虛函數化,只會用當前類的函數, 如果是純虛函數,就會調用到純虛函數,會導致構造函數拋異常:pure virtual method calle;對于析構函數:同樣,由于對象不完整,編譯器會去虛函數化,函數調用本類的虛函數,如果本類虛函數是純虛函數,就會到賬析構函數拋出異常: pure virtual method called;
3 C++對象構造順序?
1.構造子類構造函數的參數
2.子類調用基類構造函數
3.基類設置vptr
4.基類初始化列表內容進行構造
5. 基類函數體調用
6. 子類設置vptr
7. 子類初始化列表內容進行構造
8. 子類構造函數體調用
4 為什么虛函數會降低效率?
是因為虛函數調用執行過程中會跳轉兩次,首先找到虛函數表,然后再查找對應函數地址,這樣CPU指令就會跳轉兩次,而普通函數指跳轉一次,CPU每跳轉一次,預取指令都可能作廢,這會導致分支預測失敗,流水線排空,所以效率會變低。設想一下,如果說不是虛函數,那么在編譯時期,其相對地址是確定的,編譯器可以直接生成jmp/invoke指令;如果是虛函數,多出來的一次查找vtable所帶來的開銷,倒是次要的,關鍵在于,這個函數地址是動態的,譬如 取到的地址在eax里,則在call eax之后的那些已經被預取進入流水線的所有指令都將失效。流水線越長,一次分支預測失敗的代價也就越大。
三 C++程序運行內存空間模型
1. C++程序大致運行內存空間:
32位:
64位:
2 Linux虛擬內存內部實現
關鍵點:
1 各個分區的意義
內核空間:在32位系統中,Linux會留1G空間給內核,用戶進程是無法訪問的,用來存放進程相關數據和內存數據,內核代碼等;在64位系統里面,Linux會采用最低48位來表示虛擬內存,這可通過 /proc/cpuinfo 來查看address sizes :
address sizes : 36 bits physical, 48 bits virtual,總的虛擬地址空間為256TB( 2^48 ),在這256TB的虛擬內存空間中, 0000000000000000 - 00007fffffffffff(128TB)為用戶空間,ffff800000000000 - ffffffffffffffff(128TB)為內核空間。目前常用的分配設計:
Virtualmemorymapwith4levelpagetables: 0000000000000000-00007fffffffffff(=47bits)userspace,differentpermm holecausedby[47:63]signextension ffff800000000000-ffff87ffffffffff(=43bits)guardhole,reservedforhypervisor ffff880000000000-ffffc7ffffffffff(=64TB)directmappingofallphys.memory ffffc80000000000-ffffc8ffffffffff(=40bits)hole ffffc90000000000-ffffe8ffffffffff(=45bits)vmalloc/ioremapspace ffffe90000000000-ffffe9ffffffffff(=40bits)hole ffffea0000000000-ffffeaffffffffff(=40bits)virtualmemorymap(1TB) ...unusedhole... ffffec0000000000-fffffbffffffffff(=44bits)kasanshadowmemory(16TB) ...unusedhole... vaddr_endforKASLR fffffe0000000000-fffffe7fffffffff(=39bits)cpu_entry_areamapping fffffe8000000000-fffffeffffffffff(=39bits)LDTremapforPTI ffffff0000000000-ffffff7fffffffff(=39bits)%espfixupstacks ...unusedhole... ffffffef00000000-fffffffeffffffff(=64GB)EFIregionmappingspace ...unusedhole... ffffffff80000000-ffffffff9fffffff(=512MB)kerneltextmapping,fromphys0 ffffffffa0000000-fffffffffeffffff(1520MB)modulemappingspace [fixmapstart]-ffffffffff5fffffkernel-internalfixmaprange ffffffffff600000-ffffffffff600fff(=4kB)legacyvsyscallABI ffffffffffe00000-ffffffffffffffff(=2MB)unusedholehttp://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
剩下的是用戶內存空間:
stack棧區:專門用來實現函數調用-棧結構的內存塊。相對空間下(可以設置大小,Linux 一般默認是8M,可通過 ulimit –s 查看),系統自動管理,從高地址往低地址,向下生長。
內存映射區:包括文件映射和匿名內存映射, 應用程序的所依賴的動態庫,會在程序執行時候,加載到內存這個區域,一般包括數據(data)和代碼(text);通過mmap系統調用,可以把特定的文件映射到內存中,然后在相應的內存區域中操作字節來訪問文件內容,實現更高效的IO操作;匿名映射,在glibc中malloc分配大內存的時候會用到匿名映射。這里所謂的“大”表示是超過了MMAP_THRESHOLD設置的字節數,它的缺省值是 128 kB,可以通過mallopt()去調整這個設置值。還可以用于進程間通信IPC(共享內存)。
heap堆區:主要用于用戶動態內存分配,空間大,使用靈活,但需要用戶自己管理,通過brk系統調用控制堆的生長,向高地址生長。
BBS段和DATA段:用于存放程序全局數據和靜態數據,一般未初始化的放在BSS段(統一初始化為0,不占程序文件的空間),初始化的放在data段,只讀數據放在rodata段(常量存儲區)。
text段:主要存放程序二進制代碼。
2 為了防止內存被攻擊,比如棧溢出攻擊和堆溢出攻擊等,Linux在特定段之間使用隨機偏移,使段的起始地址是隨機值, Linux 系統上的ASLR 等級可以通過文件 /proc/sys/kernel/randomize_va_space 來進行設置,它支持以下取值:
0 - 關閉的隨機化。一切都是靜止的。
1 - 保守的隨機化。共享庫、棧、mmap()、VDSO以及堆將被隨機化。
2 - 完全的隨機化。除了上面列舉的要素外,通過 brk() 分配得到的內存空間也將被隨機化。
3 每個段都有特定的安全控制(權限):
vm_flags | 第三列,如r-xp | 此段虛擬地址空間的屬性。每種屬性用一個字段表示,r表示可讀,w表示可寫,x表示可執行,p和s共用一個字段,互斥關系,p表示私有段,s表示共享段,如果沒有相應權限,則用’-’代替 |
4 Linux虛擬內存是按頁分配,每頁大小為4KB或者2M,1G等(大頁內存), 默認是4K;
5 例子-通過pmap 查看程序內存布局(綜合proc/x/maps與proc/x/smaps數據):
#include
關閉內存地址隨機化
pmap-X8117 8117:./main AddressPermOffsetDeviceInodeSizeRssPssReferencedAnonymousSwapLockedMapping 00400000r-xp0000000008:11430142354444000main 00601000r--p0000100008:11430142354444400main 00602000rw-p0000200008:11430142354444400main //程序的text段,只讀數據段,和全局/靜態數據段; 00603000rw-p0000000000:000136888800[heap] //程序的堆內存段; 7ffff71e2000r-xp0000000008:1126640188881888000libgcc_s.so.1 7ffff71f8000---p0001600008:112664012044000000libgcc_s.so.1 7ffff73f7000rw-p0001500008:112664014444400libgcc_s.so.1 7ffff73f8000r-xp0000000008:1126643110522243224000libm-2.21.so 7ffff74ff000---p0010700008:112664312044000000libm-2.21.so 7ffff76fe000r--p0010600008:112664314444400libm-2.21.so 7ffff76ff000rw-p0010700008:112664314444400libm-2.21.so 7ffff7700000r-xp0000000008:112663721792115281152000libc-2.21.so 7ffff78c0000---p001c000008:112663722048000000libc-2.21.so 7ffff7ac0000r--p001c000008:11266372161616161600libc-2.21.so 7ffff7ac4000rw-p001c400008:112663728888800libc-2.21.so 7ffff7ac6000rw-p0000000000:000161212121200 7ffff7aca000r-xp0000000008:1146146360960856283856000libstdc++.so.6.0.20 7ffff7bba000---p000f000008:11461463602048000000libstdc++.so.6.0.20 7ffff7dba000r--p000f000008:1146146360323232323200libstdc++.so.6.0.20 7ffff7dc2000rw-p000f800008:11461463608888800libstdc++.so.6.0.20 7ffff7dc4000rw-p0000000000:000841616161600 7ffff7dd9000r-xp0000000008:112663441441441144000ld-2.21.so //程序的內存映射區,主要是動態庫加載到該內存區,包括動態庫的text代碼段和數據data段。 //中間沒有名字的,屬于程序的匿名映射段,主要提供大內存分配。 7ffff7fd4000rw-p0000000000:000202020202000 7ffff7ff5000rw-p0000000000:000121212121200 7ffff7ff8000r--p0000000000:0008000000[vvar] 7ffff7ffa000r-xp0000000000:0008404000[vdso] //vvar page,kernel的一些系統調用的數據會映射到這個頁面,用戶可以直接在用戶空間訪問; //vDSO -virtual dynamic shared object,is a small shared library exported by the kernel to accelerate the execution of certain system calls that do not necessarily have to run in kernel space, 就是內核實現了glibc的一些系統調用,然后可以直接在用戶空間執行,提高系統調用效率和減少與glibc的耦合。 7ffff7ffc000r--p0002300008:112663444444400ld-2.21.so 7ffff7ffd000rw-p0002400008:112663444444400ld-2.21.so 7ffff7ffe000rw-p0000000000:0004444400 7ffffffde000rw-p0000000000:000136888800[stack] //此段為程序的棧區 ffffffffff600000r-xp0000000000:0004000000[vsyscall] //此段是Linux實現vsyscall系統調用vsyscall庫代碼段 ========================================= 127442644489264417200KB
思考問題:
1 棧為什么要由高地址向低地址擴展,堆為什么由低地址向高地址擴展?
歷史原因:在沒有MMU的時代,為了最大的利用內存空間,堆和棧被設計為從兩端相向生長。那么哪一個向上,哪一個向下呢?人們對數據訪問是習慣于向上的,比如你在堆中new一個數組,是習慣于把低元素放到低地址,把高位放到高地址,所以堆 向上生長比較符合習慣, 而棧則對方向不敏感,一般對棧的操作只有PUSH和pop,無所謂向上向下,所以就把堆放在了低端,把棧放在了高端. 但現在已經習慣這樣了。這個和處理器設計有關系,目前大多數主流處理器都是這樣設計,但ARM 同時支持這兩種增長方式。
2 如何查看進程虛擬地址空間的使用情況?
3 對比堆和棧優缺點?
四 C++棧內存空間模型
C++程序運行調用棧示意圖:
函數調用過程中,棧(有俗稱堆棧)的變化:
fromhttps://zhuanlan.zhihu.com/p/25816426
當主函數調用子函數的時候:
在主函數中,將子函數的參數按照一定調用約定(參考調用約定),一般是從右向左把參數push到棧中;
然后把下一條指令地址,即返回地址(return address)push入棧(隱藏在call指令中);
然后跳轉到子函數地址處執行:call 子函數;此時
2. 子函數執行:
push %rbp : 把當前rbp的值保持在棧中;
mov %rsp, %rbp:把rbp移到最新棧頂位置,即開啟子函數的新幀;
[可選]sub $xxx, %esp:在棧上分配XXX字節的臨時空間。(抬高棧頂)(編譯器根據函數中的局部變量的總大小確定臨時空間的大小);
[可選]push XXX: 保存(push)一些寄存器的值;
3. 子函數調用返回:
保持返回值:一般將函數函數值保持在eax寄存器中;
[可選]恢復(pop)一些寄存器的值;
mov %rbp,%rsp: 收回棧空間,恢復主函數的棧頂;
pop %rbp;恢復主函數的棧底;
在AT&T中:
以上兩條指令可以被leave指令取代
leave
ret;從棧頂獲取之前保持的返回地址(return address),并跳轉到此位置執行;
棧攻擊
由上面棧內存布局可以看出,棧很容易被破壞和攻擊,通過棧緩沖器溢出攻擊,用攻擊代碼首地址來替換函數幀的返回地址,當子函數返回時,便跳轉到攻擊代碼處執行,獲取系統的控制權,所以操作系統和編譯器采用了一些常用的防攻擊的方法:
ASLR(地址空間布局隨機化):操作系統可以將函數調用棧的起始地址設為隨機化(這種技術被稱為內存布局隨機化,即Address Space Layout Randomization (ASLR) ),加大了查找函數地址及返回地址的難度。
Cannary
gcc關于棧溢出檢測的幾個參數:
開啟Canary之后,函數開始時在ebp和臨時變量之間插入一個隨機值,函數結束時驗證這個值。如果不相等(也就是這個值被其他值覆蓋了),就會調用 _stackchk_fail函數,終止進程。對應GCC編譯選項-fno-stack-protector解除該保護。
NX.
棧異常處理
一個函數(或方法)拋出異常,那么它首先將當前棧上的變量全部清空(unwinding),如果變量是類對象的話,將調用其析構函數,接著,異常來到call stack的上一層,做相同操作,直到遇到catch語句。
指針是一個普通的變量,不是類對象,所以在清空call stack時,指針指向資源的析構函數將不會調用。
思考問題:
1 遞歸調用函數怎么從20層直接返回到17層,程序可以正常運行?
參考上面棧幀的結構,中心思想是當遞歸函數執行到第20層的時候,把當前棧幀的rbp值替換為17層的rbp的值, 怎么得到17層rbp的值, 就是通過反復取rbp的值(rbp保持了上一幀的rbp),
核心代碼如下:
/*changestack*/ intret_stack(intlayer) { unsignedlongrbp=0; unsignedlonglayer_rbp=0; intdepth=0; /*1.得到首層函數的棧基址*/ __asm__volatile( "movq%%rbp,%0 " :"=r"(rbp) : :"memory"); layer_rbp=rbp; cout<
2調用約定有哪些?
我們最常用是以下幾種約定
1. cdec
?是c/c++默認的調用約定
它是微軟Win32 API的一準標準,我們常用的回調函數就是通過這種調用方式
3.thiscall
thiscall 是c++中非靜態類成員函數的默認調用約定
五 C++堆內存空間模型
1. C++ 程序動態申請內存new/delete:
new/delete 操作符,C++內置操作符
1. new操作符做兩件事,分配內存+調用構造函數初始化。你不能改變它的行為;
2. delete操作符同樣做兩件事,調用析構函數+釋放內存。你不能改變它的行為;
operator new/delete 函數
operator new :
The defaultallocation and deallocation functionsare special components of the standard library; They have the following unique properties:
Global:All three versions ofoperator neware declared in the global namespace, not within thestdnamespace.
Implicit:The allocating versions ((1)and(2)) areimplicitly declaredin every translation unit of a C++ program, no matter whether header
Replaceable: The allocating versions ((1)and(2)) are alsoreplaceable: A program may provide its own definition that replaces the one provided by default to produce the result described above, or can overload it for specific types.
Ifset_new_handlerhas been used to define anew_handlerfunction, thisnew-handlerfunction is called by the default definitions of the allocating versions ((1)and(2)) if they fail to allocate the requested storage.
fromhttp://www.cplusplus.com/reference/new/operator%20new/
1.是用來專門分配內存的函數,為new操作符調用,你能增加額外的參數重載函數operator new(有限制):
限制1:第一個參數類型必須是size_t;
限制2:函數必須返回void*;
2.operator new 底層一般調用malloc函數(gcc+glibc)分配內存;
3.operator new 分配失敗會拋異常(默認),通過傳遞參數也可以不拋異常,返回空指針;
operator delete :
1.是用來專門分配內存的函數,為delete操作符調用,你能增加額外的參數重載函數operator delete(有限制):
限制1:第一個參數類型必須是void*;
限制2:函數必須返回void;
2.operator delete底層一般調用free函數(gcc+glibc)釋放內存;
3.operator delete分配失敗會拋異常(默認),通過傳遞參數也可以不拋異常,返回空指針;
placement new/delete 函數
1. placement new 其實就是new的一種重載,placement new是一種特殊的operator new,作用于一塊已分配但未處理或未初始化的raw內存,就是用一塊已經分配好的內存上重建對象(調用構造函數);
2. 它是C++庫標準的一部分;
3. placement delete 什么都不做;
4.數組分配 new[]/delete[] 表達式
對應會調用operator new[]/delete[]函數;
按對象的個數,分別調用構造函數和析構函數;
http://www.cplusplus.com/reference/new/operator%20new[]/
class-specific allocation functions(成員函數)
定制對象特殊new/delete函數;
實現一般是使用全局:
::operatornew
::operatordelete
關鍵點:
你想在堆上建立一個對象,應該用new操作符。它既分配內存又為對象調用構造函數。
如果你僅僅想分配內存,就應該調用operator new函數;它不會調用構造函數。
如果你想定制自己的在堆對象被建立時的內存分配過程,你應該寫你自己的operator new函數,然后使用new操作符,new操作符會調用你定制的operator new。
如果你想在一塊已經獲得指針的內存里建立一個對象,應該用placement new。
C++可以為分配失敗設置自己的異常處理函數:
If set_new_handler has been used to define a new_handlerfunction, thisnew-handlerfunction is called by the default definitions of the allocating versions ((1)and(2)) if they fail to allocate the requested storage.
如果在構造函數時候拋出異常,new表達式后面會調用對應operator delete函數釋放內存:
The other signatures ((2)and(3)) arenevercalled by adelete-expression(thedeleteoperator always calls the ordinary version of this function, and exactly once for each of its arguments). These other signatures are only called automatically by anew-expressionwhen their object construction fails (e.g., if the constructor of an object throws while being constructed by anew-expressionwithnothrow, the matchingoperator deletefunction accepting anothrowargument is called).
思考問題:
1 malloc和free是怎么實現的?
2 malloc 分配多大的內存,就占用多大的物理內存空間嗎?
3 free 的內存真的釋放了嗎(還給 OS ) ?
4 既然堆內內存不能直接釋放,為什么不全部使用 mmap 來分配?
5 如何查看堆內內存的碎片情況?
6 除了 glibc 的 malloc/free ,還有其他第三方實現嗎?
2. C++11的智能指針與垃圾回收
C++智能指針出現是為了解決由于支持動態內存分配而導致的一些C++內存問題,比如內存泄漏,對象生命周期的管理,懸掛指針(dangling pointer)/空指針等問題;
C++智能指針通過RAII設計模式去管理對象生命周期(動態內存管理),提供帶少量異常類似普通指針的操作接口,在對象構造的時候分配內存,在對象作用域之外釋放內存,幫助程序員管理動態內存;
老的智能指針auto_ptr由于設計語義不好而導致很多不合理問題:不支持復制(拷貝構造函數)和賦值(operator =),但復制或賦值的時候不會提示出錯。因為不能被復制,所以不能被放入容器中。而被C++11棄用(deprecated);
新的智能指針:
1. shared_ptr
shared_ptr是引用計數型(reference counting)智能指針, shared_ptr包含兩個成員,一個是指向真正數據的指針,另一個是引用計數ref_count模塊指針,對比GCC實現,大致原理如下,
共享對象(數據)(賦值拷貝),引用計數加1,指針消亡,引用計數減1,當引用計數為0,自動析構所指的對象,引用計數是線程安全的(原子操作)。
shared_ptr關鍵點:
用shared_ptr就不要new,保證內存管理的一致性;
使用weak_ptr來打破循環引用;
用make_shared來生成shared_ptr,提高效率,內存分配一次搞定,防止異常導致內存泄漏,參考https://herbsutter.com/gotw/_102/;
大量的shared_ptr會導致程序性能下降(相對其他指針),需要等到所有的weak引用為0時才能最終釋放內存(delete);
用enable_shared_from_this來使一個類能獲取自身的shared_ptr;
不能在對象的構造函數中使用shared_from_this()函數,因為對象還沒有構造完畢,share_ptr還沒有初始化構造完全;構造順序:先需要調用enable_shared_from_this類的構造函數,接著調用對象的構造函數,最后需要調用shared_ptr類的構造函數初始化enable_shared_from_this的成員變量weak_this_。然后才能使用shared_from_this()函數;
2. unique_ptr
獨占指針,不共享,不能賦值拷貝;
unique_ptr關鍵點:
1. 如果對象不需要共享,一般最好都用unique_ptr,性能好,更安全;
2. 可以通過move語義傳遞對象的生命周期控制權;
3. 函數可以返回unique_ptr對象,為什么?
RVO和NRVO
當函數返回一個對象時,理論上會產生臨時變量,那必然是會導致新對象的構造和舊對象的析構,這對效率是有影響的。C++編譯針對這種情況允許進行優化,哪怕是構造函數有副作用,這叫做返回值優化(RVO),返回有名字的對象叫做具名返回值優化(NRVO),就那RVO來說吧,本來是在返回時要生成臨時對象的,現在構造返回對象時直接在接受返回對象的空間中構造了。假設不進行返回值優化,那么上面返回unique_ptr會不會有問題呢?也不會。因為標準允許編譯器這么做:
1.如果支持move構造,那么調用move構造。
2.如果不支持move,那就調用copy構造。
3.如果不支持copy,那就報錯吧。
顯然的,unique_ptr是支持move構造的,unique_ptr對象可以被函數返回。
3. weak_ptr
引用對象,不增加引用計數,對象生命周期,無法干預;
配合shared_ptr解決shared_ptr循環引用問題;
可以影響到對象內存最終釋放的時間;
更詳細參考:
http://en.cppreference.com/w/cpp/memory/shared_ptr
思考問題:
1 C++的賦值和Java的有什么區別?
C++的賦值可以是對象拷貝也可以對象引用,java的賦值是對象引用;
2 smart_ptr有哪些坑可以仍然導致內存泄漏?
2.1.shared_ptr初始化構造函數指針,一般是可以動態管理的內存地址,如果不是就可能導致內存泄漏;
2.2.shared_ptr要求內部new和delete實現必須是成對,一致性,如果不是就可能導致內存泄漏;
2.3. shared_ptr對象和其他大多數STL容器一樣,本身不是線程安全的,需要用戶去保證;
3 unique_ptr有哪些限制?
只能移動賦值轉移數據,不能拷貝;
不支持類型轉換(cast);
4 智能指針是異常安全的嗎?
所謂異常安全是指,當異常拋出時,帶有異常安全的函數會:
不泄露任何資源
不允許數據被破壞
智能指針就是采用RAII技術,即以對象管理資源來防止資源泄漏。
Exception Safety
Several functions in these smart pointer classes are specified as having "no effect" or "no effect except such-and-such" if an exception is thrown. This means that when an exception is thrown by an object of one of these classes, the entire program state remains the same as it was prior to the function call which resulted in the exception being thrown. This amounts to a guarantee that there are no detectable side effects. Other functions never throw exceptions. The only exception ever thrown by functions which do throw (assumingTmeets the common requirements) isstd::bad_alloc, and that is thrown only by functions which are explicitly documented as possibly throwingstd::bad_alloc.
https://www.boost.org/doc/libs/1_61_0/libs/smart_ptr/smart_ptr.htm
5 智能指針是線程安全的嗎?
智能指針對象的引用計數模塊是線程安全的,因為 shared_ptr 有兩個數據成員,讀寫操作不能原子化,所以對象本身不是線程安全的,需要用戶去保證線程安全。
Thread Safety
shared_ptrobjects offer the same level of thread safety as built-in types. Ashared_ptrinstance can be "read" (accessed using only const operations) simultaneously by multiple threads. Differentshared_ptrinstances can be "written to" (accessed using mutable operations such asoperator=orreset) simultaneously by multiple threads (even when these instances are copies, and share the same reference count underneath.)
Any other simultaneous accesses result in undefined behavior.
https://www.boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_thread_safety
C++標準垃圾回收
C++11 提供最小垃圾支持
declare_reachable undeclare_reachable declare_no_pointers undeclare_no_pointers pointer_safety get_pointer_safety
由于很多場景受限,當前幾乎沒有人使用;
感興趣可以參考:
http://www.stroustrup.com/C++11FAQ.html#gc-abi
http://www.openstd.org/jtc1/sc22/wg21/docs/papers/2008/n2585.pdf
思考問題:
1 C++可以通過哪些技術來支持“垃圾回收”?
smart_ptr,RAII, move語義等;
2 RAII是指什么?
RAII是指ResourceAcquisitionIsInitialization的設計模式,
RAII要求,資源的有效期與持有資源的對象的生命期嚴格綁定,即由對象的構造函數完成資源的分配(獲取),同時由析構函數完成資源的釋放。在這種要求下,只要對象能正確地析構,就不會出現資源泄露問題。)
當一個函數需要通過多個局部變量來管理資源時,RAII就顯得非常好用。因為只有被構造成功(構造函數沒有拋出異常)的對象才會在返回時調用析構函數,同時析構函數的調用順序恰好是它們構造順序的反序,這樣既可以保證多個資源(對象)的正確釋放,又能滿足多個資源之間的依賴關系。
由于RAII可以極大地簡化資源管理,并有效地保證程序的正確和代碼的簡潔,所以通常會強烈建議在C++中使用它。
fromhttps://zh.wikipedia.org/wiki/RAII
3. C++ STL 內存模型
STL(C++標準模板庫)引入的一個Allocator概念。整個STL所有組件的內存均從allocator分配。也就是說,STL并不推薦使用 new/delete 進行內存管理,而是推薦使用allocator。
SGI STL allocator總體設計:
對象的構造和析構采用placement new函數:
內存配置:
分配算法:
思考問題:
1. vector內存設計和array的區別和適用的場景?
2. 遍歷map與遍歷vector哪個更快,為什么?
3. STL的map和unordered_map內存設計各有什么不同?
六 C++內存問題及常用的解決方法
1. 內存管理功能問題
由于C++語言對內存有主動控制權,內存使用靈活和效率高,但代價是不小心使用就會導致以下內存錯誤:
? memory overrun:寫內存越界
常用的解決內存錯誤的方法
代碼靜態檢測
靜態代碼檢測是指無需運行被測代碼,通過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證明,在整個軟件開發生命周期中,30%至70%的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。在C++項目開發過程中,因為其為編譯執行語言,語言規則要求較高,開發團隊往往要花費大量的時間和精力發現并修改代碼缺陷。所以C++靜態代碼分析工具能夠幫助開發人員快速、有效的定位代碼缺陷并及時糾正這些問題,從而極大地提高軟件可靠性并節省開發成本。
靜態代碼分析工具的優勢:
1、自動執行靜態代碼分析,快速定位代碼隱藏錯誤和缺陷。
2、幫助代碼設計人員更專注于分析和解決代碼設計缺陷。
3、減少在代碼人工檢查上花費的時間,提高軟件可靠性并節省開發成本。
一些主流的靜態代碼檢測工具,免費的cppcheck,clang static analyzer;
商用的coverity,pclint等
各個工具性能對比:
http://www.51testing.com/html/19/n-3709719.html
代碼動態檢測
所謂的代碼動態檢測,就是需要再程序運行情況下,通過插入特殊指令,進行動態檢測和收集運行數據信息,然后分析給出報告。
1.為了檢測內存非法使用,需要hook內存分配和操作函數。hook的方法可以是用C-preprocessor,也可以是在鏈接庫中直接定義(因為Glibc中的malloc/free等函數都是weak symbol),或是用LD_PRELOAD。另外,通過hook strcpy(),memmove()等函數可以檢測它們是否引起buffer overflow。
工具總結對比,常用valgrind(檢測內存泄露),gperftools(統計內存消耗)等:
BI: dynamic binary instrumentation
https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools
2. C++內存管理效率問題
內存管理可以分為三個層次
自底向上分別是:
第一層:操作系統內核的內存管理-虛擬內存管理
第二層:glibc層維護的內存管理算法
第三層:應用程序從glibc動態分配內存后,根據應用程序本身的程序特性進行優化, 比如SGI STL allocator,使用引用計數std::shared_ptr,RAII,實現應用的內存池等等。
當然應用程序也可以直接使用系統調用從內核分配內存,自己根據程序特性來維護內存,但是會大大增加開發成本。
2. C++內存管理問題
頻繁的new/delete勢必會造成內存碎片化,使內存再分配和回收的效率下降;
new/delete分配內存在linux下默認是通過調用glibc的api-malloc/free來實現的,而這些api是通過調用到linux的系統調用:
brk()/sbrk()//通過移動Heap堆頂指針brk,達到增加內存目的 mmap()/munmap()//通過文件影射的方式,把文件映射到mmap區
分配內存
分配內存 >DEFAULT_MMAP_THRESHOLD,走mmap,直接調用mmap系統調用
其中,DEFAULT_MMAP_THRESHOLD默認為128k,可通過mallopt進行設置。
sbrk/brk系統調用的實現:分配內存是通過調節堆頂的位置來實現, 堆頂的位置是通過函數 brk 和 sbrk 進行動態調整,參考例子:
(1) 初始狀態:如圖 (1) 所示,系統已分配 ABCD 四塊內存,其中 ABD 在堆內分配, C 使用 mmap 分配。為簡單起見,圖中忽略了如共享庫等文件映射區域的地址空間。
(2) E=malloc(100k) :分配 100k 內存,小于 128k ,從堆內分配,堆內剩余空間不足,擴展堆頂 (brk) 指針。
(3) free(A) :釋放 A 的內存,在 glibc 中,僅僅是標記為可用,形成一個內存空洞 ( 碎片 ),并沒有真正釋放。如果此時需要分配 40k 以內的空間,可重用此空間,剩余空間形成新的小碎片。
(4) free(C) :C 空間大于 128K ,使用 mmap 分配,如果釋放 C ,會調用 munmap 系統調用來釋放,并會真正釋放該空間,還給 OS ,如圖 (4) 所示。
所以free的內存不一定真正的歸還給OS,隨著系統頻繁地 malloc 和 free ,尤其對于小塊內存,堆內將產生越來越多不可用的碎片,導致“內存泄露”。而這種“泄露”現象使用 valgrind 是無法檢測出來的。
綜上,頻繁內存分配釋放還會導致大量系統調用開銷,影響效率,降低整體性能;
3. 常用解決上述問題的方案
內存池技術
內存池方案通常一次從系統申請一大塊內存塊,然后基于在這塊內存塊可以進行不同內存策略實現,可以比較好得解決上面提到的問題,一般采用內存池有以下好處:
1.少量系統申請次數,非常少(幾沒有) 堆碎片。
6.減少額外系統內存管理開銷,可以節約內存;
內存管理方案實現的指標:
額外的空間損耗盡量少
分配速度盡可能快
盡量避免內存碎片
多線程性能好
緩存本地化友好
通用性,兼容性,可移植性,易調試等
各個內存分配器的實現都是在以上的各種指標中進行權衡選擇.
4. 一些業界主流的內存管理方案
SGI STL allocator
是比較優秀的 C++庫內存分配器(細節參考上面描述)
ptmalloc
是glibc的內存分配管理模塊, 主要核心技術點:
Arena-main /thread;支持多線程
Heap segments;for thread arena via by mmap call ;提高管理
chunk/Top chunk/Last Remainder chunk;提高內存分配的局部性
bins/fast bin/unsorted bin/small bin/large bin;提高分配效率
ptmalloc的缺陷
后分配的內存先釋放,因為 ptmalloc 收縮內存是從 top chunk 開始,如果與 top chunk 相鄰的 chunk 不能釋放, top chunk 以下的 chunk 都無法釋放。
多線程鎖開銷大, 需要避免多線程頻繁分配釋放。
內存從thread的areana中分配, 內存不能從一個arena移動到另一個arena, 就是說如果多線程使用內存不均衡,容易導致內存的浪費。比如說線程1使用了300M內存,完成任務后glibc沒有釋放給操作系統,線程2開始創建了一個新的arena, 但是線程1的300M卻不能用了。
每個chunk至少8字節的開銷很大
不定期分配長生命周期的內存容易造成內存碎片,不利于回收。64位系統最好分配32M以上內存,這是使用mmap的閾值。
tcmalloc
google的gperftools內存分配管理模塊, 主要核心技術點:
thread-localcache/periodic garbagecollections/CentralFreeList;提高多線程性能,提高cache利用率
TCMalloc給每個線程分配了一個線程局部緩存。小分配可以直接由線程局部緩存來滿足。需要的話,會將對象從中央數據結構移動到線程局部緩存中,同時定期的垃圾收集將用于把內存從線程局部緩存遷移回中央數據結構中:
2. Thread Specific Free List/size-classes [8,16,32,…32k]: 更好小對象內存分配;
每個小對象的大小都會被映射到170個可分配的尺寸類別中的一個。例如,在分配961到1024字節時,都會歸整為1024字節。尺寸類別這樣隔開:較小的尺寸相差8字節,較大的尺寸相差16字節,再大一點的尺寸差32字節,如此類推。最大的間隔(對于尺寸 >= ~2K的)是256字節。一個線程緩存對每個尺寸類都包含了一個自由對象的單向鏈表
3. The central page heap:更好的大對象內存分配,一個大對象的尺寸(> 32K)會被除以一個頁面尺寸(4K)并取整(大于結果的最小整數),同時是由中央頁面堆來處理 的。中央頁面堆又是一個自由列表的陣列。對于i < 256而言,第k個條目是一個由k個頁面組成的自由列表。第256個條目則是一個包含了長度>= 256個頁面的自由列表:
4. Spans:
TCMalloc管理的堆由一系列頁面組成。連續的頁面由一個“跨度”(Span)對象來表示。一個跨度可以是已被分配或者是自由的。如果是自由的,跨度則會是一個頁面堆鏈表中的一個條目。如果已被分配,它會是一個已經被傳遞給應用程序的大對象,或者是一個已經被分割成一系列小對象的一個頁面。如果是被分割成小對象的,對象的尺寸類別會被記錄在跨度中。
由頁面號索引的中央數組可以用于找到某個頁面所屬的跨度。例如,下面的跨度a占據了2個頁面,跨度b占據了1個頁面,跨度c占據了5個頁面最后跨度d占據了3個頁面。
tcmalloc的改進
ThreadCache會階段性的回收內存到CentralCache里。解決了ptmalloc2中arena之間不能遷移的問題。
Tcmalloc占用更少的額外空間。例如,分配N個8字節對象可能要使用大約8N * 1.01字節的空間。即,多用百分之一的空間。Ptmalloc2使用最少8字節描述一個chunk。
更快。小對象幾乎無鎖, >32KB的對象從CentralCache中分配使用自旋鎖。并且>32KB對象都是頁面對齊分配,多線程的時候應盡量避免頻繁分配,否則也會造成自旋鎖的競爭和頁面對齊造成的浪費。
jemalloc
FreeBSD的提供的內存分配管理模塊, 主要核心技術點:
1.與tcmalloc類似,每個線程同樣在<32KB的時候無鎖使用線程本地cache;
2. Jemalloc在64bits系統上使用下面的size-class分類:
3. small/large對象查找metadata需要常量時間, huge對象通過全局紅黑樹在對數時間內查找
4. 虛擬內存被邏輯上分割成chunks(默認是4MB,1024個4k頁),應用線程通過round-robin算法在第一次malloc的時候分配arena, 每個arena都是相互獨立的,維護自己的chunks, chunk切割pages到small/large對象。free()的內存總是返回到所屬的arena中,而不管是哪個線程調用free().
上圖可以看到每個arena管理的arena chunk結構, 開始的header主要是維護了一個page map(1024個頁面關聯的對象狀態), header下方就是它的頁面空間。Small對象被分到一起, metadata信息存放在起始位置。large chunk相互獨立,它的metadata信息存放在chunk header map中。
5. 通過arena分配的時候需要對arena bin(每個small size-class一個,細粒度)加鎖,或arena本身加鎖。并且線程cache對象也會通過垃圾回收指數退讓算法返回到arena中。
jemalloc的優化
Jmalloc小對象也根據size-class,但是它使用了低地址優先的策略,來降低內存碎片化。
Jemalloc大概需要2%的額外開銷。(tcmalloc 1%, ptmalloc最少8B).
Jemalloc和tcmalloc類似的線程本地緩存,避免鎖的競爭 .
相對未使用的頁面,優先使用dirty page,提升緩存命中。
性能比較
測試環境:2x Intel E5/2.2Ghz with 8 real cores per socket,16 real cores, 開啟hyper-threading, 總共32個vcpu。16個table,每個5M row。OLTP_RO測試包含5個select查詢:select_ranges, select_order_ranges, select_distinct_ranges, select_sum_ranges:
facebook的測試結果:
服務器吞吐量分別用6個malloc實現的對比數據,可以看到tcmalloc和jemalloc最好(tcmalloc這里版本較舊)。
詳細參考:
https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919
總結
可以看出tcmalloc和jemalloc性能接近,比ptmalloc性能要好,在多線程環境使用tcmalloc和jemalloc效果非常明顯。一般支持多核多線程擴展情況下可以使用jemalloc;反之使用tcmalloc可能是更好的選擇。
可以參考:
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/
http://goog-perftools.sourceforge.net/doc/tcmalloc.html
https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919
https://blog.csdn.net/junlon2006/article/details/77854898
思考問題:
1 jemalloc和tcmalloc最佳實踐是什么?
2 內心池的設計有哪些套路?為什么?
七 C++程序內存性能測試
用系統工具抓取性能數據
pmap
通過讀取/proc/$PID/maps 和 smaps 的數據,解析數據,生成進程的虛列內存映像和一些內存統計:
pmap-X-p31931 31931:./bug_tc AddressPermOffsetDeviceInodeSizeRssPssReferencedAnonymousSwapLockedMapping … 7f37e4c36000rw-p0000000000:00013288888088440[heap] 7fffff85c000rw-p0000000000:0007824782078207820782000[stack] … ============================================ 713961654013902165401304800KB
里面可以查看程序堆和棧內存大小區間,程序所占內存大小,主要是關注PSS
以下內存統計名稱解釋:
VSS:Virtual Set Size,虛擬內存耗用內存,包括共享庫的內存;
RSS:Resident Set Size,實際使用物理內存,包括共享庫;
PSS:Proportional Set Size,實際使用的物理內存,共享庫按比例分配;
USS:Unique Set Size,進程獨占的物理內存,不計算共享庫,也可以理解為將進程殺 死能釋放出的內存;
一般VSS >= RSS >= PSS >= USS,一般統計程序的內存占用,PSS是最好的選擇,比較合理。
top
實時顯示內存當前使用情況和各個進程使用內存信息
free
查看系統可用內存和占用情況
/proc/meminfo
查看機器使用內存使用統計和內存硬件基本信息。
vmstat
監控內存變化
詳細請參考man手冊:
http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/
思考問題:
1 各個工具優缺點和使用場景?
2 linux內存統計里面,劃分了哪些統計?
參加答案
2.valgrind massif
堆棧分析器,指示程序中使用了多少堆內存等信息,可以幫助你減少程序內存使用量,因為更小程序更能多占cache,減少分頁,加速程序;對于需要大量內存的程序,可以讓程序能夠減少交換分區使用,加速程序。
valgrind massif 采集完數據生成數據文件,數據文件會顯示每一幀的程序使用的堆內存大小,
The Snapshot Details 顯示更多細節:
更多細節參考:
http://valgrind.org/docs/manual/ms-manual.html
3. gperftools--heapprofile
gperftools工具里面的內存監控器,統計監控程序使用內存的多少,可以查看內存使用熱點,默認是100ms一次采樣。
text模式:% pprof --text test_tc test.prof
Total: 38 samples
第一列代表這個函數調用本身直接使用了多少內存,
第二列表示第一列的百分比,
第三列是從第一行到當前行的所有第二列之和,
第四列表示這個函數調用自己直接使用加上所有子調用使用的內存總和,
第五列是第四列的百分比。
基本上只要知道這些,就能很好的掌握每一時刻程序運行內存使用情況了,并且對比不同時段的不同profile數據,可以分析出內存走向,進而定位熱點和泄漏。
pdf模式:可以把采樣的結果轉換為圖模式,這樣查看更為直觀:
Kcachegrind模式:利用pprof生成callgrind格式的文件即可,KCachegrind的GUI工具,用于分析callgrind:
圖形化地瀏覽源碼和執行次數,并使用各種排序來搜索可優化的東西。
分析不同的圖表,來可視化地觀察什么占據了大多數時間,以及它調用了什么。
查看真實的匯編機器碼輸出,使你能夠看到實際的指令,給你更多的線索。
可視化地顯示源碼中的循環和分支的跳躍方式,便于你更容易地找到優化代碼的方法。
更多細節參考
https://github.com/gperftools/gperftools/blob/master/docs/heapprofile.html
windows 版本:
https://sourceforge.net/projects/precompiledbin/files/latest/download?source=files
思考問題:
責任編輯:lq
開啟NX保護之后,程序的堆棧將會不可執行。對應GCC編譯選項-z execstack解除該保護。
2. stdcall
operator newcan be called explicitly as a regular function, but in C++,newis an operator with a very specific behavior: An expression with thenewoperator, first calls functionoperator new(i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.
http://en.cppreference.com/w/cpp/memory/new/operator_new
? double free:同一塊內存釋放兩次
? use after free:內存釋放后使用
? wild free:釋放內存的參數為非法值
? access uninitialized memory:訪問未初始化內存
? read invalid memory:讀取非法內存,本質上也屬于內存越界
? memory leak:內存泄露
? use after return:caller訪問一個指針,該指針指向callee的棧內內存
? stack overflow:棧溢出
2. 為了檢查內存的非法訪問,需要對程序的內存進行bookkeeping,然后截獲每次訪存操作并檢測是否合法。bookkeeping的方法大同小異,主要思想是用shadow memory來驗證某塊內存的合法性。至于instrumentation的方法各種各樣。有run-time的,比如通過把程序運行在虛擬機中或是通過binary translator來運行;或是compile-time的,在編譯時就在訪存指令時就加入檢查操作。另外也可以通過在分配內存前后加設為不可訪問的guard page,這樣可以利用硬件(MMU)來觸發SIGSEGV,從而提高速度。
3.為了檢測棧的問題,一般在stack上設置canary,即在函數調用時在棧上寫magic number或是隨機值,然后在函數返回時檢查是否被改寫。另外可以通過mprotect()在stack的頂端設置guard page,這樣棧溢出會導致SIGSEGV而不至于破壞數據。
technology
CTI
DBI
DBI
CTI
Library
Library
ARCH
x86, ARM, PPC
x86, ARM, PPC, MIPS, S390X, TILEGX
x86
all(?)
all(?)
all(?)
OS
Linux, OS X, Windows, FreeBSD, Android, iOS Simulator
Linux, OS X, Solaris, Android
Windows, Linux
Linux, Mac(?)
All (1)
Linux, Windows
Slowdown
2x
20x
10x
2x-40x
?
?
Detects:
Heap OOB
yes
yes
yes
yes
some
some
Stack OOB
yes
no
no
some
no
no
Global OOB
yes
no
no
?
no
no
UAF
yes
yes
yes
yes
yes
yes
UAR
yes(seeAddressSanitizerUseAfterReturn)
no
no
no
no
no
UMR
no (see MemorySanitizer)
yes
yes
?
no
no
Leaks
yes(see LeakSanitizer)
yes
yes
?
no
yes
AddressSanitize
Valgrind/Memcheck
Dr. Memory
Mudflap
Guard Page
gperftools
CTI: compile-time instrumentation
UMR: uninitialized memory reads
UAF: use-after-free (aka dangling pointer)
UAR: use-after-return
OOB: out-of-bounds
x86: includes 32- and 64-bit.
mudflapwas removed in GCC 4.9, as it has been superseded by AddressSanitizer.
Guard Page: a family of memory error detectors (Electric fenceorDUMAon Linux, Page Heap on Windows, libgmalloc on OS X)
gperftools: various performance tools/error detectors bundled with TCMalloc.Heap checker(leak detector) is only available on Linux.Debug allocatorprovides both guard pages and canaryonlydetectors.values for more precise detection of OOB writes, so it's better than guard page.
2.由于沒有系統調用等,比通常的內存申請/釋放(比如通過malloc, new等)的方式快。
3.可以檢查應用的任何一塊內存是否在內存池里。
4.寫一個”堆轉儲(Heap-Dump)”到你的硬盤(對事后的調試非常有用)。
5.可以更方便實現某種內存泄漏檢測(memory-leak detection)。
Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
Huge: [4 MiB, 8 MiB, 12 MiB, …]
7 18.4% 18.4% 7 18.4% operator delete[] (inline)
3 7.9% 26.3% 3 7.9% PackedCache::TryGet (inline)
3 7.9% 34.2% 37 97.4% main::{lambda#1}::operator
3 7.9% 42.1% 5 13.2% operator new (inline)
3 7.9% 50.0% 4 10.5% tcmalloc::ReleaseToSpans
2 5.3% 55.3% 2 5.3% SpinLock::SpinLoop
2 5.3% 60.5% 2 5.3% _init
2 5.3% 65.8% 2 5.3% tcmalloc::FetchFromOneSpans
2 5.3% 71.1% 2 5.3% tcmalloc::GetThreadHeap (inline)
2 5.3% 76.3% 2 5.3% tcmalloc::ReleaseToCentralCache (inline)
1 2.6% 78.9% 1 2.6% ProfileData::FlushTable
1 2.6% 81.6% 4 10.5% SpinLock::Lock (inline)
1 2.6% 84.2% 1 2.6% TCMalloc_PageMap2::get (inline)
1 2.6% 86.8% 5 13.2% tcmalloc::ReleaseListToSpans
1 2.6% 89.5% 6 15.8% tcmalloc::RemoveRange
1 2.6% 92.1% 1 2.6% tcmalloc::GetSizeClass (inline)
-
編程語言
+關注
關注
10文章
1938瀏覽量
34594 -
C++
+關注
關注
22文章
2104瀏覽量
73489 -
內存管理
+關注
關注
0文章
168瀏覽量
14125
原文標題:C++內存管理全景指南
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論