linux內存


在Linux的世界中,從大的方面來講,有兩塊內存,一塊叫做內存空間,Kernel Space,另一塊叫做用戶空間,即User Space。它們是相互獨立的,Kernel對它們的管理方式也完全不同

 

驅動模塊和內核本身運行在Kernel Space當中

 

 

一 linux內存模型

 

Linux內存管理系統主要解決以下三個大的問題

 

  1. 1.    進程地址空間不能隔離

由於程序直接訪問的是物理內存,這個時候程序所使用的內存空間不是隔離的。舉個例子,就像上面說的A的地址空間是0-10M這個范圍內,但是如果A中有一 段代碼是操作10M-128M這段地址空間內的數據,那么程序B和程序C就很可能會崩潰(每個程序都可以系統的整個地址空間)。這樣很多惡意程序或者是木 馬程序可以輕而易舉的破快其他的程序,系統的安全性也就得不到保障了,這對用戶來說也是不能容忍的。

  1. 2.    內存使用的效率低

如上面提到的,如果我們要像讓程序A、B、C同時運行,那么唯一的方法就是使用虛擬內存技術將一些程序暫時不用的數據寫到磁盤上,在需要的時候再從磁盤讀 回內存。這里程序C要運行,將A交換到磁盤上去顯然是不行的,因為程序是需要連續的地址空間的,程序C需要20M的內存,而A只有10M的空間,所以需要 將程序B交換到磁盤上去,而B足足有100M,可以看到為了運行程序C我們需要將100M的數據從內存寫到磁盤,然后在程序B需要運行的時候再從磁盤讀到 內存,我們知道IO操作比較耗時,所以這個過程效率將會十分低下。

  1. 3.    程序運行的地址不能確定

程序每次需要運行時,都需要在內存中非配一塊足夠大的空閑區域,而問題是這個空閑的位置是不能確定的,這會帶來一些重定位的問題,重定位的問題確定就是程序中引用的變量和函數的地址,如果有不明白童鞋可以去查查編譯願意方面的資料。

 

這里引用計算機界一句無從考證的名言:“計算機系統里的任何問題都可以靠引入一個中間層來解決。”

 

程序和物理內存之間引入了虛擬內存

 

1.分段

分段(Segmentation):這種方法是人們最開始使用的一種方法,基本思路是將程序所需要的內存地址空間大小的虛擬空間映射到某個
物理地址空間。

 

段映射機制

每個程序都有其獨立的虛擬的獨立的進程地址空間,可以看到程序A和B的虛擬地址空間都是從0x00000000開始的。我們將兩塊大小相同的虛擬地 址空間和實際物理地址空間一一映射,即虛擬地址空間中的每個字節對應於實際地址空間中的每個字節,這個映射過程由軟件來設置映射的機制,實際的轉換由硬件 來完成。

這種分段的機制解決了文章一開始提到的3個問題中的進程地址空間隔離和程序地址重定位的問題。程序A和程序B有自己獨立的虛擬地址空間,而且該虛擬 地址空間被映射到了互相不重疊的物理地址空間,如果程序A訪問虛擬地址空間的地址不在0x00000000-0x00A00000這個范圍內,那么內核就 會拒絕這個請求,所以它解決了隔離地址空間的問題。我們應用程序A只需要關心其虛擬地址空間0x00000000-0x00A00000,而其被映射到哪 個物理地址我們無需關心,所以程序永遠按照這個虛擬地址空間來放置變量,代碼,不需要重新定位。

 

2分頁

分頁機制就是把內存地址空間分為若干個很小的固定大小的頁,每一頁的大小由內存決定,就像Linux中ext文件系統將磁盤分成若干個Block一樣,這 樣做是分別是為了提高內存和磁盤的利用率。試想以下,如果將磁盤空間分成N等份,每一份的大小(一個Block)是1M,如果我想存儲在磁盤上的文件是 1K字節,那么其余的999字節是不是浪費了。所以需要更加細粒度的磁盤分割方式,我們可以將Block設置得小一點,這當然是根據所存放文件的大小來綜 合考慮的,好像有點跑題了,我只是想說,內存中的分頁機制跟ext文件系統中的磁盤分割機制非常相似。

分頁機制的原理,當然Linux中的分頁機制的實現還是比較復雜的,通過了也全局目錄,也上級目錄,頁中級目錄,頁表等幾級的分頁機制來實現的,但是基本的工作原理是不會變的。

分頁機制的實現需要硬件的實現,這個硬件名字叫做MMU(Memory Management Unit),他就是專門負責從虛擬地址到物理地址轉換的,也就是從虛擬頁找到物理頁。

3缺頁異常

缺頁異常處理。基於 CPU 的這一特性,Linux 采用了請求調頁(Demand Paging)和寫時復制(Copy On Write)的技術

 1.請求調頁是 一種動態內存分配技術,它把頁框的分配推遲到不能再推遲為止。這種技術的動機是:進程開始運行的時候並不訪問地址空間中的全部內容。事實上,有一部分地址 也許永遠也不會被進程所使用。程序的局部性原理也保證了在程序執行的每個階段,真正使用的進程頁只有一小部分,對於臨時用不到的頁,其所在的頁框可以由其 它進程使用。因此,請求分頁技術增加了系統中的空閑頁框的平均數,使內存得到了很好的利用。從另外一個角度來看,在不改變內存大小的情況下,請求分頁能夠 提高系統的吞吐量。當進程要訪問的頁不在內存中的時候,就通過缺頁異常處理將所需頁調入內存中。

  2.寫時復制主 要應用於系統調用fork,父子進程以只讀方式共享頁框,當其中之一要修改頁框時,內核才通過缺頁異常處理程序分配一個新的頁框,並將頁框標記為可寫。這 種處理方式能夠較大的提高系統的性能,這和Linux創建進程的操作過程有一定的關系。在一般情況下,子進程被創建以后會馬上通過系統調用 execve將一個可執行程序的映象裝載進內存中,此時會重新分配子進程的頁框。那么,如果fork的時候就對頁框進行復制的話,顯然是很不合適的。

   在上述的兩種情況下出現缺頁異常,進程運行於用戶態,異常處理程序可以讓進程從出現異常的指令處恢復執行,使用戶感覺不到異常的發生。當然,也會有異常 無法正常恢復的情況,這時,異常處理程序會進行一些善后的工作,並結束該進程。也就是說,運行在用戶態的進程如果出現缺頁異常,不會對操作系統核心的穩定 性造成影響

 

二 linux內存管理

Free命令

作為一名Linux系統管理員,監控內存的使用狀態是非常重要的,通過監控有助於了解內存的使用狀態,比如內存占用是否正常,內存是否緊缺等等,監控內存最常使用的命令有free、top等,下面是某個系統free的輸出:

 

每個選項的含義:

第一行:

total:物理內存的總大小

used:已經使用的物理內存大小

free:空閑的物理內存大小

shared:多個進程共享的內存大小

buffers/cached:磁盤緩存的大小

第二行Mem:代表物理內存使用情況

第三行(-/+ buffers/cached):代表磁盤緩存使用狀態

第四行:Swap表示交換空間內存使用狀態

free命令輸出的內存狀態,可以通過兩個角度來查看:一個是從內核的角度來看,一個是從應用層的角度來看的。

內核的角度:內核目前可以直接分配到,不需要額外的操作,即為上面free命令輸出中第二行Mem項的值,可以看出,此系統物理內存有3894036K,空閑的內存只有420492K,也就是40M多一點

應用層的角度:對於應用程序來說,buffers/cached占有的內存是可用的,因為buffers/cached是為了提高文件讀取的性能,當應用程序需要用到內存的時候,buffers/cached會很快地被回收,以供應用程序使用

buffers與cached的異同

在Linux 操作系統中,當應用程序需要讀取文件中的數據時,操作系統先分配一些內存,將數據從磁盤讀入到這些內存中,然后再將數據分發給應用程序;當需要往文件中寫 數據時,操作系統先分配內存接收用戶數據,然后再將數據從內存寫到磁盤上。然而,如果有大量數據需要從磁盤讀取到內存或者由內存寫入磁盤時,系統的讀寫性 能就變得非常低下,因為無論是從磁盤讀數據,還是寫數據到磁盤,都是一個很消耗時間和資源的過程,在這種情況下,Linux引入了buffers和 cached機制。

buffers與cached都是內存操作,用來保存系統曾經打開過的文件以及文件屬性信息,這樣當操作系統需要讀取某些文件時,會首先在 buffers與cached內存區查找,如果找到,直接讀出傳送給應用程序,如果沒有找到需要數據,才從磁盤讀取,這就是操作系統的緩存機制,通過緩 存,大大提高了操作系統的性能。但buffers與cached緩沖的內容卻是不同的。

buffers是用來緩沖塊設備做的,它只記錄文件系統的元數據(metadata)以及 tracking in-flight pages,而cached是用來給文件做緩沖。更通俗一點說:buffers主要用來存放目錄里面有什么內容,文件的屬性以及權限等等。而cached直接用來記憶我們打開過的文件和程序

top命令

使用top命令。在top的輸出中,SIZE顯示了每個程序的虛地址空間的大小(您的整個程序代碼、數據、棧,其中一些應該已被交換出到交換區間)。RSS 列(Resident set size,持久集合大小)顯示了程序所占用的的物理內存大小。所有當前運行程序的 RSS 數值總和不會超過您的計算機物理內存大小,並且所有地址空間的大小限制值為2GB(對於32字節版本的Linux來說)

 

三 linux內存實現

glibc 內存管理的實現,特別是高並發性能低下和內存碎片化問題都比較嚴重,因此,陸續出現一些第三方工具來替換 glibc 的實現,最著名的當屬 google 的tcmalloc和facebook 的jemalloc 。

tcmalloc主要是為了多線程設計的

 

 malloc調用之后

在glibc實現的內存管理算法中,

Malloc小塊內存是在小於0x4000 0000的內存中分配的,通過brk/sbrk不斷向上擴展,

請求內存大於128K,而分配大塊內存,malloc直接通過系統調用mmap實現,分配得到的地址在文件映射區

 

brk分配的內存需要等到高地址內存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的),mmap分配的內存可以單獨釋放

 

Linux下默認棧的大小限制是10M

window下默認棧的大小限制是1M 可以修改

Mmap 分配大空間內存

mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最后一個頁不被使用的空間將會清零。mmap在用戶空間映射調用系統中作用很大

#include<sys/mman.h>

 

Void *mmap(void*start,

size_t length,

int prot,

int flags,

int fd,

off_t offset);

 

int munmap(void*start,size_tlength);

 

start:映射區的開始地址,設置為0時表示由系統決定映射區的起始地址。

 

length:映射區的長度。//長度單位是 以字節為單位,不足一內存頁按一內存頁處理

 

prot:期望的內存保護標志,不能與文件的打開模式沖突。是以下的某個值,可以通過or運算合理地組合在一起

PROT_EXEC //頁內容可以被執行

PROT_READ //頁內容可以被讀取

PROT_WRITE //頁可以被寫入

PROT_NONE //頁不可訪問

 

flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體

MAP_FIXED //使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。

MAP_SHARED //與其它所有映射這個對象的進程共享映射空間。對共享區的寫入,相當於輸出到文件。直到msync()或者munmap()被調用,文件實際上不會被更新。

MAP_PRIVATE //建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標志和以上標志是互斥的,只能使用其中一個。

MAP_DENYWRITE //這個標志被忽略。

MAP_EXECUTABLE //同上

MAP_NORESERVE //不要為這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會得到保證。當交換空間不被保留,同時內存不足,對映射區的修改會引起段違例信號。

MAP_LOCKED //鎖定映射區的頁面,從而防止頁面被交換出內存。

MAP_GROWSDOWN //用於堆棧,告訴內核VM系統,映射區可以向下擴展。

MAP_ANONYMOUS //匿名映射,映射區不與任何文件關聯。

MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用。

MAP_FILE //兼容標志,被忽略。

MAP_32BIT //將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標志只在x86-64平台上得到支持。

MAP_POPULATE //為文件映射通過預讀的方式准備好頁表。隨后對映射區的訪問不會被頁違例阻塞。

MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於內存中的頁面建立頁表入口。

 

fd:有效的文件描述詞。一般是由open()函數返回,其值也可以設置為-1,此時需要指定flags參數中的MAP_ANON,表明進行的是匿名映射。

 

off_toffset:被映射對象內容的起點。

 

mmap()必須以PAGE_SIZE()為單位進行映射,而內存也只能以頁為單位進行映射,若要映射非PAGE_SIZE整數倍的地址范圍,要先進行內存對齊,強行以PAGE_SIZE的倍數大小進行映射。

start:映射區的開始地址,設置為0時表示由系統決定映射區的起始地址。

length:映射區的長度。//長度單位是 以字節為單位,不足一內存頁按一內存頁處理

 

使用mmap分配大空間內存后,需要鎖住內存,有mlock操作只能root用戶進行

鎖住內存是為了防止這段內存被操作系統swap掉。並且由於此操作風險高,僅超級用戶可以執行

#include <sys/mman.h>

       int mlock(const void *addr, size_t len);

       int munlock(const void *addr, size_t len);

       int mlockall(int flags);

       int munlockall(void);

僅分配內存並調用 mlock 並不會為調用進程鎖定這些內存,因為對應的分頁可能是寫時復制(copy-on-write)5。因此,你應該在每個頁面中寫入一個假的值這樣針對每個內存分頁的寫入操作會強制 Linux 為當前進程分配一個獨立、私有的內存頁

 

既然堆內內存brk和sbrk不能直接釋放,為什么不全部使用 mmap 來分配,munmap直接釋放呢? 
        既 然堆內碎片不能直接釋放,導致疑似“內存泄露”問題,為什么 malloc 不全部使用 mmap 來實現呢(mmap分配的內存可以會通過 munmap 進行 free ,實現真正釋放)?而是僅僅對於大於 128k 的大塊內存才使用 mmap ? 

        其實,進程向 OS 申請和釋放地址空間的接口 sbrk/mmap/munmap 都是系統調用,頻繁調用系統調用都比較消耗系統資源的。並且, mmap 申請的內存被 munmap 后,重新申請會產生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調用產生了大量缺頁中斷 (1M/4K 次 ) ,當munmap 后再次分配 1M 空間,會再次產生大量缺頁中斷。缺頁中斷是內核行為,會導致內核態CPU消耗較大。另外,如果使用 mmap 分配小內存,會導致地址空間的分片更多,內核的管理負擔更大。
        同時堆是一個連續空間,並且堆內碎片由於沒有歸還 OS ,如果可重用碎片,再次訪問該內存很可能不需產生任何系統調用和缺頁中斷,這將大大降低 CPU 的消耗。 因此, glibc 的 malloc 實現中,充分考慮了 sbrk 和 mmap 行為上的差異及優缺點,默認分配大塊內存 (128k) 才使用 mmap 獲得地址空間,也可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個臨界值。

 

 

四 內存分配多線程安全

原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位

 

原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用匯編語言實現,因為C語言並不能實現這樣的操作

 

定義在include/asm/atomic.h中。 用戶程序include它,在自己控制CONFIG_SMP定義。

在單處理器時 atomic_inc() 就是incl xxxx
在CONFIG_SMP時, atomic_inc()是 lock incl xxxx

因此incl XXXX在SMP時不是原子的。必須用lock.

 

 

 

typedef struct  {  volatile int counter;  }  atomic_t;


  volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。

 

  原子操作API包括:

atomic_read(atomic_t * v);


  該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。

 

 

atomic_set(atomic_t * v, int i);


  該函數設置原子類型的變量v的值為i。

 

 

void atomic_add(int i, atomic_t *v);


  該函數給原子類型的變量v增加值i。

 

 

atomic_sub(int i, atomic_t *v);


  該函數從原子類型的變量v中減去i。

 

 

int atomic_sub_and_test(int i, atomic_t *v);


  該函數從原子類型的變量v中減去i,並判斷結果是否為0,如果為0,返回真,否則返回假。

 

 

void atomic_inc(atomic_t *v);


  該函數對原子類型變量v原子地增加1。

 

 

void atomic_dec(atomic_t *v);


  該函數對原子類型的變量v原子地減1。

 

 

int atomic_dec_and_test(atomic_t *v);


  該函數對原子類型的變量v原子地減1,並判斷結果是否為0,如果為0,返回真,否則返回假。

 

 

int atomic_inc_and_test(atomic_t *v);


  該函數對原子類型的變量v原子地增加1,並判斷結果是否為0,如果為0,返回真,否則返回假。

 

 

int atomic_add_negative(int i, atomic_t *v);


  該函數對原子類型的變量v原子地增加I,並判斷結果是否為負數,如果是,返回真,否則返回假。

 

 

int atomic_add_return(int i, atomic_t *v);


  該函數對原子類型的變量v原子地增加i,並且返回指向v的指針。

 

 

int atomic_sub_return(int i, atomic_t *v);


  該函數從原子類型的變量v中減去i,並且返回指向v的指針。

 

 

int atomic_inc_return(atomic_t * v);


  該函數對原子類型的變量v原子地增加1並且返回指向v的指針。

 

 

int atomic_dec_return(atomic_t * v);

  該函數對原子類型的變量v原子地減1並且返回指向v的指針。

  原子操作通常用於實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構struct ipq描述了一個IP碎片,字段refcnt就是引用計數器,它的類型為atomic_t,當創建IP碎片時(在函數ip_frag_create中), 使用atomic_set函數把它設置為1,當引用該IP碎片時,就使用函數atomic_inc把引用計數加1。

  當不需要引用該IP碎片時,就使用函數ipq_put來釋放該IP碎片,ipq_put使用函數atomic_dec_and_test把引用 計數減1並判斷引用計數是否為0,如果是就釋放IP碎片。函數ipq_kill把IP碎片從ipq隊列中刪除,並把該刪除的IP碎片的引用計數減1(通過 使用函數atomic_dec實現)。

五Linux 下so文件的制作

所謂鏈接,也就是說編譯器找到程序中所引用的函數或全局變量所存在的位置

 

程序的鏈接分為靜態鏈接和動態鏈接

 

LD_PRELOAD就是這樣一個環境變量,它可以影響程序的運行時的鏈接(Runtime linker), 它允許你定義在程序運行前優先加載的動態鏈接庫。這個功能主要就是用來有選擇性的載入不同動態鏈接庫中的相同函數。通過這個環境變量,我們可以在主程序和 其動態鏈接庫的中間加載別的動態鏈接庫,甚至覆蓋正常的函數庫。一方面,我們可以以此功能來使用自己的或是更好的函數(無需別人的源碼),而另一方面,我 們也可以以向別人的程序注入惡意程序,從而達到那不可告人的罪惡的目的。

 

設置LD_PRELOAD變量:(使我們重寫過的strcmp函數的hack.so成為優先載入鏈接庫)

      $ export LD_PRELOAD="./hack.so"

 

libc.so.6(GLIBC_2.4) :    libc.so.6(GLIBC_2.14) :

 

生成動態庫:libtest.so

# gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so

 

測試是否動態連接,如果列出libtest.so,那么應該是連接正常了

#  ldd test

 

-shared:

該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號);

不用該標志外部程序無法連接,相當於一個可執行文件;

 

-fPIC:

表示編譯為位置獨立的代碼;

不用此選項的話編譯后的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的;

 

六 內存人工釋放

Linux系統下,我們一般不需要去釋放內存,因為系統已經將內存管理的很好。但是凡事也有例外,有的時候內存會被緩存占用掉,導致系統使用SWAP空間影響性能,此時就需要執行釋放內存(清理緩存)的操作了。

 

Linux系統的緩存機制是相當先進的,他會針對 dentry(用於VFS,加速文件路徑名到inode的轉換)、Buffer Cache(針對磁盤塊的讀寫)和Page Cache(針對文件inode的讀寫)進行緩存操作。但是在進行了大量文件操作之后,緩存會把內存資源基本用光。但實際上我們文件操作已經完成,這部分 緩存已經用不到了。這個時候,我們難道只能眼睜睜的看着緩存把內存空間占據掉么?

所以,我們還是有必要來手動進行Linux下釋放內存的操作,其實也就是釋放緩存的操作了。

要達到釋放緩存的目的,我們首先需要了解下關鍵的配置文件/proc/sys/vm/drop_caches。這個文件中記錄了緩存釋放的參數,默認值為0,也就是不釋放緩存。他的值可以為0~3之間的任意數字,代表着不同的含義:

0 – 不釋放
1 – 釋放頁緩存
2 – 釋放dentries和inodes
3 – 釋放所有緩存

知道了參數后,我們就可以根據我們的需要,使用下面的指令來進行操作。

首先我們需要使用sync指令,將所有未寫的系統緩沖區寫到磁盤中,包含已修改的 i-node、已延遲的塊 I/O 和讀寫映射文件。否則在釋放緩存的過程中,可能會丟失未保存的文件。

#sync

接下來,我們需要將需要的參數寫進/proc/sys/vm/drop_caches文件中,比如我們需要釋放所有緩存,就輸入下面的命令:

#echo 3 > /proc/sys/vm/drop_caches

此指令輸入后會立即生效,可以查詢現在的可用內存明顯的變多了。

要查詢當前緩存釋放的參數,可以輸入下面的指令:

#cat /proc/sys/vm/drop_caches

七 多線程內存申請

pthread不是Linux下的默認的庫,也就是在鏈接的時候,無法找到phread庫中哥函數的入口地址,於是鏈接會失敗。

解決:在gcc編譯的時候,附加要加 -lpthread參數即可解決。

 

幾個好的關於linux內存討論的文章

http://www.cnblogs.com/zhaoyl/p/3695517.html

http://www.ibm.com/developerworks/cn/linux/l-memmod/?S_TACT=105AGX52&S_CMP=tech-51CTO

目前除了glibc里面的ptmalloc(malloc)還有jemalloc,tcmalloc,但是他們大部分是針對小塊內存單元的分配,大塊內存單元都是調用mmap。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM