【Linux】malloc 與共享內存原理區別

本文主要分析內存以及I/O相關的系統調用和庫函數的實現原理,根據原理給出在使用過程中需要注意的問題和優化的側重點,本文涉及到的系統調用包括readahead,pread/pwrite,read/write,mmap,readv/writev,sendfile,fsync/fdatasync/msync,shmget,malloc。

本文先簡單介紹應用程序對內存的使用以及I/O系統對內存的使用的基本原理,這對理解上述系統調用和庫函數的實現有很大幫助。

1 內存管理基礎

Linux對物理內存的管理是以頁爲單位的,通常頁大小爲4KB,Linux在初始化時爲所有物理內存也分配了管理數據結構,管理所有物理頁面。

每一個應用程序有獨立的地址空間,當然這個地址是虛擬的,通過應用程序的頁表可以把虛擬地址轉化爲實際的物理地址進行操作,雖然系統可以實現從虛擬地址到物理地址的轉換,但並非應用程序的每一塊虛擬內存都對應一塊物理內存。Linux使用一種按需分配的策略爲應用程序分配物理內存,這種按需分配是使用缺頁異常實現的。比如一個應用程序動態分配了10MB的內存,這些內存在分配時只是在應用程序的虛擬內存區域管理結構中表示這一區間的地址已經被佔用,內核此時並沒有爲之分配物理內存,而是在應用程序使用(讀寫)該內存區時,發現該內存地址對應得物理內存並不存在,此時產生缺頁異常,促使內核爲當前訪問的虛擬內存頁分配一個物理內存頁。

一個已經分配給應用程序的物理頁,在某些情況下也會被系統回收作爲其他用途,比如對於上述的動態分配的內存,其內容可能被換到交換分區,系統暫時回收物理頁面,當應用程序再次使用這個內存頁時,系統再分配物理頁面,把該頁的內容從交換分區上換回到這個物理頁,再重新建立頁表映射關係。不同類型的虛擬內存頁對應的物理內存的分配回收處理過程是不同的,在分析具體系統調用細節時,我們再做詳細說明。

2 文件系統I/O原理

操作系統I/O部分不僅涉及到對普通塊設備的操作,也涉及到對字符設備和網絡設備的操作,本文只涉及對普通塊設備的描述。

應用程序對文件的操作基本可以通過兩種方式實現:普通的read/write和mmap方式,但這兩種方式都並不是應用程序在讀寫文件內容時直接操作塊設備(有一些特殊的例外情況),而是經過了操作系統層的page cache,即,無論應用程序以哪種方式讀文件數據時,都是由操作系統把這部分文件數據加載到內核層,應用程序再通過不同的方式操作在內存中的文件數據,寫的過程也一樣,應用程序實際上只是把數據寫到文件在內存中所對應的頁上,然後在一定的時機或強行回寫到塊設備上。

我們需要對page cache作一些說明,page cache可以理解爲對所有文件數據的緩衝,在一般情況下,對文件操作都需要通過page cache這個中間層(特殊情況我們下面會描述),page cache並不單單只是一個數據中轉層,在page cache層,內核對數據做了有效的管理,暫時不使用的數據在內存允許的情況下仍然放在page cache中,所有的應用程序共用一個page cache,不同的應用程序對同一塊文件數據的訪問,或者一個應用程序對一塊數據的多次訪問都不需要多次訪問塊設備獲得,這樣就加快了I/O操作的性能。

不同的系統調用對文件數據的訪問區別在於page cache之上對數據的訪問方式的不同,數據在page cache和塊設備之間的操作過程是基本類似的。這種區別主要體現在read/write方式和mmap方式。它們各自得細節我們下面會分別描述。

3 readahead

在描述了page cache的原理和功能之後,readahead就比較容易理解了,當使用系統調用read讀取文件部分數據時,如果數據沒有在page cache中,就需要從塊設備讀取對應數據,對於像磁盤這樣的塊設備,尋道是最耗時的操作,讀一小塊數據和讀一大塊連續數據所花的時間相差不大,但如果這一大塊數據分多次讀取,就需要多次尋道,這樣花費的時間就比較長。

readahead是基於這樣的策略:在需要讀取一塊數據的時候,如果後繼的操作是連續讀,可以在多讀一些數據到page cache中,這樣下次訪問的連續數據的時候,這些數據已經在page cache中了,就無需I/O操作,這樣會大大提高數據訪問的效率。

Linux的readahead分爲自動模式和用戶強制模式,自動預讀是指在read系統調用的時候,如果需要從塊設備傳輸數據,系統會自動根據當前的狀態設置預讀的數據的大小,啓用預讀過程。每次預讀的數據的大小是動態調整的,調整地原則是根據預讀後的命中情況適當擴大或縮小預讀大小。每次預讀的默認大小是可以設置的,而且不同的塊設備可以有不同的默認預讀大小,察看和設置塊設備默認預讀大小都可以通過blockdev命令。

這種自動模式的預讀機制在每次I/O操作前是都會被啓用,所以預讀默認大小的設置對性能有一些影響,如果是大量隨機讀操作,在這種情況下就需要讓預讀值調小, 但並不是越小越好,一般情況下需要估算一下應用程序平均每次read請求讀取的數據量的平均大小,將預讀值設成比平均大小稍大一些比較合適;如果是大量順序讀操作,則預讀值可以調大一點(對於使用RAID的情況下,預讀值的設置還要參考條帶大小和條帶數)。

在自動預讀模式中需要注意的問題還有,如果文件本身有許多很小的碎片,即使是連續讀,而且也設置了較大的預讀值,其效率也不會太高,因爲如果一次被讀取的數據在磁盤中不連續的話,仍然不可避免磁盤尋道,所以預讀起的作用就不大了。

Linux提供一個readahead的系統調用設置對文件進行強制預讀,這個操作是把數據從塊設備加載到page cache中,可以提高之後對文件數據訪問的速度,用戶可以根據自己的需要決定是否使用強制預讀。

4 read/write

read/write是讀寫I/O的基本過程,除了mmap之外,其他I/O讀寫系統調用的基本原理和調用過程都是和read/write一樣的。

read過程:把需要讀取得數據轉換成對應的頁,對需要讀入的每一個頁執行如下過程:首先調用page_cache_readahead(如果預讀打開),根據當前預讀的狀態和執行預讀策略(預讀狀態結構根據命中情況和讀模式動態調整,預讀策略也動態調整),預讀過程會進行I/O操作也可能不會,預讀過程完畢之後,首先檢查page cache中是否已經有所需數據,如果沒有,說明預讀沒有命中,調用handle_ra_miss調整預讀策略,進行I/O操作把該頁數據讀入內存並加入page cache,當該頁數據讀入page cache之後(或者之前就在page cache中),標記該頁mark_page_accessed,然後把該頁數據拷貝到應用程序地址空間。

write過程:和read過程一樣,需要把需要寫的數據轉換成對應頁,從應用程序地址空間把數據拷貝到對應頁,並標記該頁狀態爲dirty,調用 mark_page_accessed  如果沒有指定爲同步寫,寫操作至此就返回了。如果文件在打開時指定了 O_SYNC,系統會把本次寫過程所有涉及到的dirty頁回寫到塊設備中,這個過程是阻塞的。關於dirty頁的同步在分析fsync/fdatasync/msync時我們再具體說明。

特殊情況:如果應用程序在打開文件時指定了O_DIRECT,操作系統在讀寫文件時會完全繞過page cache,讀的時候數據直接從塊設備傳送到應用程序指定的緩存中,寫的時候數據也是直接從應用程序指定的緩存中寫到塊設備中,由於沒有經過page cache層,這種方式的寫總是同步寫。

5 mmap

mmap的用途很廣泛,不僅可以把文件映射到內存地址空間讀寫文件,也可以用mmap實現共享內存,malloc分配內存是也是用了mmap,本節我們先討論使用mmap讀寫文件的實現。

每個進程對虛擬內存都是通過分區域管理的,在虛擬內存分配時,爲不同的用途劃分不同的虛擬內存區域,這些虛擬內存區域在分配之初並沒有爲止分配對應的物理內存,而只是分配和設置了管理結構,當進程使用到某個區域的內存,而其又沒有對應的物理內存時,系統產生缺頁異常,在缺頁異常中,系統根據這塊內存對應的虛擬內存管理結構爲之分配物理內存,在必要情況下(如mmap)加載數據到這塊物理內存,建立虛擬內存到物理內存的對應關係,然後進程可以繼續訪問剛纔的虛擬內存。

mmap的實現也是基於上述原理,在使用mmap映射某個文件(或者文件的一部分)到進程的地址空間時,並沒有加載文件的數據,而只是在進程的虛擬地址空間劃分出一塊區域,標記這塊區域用於映射到文件的數據區域,mmap的操作就完成了。

當進程試圖讀或者寫文件映射區域時,如果沒有對應的物理頁面,系統發生缺頁異常並進入缺頁異常處理程序,缺頁異常處理程序根據該區域內存的類型使用不同的策略解決缺頁。對於使用mmap映射文件的虛擬內存區域,處理程序首先找到相關的文件的管理數據結構,確定所需頁面對應的文件偏移,此時需要從文件中把對應數據加載到page_cache中,與read系統調用流程不同的是,在加載的過程中如果虛擬內存區域管理結構設置了VM_RAND_READ標誌,系統只是把所需的頁面數據加載,如果設置了VM_SEQ_READ標誌,系統會進行和read系統調用相同預讀過程,至此應用程序所需的頁面已經在page cache中了,系統調整頁表把物理頁面對應到應用程序的地址空間。mmap對缺頁的處理沒有讀和寫的區別,無論是讀還是寫造成的缺頁異常都要執行上述過程。

虛擬內存區域管理結構的VM_RAND_READ標誌和VM_SEQ_READ標誌可以使用madvise系統調用調整。

使用mmap讀寫文件需要注意的問題:當讀寫映射的內存區域的物理頁面不存在時,發生缺頁異常時系統才能進入內核態,如果物理頁面存在,應用程序在用戶態直接操作內存,不會進入內核態,大家注意到在調用read/write系統調用時,系統對涉及到的頁面都調用了mark_page_accessed函數,mark_page_accessed可以標記物理頁面的活動狀態,活動的頁面就不容易被回收,而是用mmap讀文件不產生缺頁異常時不能進入內核態,就無法標記頁面的活動狀態,這樣頁面就容易被系統回收(進入缺頁異常處理時也只是對新分配所缺頁面調用了mark_page_accessed)。除此之外,在寫該內存區域時,如果不進入內核態也無法標記所寫的物理頁面爲dirty(只用把頁表項的dirty位置位),這個問題我們會在後面的msync說明中詳細描述。

6 pread/pwrite,readv/writev

這幾個系統調用在內核中的實現和read/write區別不大,只是參數不同而已,在read/write中使用的是文件默認偏移,pread/pwrite在參數種指定文件操作的偏移,這樣在多線程操作中避免了爲讀寫偏移加鎖。readv/writev可以把把文件的內容寫到多個位置,也可以從多個位置向文件中寫數據,這樣就可以避免多次系統調用的開銷。

7 sendfile

sendfile把文件的從某個位置開始的內容送入另一個文件中(可能會是一個套接字),這種操作節省了數據在內存中的拷貝次數,如果使用read/write實現,會增加兩次數據拷貝操作。其內核實現方法和read/write也沒有太大區別。

8 fsync/fdatasync/msync

這三個系統調用都涉及把內存中的dirty page同步到的塊設備上的文件中去,它們之間有一些區別。

fsync把文件在page cache中的dirty page寫回到磁盤中去,一個文件在page cache中的內容包括文件數據也包括inode數據,當寫一個文件時,除了修改文件數據之外,也修改了inode中的數據(比如文件修改時間),所以實際上有這兩部分的數據需要同步,fsync把和指定文件相關的這兩種dirty page回寫到磁盤中。除了使用fsync強行同步文件之外,系統也會定期自動同步,即把dirty page回寫到磁盤中。

Fdatasync只回寫文件數據的dirty page到磁盤中,不回寫文件inode相關的dirty page。

msync與fsync有所不同,在使用mmap映射文件到內存地址,向映射地址寫入數據時如果沒有缺頁,就不會進入內核層,也無法設置寫入頁的狀態爲dirty,但cpu會自動把頁表的dirty位置位,如果不設置頁爲dirty,其他的同步程序,如fsync以及內核的同步線程都無法同步這部分數據。msync的主要作用就是檢查一個內存區域的頁表,把dirty位置位的頁表項對應的頁的狀態設置爲dirty,如果msync指定了M_SYNC參數,msync還會和fsync一樣同步數據,如果指定爲M_ASYNC,則用內核同步線程或其他調用同步數據。

在munmap時,系統會對映射的區域執行類似msync的操作,所以如果不調用msync數據也不一定會丟失(進程在退出時對映射區域也會自動調用munmap),但寫大量數據不調用msync會有丟失數據的風險。

9 shmget/shmat

實際上無論是posix還是system v接口的共享內存,都是使用mmap來實現的,其原理也是一樣的。把一個文件(可以是特殊文件或普通文件)映射到不同進程的地址空間,從上面描述的mmap的原理可以得知,一個文件在內核page cache中只有一份,不同的進程操作對同一個文件區域的映射,實際上就實現了對內存的共享。

基於上面的原理,posix接口的共享內存就很容易理解了,system v接口的共享內存看起來沒有那麼直觀,實際上一樣,只不過它使用了特殊文件系統shmfs來做內存映射實現內存共享,shmfs實現了一個特殊的功能,使用普通文件進行文件共享時,當系統需要回收物理頁面時,是把dirty頁回寫到磁盤上,然後回收該頁,但如果沒有調用msync,系統就無法知道該頁是dirty頁,在頁面回收時就會直接拋棄掉該頁的內容(因爲它認爲磁盤上還有)。這樣就導致數據不一致,而shmfs的實現很特殊,它所有的頁永遠都是髒頁,它的回寫函數不是把數據回寫到普通文件中,而是在交換分區(如果有的話)分配一塊空間,把物理頁內容寫到交換分區上並標記它。shmfs避免了mmap在使用過程中可能出現的風險,而且對用戶是透明的,它專爲內存共享設計。

shmget的作用就是想系統申請一定大小的共享內存區域,它只是在操作系統中唯一標示了一塊共享內存區,但此時並沒有爲之分配物理內存,只是分配了管理結構,可以理解爲在shmfs中創建了一個文件(如果已經存在,相當於打開了一個文件)。shmat間接使用mmap把shmget打開(或創建)的shmfs文件映射到應用程序的地址空間,其他過程就和mmap普通文件的處理一樣了,只不過共享內存通過shmfs巧妙的避開了mmap的缺點。

10 malloc

malloc只是一個庫函數,在不同的平臺對malloc有不同的實現,glibc使用的是ptmalloc的實現。malloc是從堆上分配內存,但在內核中並沒有堆的概念,堆只是一個應用程序的概念,在進程創建的時候,在進程的虛擬地址空間中劃分出一塊區域作爲堆,這塊區域並沒有對應的物理內存,使用malloc分配內存實際上只是從這塊虛擬內存區域分出更小的區域給應用程序,只有當應用程序訪問這個小區域時纔會產生缺頁中斷,從而獲得物理內存。而free並不會釋放物理內存,而是把在堆上分配的小區域歸還給堆,這些操作都是glibc在應用層實現的。

malloc的使用過程中會使用兩個系統調用brk和mmap,brk用於增長(或減小)堆的大小,在進程創建時會指定堆的起始地址(堆空間是向上增長的),而堆的大小爲0,當使用malloc分配內存時發現當前堆剩餘空間不夠時就會調用brk增長堆的大小,實際上brk的作用就是把堆所在的虛擬內存區域的結束地址增長(或減小)到某個位置。當malloc一次分配的空間大於一個閥值大小時(比如128K),malloc不再從堆上分配空間,而是使用mmap重新映射一塊虛擬地址區域,在free時調用munmap釋放這一區域。這樣的策略主要是方便堆管理,避免在一個很大的尺度管理堆,這樣做是基於大內存分配並不常使用這個假設。

可以注意到如果分配的內存過大,在分配和釋放時都要通過系統調用,效率會有降低,所以如果應用程序頻繁分配大於分配閥值的內存,效率就會很低,這種應用可以通過調整分配閥值使內存分配和釋放都在用戶態(在堆中)完成。使用mallopt可以調整malloc的參數,M_TRIM_THRESHOLD表示如果堆大小大於該值,就應該在適當的時候收縮堆的大小,M_MMAP_THRESHOLD表示大於此值的內存分配請求要使用 mmap 系統調用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章