您的位置:首页 > 运维架构 > Linux

Linux System Programming:Memory Management

2011-03-15 10:49 337 查看
原文链接:http://wiki.oss.org.tw/index.php/Linux_System_Programming%EF%BC%9AMemory_Management

目錄

[隱藏]

1 前言

2 進程位址空間(Process Address Space)

2.1 頁面和頁面調度(Pages & Paging)

2.2 Sharing and copy-on-write

2.3 記憶體區域 (Memory Regions)

3 動態記憶體分配(Allocating Dynamic Memory)

3.1 配置陣列(Allocating Arrays)

3.2 調整配置記憶體的大小(Resizing Allocations)

3.3 釋放動態記憶體(Freeing Dynamic Memory)

3.4 資料對齊(Alignment)

3.5 配置要對齊的記憶體

3.6 其他對齊注意事項

4 匿名記憶體映射(Anonymous Memory Mappings)

4.1 建立匿名記憶體映射

4.2 映射到/dev/zero

5 進階記憶體配置(Advanced Memory Allocation)

5.1 使用malloc_usable_size( )和malloc_trim( )來調整性能(Fine-Tuning)

6 記憶體配置除錯(Debugging Memory Allocations)

6.1 獲得統計數據

7 基於堆疊的配置(Stack-Based Allocations)

8 在堆疊中複製字串(Duplicating Strings on the Stack)

8.1 變數長度的陣列(Variable-Length Arrays)

9 選擇適當的記憶體分配機制

10 記憶體操作(Manipulating Memory)

10.1 Setting Bytes

10.2 Comparing Bytes

10.3 Moving Bytes

10.4 Searching Bytes

10.5 Frobnicating Bytes

11 鎖住記憶體(Locking Memory)

11.1 鎖定部份位址空間 (Locking Part of an Address Space)

11.2 鎖定全部地址空間(Locking All of an Address Space)

11.3 解除記憶體鎖定(Unlocking Memory)

11.4 鎖定的限制

11.5 該頁面在物理實體記憶體中嗎?

12 投機性記憶體配置策略(Opportunistic Allocation)

12.1 過量使用以及記憶體耗盡(Overcommitting and OOM)

前言

記憶體是最基本也最重要的資源.記憶體管理包括: 資源管理,資源分配(allocation),還有記憶體操作(manipulation)以及記憶體釋放(release).

進程位址空間(Process Address Space)

和現代作業系統一樣,Linux將其物理實體記憶體資源虛擬化,進程並不能直接在實體上找尋位址,而是由Linux核心為每一個進程維護一特殊的虛擬地址空間(virtual address space), 這個空間是線性的,從0到某個最大值.

頁面和頁面調度(Pages & Paging)

虛擬位址空間由許多頁面(pages)組成.作業系統體系結構及機型決定頁的數量(頁的大小是固定的).典型頁有4k(32位元系統)和8k(64位元系統).每個頁面都只有有效(valid)和無效(invalid)兩種狀態.一個有效頁和一個實體物理頁面或是是一些二級儲存介質相關連,像是一個swap分區或是一個硬碟上的檔案. 一個無效頁沒有跟任何位址空間關聯,表示沒有被分配使用. 存取無效頁面會導致錯誤產生.一個進程不能使用一個處在二級儲存媒介中的頁面,除非這個頁面和物理實體中的頁面戶相關連.如過試著存取,那記憶體管理單元(MMU)就會產生一個頁面錯誤(page fault).通常虛擬記憶體要比實體記憶體要來的大,核心通常需要經常把頁面從物理記憶體抽出(paging out)到二級儲存裝置.核心通常會把未來最不可能使用的頁面抽出,來達到最佳化性能.

Sharing and copy-on-write

虛擬記憶體中的多個頁面,甚至在被不同進程擁有的不同虛擬位址空間,也有可能被映射到同一個物理實體頁面.這樣允許不同的虛擬位址空間共享(share)實體物理記憶體上的資料.當一個進程要寫入某個共享的可寫的頁面時,一是核心允許了其操作,一是MMU會擷取這次寫入操作並產生一個異常做為回應,核心會建立一個這個頁面的拷貝以供該進程進行寫入操作.我們稱這種方法為copy-on-write(COW).
允許讀取共享的資料可以有效的節省空間.當一個進程試圖寫入一個共享頁面的時候,就可以立刻獲得一份該頁面的拷貝, 這使得核心工作起來就像每個進程都始終擁有其私有的拷貝.COW是以頁面為單位,因此一個大檔案可以有效的被許多進程共享,並且進程只有在要寫入共享頁面的時候,才會取得一份拷貝.

記憶體區域 (Memory Regions)

核心將具有相同特徵的頁面組成區塊(blocks),例如有讀寫權限的.這些區塊我們就叫做記憶體區域(Memory Regions),區段(segments),或是映射(mappings).下面是一個在每個進程都可以見到的記憶體區域:

文字區段(text segment) : 包含進程的程式碼,字串,常態變數,及一些只可讀取的資料.Linux裡面,text segment均為只能讀取,並且直接從目標檔案直接映射到記憶體中.

堆疊(stack) : 包括一個進程的堆疊.可以動態增長或縮短.包含了區域變數和函數傳回值.

資料區段(data segment) : 也叫做堆積(heap).是一個進程的動態可讀寫的記憶體.

BSS區段(bss segment) : 包含了沒有被初始化的全域變數.這些變數包含了一些特殊值(像是全部為0)

動態記憶體分配(Allocating Dynamic Memory)

記憶體同樣可以透過自動或是靜態的變數獲得,但是任何記憶體管理系統的機制都是使用動態記憶體的方式配置(allocation),使用,以及最終的傳回. 動態記憶體是在進程執行(runtime)的時候才分配,而不是在編譯的時候就分配好, 分配的大小也只有在分配的時候才確定.C語言不提供支援動態記憶體的變數,程式設計者透過一個指標pointer來對某段記憶體進行操作.C語言裡面最經典的存取動態記憶體的介面就是malloc():
#include <stdlib.h>
void *malloc (size_t size);

呼叫成功會得到一個size大小的記憶體區域,並傳回一個指向這個記憶體第一個位址的指標.這記憶體的內容是未定義的,但不全為0.失敗的話傳回NULL,並設置errno為ENOMEM.範例:
char *p;
/* give me 2 KB! */
p = malloc (2048);
if (!p)
perror (”malloc”);

或是如下範例:
struct treasure_map *map;
/*
* allocate enough memory to hold a treasure_map
stucture
* and point ’map’ at it
*/
map = malloc (sizeof (struct treasure_map));
if (!map)
perror (”malloc”);

C語言會把傳回值void指標轉成任何型別type,但是C++不會,要用下面的方式轉換:
char *name;
/* allocate 512 bytes */
name = (char *) malloc (512);
if (!name)
perror (”malloc”);

避免將傳回值轉成void. 另外malloc可以傳回NULL,當傳回NULL就印出錯誤和終止程序,程式設計者將這種封裝叫做 xmalloc():
/* like malloc(), but terminates on failure */
void *xmalloc (size_t size)
{
void *p;
p = malloc (size);
if (!p) {
perror (”xmalloc”);
exit (EXIT_FAILURE);
}
return p;
}


配置陣列(Allocating Arrays)

用陣列來處理動態記憶體.陣列長度是固定的, 但是元素數量是可以變化的. C語言提供calloc()函式:
#include <stdlib.h>
void *calloc (size_t nr, size_t size);

呼叫calloc()成功會傳回一個指標, 指向一個可以存入整個陣列的記憶體區塊(nr個元素,每個元素大小為size個位元組), 下面有兩種方式結果都得到相同的記憶體大小:
int *x, *y;
x = malloc (50 * sizeof (int));
if (!x) {
perror (”malloc”);
return -1;
}
y = calloc (50, sizeof (int));
if (!y) {
perror (”calloc”);
return -1;
}

不過兩個函數還是有區別, calloc將分配的區域全部用0初始化.y的50個元素都為0,x陣列裡面的元素都是未定義.
Calloc失敗時會傳回NULL,並設errno為ENOMEM.

使用者也可以自行定義介面:
/* works identically to malloc( ), but memory is
zeroed */
void *malloc0 (size_t size)
{
return calloc (1, size);
}

也可以將malloc()和xmalloc結合:
/* like malloc( ), but zeros memory and
terminates on failure */
void *xmalloc0 (size_t size)
{
void *p;
p = calloc (1, size);
if (!p) {
perror (”xmalloc0”);
exit (EXIT_FAILURE);
}
return p;
}


調整配置記憶體的大小(Resizing Allocations)

使用realloc():
#include <stdlib.h>
void *realloc (void *ptr, size_t size);

呼叫成功會將ptr指向的記憶體區域大小變為size位元組,傳回一個新空間的指標.如果size為0,就跟在ptr上呼叫free()相同.若ptr為NULL,結果就跟malloc()一樣.若ptr不為NULL,就必須是之前呼叫的malloc(),calloc()或realloc()的傳回值.若要縮小記憶體空間,就先使用calloc()來申請存放一個由兩個map結構所組成的陣列: struct map *p:
/* allocate memory for two map structures */
p = calloc (2, sizeof (struct map));
if (!p) {
perror (”calloc”);
return -1;
}
/* use p[0] and p[1]... */


然後修改記憶體大小,將一半空間還給作業系統:
/* we now need memory for only one map */
r = realloc (p, sizeof (struct map));
if (!r) {
/* note that ’p’ is still valid! */
perror (”realloc”);
return -1;
}
/* use ’r’... */
free (r);

這個例子中, realloc()被呼叫後, p[0]被保留了下來,所有資料都維持不變.

釋放動態記憶體(Freeing Dynamic Memory)

不像自動配置的記憶體, 動態記憶體會永久佔有一個進程地址空間的一部分.所以程式設計者必須藥名白地將申請到的動態記憶體歸還給系統.使用free():
#include <stdlib.h>
void free (void *ptr);

呼叫free()會釋放ptr所指向的記憶體,但ptr必須是之前呼叫malloc(),calloc()或是realloc()的傳回值.
ptr可能為NULL,則free()當然就不做傳回的動作.

以下範例:
void print_chars (int n, char c)
{
int i;
for (i = 0; i < n; i++) {
char *s;
int j;
/*
* Allocate and zero an i+2 element array
* of chars. Note that ’sizeof (char)’
* is always 1.
*/
s = calloc (i + 2, 1);
if (!s) {
perror (”calloc”);
break;
}
for (j = 0; j < i + 1; j++)
s[j] = c;
printf (”%s/n”, s);
/* Okay, all done. Hand back the memory. */
free (s);
}
}

這個例子我們將n個字元陣列分配記憶體, n個元素個數依次遞增,從兩個元素(2 bytes)一直增加到n+1個元素(n+1 bytes), 然後將每個陣列中的最後一個元素外的元素給值為c(最後一個byte為0),將結果列印, 將s釋放. 呼叫print_chars(),讓n為5, c為X, 可以得到以下結果:
X
XX
XXX
XXXX
XXXXX

注意這個例子沒有呼叫free()的結果,我們沒有辦法再對這個記憶體區塊進行操作.我們叫做這個程式設計錯誤為記憶體洩漏(memory leak).
另外常見錯誤就是釋放記憶體後再使用(use-after-free).

資料對齊(Alignment)

資料對齊是指將其位址跟由硬體決定的記憶體區塊間的關係做對應.一個變數的位址是其大小的倍數時,叫做自然對齊(maturally aligned). 例如一個變數是32bit長度,他的位址是4(bytes)的倍數時—位址最低的兩個byte是0,那麼就是自然對齊. 對齊的規則是根據硬體而定.

配置要對齊的記憶體

多數情況下,編譯器和C語言函式庫會自動處理對齊問題.處理更大的邊界像是頁面的話,需要動態的對齊,可以使用posix_memalign()來處理:
/* one or the other -- either suffices */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE
#include <stdlib.h>
int posix_memalign (void **memptr,
size_t alignment,
size_t size);

呼叫成功會傳回size bytes大小的動態記憶體,並保證是按照aligment對齊的.參數aligment必須是2的冪次,以及void指標大小的倍數.傳回的記憶體的位址保存在memptr裡,函數傳回0.呼叫失敗則沒有配置記憶體, memptr值沒有被定義, 回傳回下列錯誤代碼之一:
EINVAL : 參數不是2的冪次,或不是void指標大小的倍數
ENOMEM : 沒有足夠記憶體滿足函式的要求.

注意這函式, errno不會被設置,會直接在傳回值中給出.由posix_memalign()取得的記憶體透過free()釋放:
char *buf;
int ret;
/* allocate 1 KB along a 256-byte boundary */
ret = posix_memalign (&buf, 256, 1024);
if (ret) {
fprintf(stderr, ”posix_memalign: %s/n”,
strerror (ret));
return -1;
}
/* use ’buf’... */
free (buf);

函數valloc()和malloc()功能一樣,但傳回的位址是頁面對齊的.memalign()是以boundary bytes對齊,而boundary必須是2的次方.下面例子兩個函式都傳回一個足夠大的記憶體去存一個ship結構,並且位址都在一個頁面的邊界上:
struct ship *pirate, *hms;
pirate = valloc (sizeof (struct ship));
if (!pirate) {
perror (”valloc”);
return -1;
}
hms = memalign (getpagesize ( ), sizeof (struct
ship));
if (!hms) {
perror (”memalign”);
free (pirate);
return -1;
}
/* use ’pirate’ and ’hms’... */
free (hms);
free (pirate);

這兩個函式獲得的記憶體,在Linux裡面都可以用free()釋放.

其他對齊注意事項

對於非標準型別來說:

1 一個結構的對齊要求和它成員中最大的那個型別是一樣的. 例如一個結構中最大的是一個4 bytes對齊的32bit整數.那麼這個結構至少要以4 bytes對齊.

2 結構也引入對padding(留白)的需求.這用來確定每個成員型別都符合各自的對齊要求.所以若一個char(一個byte對齊)型別後跟著一個int(4 bytes對齊),編譯器會自動插入3 bytes來填充,保證int以4 bytes對齊.

注意型別轉換:
char greeting[] = ”Ahoy Matey”;
char *c = greeting[1];
unsigned long badnews = *(unsigned long *) c;

一個unsigned long可能以4或8 bytes為邊界對齊.而c當然只以1 byte為邊界對齊,因為當c被強制型別轉換之後在進行讀取,會導致對齊錯誤.這樣會導致性能下降或是系統崩潰.

匿名記憶體映射(Anonymous Memory Mappings)

Glibc裡的記憶體配置使用資料區段以及記憶體映射. 實現malloc()最經典的方式就是將資料分為一個系列大小的2的次方的區塊,傳回最小的那個區塊來滿足請求. 釋放則是將這個區塊標記為未使用,若相鄰區域是空閒的,就會被合成更大的分區.若堆疊的頂是空的, 系統就可用brk()來降低中斷點(break point),讓堆疊變小,將記憶體返回給作業系統.這種我們叫做夥伴記憶體分配演算法(buddy memory allocation scheme). 好處是快跟簡單,壞處是會產生兩種類型的碎片.當使用記憶體區塊大於請求的大小時產生內部碎片(Internal fragmentation),導致記憶體使用率降低. 外部碎片(external fragmentation)是空閒的記憶體空間合起來可以滿足一個請求,但是沒有一個單獨的空間可以滿足這個請求。這樣同樣會導致記憶體利用不足,或是分配失敗。另外這個演算法會使一個記憶體的分配'栓'(pin)住另外一個,導致glibc不能將釋放的記憶體返回給作業系統。不過這不是問題,對於較大的記憶體分配,glibc不使用heap,而是建立一個匿名記憶體映射(anonymous memory mapping).匿名記憶體映射類似file-based mappings,不過不是用檔案的形式,只是一塊已經用0初始化的大塊記憶體來給使用者使用. 可以想像為單獨為某次分配而使用的heap.因為這種映射不是基於映射heapp,,所以不會在記憶體內產生碎片。使用匿名記憶體映射的好處:

1 不用擔心碎片

2 匿名儲存記憶體映射大小可以調整

3 每個配置(allocation)存在於獨立的記憶體映射,不用管管全域的的heap

4 每個記憶體映射都是頁面大小的整數倍

5 建立一個新的記憶體映射比比heapp中返回記憶體的負載要大.越小的配置這樣的問題也明顯。

所以, glibc的的malloc())使用資料區段資料區段(data segment)來滿足較小的配置的配置,使用匿名記憶體映射來作較大的配置的配置。兩者臨界點是可以調整的,一般來說是一般來說是128kb.

建立匿名記憶體映射

使用使用mmap())建立,使用minmap()銷毀:
#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_t length);

因為不需要打開檔案,所以建立匿名記憶體映射更簡單.兩者差別在於有無匿名標記匿名標記(signifying that the mapping is anonymous).
void *p;
p = mmap (NULL,                         /* do not care where */
512 * 1024,                  /* 512 KB */
PROT_READ | PROT_WRITE,      /* read/write */
MAP_ANONYMOUS | MAP_PRIVATE, /* anonymous, private */
-1,                          /* fd (ignored) */
0);                          /* offset (ignored) */
if (p == MAP_FAILED)
perror ("mmap");
else
/* 'p' points at 512 KB of anonymous memory... */

參數說明如下:

1 start參數設為參數設為NULL,意味匿名記憶體映射可以讓核心安排在任意位址上

2 prot參數同時設置PROT_READ和PROT_WRITE bits, 使得映射可以讀寫.

3 flags參數設置MAP_ANONYMOUSS使映射為匿名,設置MAP_PRIVATE使得映射為私有的.

4 假如MAP_ANONYMOUSS被設置,那fd,offset參數會被忽略.

使用匿名記憶體映射的好處,就是已經分配好的頁面全部用全部用0進行了初始化. 核心使用核心使用copy-on-write將記憶體映射到一個為0的頁面上,也避免了額外的開銷。
int ret;
/* all done with ’p’, so give back the 512 KB
mapping */
ret = munmap (p, 512 * 1024);
if (ret)
perror (”munmap”);


映射到/dev/zero

像BSD並沒有MAP_ANONYMOUSS,使用特殊的檔案叫做/dev/zero來實現
void *p;
int fd;
/* open /dev/zero for reading and writing */
fd = open (”/dev/zero”, O_RDWR);
if (fd < 0) {
perror (”open”);
return -1;
}
/* map [0,page size) of /dev/zero */
p = mmap (NULL, /* do not care where */
getpagesize ( ), /* map one page */
– 274 –
内存  理
8
PROT_READ | PROT_WRITE, /* map
read/write */
MAP_PRIVATE, /* private mapping */
fd, /* map /dev/zero */
0); /* no offset */
if (p == MAP_FAILED) {
perror (”mmap”);
if (close (fd))
perror (”close”);
return -1;
}
/* close /dev/zero, no longer needed */
if (close (fd))
perror (”close”);
/* ’p’ points at one page of memory, use it... */

這種需要開啟檔案,所以還是匿名記憶體映射速度比較快.

進階記憶體配置(Advanced Memory Allocation)

使用mallopt():
#include <malloc.h>
int mallopt (int param, int value);

mallopt()會將param確定的儲存管理相關參數設為value.成功時傳回一個非0值,失敗傳回0.Linux目前支援六種param值,定義在<malloc.h>中:
M_CHECK_ACTION : 環境變數 MALLOC_CHECK 的變數值
M_MMAP_MAX : 滿足動態記憶體請求的最大記憶體映射數量
M_MMAP_THRESHOLD : 決定是用匿名記憶體映射還是記憶體區段來滿足記憶體分配請求
M_MXFAST  : fast bin的最大size.用bytes為單位.
M_TOP_PAD : 調整資料區段的長度而使用的padding bytes數量

XPG標準中定義了mallopt(),並指定另外三個參數M_GRAIN, M_KEEP,和M_NLBLKS,不過實際上沒有作用.下表定義了所有合法參數,還有其預設值與可接受的範圍:
Parameter      Origin         Default value        Valid values Special values
M_CHECK_ACTION                0
Linux-specific                      0–2
M_GRAIN        XPG standard   Unsupported on Linux >= 0
M_KEEP         XPG standard   Unsupported on Linux >= 0
M_MMAP_MAX                      64 * 1024
Linux-specific                      >= 0   0 disables use of mmap( )
M_MMAP_THRESHOLD                128 * 1024
Linux-specific                      >= 0   0 disables use of the heap
M_MXFAST                        64
XPG standard                        0 – 80 0 disables fast bins
M_NLBLKS         XPG standard   Unsupported on Linux >= 0
M_TOP_PAD                       0
Linux-specific                      >= 0   0 disables padding

程式必須在呼叫malloc()或是其他記憶體分配函數前使用mallopt():
/* use mmap( ) for all allocations over 64 KB */
ret = mallopt (M_MMAP_THRESHOLD, 64 * 1024);
if (!ret)
fprintf (stderr, ”mallopt failed!/n”);


使用malloc_usable_size( )和malloc_trim( )來調整性能(Fine-Tuning)

Linux提供兩個用來控制glibc記憶體分配系統的底層函數:
#include <malloc.h>
size_t malloc_usable_size (void *ptr);

呼叫 malloc_usable_size()成功時, 傳回指向動態記憶體實際大小的指標.因為glibc可能擴大動態記憶體來適應一個可存在的區塊或匿名記憶體映射.如下例:

size_t len = 21;
size_t size;
char *buf;
buf = malloc (len);
if (!buf) {
perror (”malloc”);
return -1;
}
size = malloc_usable_size (buf);
/* we can actually use ’size’ bytes of ’buf’... */

第二個函式允許程序強制glibc歸還所有可釋放的動態記憶體給核心:
#include <malloc.h>
int malloc_trim (size_t padding);

呼叫malloc_trim()成功時,資料區段會盡可能的收縮,但是padding bytes被保留下來,然後傳回1.失敗時傳回0. 一般來說當空閒的記憶體達到 M_TRIM_THRESHOLD bytes時,會使用M_TOP_PAD數目來作padding.

記憶體配置除錯(Debugging Memory Allocations)

程式可以設置MALLOC_CHECK_ 環境變數來啟動記憶體子系統的除錯功能, 可以直接執行指令:
$ MALLOC_CHECK_=1 ./rudder

若設為0,則系統會忽略所有錯誤, 若設為1,訊息會被輸出到標準錯誤stderr. 若設為2,進程會立刻通過abort()終止.

獲得統計數據

使用mallinfo():
#include <malloc.h>
struct mallinfo mallinfo (void);

mallinfo()將統計數據存到mallinfo結構中.這個結構是通過值傳回,而非指標. 結構定義在<malloc.h>:

/* all sizes in bytes */
struct mallinfo {
int arena;    /* size of data segment used by malloc */
int ordblks; /* number of free chunks */
Debugging Memory Allocations | 263
int smblks;   /* number of fast bins */
int hblks;    /* number of anonymous mappings */
int hblkhd;   /* size of anonymous mappings */
int usmblks;  /* maximum total allocated size */
int fsmblks;  /* size of available fast bins */
int uordblks; /* size of total allocated space */
int fordblks; /* size of available chunks */
int keepcost; /* size of trimmable space */
};

用法如下:
struct mallinfo m;
m = mallinfo();
printf (”free chunks: %d/n”, m.ordblks);

Linux也提供stats()函數,將與記憶體相關的統計數據輸出到標準錯誤輸出(stderr):
#include <malloc.h>
void malloc_stats (void);

在記憶體操作頻繁的程式中往往會產生一些較大的數字:
Arena 0:
system bytes = 865939456
in use bytes = 851988200
Total (incl. mmap):
system bytes = 3216519168
in use bytes = 3202567912
max mmap regions = 65536
max mmap bytes = 2350579712


基於堆疊的配置(Stack-Based Allocations)

堆疊(stack)是用來存放程式的自動變數(automatic variables).當然也可以使用堆疊來作動態記憶體配置:
#include <alloca.h>
void * alloca (size_t size);

呼叫alloca(),成功會傳回一個指向size bytes大小的記憶體指標.失敗時有的回傳NULL,不過基本上都不可能失敗,失敗就是堆疊超出.下面例子是在系統配置目錄裡打開一個特定的檔案,這個函數必須申請一個新的緩衝區,複製系統配置路徑到達這個緩衝區,然後將提供的檔案名稱接到緩衝區的後面:
int open_sysconf (const char *file, int flags,
int mode)
{
const char *etc = SYSCONF_DIR; /* ”/etc/” */
char *name;
name = alloca (strlen (etc) + strlen (file) +
1);
strcpy (name, etc);
strcat (name, file);
return open (name, flags, mode);
}

在open_sysconf函式傳回時,從alloca()分配到的記憶體隨著堆疊的收縮而被自動釋放.我們不需作任何釋放工作.下面是通過malloc()實現相同功能的函數:
int open_sysconf (const char *file, int flags,
int mode)
{
const char *etc = SYSCONF_DIR; /* ”/etc/” */
char *name;
int fd;
name = malloc (strlen (etc) + strlen (file) +
1);
if (!name) {
perror (”malloc”);
return -1;
}
strcpy (name, etc);
strcat (name, file);
fd = open (name, flags, mode);
free (name);
return fd;
}

注意不能使用由alloca()得到的記憶體來作為一個函式呼叫的參數,因為分配到的記憶體會被當作參數保存在函式的堆疊中,下面這樣不行:
/* DO NOT DO THIS! */
ret = foo (x, alloca (10));


在堆疊中複製字串(Duplicating Strings on the Stack)

alloca()常見用法是用來臨時複製一個字串:
/* we want to duplicate ’song’ */
char *dup;
dup = alloca (strlen (song) + 1);
strcpy (dup, song);
/* manipulate ’dup’... */
return; /* ’dup’ is automatically freed */

Linux提供strdup()來將一個特定的字串複製到堆疊中:
#define _GNU_SOURCE
#include <string.h>
char * strdupa (const char *s);
char * strndupa (const char *s, size_t n);

呼叫strdupa()會傳回一個s的拷貝,stmdupa()會複製s中的n個bytes.若s大於n,那複製s前n個bytes,後面自動加上一個空bytes.這些函式都具有alloca()的優點.

變數長度的陣列(Variable-Length Arrays)

C99導入變數長度陣列(variable-length arrays,VLAs)先看例子:
for (i = 0; i < n; ++i) {
char foo[i + 1];
/* use ’foo’... */
}

每次loop中,foo都被動態建立,並在loop結束後自動釋放,若用alloca(),那記憶體空間要到函數傳回時才被釋放.使用VLA最多n個bytes,alloca()會用掉n*(n+1)/2個bytes, 我們可以這樣重寫我們的open_sysconf()函式:
int open_sysconf (const char *file, int flags,
int mode)
{
const char *etc; = SYSCONF_DIR; /* ”/etc/” */
char name[strlen (etc) + strlen (file) + 1];
strcpy (name, etc);
strcat (name, file);
return open (name, flags, mode);
}


選擇適當的記憶體分配機制

大部分情況下malloc()是最好地選擇,以下作點總結:

malloc( ) : 簡單方便好用, 缺點是傳回的記憶體用0初始化

calloc( ) : 讓配置陣列變得容易.用0初始化記憶體. 缺點若非配置陣列會很複雜

realloc( ) : 可以重新配置已分配的空間大小, 缺點是只能用來配置已分配的空間大小

brk( )和sbrk( ) : 允許對heap深入控制. 缺點是大多用在底層匿名記憶體映射: 使用簡單可共享,適合大空間分配. 缺點就是不適合小分配

posix_memalign( ) : 分配的記憶體按照任何合理大小進行對齊. 缺點是比較新,可移植性是個問題

memalign( ) and valloc( ) : 比 posix_memalign( )常見, 但是POSIX標準,對齊控制不如 posix_memalign( )

alloca( ) : 配置很快,不需要知道確切大小,適合小型配置. 缺點就是不適合大配置變數長度陣列 : 與alloca()相似,但是退出loop時就會釋出記憶體空間. 缺點就是只能用來分配陣列

記憶體操作(Manipulating Memory)

C語言提供很多對記憶體的操作.

Setting Bytes

最常用的就是memeset():
#include <string.h>
void * memset (void *s, int c, size_t n);

memset()將s指向n個bytes開始的區域設為c, 並傳回指標s.該函式常被用來將一塊記憶體清空設為0:
/* zero out [s,s+256) */
memset (s, ’/0’, 256);


Comparing Bytes

使用memcmp():
#include <string.h>
int memcmp (const void *s1, const void *s2,
size_t n);

比較s1和s2兩個前面n個bytes, 若兩個記憶體相同就傳回0, 若s1<s2就傳回一個小於0的數,反之傳回大於0的數值.程式設計者若要比較結構,就必須一個一個比較結構中的每一個元素.如下面例子:
/* are two dinghies identical? */
int compare_dinghies (struct dinghy *a, struct
dinghy *b)
{
int ret;
if (a->nr_oars < b->nr_oars)
return -1;
if (a->nr_oars > b->nr_oars)
return 1;
ret = strcmp (a->boat_name, b->boat_name);
if (ret)
return ret;
/* and so on, for each member... */
}


Moving Bytes

使用mememove()複製src的前n個bytes到dst,傳回dst:
#include <string.h>
void * memmove (void *dst, const void *src,
size_t n);

也可使用memcpy:
#include <string.h>
void * memcpy (void *dst, const void *src, size_t  n);

除了dst和src間不能重疊,基本和memmove()一樣.另外一個安全的複製函式為memcpy():
#include <string.h>
void * memccpy (void *dst, const void *src, int
c, size_t n);

memccpy()和memcpy()類似,但發現c byte在src指向的前n個bytes時會停止複製.它傳回指向dst中c後一個byte的指標,當沒有找到c時傳回NULL.最後我們用mempcpy()來跨過複製的記憶體:
#define _GNU_SOURCE
#include <string.h>
void * mempcpy (void *dst, const void *src,
size_t n);

mempcpy()和memcpy()幾乎一樣,區別在於memeccpy()傳回的是指向被複製的記憶體最後一個byte的下一個byte的指標.

Searching Bytes

使用memechr()和memrchr():
#include <string.h>
void * memchr (const void *s, int c, size_t n);

函數memechr()從s指向的n個byte中尋找c, c將被轉換為unsigned char:
#define _GNU_SOURCE
#include <string.h>
void * memrchr (const void *s, int c, size_t n);

函數傳回符合c的第一個byte的指標,若沒找到c就傳回NULL.memrchr()與memchr()類似,但是他是從s指向的記憶體開始反向搜尋n個bytes.另外用複雜搜索的話使用 memmem()函式:
#define _GNU_SOURCE
#include <string.h>
void * memmem (const void *haystack,
size_t haystacklen,
const void *needle,
size_t needlelen);

memmem()函式在指向長度為haystacklen 的記憶體區塊haystack中尋找,並傳回第一塊和長為needlelen的needle匹配的子區塊的指標. 或在haystack找不到needle,就傳回NULL.

Frobnicating Bytes

Linux C函式庫提供了資料加密的介面:
#define _GNU_SOURCE
#include <string.h>
void * memfrob (void *s, size_t n);

memfrob()將s指向的位置開始的n個bytes, 每個都與42進行XOR操作來進行加密,函數傳回s.再做一次memefrob()就可以將其轉換回來:
memfrob (memfrob (secret, len), len);

這僅是一個對字串的簡單處理函式. 不適合用於資料加密.

鎖住記憶體(Locking Memory)

Linux實現了demand padding. Demand padding的意思是說有用到時將頁面從硬碟交換進來,不需要時再交換出去.這使得系統中進程的虛擬位址空間與實際物理記憶體大小沒有直接關係, 同時硬碟上的交換空間(swap space)提供一個擁有幾乎無限空間記憶體的假象.Swap過程是透明化的,一般來說不需要擔心.只有下面情形應用程式希望影響系統的頁面調度:

確定性Determinism : 有需要時間嚴格限制的應用程式,就需要自行決定頁面的調度行為.

安全性Security : 若記憶體中有私人訊息,有可能最後會被頁面調度以不加密的形式存到硬碟,

當然, 改變核心的行為可能會對系統整理表現產生負面影響, 這是要注意的.

鎖定部份位址空間 (Locking Part of an Address Space)

使用mlock()將一個或更多的頁面鎖定(locking)在物理實體記憶體中,來保證他們不會被交換到硬碟:

#include <sys/mman.h>

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

mlock()會鎖定addr開始長度為len bytes的虛擬記憶體,成功會傳回0, 失敗傳回-1,並設置errno.成功的話會將所有包含 [addr,addr+len]的實體記憶體頁面鎖定.合法的errno如下:

EINVAL : 參數len是負數

ENOMEM : 函數嘗試鎖定多於 RLIMIT_MEMLOCK 限制的頁面

EPERM : RLIMIT_MEMLOCK 來源限制是0, 但是進程並沒有CAP_IPC_LOCK 權限

下面例子假設一個程式在記憶體中有一個加密的字串, 一個進程可以使用下面的方式來鎖定擁有這個字串的頁面:
int ret;

/* lock 'secret' in memory */

ret = mlock (secret, strlen (secret));

if (ret)

perror ("mlock");


鎖定全部地址空間(Locking All of an Address Space)

使用mlockall():
#include <sys/mman.h>

int mlockall (int flags);

flag參數是下面兩個值的按位或操作,用意控制函數行為:

MCL_CURRENT : 若設該值, mlockall()會將所有已映射的頁面鎖定在進程位址空間中.

MCL_FUTURE : 若設該值, mlockall()會將所有未來要映射的頁面也鎖定在進程位址空間中.

呼叫成功傳回0, 失敗傳回-1, 並設定errno為下列錯誤碼之一:

EINVAL : 參數len是負數

ENOMEM : 函數要鎖定的頁面比RLIMIT_MEMLOCK 限制的要多.

EPERM

:RLIMIT_MEMLOCK資源限制是0, 但是進程沒有CAP_IPC_LOCK權限


解除記憶體鎖定(Unlocking Memory)

使用方法如下:
#include <sys/mman.h>

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

int munlockall (void);

munlock()解除mlock()的效果, munlockall()解除mlockall()的效果, 均在成功時傳回0失敗傳回-1, 並設定errno為下列錯誤碼之一:

EINVAL : 參數len是負數

ENOMEM : 被指定的頁面有些不合法

EPERM : RLIMIT_MEMLOCK資源限制是0, 但是進程沒有CAP_IPC_LOCK權限

鎖定的限制

記憶體鎖定會影響系統效能, 鎖定太多頁面, 記憶體分配會失敗. 所以Linux對一個進程可以鎖定的頁面數量做了限制.有CAP_IPC_LOCK權限的進程可以任意鎖定多個頁面,沒有權限的進程只能鎖定RLIMIT_MEMLOCK個bytes. 預設限制是32KB.

該頁面在物理實體記憶體中嗎?

:使用mincore()來作確認:

#include <unistd.h>

#include <sys/mman.h>

int mincore (void *start,

size_t length,

unsigned char *vec);

呼叫mincore()提供了一個向量(vector),來表示呼叫時映射中哪個頁面是在物理實體記憶體中. 函數通過vec來傳回向量,這個向量描述start開始長度為length個bytes的記憶體中的情況.成功時函數傳回0, 失敗傳回-1, 並設定errno為下列值之一:

EAGAIN : 核心沒有足夠資源可滿足請求

EFAULT : 參數vec 指向一個無效位址.

EINVAL : 參數start 並沒有頁面對齊.

ENOMEM : [address,address+1) 的記憶體不在某個基於檔案的映射中.

投機性記憶體配置策略(Opportunistic Allocation)

Linux使用投機性記憶體配置策略, 當一個進程向核心請求額外的記憶體,像是擴大他的資料區段的時候, 核心會做出分配承諾,但實際上還沒有分給進程任何的實體物體記憶體. 只有當進程新分配到的記憶體區塊需要作寫入操作的時候, 核心才會實際履行承諾.

過量使用以及記憶體耗盡(Overcommitting and OOM)

和在應用程式請求頁面的時候就分配實體記憶體相比, 在要用到的時候才分配實體記憶體的過量使用(overcommiting)機制,允許系統可以執行更多更大的應用程式. 若沒有過量使用,寫入時複製映射2GB檔案,就需要核心分配出2GB的實體物理記憶體. 使用過量使用,只需要進程映射區域中真正進行寫入操作時寫入頁面的大小.當過量使用導致記憶體不足以滿足一個請求時, 就發生了所謂的OOM(out of memory)記憶體耗盡現象.為處理OOM,核心使用killer來挑選一個進程並終止它. 核心會嘗試選擇一個最不重要且又佔用很多記憶體的進程.對於不想這種狀況出現的系統, 核心允許使用透過檔案/proc/sys/vm/overcommit_memory關閉過量使用, 與此功能相似的還有sysctl
的parameter vm.overcommit_memory.

參數預設值為0. 參數設為1, 就會不顧一切確認所有的分配請求. 當設定為2,關閉所有過量使用,採用嚴格審計(strict accounting)的策略, 承諾的記憶體大小被嚴格限制在swap空間大小加上可調比例的物理實體記憶大小.可在 /proc/sys/vm/overcommit_ratio設定.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: