linux內存管理(上)

linux內存管理

 

對於一個進程來說,內存是最基本也是最重要的資源。這一章的內容包括:存儲器分配、內存操控和內存釋放以及內存釋放

 

瞭解如何鎖定內存,從而避免了你再程序中等待內核從交換區換頁。

 

看到鎖定內存我思考,鎖定內存是什麼?

 

 

學習自csdn

https://blog.csdn.net/jidonghui/article/details/7332266

交換對進程來說是透明的,應用程序一般都不需要關心(甚至不需要知道)內核頁面調度的行爲。

如果用戶不希望某塊內存在暫時不用時置換到磁盤上,可以對該內存進行內存鎖定。

 

#include <sys/types.h>  
  
int mlock(const void *addr,size_t length)  
  
int munlock(void *addr,size_t length)  
  
int mlockall(int flag)  
  
int munlockall(void  )

 

函數:mlock鎖定一片內存區域,addr爲內存地址,length要鎖定的長度。

munlock接觸已鎖定的內存

mlockall一次鎖定多個內存頁。flag取值有兩個MCL_CURRENT鎖定所用內存頁,MCL_FUTURE鎖定爲進程分配的地址空間內存頁。munlockall用於解除鎖定的內存。這些函數會在後面的內容詳細說

Linux 實現了請求頁面調度,頁面調度是說頁面從硬盤按需交換進來,當不再需要的時候交換出去。這樣做允許系統中每個進程的虛擬地址空間和實際物理內存的總量再沒有直接的聯繫,因爲在硬盤上的交換空間能給進程一個物理內存幾乎無限大的錯覺。


 

8.1進程的地址空間

 

和所有的現代操作系統一樣,linux將物理內存虛擬化。進程並不能直接在物理內存上尋址,而是由linux內核爲每個進程維護一個特殊的虛擬空間(也就是說我們平時寫程序操作的是虛擬地址)。這個空間地址是線性的,從0開始,到某個最大值。

 

8.1.1頁和頁面調度

 

虛擬空間由許多頁組成。系統的體系結構以及機型決定了也的大小(頁的大小是固定的),典型的也的大小包括4k(32)位系統以及8k(64位系統).每一個頁面只有無效和有效兩種狀態,一種是有效頁面和一個物理頁或者一些二級存儲介質.

 

8.1.1頁和頁面調度

 

虛擬空間由許多頁組成。系統的體系結構以及機型決定了也的大小(頁的大小是固定的),典型的也的大小包括4k(32)位系統以及8k(64位系統).每一個頁面只有無效和有效兩種狀態,一種是有效頁面和一個物理頁或者一些二級存儲介質

 

如一個交換分區或者一個在硬盤上的文件。一個無效頁面沒有關聯,代表它沒有被分配使用。對於無效頁面的訪問會引發一個段錯誤。地址空間不需要是連續的。雖然是線性編址,但實際上中間有很多未編址的小區域。

 

一個進程不能訪問一個處在二級存儲中的頁,除非這個頁和物理內存中的頁相互關聯。如果一個進程嘗試訪問這樣的頁面會發生一個頁錯誤。然後內核會透明的從二級存儲換入需要的頁面。因爲一般來說虛擬存儲器總是比物理內存大,所以內核也需要經常的把頁面從物理內存換出到二級存儲,從而爲將要轉入的頁面騰出空間。內核總是將未來不可能使用的頁換出來優化性能。

當沒有足夠的物理內存時,系統通過把進程的一部份轉移到硬盤上

以設法容納進程。當再次需要進程中的被轉移到硬盤上的那一部分時,

再返回到物理內存中。這個上過程稱爲頁面調度,它使得系統即使在有限的物理

內存的條件下也能夠具備多任務處理的能力.

Unix中用作虛擬內存的硬盤分段稱爲交換空間,交換空間耗盡將引起嚴重的問題

直至使系統失效

 

8.1.1.1共享和寫時複製

虛存中的多個頁面,甚至是屬於不通進程的虛擬地址空間,也有可能被反映射到同一個物理頁面。
這樣允許不通的虛擬地址空間共享物理內存上的數據。共享數據可能是隻讀的,或者是可讀可寫的。

當一個進程試圖寫某個共享的可寫頁的時候,可能發生以下兩種情況。最簡單的是內核允許這個操作,在這種情形下所有共享這個頁的進程都將看到這次寫操作的結果。通常大量進程對同一個頁面進行讀寫的時候需要某種成都上的合作和同步操作機制。

這個我感覺有點像共享內存

另一種情況是MMU會截取這次寫操作併產生一個異常;作爲迴應,內核會透明的創造一份這個頁的拷貝以供給這個進程進行寫操作,這就叫做寫時複製。允許讀取共享的數據可以節省空間。當一個進程試圖寫這個共享頁面的時候,可以立刻獲得一個這個頁的拷貝,是的進程內核工作起來像每個進程都始終有自己的私有拷貝。寫時拷貝是以頁爲單位進行的,因此一個大文件可以有效的被衆多進程共享。而每個進程只有在對共享頁寫時纔會獲得一份新的拷貝。

 

####8.1.2存儲器區域

 

內核將具有某些相同特徵的頁組織成塊,例如讀寫權限。這些塊叫做存儲器區域,段(segments),或者映射,下面是一些在每個進程都可以見到的存儲器區域:

 

1)文本段包含着一個進程的代碼,字符串,常量和一些只讀數據。

 

2)堆棧段包含一個進程的執行棧,隨着棧的深度動態的伸長或者收縮。執行棧中包含了程序的局部變量和函數的返回值。

 

3)數據段 又叫堆,包含這一個進程的動態存儲空間。這個端是科協的,而且它的大小是可變化的。這部分空間往往是由malloc分配的。

 

4)BSS段,包含了沒有被初始化的全局變量。這些變量根據不通的c標準都有特殊的值。

 

linux從兩個方面優化這些變量。首先,因爲附加段是用來存放沒有被初始化的數據的,所以連接器實際上並不會將特殊值存儲在對象文件中。這樣可以減少二進制文件的大小。其次,當這個段被加載到內存時候,內核只是需要簡單的根據寫時複製的原則將他們映射到一個全是0的頁上,這樣十分有效的設置了這些變量的初始值。

 

大多數地址空間中包含了很多映射文件,比如可執行文件自己,c或者是其他的可鏈接庫和數據文件。可以看看/proc/self/maps, 或者pmap程序的輸出,我們可以看到一個進程裏面有很多映像文件。

 

/proc/meminfo可以看到整個系統內存消耗情況,使用top可以看到每個進程的VIRT(虛擬內存)和RES(實際佔用內存),基本上就可以將泄漏內存定位到進程範圍。

 

linux提供了/proc/self/目錄,這個目錄比較獨特,不同的進程訪問該目錄時獲得的信息是不同的,內容等價於/proc/本進程pid/。進程可以通過訪問/proc/self/目錄來獲取自己的系統信息,而不用每次都獲取pid。

 

也就是說/proc/self等價於/proc/pid

我們使用

man 5 proc 

查看maps 的詳細說明

 /proc/[pid]/maps
              A file containing the currently mapped memory regions and their access permissions.  See mmap(2) for some further information about memory mappings.

              Permission to access this file is governed by a ptrace access mode PTRACE_MODE_READ_FSCREDS check; see ptrace(2).

              The format of the file is:

    address           perms offset  dev   inode       pathname
    00400000-00452000 r-xp 00000000 08:02 173521      /usr/bin/dbus-daemon
    00651000-00652000 r--p 00051000 08:02 173521      /usr/bin/dbus-daemon
    00652000-00655000 rw-p 00052000 08:02 173521      /usr/bin/dbus-daemon
    00e03000-00e24000 rw-p 00000000 00:00 0           [heap]
    00e24000-011f7000 rw-p 00000000 00:00 0           [heap]
    ...
    35b1800000-35b1820000 r-xp 00000000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a20000-35b1a21000 rw-p 00020000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a21000-35b1a22000 rw-p 00000000 00:00 0
    35b1c00000-35b1dac000 r-xp 00000000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1dac000-35b1fac000 ---p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870  /usr/lib64/libc-2.15.so
    ...
    f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0    [stack:986]
    ...
    7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0   [stack]
    7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0   [vdso]

              The address field is the address space in the process that the mapping occupies.  The perms field is a set of permissions:

                  r = read
                  w = write
                  x = execute
                  s = shared
                  p = private (copy on write)

              The  offset  field  is  the  offset into the file/whatever; dev is the device (major:minor); inode is the inode on that device.  0 indicates that no inode is associated with the memory
              region, as would be the case with BSS (uninitialized data).

              The pathname field will usually be the file that is backing the mapping.  For ELF files, you can easily coordinate with the offset field by looking at the Offset field in the ELF  pro‐
              gram headers (readelf -l).

              There are additional helpful pseudo-paths:

                   [stack]
                          The initial process's (also known as the main thread's) stack.

                   [stack:<tid>] (since Linux 3.4)
                          A thread's stack (where the <tid> is a thread ID).  It corresponds to the /proc/[pid]/task/[tid]/ path.

                   [vdso] The virtual dynamically linked shared object.  See vdso(7).

                   [heap] The process's heap.

              If  the  pathname field is blank, this is an anonymous mapping as obtained via mmap(2).  There is no easy way to coordinate this back to a process's source, short of running it through
              gdb(1), strace(1), or similar.

              Under Linux 2.0, there is no field giving pathname.

 

我可以通過這個文件很輕鬆的發現這個二進制程序除了自己本身還鏈接過哪些動態庫。

 

一張圖我們就很容易看出一個進程的存儲區域的劃分

 

8.2動態內存分配

 

內存同樣可以通過自動變量和靜態變量獲得,但是所有內存管理系統的基礎都是動態的內存分配,使用以及動態的返回。動態內存是在進程運行時候才分配的,而不是在編譯的時候就分配好的,而分配的大小只有在分配的時候才知道的。作爲一個程序員在程序員你不會知道程序佔用了多少內存,或者你使用這塊內存的時間不定,則需要使用動態內存

例如c不會提供在動態內存中獲取結構體struct priate_ship的機制,而是提供一種機制在動態內存中分配一個足夠大的空間來保存priate_ship的機制,而是提供了一種機制在動態內存中分配一個足夠大的空間來保護priate_ship。程序員通過一個指針來對這塊內存進行操作,這個指針就是struct priate_ship*。
 

malloc 失敗的時候會返回NULL,並把errno設置爲ENOMEN。

 

8.2.1數組分配

當分配數據內存本身大小可變,動態分配內存更加複雜。爲數組動態分配內存就是一個更好的例子

	#include <stdlib.h>
	void *calloc(size_t nr,size_t size);

calloc與malloc的區別,calloc分配的區域全部用0進行了初始化,因此y中的50個元素都被賦值爲0,但是x數組裏面的元素卻是未定義的。注意calloc會比memset快

 

8.2.2調整已經分配的內存大小

 

void *realloc(void *ptr,size_t size);

 

成功調用realloc將ptr指向的內存區域大小變爲size字節。他返回了一個指向新空間的指針,當試圖擴大內存的時候返回的指針可能不在是ptr。如果realloc不能在已經由的空間上增加到size大小,那麼就會額外申請一塊size大小的空間,並且將原本內容拷貝到新的空間。因爲有潛在的拷貝操作所以realloc的操作是比較耗時的。

如果ptr是NULL,結果就會跟malloc一樣。如果ptr是非NULL的,那麼他必須是之前調用的malloc,calloc或realloc之一的返回值。當失效的
c不會提供支持動態內存的變量。

 

8.2.3動態內存的釋放free

 

free掉對應地址之後就可以

 

8.2.4對齊

 

數據對齊是指數據地址和硬件確定的內存塊之間的關係。一個變量地址是它大小的倍數時,就叫做自然對其,如果一個變量長32位是4的倍數就是自然對其。如果一個類型大小是2n個直接,那麼他的地址中,至少低n位是0.對齊規則是根據硬件制定的。在一些系統中載入一個沒對齊的數據會發生錯誤。在可移植的代碼中對齊問題一定要注意!!!

 

32位系統是8字節對齊,64位系統是16字節對齊。

 

posix提供了一個叫做posix_memalign的函數:

 

#include <stdlib.h>
void* valloc(size_t size);
posix_memalign(void **memptr,size_t alignment,size_t size);

調用posix_memalign成功時候會返回size字節的動態內存,並保證是按照alignment進行對其的,參數alignment必須是2的冪次方,以及void指針大小的倍數。返回的內存塊的地址保存在memptr裏,函數返回0

調用失敗返回下面這些錯誤碼

EINVAL 參數不是2的冪次方,或者不是void指針的倍數

ENOMEM沒有足夠的內存去滿足函數的申請

函數valloc的功能和malloc一模一樣,但是返回的地址是頁面對齊的。回顧一下第四章,頁面的大小很容易通過getpagesize得到。

相似地,函數memealign是以boundary字節對齊的,而boundary必須是2的冪。在這個例子中,兩個函數都返回一塊足夠大的內存去存放一個ship結構,並且地址都是在一個頁面的邊界上,這些函數申請的api都可以通過free來釋放。

 

int main()
{
    void* buf[128];
    int i=0;
    for(i=0;i<128;i++)
    {
       int res = posix_memalign(&buf[i],8,1024);
       printf("%d\n",res);
    }

    for(i=0;i<128;i++)
    {
        free(buf[i]);
    }
}

 

在posix_memalign之前使用

#include <malloc.h>
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);

valloc和malloc一樣但是返回的是字節對齊的malloc

 

下面是四條有用的規則:

一個結構的對其要求和它的成員中最大的類型是一樣的。例如,一個結構中最大的是以4字節對齊的32bit的整形,那麼這個結構至少要以4個字節對齊。

結構體引入了對填充的需求,以此來保證每一個成員都複合各自的對齊要求。所以如果一個char後面跟着一個int,編譯器會自動插入3個字節來保證int以4個字節對齊。程序員要注意結構體中成員變量的順序,來減少填充鎖導致的空間浪費。

 

例子1:


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int bss_end;

typedef struct _demoOne{
    char one;
    char two;
    int three;
}demoOne;

int main()
{
    int a;
    printf("%ld\n",sizeof(demoOne));
    return 0;
}

上面這個佔用了8個字節,並不是12個字節因爲是

 

例子2:

typedef struct _demoOne{
    char one;
    int three;
    char two;
}demoOne;

佔用了12個字節

 

這樣就會導致佔用12個字節,所以我們在使用結構體的時候一定要注意變量順序避免字節浪費!!!

一個聯合的對齊和聯合裏面最大的類型一致。

一個數組的對齊和數組裏的元素類型一致。所以對數組裏面的元素做對齊以外,沒有其他的對齊要求    。

使用指針。因爲編譯器透明的處理了絕大多數的對齊問題,所以要找到潛在的錯誤的時候比較困難,然而以這樣的錯誤並不少見,特別是在處理指針和強轉的時候。

假設一個指針從一個較少字節對齊類型強轉爲一個較多的字節對齊類型,通過這樣的指針來訪問的時候,會導致處理器不能對較多字節類型的數據正確對齊、例如下面的代碼片段,c到badnews的強轉使得程序將c轉爲unsigned long來讀:

 

char greeting[] = ”Ahoy Matey”;
char *c = greeting[1];
unsigned long badnews = *(unsigned long *) c;

這個代碼例子會導致段錯誤。

 

一個unsigned long 可能以4或8個字節對齊;而c當然只以1字節爲邊界對齊。因此當c被強轉之後再進行讀取將會導致對齊錯誤。

 

8.3數據段的管理

#include <unistd.h>
int brk(void* end);
void* sbrk(int ptr_t increment);

這些函數繼承了一些老版本的unix系統中函數的名字,同事堆和棧還在同一個段中。堆中動態存儲器的分配由數據段的底部向上生長;棧從數據段的頂部向下生長。棧和堆的分界線叫做中斷或中斷點。在現代系統中,數據段存在與自己的內存映射中,我們仍用中斷點來標記映射結束地址。

調用brk會設置中斷點的地址爲end。在成功的時候返回0,失敗的時候返回-1,並設置errno爲ENOMEM

調用sbrk將數據段末端增加increment字節,increment可正可負。sbrk返回修改後的斷點。所以increment爲0的時候得到的是現在的斷點地址:
 

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