我們知道m(xù)alloc() 并不是系統(tǒng)調(diào)用,也不是運算符,而是 C 庫里的函數(shù),用于動態(tài)分配內(nèi)存。
malloc 申請內(nèi)存的時候,會有兩種方式向操作系統(tǒng)申請堆內(nèi)存:
- 方式一:通過 brk() 系統(tǒng)調(diào)用從堆分配內(nèi)存
- 方式二:通過 mmap() 系統(tǒng)調(diào)用在文件映射區(qū)域分配內(nèi)存;
一、brk()系統(tǒng)調(diào)用
1、brk()的申請方式
一般如果用戶分配的內(nèi)存小于 128 KB,則通過 brk() 申請內(nèi)存。而brk()的實現(xiàn)的方式很簡單,就是通過 brk() 函數(shù)將堆頂指針向高地址移動,獲得新的內(nèi)存空間。如下圖:
malloc 通過 brk() 方式申請的內(nèi)存,free 釋放內(nèi)存的時候,并不會把內(nèi)存歸還給操作系統(tǒng),而是緩存在 malloc 的內(nèi)存池中,待下次使用,這樣就可以重復使用。
2、brk()系統(tǒng)調(diào)用的優(yōu)缺點
所以使用brk()方式的點很明顯:可以減少缺頁異常的發(fā)生,提高內(nèi)存訪問效率。
但它的缺點也同樣明顯:由于申請的內(nèi)存沒有歸還系統(tǒng),在內(nèi)存工作繁忙時,頻繁的內(nèi)存分配和釋放會造成內(nèi)存碎片。brk()方式之所以會產(chǎn)生內(nèi)存碎片,是由于brk通過移動堆頂?shù)奈恢脕矸峙鋬?nèi)存,并且使用完不會立即歸還系統(tǒng),重復使用,如果高地址的內(nèi)存不釋放,低地址的內(nèi)存是得不到釋放的。
正是由于使用brk()會出現(xiàn)內(nèi)存碎片,所以在我們申請大塊內(nèi)存的時候才會使用mmap()方式,mmap()是以頁為單位進行內(nèi)存分配和管理的,釋放后就直接歸還系統(tǒng)了,所以不會出現(xiàn)這種小碎片的情況。
3、brk()系統(tǒng)調(diào)用的優(yōu)化
一、Ptmalloc :malloc采用的是內(nèi)存池的管理方式,Ptmalloc 采用邊界標記法將內(nèi)存劃分成很多塊,從而對內(nèi)存的分配與回收進行管理。為了內(nèi)存分配函數(shù)malloc的高效性,ptmalloc會預先向操作系統(tǒng)申請一塊內(nèi)存供用戶使用,當我們申請和釋放內(nèi)存的時候,ptmalloc會將這些內(nèi)存管理起來,并通過一些策略來判斷是否將其回收給操作系統(tǒng)。這樣做的最大好處就是,使用戶申請和釋放內(nèi)存的時候更加高效,避免產(chǎn)生過多的內(nèi)存碎片。
二、Tcmalloc:Ptmalloc在性能上還是存在一些問題的,比如不同分配區(qū)(arena)的內(nèi)存不能交替使用,比如每個內(nèi)存塊分配都要浪費8字節(jié)內(nèi)存等等,所以一般傾向于使用第三方的malloc。
Tcmalloc是Google gperftools里的組件之一。全名是 thread cache malloc(線程緩存分配器)其內(nèi)存管理分為線程內(nèi)存和中央堆兩部分。
1.小塊內(nèi)部的分配:對于小塊內(nèi)存分配,其內(nèi)部維護了60個不同大小的分配器(實際源碼中看到的是86個),和ptmalloc不同的是,它的每個分配器的大小差是不同的,依此按8字節(jié)、16字節(jié)、32字節(jié)等間隔開。在內(nèi)存分配的時候,會找到最小符合條件的,比如833字節(jié)到1024字節(jié)的內(nèi)存分配請求都會分配一個1024大小的內(nèi)存塊。如果這些分配器的剩余內(nèi)存不夠了,會向中央堆申請一些內(nèi)存,打碎以后填入對應分配器中。同樣,如果中央堆也沒內(nèi)存了,就向中央內(nèi)存分配器申請內(nèi)存。
在線程緩存內(nèi)的60個分配器分別維護了一個大小固定的自由空間鏈表,直接由這些鏈表分配內(nèi)存的時候是不加鎖的。但是中央堆是所有線程共享的,在由其分配內(nèi)存的時候會加自旋鎖(spin lock)。
2.大內(nèi)存的分配:對于大內(nèi)存分配(大于8個分頁, 即32K),tcmalloc直接在中央堆里分配。中央堆的內(nèi)存管理是以分頁為單位的,同樣按大小維護了256個空閑空間鏈表,前255個分別是1個分頁、2個分頁到255個分頁的空閑空間,最后一個是更多分頁的小的空間。這里的空間如果不夠用,就會直接從系統(tǒng)申請了。
3.ptmalloc與tcmalloc的不足:都是針對小內(nèi)存分配和管理;對大塊內(nèi)存還是直接用了系統(tǒng)調(diào)用。應該盡量避免大內(nèi)存的malloc/new、free/delete操作。頻繁分配小內(nèi)存,例如:對bool、int、short進行new的時候,造成內(nèi)存浪費。
三、Jemalloc: jemalloc 是由 Jason Evans 在 FreeBSD 項目中引入的新一代內(nèi)存分配器。它是一個通用的malloc實現(xiàn),側(cè)重于減少內(nèi)存碎片和提升高并發(fā)場景下內(nèi)存的分配效率,其目標是能夠替代 malloc。下面是Jemalloc的兩個重要部分:
1.arena:arena 是 jemalloc 最重要的部分,內(nèi)存由一定數(shù)量的 arenas 負責管理。每個用戶線程都會被綁定到一個 arena 上,線程采用 round-robin 輪詢的方式選擇可用的 arena 進行內(nèi)存分配,為了減少線程之間的鎖競爭,默認每個 CPU 會分配 4 個 arena,各個 arena 所管理的內(nèi)存相互獨立。
struct arena_s {
atomic_u_t nthreads[2];
tsdn_t *last_thd;
arena_stats_t stats; // arena的狀態(tài)
ql_head(tcache_t) tcache_ql;
ql_head(cache_bin_array_descriptor_t) cache_bin_array_descriptor_ql;
malloc_mutex_t tcache_ql_mtx;
prof_accum_t prof_accum;
uint64_t prof_accumbytes;
atomic_zu_t offset_state;
atomic_zu_t extent_sn_next; // extent的序列號生成器狀態(tài)
atomic_u_t dss_prec;
atomic_zu_t nactive; // 激活的extents的page數(shù)量
extent_list_t large; // 存放 large extent 的 extents
malloc_mutex_t large_mtx; // large extent的鎖
extents_t extents_dirty; // 剛被釋放后空閑 extent 位于的地方
extents_t extents_muzzy; // extents_dirty 進行 lazy purge 后位于的地方,dirty - > muzzy
extents_t extents_retained; // extents_muzzy 進行 decommit 或 force purge 后 extent 位于的地方,muzzy - > retained
arena_decay_t decay_dirty; // dirty -- > muzzy
arena_decay_t decay_muzzy; // muzzy -- > retained
pszind_t extent_grow_next;
pszind_t retain_grow_limit;
malloc_mutex_t extent_grow_mtx;
extent_tree_t extent_avail; // heap,存放可用的 extent 元數(shù)據(jù)
malloc_mutex_t extent_avail_mtx; // extent_avail的鎖
bin_t bins[NBINS]; // 所有用于分配小內(nèi)存的 bin
base_t *base; // 用于分配元數(shù)據(jù)的 base
nstime_t create_time; // 創(chuàng)建時間
};
2.extent:管理 jemalloc 內(nèi)存塊(即用于用戶分配的內(nèi)存)的結(jié)構(gòu),每一個內(nèi)存塊大小可以是 N * page_size(4KB)(N >= 1)。每個 extent 有一個序列號(serial number)。一個 extent 可以用來分配一次 large_class 的內(nèi)存申請,但可以用來分配多次 small_class 的內(nèi)存申請。
struct extent_s {
uint64_t e_bits; // 8字節(jié)長,記錄多種信息
void *e_addr; // 管理的內(nèi)存塊的起始地址
union {
size_t e_size_esn; // extent和序列號的大小
size_t e_bsize; // 基本extent的大小
};
union {
/*
* S位圖,當此 extent 用于分配 small_class 內(nèi)存時,用來記錄這個 extent 的分配情況,
* 此時每個 extent 的內(nèi)的小內(nèi)存稱為 region
*/
arena_slab_data_t e_slab_data;
atomic_p_t e_prof_tctx; // 一個計數(shù)器,用于large object
};
}
二、mmap()系統(tǒng)調(diào)用
1、mmap基礎概念
mmap 是一種內(nèi)存映射文件的方法,即將一個文件或者其他對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一映射關系。
實現(xiàn)這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間的這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程的文件共享。如下圖所示:
由上圖可以看出,進程的虛擬地址空間,由多個虛擬內(nèi)存區(qū)域構(gòu)成。虛擬內(nèi)存區(qū)域是進程的虛擬地址空間中的一個同質(zhì)區(qū)間,即具有同樣特性的連續(xù)地址范圍。上圖中所示的text數(shù)據(jù)段、初始數(shù)據(jù)段、Bss數(shù)據(jù)段、堆、棧、內(nèi)存映射,都是一個獨立的虛擬內(nèi)存區(qū)域。而為內(nèi)存映射服務的地址空間處在堆棧之間的空余部分。
linux 內(nèi)核使用的vm_area_struct 結(jié)構(gòu)來表示一個獨立的虛擬內(nèi)存區(qū)域,由于每個不同質(zhì)的虛擬內(nèi)存區(qū)域功能和內(nèi)部機制不同;因此同一個進程使用多個vm_area_struct 結(jié)構(gòu)來分別表示不同類型的虛擬內(nèi)存區(qū)域。各個vm_area_struct 結(jié)構(gòu)使用鏈表或者樹形結(jié)構(gòu)鏈接,方便進程快速訪問。如下圖所示:
vm_area_struct 結(jié)構(gòu)中包含區(qū)域起始和終止地址以及其他相關信息,同時也包含一個vm_ops 指針,其內(nèi)部可引出所有針對這個區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)。這樣,進程對某一虛擬內(nèi)存區(qū)域的任何操作都需要的信息,都可以從vm_area_struct 中獲得。mmap函數(shù)就是要創(chuàng)建一個新的vm_area_struct結(jié)構(gòu) ,并將其與文件的物理磁盤地址相連。
2、mmap 內(nèi)存映射原理
mmap 內(nèi)存映射實現(xiàn)過程,總的來說可以分為三個階段:
(一)進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
1、進程在用戶空間調(diào)用函數(shù)mmap ,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在當前進程虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
3、為此虛擬區(qū)分配一個vm_area_struct 結(jié)構(gòu),接著對這個結(jié)構(gòu)各個區(qū)域進行初始化
4、將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進程的虛擬地址區(qū)域鏈表或樹中
(二)調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap (不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關系
5、為映射分配新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文集”中該文件結(jié)構(gòu)體,每個文件結(jié)構(gòu)體維護者和這個已經(jīng)打開文件相關各項信息。
6、通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數(shù)。
7、內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
8、通過remap_pfn_range函數(shù)建立頁表,即實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關系。此時,這片虛擬地址并沒有任何數(shù)據(jù)關聯(lián)到主存中。
(三)進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝。
前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)拷貝至主存。真正的文件讀取是當進程發(fā)起讀或者寫操作時。
9、進程的讀寫操作訪問虛擬地址空間這一段映射地址后,通過查詢頁表,先這一段地址并不在物理頁面。因為目前只建立了映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。
10、缺頁異常進行一系列判斷,確定無法操作后,內(nèi)核發(fā)起請求掉頁過程。
11、調(diào)頁過程先在交換緩存空間中尋找需要訪問的內(nèi)存頁,,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
12、之后進程即可對這片主存進行讀或者寫的操作了,如果寫操作改變了內(nèi)容,一定時間后系統(tǒng)自動回寫臟頁面到對應的磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件,而是有一段時間延遲,可以調(diào)用msync() 來強制同步,這樣所寫的內(nèi)容就能立即保存到文件里了。
3、mmap優(yōu)點
1、對文件的讀取操作跨過了頁緩存,減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代了I/O讀寫,提高了讀取的效率。
2、實現(xiàn)了用戶空間和內(nèi)核空間的高校交互方式,兩空間的各自修改操作可以直接反映在映射的區(qū)域內(nèi),從而被對方空間及時捕捉。
3、提供進程間共享內(nèi)存及互相通信的方式。不管是父子進程還是無親緣關系進程,都可以將自身空間用戶映射到同一個文件或者匿名映射到同一片區(qū)域。從而通過各自映射區(qū)域的改動,打到進程間通信和進程間共享的目的。
同時,如果進程A和進程 B 都映射了區(qū)域C,當A第一次讀取C時候,通過缺頁從磁盤復制文件頁到內(nèi)存中,但當B再讀C的相同頁面時,雖然也會產(chǎn)生缺頁異常,但是不會從磁盤中復制文件過來,而是直接使用已經(jīng)保存再內(nèi)存中的文件數(shù)據(jù)。
4、適用場景
可用于實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸。內(nèi)存空間不足,是制約大數(shù)據(jù)操作的一個方面,解決方案往往是借助于硬盤空間的協(xié)助,補充內(nèi)存的不足。但是進一步造成大量的文件I/O操作,極大影響效率。這個問題可以通過mmap映射很好地解決。換句話說,但凡需要磁盤空間代替內(nèi)存的時候,mmap都可以發(fā)揮功效。
-
內(nèi)存
+關注
關注
8文章
2966瀏覽量
73812 -
操作系統(tǒng)
+關注
關注
37文章
6684瀏覽量
123140 -
函數(shù)
+關注
關注
3文章
4277瀏覽量
62323 -
malloc
+關注
關注
0文章
52瀏覽量
62
發(fā)布評論請先 登錄
相關推薦
評論