編者按:本文順著c++關(guān)鍵字new向下,旨在分析介紹底層各層到底做了什么,為什么這么做。
1.c++用戶層
1.1提供的接口
1.1.1new
l 調(diào)用operator new 從自由存儲區(qū)分配一塊足夠大的內(nèi)存(sizeof(結(jié)構(gòu)))
l 調(diào)用相應(yīng)的構(gòu)造函數(shù)
l 構(gòu)造完成后返回指向該對象的指針
1.1.2delete
l 調(diào)用相應(yīng)的析構(gòu)函數(shù)
l 調(diào)用operator delete將內(nèi)存歸還給自由存儲區(qū)
1.1.3new數(shù)組
l 調(diào)用operator new[] 從自由存儲區(qū)分配一塊足夠大的內(nèi)存(sizeof(結(jié)構(gòu))+用區(qū)分對象數(shù)組指針和對象指針以及對象數(shù)組大小的額外數(shù)據(jù)),注意簡單對象(即不需要構(gòu)造函數(shù)的類型)將不會有額外數(shù)據(jù)的申請。
l 依次在內(nèi)存中調(diào)用相應(yīng)的構(gòu)造函數(shù)
l 構(gòu)造完成后返回指向該對象數(shù)組的起始地址,不包括前面的額外數(shù)據(jù)部分。
1.1.4delete數(shù)組
l 獲取數(shù)組起始地址前面的額外數(shù)據(jù),計算出數(shù)組長度
l 根據(jù)數(shù)據(jù)長度依次調(diào)用相應(yīng)的析構(gòu)函數(shù)
l調(diào)用operator delete將內(nèi)存歸還給自由存儲區(qū)
1.2operator new 的三種形式
形式1.void* operator new (std::size_t size)throw (std::bad_alloc);
形式2.void* operator new (std::size_t size,const std::nothrow_t& nothrow_value) throw();
形式3.void* operator new (std::size_t size,void* ptr) throw();
形式1跟形式2的區(qū)別僅僅是是否拋出異常,當(dāng)分配失敗時,前者會拋出bad_alloc異常,后者返回NULL,不會拋出異常。它們都分配一個固定大小的連續(xù)內(nèi)存。
形式3又被稱為placement new,它多接收一個ptr參數(shù),并且只是簡單地返回該ptr。調(diào)用形式為 A* a=new(ptr)A()。在內(nèi)存池中有廣泛應(yīng)用,ptr即來自自由存儲區(qū),可以是堆、?;蛘哳A(yù)分配的內(nèi)存塊。
上述形式1和形式2都可以被重載,遵循作用域覆蓋原則,即在里向外尋找operator new的重載時,只要找到operator new()函數(shù)就不再向外查找,如果參數(shù)符合則通過,如果參數(shù)不符合則報錯,而不管全局是否還有相匹配的函數(shù)原型。
注意在形式1中,如果new分配異常,將拋出異常導(dǎo)致后續(xù)代碼不能被正常執(zhí)行。即如果在new操作后有解鎖操作,該解鎖操作將不會執(zhí)行導(dǎo)致死鎖。
1.3設(shè)定內(nèi)存分配失敗入口函數(shù)
1.4自由存儲區(qū)和堆的區(qū)別
從技術(shù)上來說,堆是C語言和操作系統(tǒng)的術(shù)語。堆是操作系統(tǒng)所維護的一塊特殊內(nèi)存,它提供了動態(tài)分配的功能,當(dāng)運行程序調(diào)用malloc()時就會從中分配,稍后調(diào)用free可把內(nèi)存交還。而自由存儲是C++中通過new和delete動態(tài)分配和釋放對象的抽象概念,通過new來申請的內(nèi)存區(qū)域可稱為自由存儲區(qū)。基本上,所有的C++編譯器默認使用堆來實現(xiàn)自由存儲,也即是缺省的全局運算符new和delete也許會按照malloc和free的方式來被實現(xiàn),這時藉由new運算符分配的對象,說它在堆上也對,說它在自由存儲區(qū)上也正確。但程序員也可以通過重載操作符,改用其他內(nèi)存來實現(xiàn)自由存儲,例如全局變量做的對象池,這時自由存儲區(qū)就區(qū)別于堆了。
我們只需要記?。憾咽遣僮飨到y(tǒng)維護的一塊內(nèi)存,而自由存儲是C++中通過new與delete動態(tài)分配和釋放對象的抽象概念。堆與自由存儲區(qū)并不等價。這種區(qū)分大概是不同語言背景造成的。
1.5默認內(nèi)存初始值
在vs2008(32bit)的debug模式下,由堆分配的內(nèi)存初始值為0xcdcd,中文“屯”;由棧分配的內(nèi)存初始值為0xcccc,中文“燙”。
1.6重載::operator new的理由
l 定位檢查代碼中內(nèi)存錯誤
l 優(yōu)化內(nèi)存分配性能
l 獲得內(nèi)存使用統(tǒng)計數(shù)據(jù)
1.7重載::operator new的兩種方式
方式1:不改變簽名,替換系統(tǒng)現(xiàn)有版本
void* operator new(size_t size);
void operator delete(void* p);
使用方不需要包含任何特殊的頭文件,也就是說不需要看見這兩個函數(shù)聲明?!靶阅軆?yōu)化”通常用這種方式。
方式2:增加新參數(shù)
// 其返回的指針必須能被普通的 ::operator delete(void*) 釋放
void* operator new(size_t size, const char* file, int line);
Foo* p = new (__FILE, __LINE__) Foo;
也可以用宏替換 new 來節(jié)省打字。此種方式使用方需要看到這兩個函數(shù)聲明,也就是說要主動包含提供的頭文件。“檢測內(nèi)存錯誤”和“統(tǒng)計內(nèi)存使用情況”通常會用這種方式重載。
1.8重載::operator new的困境
1.8.1絕不能在library中重載::operator new
如果以上文提到的方式1來重載全局的::operator new,非常具有侵略性。使用該library的程序被迫使用了被重載的::operator new,并且一旦有另外的library也同樣重載了::operator new,就將會導(dǎo)致鏈接問題。
那么如果采用上文提到的方式2來額外提供一個::operator new 版本呢,那就需要考慮重載后的::operator new 返回的指針能否被系統(tǒng)默認的::operator delete釋放。如果不兼容系統(tǒng)則需要以方式1重載::operator new ,回到了上文提過的問題。如果兼容,那么在新版本的::operator new中能做的事比較有限,比如不能額外申請內(nèi)存記錄統(tǒng)計信息,除非定義一個包含統(tǒng)計信息的基類來作為所有申請對象的父類,但這樣就相當(dāng)于設(shè)定了開發(fā)規(guī)范,稍有不注意可能就會出錯。
1.8.2使用重載帶新參數(shù)的版本會有什么影響
如果使用方式1重載::operator new 使用起來似乎沒有什么問題,但要考慮上節(jié)中提到的鏈接問題。
如果使用方式2來重載::operator new,分成以下兩種場合。
對于以頭文件形式提供的library,可以在所有的cpp實現(xiàn)文件起始部分包含重載::operator new 的頭文件,但這具有侵略性。
對于以頭文件加二進制庫提供的library,實際上帶新參數(shù)的版本并不會被這些庫使用。
1.9單獨為特定類重載成員函數(shù)operator new怎么樣
與全局 ::operator new() 不同,per-class operator new() 和 operator delete () 的影響面要小得多,它只影響本 class 及其派生類。似乎重載 member operator new() 是可行的。但是我并不贊同這種做法。
如果一個類需要重載成員函數(shù)operator new(),說明它用到了特殊的內(nèi)存分配策略,常見的情況是使用了內(nèi)存池或?qū)ο蟪?。寧愿把這一事實明顯地擺出來,而不是改變 new的默認行為。
這可以歸結(jié)為最小驚訝原則:如果我們在代碼里讀到 Node* p = new Node,通常我們會認為它在堆上分配了內(nèi)存,如果 Node 類重載了成員函數(shù)operator new(),那么就需要事先仔細閱讀 node.h 才能發(fā)現(xiàn)其實這行代碼使用了私有的內(nèi)存池。為什么不寫得明確一點呢?如果寫成Node*p = Node::createNode(),那么我們可能能猜到 Node::createNode() 肯定做了什么與 new不一樣的事情,免得將來大吃一驚。
1.10代替重載::operator new的方案
從glibc的malloc入手,替換掉malloc。具體方式參考tcmalloc中的override方式,點此鏈接[1]。
主要使用了gcc提供的alias別名屬性和weak屬性,我們能實現(xiàn)替換掉系統(tǒng)默認的malloc原因在于系統(tǒng)提供的malloc系列函數(shù)都是被weak屬性修飾的。
對于全局函數(shù),如果沒有顯示修飾稱weak屬性,那么他屬于強符號;對于全局變量,已初始化完畢的屬于強符號,沒有初始化完畢的則屬于弱符號。
有如下3點規(guī)則:
l 鏈接時強弱符號都存在時以強符號為準(zhǔn);
l 鏈接時如果只有弱符號時以弱符號為準(zhǔn);
l 鏈接時如兩個都是弱符號,則以內(nèi)存占用大小較大的那個符號為準(zhǔn);
2.glibc層
2.1概述
實際上glibc采用了一種批發(fā)和零售的方式來管理內(nèi)存。glibc每次通過系統(tǒng)調(diào)用的方式申請一大塊內(nèi)存(虛擬內(nèi)存),當(dāng)進程申請內(nèi)存時,glibc就從自己獲得的內(nèi)存中取出一塊給進程。
glibc對于heap內(nèi)存申請大于128k的內(nèi)存申請,glibc采用mmap的方式向內(nèi)核申請內(nèi)存,也就是此時的malloc是由mmap來實現(xiàn)的,這不能保證內(nèi)存地址向上增長;小于128k的則采用brk,malloc調(diào)用系統(tǒng)調(diào)用brk來實現(xiàn)向內(nèi)核批發(fā)虛擬內(nèi)存,對于它來講是正確的。128k的閥值,可以通過glibc的庫函數(shù)進行設(shè)置。
審核編輯:湯梓紅
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1361瀏覽量
40191 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
2981瀏覽量
73824 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4286瀏覽量
62335 -
C++
+關(guān)注
關(guān)注
21文章
2102瀏覽量
73457
原文標(biāo)題:內(nèi)存剖析:從用戶態(tài)到內(nèi)核態(tài)內(nèi)存都做了什么?
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論