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