成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Linux內核內存碎片:悄然蠶食程序性能的 “蛀蟲”

系統 Linux
隨著時間的推移,你開始不斷地購買新家具,又時不時地丟棄一些舊家具。有時候,新買的家具尺寸不規(guī)則,放置后會在周圍留下一些小空間;而丟棄舊家具后,留下的空位又可能因為太小,無法放下新的大型家具。這些零散的、無法被有效利用的小空間,就類似于計算機內存中的碎片。

在深入探討內存碎片之前,讓我們先從一個日常生活場景入手。想象你有一個寬敞的房間,最初房間是空的,你可以自由地放置各種家具,讓它們整齊排列,充分利用空間。這就好比計算機系統剛啟動時,內存是一片連續(xù)的空閑空間,程序可以隨意申請和使用內存。

隨著時間的推移,你開始不斷地購買新家具,又時不時地丟棄一些舊家具。有時候,新買的家具尺寸不規(guī)則,放置后會在周圍留下一些小空間;而丟棄舊家具后,留下的空位又可能因為太小,無法放下新的大型家具。這些零散的、無法被有效利用的小空間,就類似于計算機內存中的碎片。

在計算機世界里,內存就像是這個房間,程序運行時需要向操作系統申請內存空間來存儲數據和執(zhí)行代碼 。當程序申請內存時,操作系統會根據程序的需求分配一塊內存區(qū)域。如果程序頻繁地申請和釋放內存,就像在房間里頻繁地放置和移除家具一樣,內存空間會逐漸變得零散,產生內存碎片。

一、內存碎片簡介

1.1內存碎片是什么

內存碎片,簡單來說,就是內存中由于各種原因產生的不連續(xù)的空閑空間 ,這些空間是一部分無法被有效利用、被浪費的內存資源。當程序向操作系統申請內存時,操作系統會根據一定的算法在內存中尋找合適的空閑區(qū)域進行分配。如果內存中存在大量的小空閑塊,且這些小空閑塊無法合并成足夠大的連續(xù)空間來滿足程序的申請需求,這些小空閑塊就形成了內存碎片。

1.2內存碎片的類型剖析

內存碎片主要分為外部碎片和內部碎片兩種類型,它們各自有著不同的形成機制和特點。

(1)外部碎片

圖片圖片

外部碎片是指那些還沒有被分配給任何進程的內存空閑區(qū)域,但由于這些區(qū)域的空間太小,無法分配給申請內存空間的新進程。就好比你有一堆小塑料袋,每個塑料袋都裝不滿一件大商品,但這些塑料袋的總容量加起來其實是夠裝下這件大商品的,然而由于它們是分散的小袋子,沒辦法用來裝這件大商品,這些小塑料袋的空余空間就類似于外部碎片。

在內存管理中,當進程頻繁地申請和釋放不同大小的內存塊時,就容易產生外部碎片。例如,系統一開始有一塊連續(xù)的大內存空間,進程 A 申請了一塊較小的內存,之后進程 B 又申請了一塊內存,接著進程 A 釋放了內存。此時,雖然釋放出來的內存和剩余的空閑內存總量可能足夠滿足一個新進程的需求,但由于它們是不連續(xù)的小塊,新進程就無法使用這些空閑內存,從而形成了外部碎片 。

(2)內部碎片

圖片圖片

內部碎片是指已經被分配給某個進程,但該進程實際使用的內存小于分配給它的內存空間,這部分多余的、未被利用的內存就是內部碎片。以文字編輯器為例,假設我們開發(fā)一個文字編輯器,設定其字數上限為 3000 字,為了保證程序能正常運行,前端和后臺數據庫用來存儲這些內容的空間都設置為 3000 字的容量。

但在實際使用中,用戶可能只寫了 1000 字,那么剩余的 2000 字的存儲空間就被閑置了,這部分閑置的空間就是內部碎片。又比如內存分配單位是固定大小的,當程序申請的內存大小不是分配單位的整數倍時,就會產生內部碎片。假設內存分配單位是 8KB,而程序只需要 6KB 內存,那么分配的 8KB 中有 2KB 是浪費的,這 2KB 就是內部碎片。

1.3反碎片技術

  • 2.6.23 引入成塊回收,3.5版本后廢除,被碎片整理程序取而代之
  • 2.6.33 引入虛擬可移動區(qū)域

其中,不管是成塊回收還是碎片整理,它們都是在碎片之后進行處理,而這往往對系統性能是有損耗的,尤其是回收熱頁的時候。而虛擬可移動區(qū)域不同,這是一種預防碎片產生的技術。

(1)反碎片的工作原理

內核從2.6.24開始引入反碎片技術,試圖從最開始就盡可能的防止碎片的產生。它根據頁的可移動性將可分配頁分為以下三種不同的類型

  1. 不可移動頁,比如核心內核分配的大多內存
  2. 可回收頁,不能移動,但可以刪除。
  3. 可移動頁,可以隨意移動,如用戶空間的頁。移動之后,需要更新頁表項。

導致碎片的原因就是不可移動的頁插在很多連續(xù)頁組之后,導致其他空閑頁不能用。而頁加入可移動性之后,就可解決這個問題。

(2)虛擬可移動區(qū)域

可移動區(qū)域(ZONE_MOVABLE)是一個抽象的內存區(qū)域,它將物理內存分為兩個區(qū)域。一個區(qū)域用于放置不可移動的頁,另一個區(qū)域用于放置可移動的頁。這樣不可移動的頁就不能插在移動區(qū)域造成碎片了。

那么如何確定兩個區(qū)域的大小呢?

  • kernelcore參數用于指定不可移動分配的內存數量,指定值保存在required_kernelcore中。
  • movablecore參數用于指定可移動分配的內存數量,計算值存在required_kernelcore中。
  • 如果兩個參數都設定,那么required_kernelcore = max(指定值,計算值)。

源碼如下:

// mm\page_alloc.c
...
static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
static unsigned long __initdata required_kernelcore;
static unsigned long __initdata required_movablecore;
static unsigned long __meminitdata zone_movable_pfn[MAX_NUMNODES];
...
/*
 * kernelcore=size sets the amount of memory for use for allocations that
 * cannot be reclaimed or migrated.
 */
static int __init cmdline_parse_kernelcore(char *p)
{
    return cmdline_parse_core(p, &required_kernelcore);
}

/*
 * movablecore=size sets the amount of memory for use for allocations that
 * can be reclaimed or migrated.
 */
static int __init cmdline_parse_movablecore(char *p)
{
    return cmdline_parse_core(p, &required_movablecore);
}

early_param("kernelcore", cmdline_parse_kernelcore);
early_param("movablecore", cmdline_parse_movablecore);

另外函數find_zone_movable_pfns_for_nodes 用于計算進入可移動區(qū)域的內存數量,對一個每個結點來說,zone_movable_pfn[node_id] 表示movable_zone內存域的起始地址,同required_kernelcore一樣, 這也是一個關鍵的變量。

二、內存碎片是如何產生的

2.1頻繁的內存分配與釋放

內存分配和釋放操作就像在圖書館書架上不斷地放置和拿走書籍。如果我們頻繁地進行這樣的操作,書架上的書籍擺放就會變得雜亂無章,出現一些零散的空位,這些空位就類似于內存碎片。

以制作下雪場景程序為例,我們需要不斷地創(chuàng)建和刪除代表雪花的對象 。每創(chuàng)建一個雪花對象,程序就會向操作系統申請一塊內存來存儲該對象的相關信息,如位置、大小、顏色等;當雪花對象不再需要時(比如雪花超出了屏幕范圍),程序會釋放這塊內存。如果這個過程非常頻繁,比如在一個密集的下雪場景中,每秒可能要創(chuàng)建和銷毀成百上千個雪花對象,就會導致內存頻繁地分配和釋放。隨著時間的推移,內存中就會出現許多零散的空閑小塊,這些小塊由于太小或者不連續(xù),無法被后續(xù)的內存分配請求所利用,從而形成了外部碎片 。

2.2內存對齊的要求

內存對齊是指系統和硬件對內存地址的一種要求,為了提升內存訪問的效率和性能,數據在內存中的起始地址需要按照一定的邊界(如 2、4、8、16 等字節(jié)的整數倍)進行對齊 。不同的硬件架構對內存對齊的要求有所不同,例如 x86 和 x64 架構在內存對齊要求方面相對寬松,而 ARM、RISC-V 等架構則較為嚴格。某些特定的硬件平臺(如 MIPS、PowerPC 以及某些 DSP)甚至不支持非對齊的內存訪問,如果發(fā)生非對齊的內存訪問,可能會導致性能下降、硬件異常甚至程序崩潰。

假設我們有一個結構體,其中包含一個 char 類型的變量(占 1 字節(jié))和一個 int 類型的變量(占 4 字節(jié))。按照內存對齊規(guī)則,為了保證 int 類型變量從 4 字節(jié)的整數倍地址開始存儲,在 char 類型變量后面會填充 3 個字節(jié)的空閑空間。這樣,當我們?yōu)檫@個結構體分配內存時,實際分配的內存大小就會大于結構體中所有變量實際占用的內存大小,多出來的這部分空閑空間就是內部碎片 。

三、內存碎片整理

從內存區(qū)域的底部掃描已分配的可移動頁,從內存區(qū)域的頂部掃描空閑頁,把底部的可移動頁移動到頂部空閑頁,在底部形成連續(xù)的空閑頁。

整理算法:首先從內存區(qū)域底部向頂部以頁塊為掃描單位,在頁塊內部起始頁向結束頁掃描,把這個頁塊里面的可移動頁組成一條鏈表,然后從內存區(qū)域頂部向底部以頁塊為單位掃描,在頁塊內部從起始頁向結束頁掃描,把空閑頁組成一條鏈表,最后把底部的可移動頁的數據復制到頂部的空閑頁,修改進程的頁表,把虛擬頁映射到新物理頁。

圖片圖片

假如有16個頁,白色表示空閑頁。這個內存區(qū)域已經碎片化,最大的連續(xù)頁是兩頁。從這個區(qū)域內存分配3頁就會失敗,甚至分配兩頁也會失敗,因為連續(xù)的空閑頁的起始地址沒有對齊到兩頁的整數倍。

3.1內存碎片整理優(yōu)先級

  • 完全同步模式(COMPACT_PRIO_SYNC_FULL):允許阻塞,允許把臟的文件頁回寫到存儲設備上,并且等回寫完成
  • 輕量級同步模式(COMPACT_PRIO_SYNC_LIGHT):允許大多數操作阻塞,但是不允許把臟數據回寫到存儲設備上;
  • 異步模式(COMPACT_PRIO_ASYNC):不允許阻塞。
  • 成本:完全同步模式>輕量級同步>異步模式

3.2碎片整理的開始時機

  • 頁分配器使用最低水線分屏頁失敗以后,如果調用者允許直接回收頁和寫存儲設備,并且是最昂貴的分配或者申請不可移動類型的連續(xù)頁,那么在嘗試直接回收頁之前,先嘗試執(zhí)行異步模式的內存碎片整理。
  • 頁分配器直接回收以后連續(xù)分配頁仍然失敗,如果調用者允許寫存儲設備,嘗試執(zhí)行輕量級同步模式的內存碎片整理。
  • 每個內存節(jié)點有一個頁回收線程和一個內存碎片整理線程,頁回收線程準備睡眠小段時間的時候,喚醒內存碎片整理線程,內存碎片整理線程執(zhí)行輕量級同步模式的內存碎片整理。
  • 系統管理員向文件/proc/sys/vm/compact_memory寫入任何整數值的時候,在所有內存節(jié)點的所有區(qū)域上執(zhí)行完全同步的內存碎片整理。
  • 內存碎片整理線程名“kcompactd<node_id>”,內存節(jié)點的pglist_data的實例成員"kcompactd"指向內存碎片整理線程的進程描述符。

3.3碎片整理結束條件

如果遷移掃描器和空閑掃描器相遇,那么內存碎片整理結束。

如果遷移掃描器和空閑掃描器沒有相遇,但是申請或備用的遷移類型至少有一個足夠大的空閑頁塊,那么內存碎片整理結束。

判斷一個內存區(qū)域是否適合執(zhí)行內存碎片整理的標準:

①如果管理員通過寫文件/proc/sys/vm/compact_memory觸發(fā)內存碎片整理,那么這個內存區(qū)域強制執(zhí)行內存碎片;

②如果內存區(qū)域同時滿足以下三個條件,適合執(zhí)行內存碎片整理

  • 如果(空閑頁數-申請頁數)低于水線,或者雖然大于或等于水線,但是沒有一個足夠大的空閑頁塊
  • 如果(空閑頁數-2倍申請頁數)大于或等于水線,說明有足夠的空閑頁作為遷移目的地,那么這個內存區(qū)域適合執(zhí)行內存碎片整理;
  • 對于昂貴的分配(階數大于3)計算碎片指數,如果碎片指數范圍[0,外部碎片的閾值]以內,說明內存分配失敗是內存不足導致的,不是外部碎片導致的,那么這個內存區(qū)域不適合執(zhí)行內存碎片整理;

③碎片計算

  • 如果不存在空閑頁塊,那么碎片指數=0;
  • 如果至少存在一個足夠大的空閑頁塊,那么碎片指數=-1000
  • 其他情況:碎片指數=1000-(1000+1000*空閑頁數/申請頁數)/空閑頁塊總數;

碎片閾值是內存不足和外部碎片的分界線:如果碎片指數小于或等于閾值,分配失敗的內存不足,應該直接回收頁;如果碎片指數大于閾值,分配失敗原因是外部碎片,應該執(zhí)行碎片整理。

3.4具體使用方式

①編譯內核時:如果需要內存碎片整理功能,需要配置CONFIG_COMPACTION

②命令行下

//設置外部碎片的閾值
root@ubuntu:/home/wy/misc/net# cat /proc/sys/vm/extfrag_threshold
500
//是否允許內部碎片整理移動不可回收的頁,1 允許
root@ubuntu:/home/wy/misc/net# cat /proc/sys/vm/compact_unevictable_allowed
1
//寫入任何值觸發(fā)內存碎片整理
root@ubuntu:/home/wy/misc/net# cat /proc/sys/vm/compact_memory 
cat: /proc/sys/vm/compact_memory: Permission denied

內存碎片整理成功標準(空閑頁數-申請頁數)大于或等于水線,并且申請或備用的遷移類型至少有一個足夠大的空閑頁塊。如果遷移掃描器和空閑掃描器相遇,沒有達到成功標準;則延遲整理。

四、內存碎片帶來的影響

4.1內存利用率降低

內存碎片的存在使得可用內存空間變得不連續(xù),導致實際可用于分配的內存總量減少 。即使系統顯示還有足夠的內存,但由于碎片的存在,程序可能無法分配到足夠大的連續(xù)內存塊,從而影響程序的正常運行。在一個需要大量連續(xù)內存的圖形處理程序中,假設程序要處理一張高分辨率的圖像,圖像數據量非常大,需要申請一塊連續(xù)的大內存來存儲圖像的像素信息。

然而,由于內存中存在大量的碎片,盡管系統的總空閑內存量可能足夠,但這些空閑內存以零散的小塊形式分布,無法合并成滿足圖像數據存儲需求的連續(xù)大內存塊。這就導致圖形處理程序無法正常分配到足夠的內存,無法加載和處理圖像,進而影響程序的性能,甚至導致程序崩潰 。

4.2程序運行速度減慢

內存碎片會導致內存分配和釋放操作變得更加復雜和耗時。當程序需要分配內存時,系統需要花費更多的時間來查找合適的內存塊。因為內存碎片使得內存空間變得零散,系統不能像內存連續(xù)時那樣快速找到滿足需求的內存區(qū)域,而是需要在眾多零散的空閑塊中進行搜索和匹配 。在釋放內存時,也需要進行一些額外的操作來合并相鄰的空閑塊,以減少碎片。

這些額外的操作會增加程序的運行時間,降低程序的執(zhí)行效率。例如,在一個頻繁進行內存分配和釋放的數據庫管理系統中,由于內存碎片的存在,每次內存分配操作都需要更長的時間來定位合適的內存塊,釋放內存時也需要花費更多時間進行合并操作。這使得數據庫的讀寫操作速度變慢,用戶查詢數據的響應時間變長,嚴重影響了數據庫系統的性能和用戶體驗 。

4.3增加內存泄漏風險

內存碎片可能會掩蓋內存泄漏問題。由于碎片的存在,即使程序中存在內存泄漏,系統可能仍然顯示有足夠的內存可用 。因為內存泄漏導致的內存占用增加可能被分散在各個碎片中,不容易被察覺。這使得內存泄漏問題更加難以被發(fā)現和解決,長期積累下來,可能會導致程序崩潰或性能嚴重下降。

比如,在一個長時間運行的服務器程序中,某個模塊存在內存泄漏問題,但由于內存碎片的干擾,內存泄漏的跡象被隱藏起來。隨著時間的推移,內存泄漏越來越嚴重,服務器的可用內存逐漸減少,最終可能導致服務器因內存不足而崩潰,影響整個服務的正常運行 。

五、內存分配算法優(yōu)化

首次適應算法(First Fit):從空閑分區(qū)表的第一個表目起查找該表,把最先能夠滿足要求的空閑區(qū)分配給作業(yè),這種方法的目的在于減少查找時間。為適應這種算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)要按地址由低到高進行排序。該算法優(yōu)先使用低址部分空閑區(qū),在低址空間造成許多小的空閑區(qū),在高地址空間保留大的空閑區(qū)。

最佳適應算法(Best Fit):從全部空閑區(qū)中找出能滿足作業(yè)要求的、且大小最小的空閑分區(qū),這種方法能使碎片盡量小。為適應此算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)要按從小到大進行排序,自表頭開始查找到第一個滿足要求的自由分區(qū)分配。該算法保留大的空閑區(qū),但造成許多小的空閑區(qū)。

最差適應算法(Worst Fit):從全部空閑區(qū)中找出能滿足作業(yè)要求的、且大小最大的空閑分區(qū),從而使鏈表中的結點大小趨于均勻,適用于請求分配的內存大小范圍較窄的系統。為適應此算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)按大小從大到小進行排序,自表頭開始查找到第一個滿足要求的自由分區(qū)分配。該算法保留小的空閑區(qū),盡量減少小的碎片產生。

伙伴算法(buddy):使用二進制優(yōu)化的思想,將內存以2的冪為單位進行分配,合并時只能合并是伙伴的內存塊,兩個內存塊是伙伴的三個條件是:

  • 大小相等(很好判斷)
  • 地址連續(xù)(也很好判斷)
  • 兩個內存塊分裂自同一個父塊(其實只要判斷低地址的內存塊首地址是否是與父塊地址對齊,即合并后的首地址為父塊大小的整數倍)使用lowbit等位運算可以o(1)判斷。

伙伴算法在實現的時候可以使用數組+鏈表的形式(有點像鄰接表那種),因為內存上限是固定的,比較容易確定。下列代碼使用的是二維鏈表(寫起來就沒有數組加鏈表簡潔)。在分配調整內存塊時使用了遞歸,如果需要提高效率可以改為循環(huán)(遞歸更能體現出思想且代碼簡單,循環(huán)效率更高但是復雜一丟丟,自行選取)。

#include<bits/stdc++.h>
using namespace std;
/*進程分配內存塊鏈表的首指針*/
struct allocated_block *allocated_block_head = NULL;
#define PROCESS_NAME_LEN 32   /*進程名長度*/
#define MIN_SLICE    10             /*最小碎片的大小*/
#define DEFAULT_MEM_SIZE 1024     /*內存大小*/
#define DEFAULT_MEM_START 0       /*起始位置*/
/* 內存分配算法 */
#define MA_FF 1	//first fit
#define MA_BF 2
#define MA_WF 3
#define Buddy 4 //伙伴算法 
/*描述每一個空閑塊的數據結構*/
struct free_block_type{
    int size;
    int start_addr;
    struct free_block_type *next;
};  
typedef struct free_block_type FB;
/*指向內存中空閑塊鏈表的首指針*/
struct free_block_type *free_block;//此處盡量按內存地址順序排列 ,
/*每個進程分配到的內存塊的描述*/
struct allocated_block{
    int pid;    int size;
    int start_addr;
    char process_name[PROCESS_NAME_LEN];
    struct allocated_block *next;
};
typedef struct allocated_block allocated_block_type;
//buddy
typedef struct b_free_block_type
{
	int size;
	free_block_type *list;
	b_free_block_type *next;
}b_free_block_type;
b_free_block_type *b_free_block=NULL;//空的時候要設置為NULL 
//end of buddy 

typedef struct allocated_block AB;

int mem_size=DEFAULT_MEM_SIZE; /*內存大小*/
int ma_algorithm = Buddy;           /*當前分配算法*/ //------------------------>>>>>>>>> 
static int pid = 0;                                      /*初始pid*/
int flag = 0;                             /*設置內存大小標志*/


//init_free_block(int mem_size);
int display_mem_usage();
int b_creat_free_blocks(free_block_type *ab);
int rearrange_Buddy();
int rearrange(int algorithm);
int allocate_mem(struct allocated_block *ab); 
int free_mem(struct allocated_block *ab);
int dispose(struct allocated_block *free_ab);
int disfree(FB *free_ab);
void free_f();
void free_b();
/*初始化空閑塊,默認為一塊,可以指定大小及起始地址*/
struct free_block_type* init_free_block(int mem_size)
{
	free_f();
	free_b();
    struct free_block_type *fb;
    fb=(struct free_block_type *)malloc(sizeof(struct free_block_type));
    if(fb==NULL){
        printf("Error.\n");
        getchar();
        return NULL;
	}
    fb->size = mem_size;
    fb->start_addr = DEFAULT_MEM_START;
    fb->next = NULL;

    free_block=(struct free_block_type *)malloc(sizeof(struct free_block_type));
    *free_block=*fb;	//free_block供rearrange_Buddy使用,會被銷毀 
    rearrange_Buddy();//初始化buddy算法 
    return fb;	//會在main中重新賦值free_block 
}
/*顯示菜單*/
void display_menu(){
    printf("\n");
    printf("1 - Set memory size (default=%d)\n", DEFAULT_MEM_SIZE);
    printf("2 - Select memory allocation algorithm\n");
    printf("3 - New process \n");
    printf("4 - Terminate a process \n");
    printf("5 - Display memory usage \n");
    printf("0 - Exit\n");
}
/*設置內存的大小*/
int set_mem_size(){	//只能設置一次, 清除現有所有鏈表,重新分配 
    int size;
    if(flag!=0){  //防止重復設置
        printf("Cannot set memory size again\n");
        return 0;
    }
    printf("Total memory size =");
    scanf("%d", &size);
    if(size>0) {
        mem_size = size;
       // free_block->size = mem_size;
        init_free_block(mem_size);
	    flag=1;   
		return 1;
    }
    cout<<"輸入不合法"<<endl;
    return 0;

}

/* 設置當前的分配算法 */
int set_algorithm(){
    int algorithm;
    printf("\t1 - First Fit\n");
    printf("\t2 - Best Fit \n");
    printf("\t3 - Worst Fit \n");
    printf("\t4 - Buddy \n"); 
    scanf("%d", &algorithm);
	//按指定算法重新排列空閑區(qū)鏈表
	if(algorithm==Buddy) 
   	{
   		if(ma_algorithm!=Buddy)
   			rearrange(algorithm); 	
	}
	else
	{
		if(ma_algorithm==Buddy)
			rearrange(algorithm); 	
	}
    if(algorithm>=1 && algorithm <=4)  
    {
    	ma_algorithm=algorithm;
        pid=0; 
	}
    else
    	cout<<"輸入錯誤!!!"<<endl;
    display_mem_usage();
}
/*按FF算法重新整理內存空閑塊鏈表*/
int rearrange_FF(){   
	//請自行補充
	//將buddy的二級制表示法展開,按地址排序 
	b_free_block_type *p=b_free_block;
	free_block_type *work,*twork;
	allocated_block_type *t=(allocated_block_type*)malloc(sizeof(allocated_block_type));	//最后要銷毀此臨時塊 
	//不排除其他函數調用此函數來完成內存列表的切換,需要暫時改變算法,結束之前再進行還原。
	int ma_algorithm_temp=ma_algorithm;//備份 
	ma_algorithm=MA_FF; //保證free_mem()的按照FF的方式工作 
	free_f();
	for(p=b_free_block;p!=NULL;p=p->next)
	{
		for(work=p->list;work!=NULL;work=work->next)//在f系列列表里增加內存而不銷毀b算法列表 
		{
			t->size=work->size;
			t->start_addr=work->start_addr;
			free_mem(t);	//不會刪除work
		}
	}
	//還原算法
	ma_algorithm=ma_algorithm_temp; 
	//不銷毀buddy.free內存列表 
	free(t);
}
/*按BF算法重新整理內存空閑塊鏈表*/
int rearrange_BF(){
	//請自行補充
	/*
	如果按地址排,則分配時復雜,free時方便
	如果按內存大小排,則分配時方便,free復雜
	綜合考慮,按地址排序還可以重用代碼,本程序選擇按地址排序 ——wbt 
	*/
	return rearrange_FF();
}
/*按WF算法重新整理內存空閑塊鏈表*/
int rearrange_WF(){
    //請自行補充
    return rearrange_FF();
}

int rearrange_Buddy()
{
	//盡量將ff,bf,wf的內存表示方式轉換(不足2的冪的補足,若無法補足返回0表示無法切換)
	//算了,太麻煩了還是按照只能開機設置設定吧。

	int i;
	free_b();
	b_free_block_type *p,*tp;//工作指針 
	p=b_free_block;
	while(p!=NULL) //釋放掉之前的 
	{
		tp=p->next;
		free(p);
		p=tp;
	}

	//將最大內存mem_size建立頭鏈表
	int size=1;
	while(size<mem_size)
		size<<=1;
	b_free_block=NULL;//頭插法,按從小到大排序 
	while(size)
	{
		p=(b_free_block_type*)malloc(sizeof(b_free_block_type));
		p->next=b_free_block;
		b_free_block=p;
		p->size=size;
		p->list=NULL;
		size>>=1;
	}
	//將free_block鏈表變換成b_free_block鏈表
	free_block_type *work,*twork;
	while(free_block!=NULL)	//邊轉換邊刪除 
	{
		work=free_block;
		free_block=free_block->next; 
		b_creat_free_blocks(work);	//新建一個 并插入 銷毀原來的free_block 
	}
	return 1;
}
//將free_block_type ab展開成buddy形式 銷毀原來的free_block 并進行合并檢查和操作 
int b_creat_free_blocks(free_block_type *ab)	//wbt class 3 grade 2016 
{
	b_free_block_type *p=b_free_block;
	free_block_type *work,*fwork,*twork;
	int i=0;
	while(p!=NULL && ab->size)
	{
		if(ab->size>>i&1)
		{
			if(p->list==NULL)//如果還沒有節(jié)點,則添加一個新節(jié)點 
			{
				work=p->list=(free_block_type*)malloc(sizeof(free_block_type));
				work->size=p->size;
				ab->size-=work->size;
				work->start_addr=ab->start_addr;
				ab->start_addr+=work->size;

				work->next=NULL;
				continue;
			}
			//從列表里選取合適的位置插入并上尋找伙伴合并(保證列表按地址排序) 
			//兩個塊由一個塊分裂而來的伙伴條件:判斷地址是不是2的冪次就可以
			fwork=NULL;
			for(work=p->list;/*work!=NULL,此處要考慮掛在尾巴上*/;work=work->next)
			{

				if(work==NULL || work->start_addr > ab->start_addr)//把一部分(p->size)插在work和fwork之間 
				{	//找到了位置,準備插入節(jié)點

					twork=(free_block_type*)malloc(sizeof(free_block_type));//新增的塊不用free 
					twork->size = p->size;
					twork->start_addr=ab->start_addr;
					ab->start_addr+=p->size; //留下低地址的部分,帶著高地址部分繼續(xù)尋找插入點 
					ab->size-=p->size;
					if(work==NULL)//掛在尾巴上  twork插在fwork和NULL(wrok)之間 
					{//特點是work=NULL,fwork!=NULL
						fwork->next=twork;
						twork->next=NULL;
						//看看能不能和前面的合并 
						work=twork;//合并統一性要求 (因為work==NULL,所以將twork替換work,即相當于對next之后的不會改動) 

					}
					else if(work==p->list)//第一個(沒有前驅) 
					{//特點是fwork==NULL ,work==p->list不等于NULL 
						twork->next=p->list;
						p->list=twork;//頭插法直接插在頭部 
						//只檢查后面的能不能合并
					}
					else//有前驅和后繼  插在fwork之后,work之前 
					{//特點是fwork和work都不為NULL 
						twork->next=work;
						fwork->next=twork;
						//和前面合并 
					}
					//兩個塊由一個塊分裂而來的伙伴條件:判斷地址是不是2的冪次就可以

					//和前面合并 (如果有前驅的話)  合并fwork 和 twork 其中twork->next= work  fwork的地址必須是size*2的倍數 
					if(fwork!=NULL && fwork->size + fwork->start_addr==twork->start_addr && fwork->start_addr%(p->size<<1)==0)
					{
						fwork->next=twork->next;
						fwork->size+=twork->size;
						//刪除fwork,因為合并一定能向上冒泡
						if(p->list==fwork)//如果fwork為第一個 
						{
							p->list=fwork->next;//刪除第一個 
						}
						else
						{
							//twork初值為第一個 
							for(work=p->list;work->next!=fwork;work=twork->next)
								;
							//此時twork為fwork的前驅
							work->next=fwork->next;
						}
						b_creat_free_blocks(fwork);//遞歸調用 
						free(twork); 
					}
					//檢查后面的能不能合并(如果有后繼的話)
					if(twork->next!=NULL && twork->start_addr+twork->size == twork->next->start_addr && twork->start_addr%(p->size<<1)==0)						
					{
						work=twork->next;
						twork->next=work->next;
						twork->size+=work->size;
						//twork為新塊  現在要刪掉twork向上冒泡 
						if(twork==p->list)//如果twork是第一個,即沒有前驅
						{
							p->list=twork->next;
						}
						else
						{
							fwork->next=twork->next;
						}
						b_creat_free_blocks(twork);
						free(work);	//和后面的二合一  
					}
				//	free(twork);
					break;//break for
				}
				else//設置前驅,繼續(xù)尋找 
				{
					fwork=work;
				}
			}//end of for 
		}//end of if
		p=p->next;
		i++;
	}//end of while
	free(ab);
	ab=NULL;
	return 1;
}

/*創(chuàng)建新的進程,主要是獲取內存的申請數量*/
int new_process(){
    struct allocated_block *ab;
    int size;    int ret;
    ab=(struct allocated_block *)malloc(sizeof(struct allocated_block));
    if(!ab) exit(-5);
    ab->next = NULL;
    pid++;
    sprintf(ab->process_name, "PROCESS-%02d", pid);
    ab->pid = pid;    
    printf("Memory for %s:", ab->process_name);
    scanf("%d", &size);
    if(size>0) ab->size=size;
    ret = allocate_mem(ab);  /* 從空閑區(qū)分配內存,ret==1表示分配ok*/

 /*如果此時allocated_block_head尚未賦值,則賦值*/
    if((ret==1) &&(allocated_block_head == NULL)){ 
        allocated_block_head=ab;
        return 1;        }
    /*分配成功,將該已分配塊的描述插入已分配鏈表*/
    else if (ret==1) {
        ab->next=allocated_block_head;//頭插法 
        allocated_block_head=ab;
        return 2;        }
    else if(ret==-1){ /*分配不成功*/
        printf("Allocation fail\n");
        free(ab);
        return -1;       
     }
    return 3;
    }
/*分配內存模塊*/
int allocate_mem(struct allocated_block *ab){	//ff bf
    struct free_block_type *fbt, *pre;
    int request_size=ab->size;
    fbt = pre = free_block;
    //根據當前算法在空閑分區(qū)鏈表中搜索合適空閑分區(qū)進行分配,分配時注意以下情況:
    // 1. 找到可滿足空閑分區(qū)且分配后剩余空間足夠大,則分割
    // 2. 找到可滿足空閑分區(qū)且但分配后剩余空間比較小,則一起分配
    // 3. 找不可滿足需要的空閑分區(qū)但空閑分區(qū)之和能滿足需要,則采用內存緊縮技術,進行空閑分區(qū)的合并,然后再分配
    // 4. 在成功分配內存后,應保持空閑分區(qū)按照相應算法有序
    // 5. 分配成功則返回1,否則返回-1
	// 請自行補充。。。。。
	if(ma_algorithm==MA_FF)	//FF	從低地址往高地址分配 
	{
		for(FB* p=free_block;p!=NULL;p=p->next)
		{
			if(p->size >= request_size)//遇到即分配  FF
			{
				ab->start_addr=p->start_addr;
				p->start_addr+=request_size;
				p->size-=request_size;
				if(p->size<MIN_SLICE)
				{
					ab->size+=p->size;
					//刪除free block 
					disfree(p); 
				}
				/*		從高往低 
				ab->start_addr=p->start_addr;
				p->size-=request_size;
				*/
				return 1;
			}
		}
		printf("FF裝載失敗\n");
		return 0;
	}
	else if(ma_algorithm==MA_BF)//free block按地址排 
	{
		FB *p=free_block;
		FB *mini;
		int minv;
		int fg=0; 
		for(;p!=NULL;p=p->next)//遍歷找到差距最小能裝載的free block    從低地址開始裝載 
		{
			if(p->size>=request_size)
			{
				if(fg==0)
				{
					minv=p->size - request_size;
					mini=p; 
					fg=1;
				}
				else
				{
					if(p->size-request_size<minv)
					{
						minv=p->size-request_size;
						mini=p;
					}
				}
			}

		}
		if(fg)
		{
			//ab 為要分配的
			ab->start_addr=mini->start_addr;
			mini->start_addr+=request_size;
			mini->size-=request_size;
			if(mini->size<MIN_SLICE)
			{
				ab->size+=mini->size;
				//刪除free block 
				disfree(mini); 
			}
			return 1;
		}
		printf("BF裝載失敗\n");
		return 0;
	}
	else if(ma_algorithm==MA_WF) //free block按地址排 
	{
		FB *p=free_block;
		FB *maxi;
		int maxv;
		int fg=0; 
		for(;p!=NULL;p=p->next)//遍歷找到差距最小能裝載的free block    從低地址開始裝載 
		{
			if(p->size>=request_size)
			{
				if(fg==0)
				{
					maxv=p->size - request_size;
					maxi=p; 
					fg=1;
				}
				else
				{
					if(p->size-request_size>maxv)
					{
						maxv=p->size-request_size;
						maxi=p;
					}
				}
			}
		}
		if(fg)
		{
			//ab 為要分配的
			ab->start_addr=maxi->start_addr;
			maxi->start_addr+=request_size;
			maxi->size-=request_size;
			if(maxi->size<MIN_SLICE)
			{
				ab->size+=maxi->size;
				//刪除free block 
				disfree(maxi);
			}
			return 1;
		}
		printf("WF裝載失敗\n");
		return 0;
	}
	else if(ma_algorithm==Buddy)
	{
		if((ab->size)&(ab->size-1))
		{
			long long i=1;
			//不是2的冪次  將ab->size向上尋找一個2的冪次 
			for(;i<ab->size;i<<=1)
				;
			ab->size=i;
		}
		//從b_free_block中從小到大找到一個能裝下的2的次方
		b_free_block_type *p=b_free_block;
		while(p!=NULL)
		{
			if(p->size>=ab->size && p->list!=NULL)
			{
				break;
			}
			p=p->next;
		}

		if(p!=NULL)//找到了可用塊
		{
			//list按照地址排序的,現在按小地址優(yōu)先的原則分配
			free_block_type *work=p->list;
			work->size-=ab->size;
			ab->start_addr=work->start_addr;
			work->start_addr+=ab->size;

			p->list=work->next;
			b_creat_free_blocks(work);	//將剩余的分割掛在鏈表合適位置 
			return 1;
		}
		else
		{
			printf("Buddy裝載失敗\n"); 
			return 0;
		}
	}//end of buddy
//	return 1;
}

struct allocated_block* find_process(int pid)
{
	int i;
	struct allocated_block *p=allocated_block_head;
	for(;p!=NULL;p=p->next)
	{
		if(p->pid==pid)
			return p;
	}
	return NULL;
}
/*刪除進程,歸還分配的存儲空間,并刪除描述該進程內存分配的節(jié)點*/
int kill_process(){
    struct allocated_block *ab;
    int pid;
    printf("Kill Process, pid=");
    scanf("%d", &pid);
    ab=find_process(pid);
    if(ab!=NULL){
        free_mem(ab); /*釋放ab所表示的分配區(qū)*/
        dispose(ab);  /*釋放ab數據結構節(jié)點*/
    }
    else
    {
    	cout<<"不存在"<<endl; 
	}
}
/*將ab所表示的已分配區(qū)歸還,并進行可能的合并*/
int free_mem(struct allocated_block *ab){	//不會刪除ab 
    struct free_block_type *fbt=NULL, *pre, *work;
  //  fbt=(struct free_block_type*) malloc(sizeof(struct free_block_type));
  //  if(!fbt) return -1;
    // 進行可能的合并,基本策略如下
    // 1. 將新釋放的結點插入到空閑分區(qū)隊列末尾
    // 2. 對空閑鏈表按照地址有序排列
    // 3. 檢查并合并相鄰的空閑分區(qū)
    // 4. 將空閑鏈表重新按照當前算法排序   /->  本程序不做次操作 
    //請自行補充……

    if(ma_algorithm==MA_FF || ma_algorithm== MA_BF || ma_algorithm== MA_WF)//free鏈表都是按地址排序,所以可以用同一種方式free 
    {
    	FB *fp=NULL,*p=NULL;
    	for(p=free_block;p!=NULL && p->start_addr < ab->start_addr;p=p->next)//尋找block屬于哪一個free block 
    	{
    		fp=p;
    			//前一個小于,后一個大于 
		}
		//fp為前一個free_block  ab為當前要free的block  p為下一個 
		if(fp!=NULL && fp->start_addr+fp->size==ab->start_addr)
		{	//當前面還有東西,就往前合并。。。,//???但如果,已經是最低地址的塊了 就不往前找了 我好像說了個廢話 ???
			fp->size+=ab->size;
			//fp為前面的塊,p為后面的塊 
			printf("和前一個合并\n");//不新增free_blocks
			if(p!=NULL && p->start_addr == fp->start_addr + fp->size)//用前面的free block嘗試往后合并 
			{
			//	fp->start_addr = p->start_addr;
				fp->size += p->size;
				fp->next=p->next; 
				free(p);//前后合并,刪除后面的free block 
				printf("和后一個合并"); 
				return 1;
			}
			return 1;
		}
		else if(p!=NULL && p->start_addr == ab->start_addr + ab->size)//嘗試往后合并	p為后面的 
		{
			p->start_addr = ab->start_addr;
			p->size += ab->size;
			printf("和后一個合并");
			return 1;
		}
		//無法合并,直接釋放,free鏈表插入節(jié)點 fp為前一個,p為后一個 fbt為新的free block 
	//	display_mem_usage();
		fbt=(struct free_block_type*) malloc(sizeof(struct free_block_type));
		fbt->size=ab->size;
		fbt->start_addr=ab->start_addr;
		fbt->next=p;
		if(fp==NULL)
		{
			fbt->next=free_block;//頭插法 
			free_block=fbt;
		}
		else
		{
			fp->next=fbt;
		}
		printf("沒法合并,直接釋放");
	}
	else if(ma_algorithm==Buddy)
	{
		fbt=(struct free_block_type*) malloc(sizeof(struct free_block_type));
  		if(!fbt) return -1;
  		fbt->size=ab->size;
  		fbt->start_addr=ab->start_addr;
  		return b_creat_free_blocks(fbt);//會銷毀fbt 
	}
    return 1;
}
/*釋放ab數據結構節(jié)點*/
int dispose(struct allocated_block *free_ab){
    struct allocated_block *pre, *ab;
   if(free_ab == allocated_block_head) { /*如果要釋放第一個節(jié)點*/
     allocated_block_head = allocated_block_head->next;
        free(free_ab);
        return 1;
        }
    pre = allocated_block_head;  
    ab = allocated_block_head->next;
    while(ab!=free_ab){ pre = ab;  ab = ab->next; }
    pre->next = ab->next;
    free(ab);
    return 2;
   }
int disfree(FB *free_ab){
    FB *pre, *ab;
   if(free_ab == free_block) { /*如果要釋放第一個節(jié)點*/
     free_block = free_block->next;
        free(free_ab);
        return 1;
        }
    pre = free_block;  
    ab = free_block->next;
    while(ab!=free_ab){ pre = ab;  ab = ab->next; }
    pre->next = ab->next;
    free(ab);
    return 2;
   }
/* 顯示當前內存的使用情況,包括空閑區(qū)的情況和已經分配的情況 */
int display_mem_usage(){
	if(ma_algorithm==Buddy)
	{
		cout<<"free blocks:"<<endl;
		b_free_block_type *p=b_free_block;
		while(p!=NULL)
		{
			printf("list:%d-----------<\n",p->size);
			free_block_type *work=p->list;
			while(work!=NULL)
			{
				printf("·start_adress:%-3d  size:%-3d\n",work->start_addr,work->size);
				work=work->next;
			}
			p=p->next;

		}
		 struct free_block_type *fbt=free_block;
		    struct allocated_block *ab=allocated_block_head;
			printf("\nUsed Memory:\n");
		    printf("%10s %20s %10s %10s\n", "PID", "ProcessName", "start_addr", " size");
		    while(ab!=NULL){
	        printf("%10d %20s %10d %10d\n", ab->pid, ab->process_name, ab->start_addr, ab->size);
	        ab=ab->next;
	        }
	}
    else 
    {
    	struct free_block_type *fbt=free_block;
	    struct allocated_block *ab=allocated_block_head;
	    if(fbt==NULL) return(-1);
	    printf("\tw.%s.t.\n","b"); 
	    printf("----------------------------------------------------------\n");

	    /* 顯示空閑區(qū) */
	    printf("Free Memory:\n");
	    printf("%20s %20s\n", "      start_addr", "       size");
	    while(fbt!=NULL){
	        printf("%20d %20d\n", fbt->start_addr, fbt->size);
	        fbt=fbt->next;
	        }    
	/* 顯示已分配區(qū) */
	    printf("\nUsed Memory:\n");
	    printf("%10s %20s %10s %10s\n", "PID", "ProcessName", "start_addr", " size");
	    while(ab!=NULL){
	        printf("%10d %20s %10d %10d\n", ab->pid, ab->process_name, ab->start_addr, ab->size);
	        ab=ab->next;
	        }
	    printf("----------------------------------------------------------\n");
	    return 0;
	}
}

	/*按指定的算法整理內存空閑塊鏈表*/
int rearrange(int algorithm){
    switch(algorithm){
        case MA_FF:  rearrange_FF(); break;
        case MA_BF:  rearrange_BF(); break;
        case MA_WF: rearrange_WF(); break;
        case Buddy:rearrange_Buddy();break;
		}
}
void free_f()
{
	struct allocated_block *f,*p;
	f=allocated_block_head;
	for(;f!=NULL;f=p)
	{
		p=f->next;
		free(f);
	}
	allocated_block_head=NULL;
	struct free_block_type *fn,*fp;
	fp=free_block;
	for(;fp!=NULL;fp=fn)
	{
		fn=fp->next;
		free(fp);
	}
	free_block=NULL;
}
void free_b()
{
	b_free_block_type *p,*tp;
	free_block_type *work,*t;
	p=b_free_block;
	while(p!=NULL)
	{
		tp=p;
		p=p->next;
		work=tp->list;
		while(work!=NULL)
		{
			t=work;
			work=work->next;
			free(t);
		}
		free(tp);
	}
	b_free_block=NULL;
}
void do_exit()
{
	free_f();
	free_b();
}

main(){
    char choice;      pid=0;
    free_block = init_free_block(mem_size); //初始化空閑區(qū)
    while(1) {
	    display_menu();	//顯示菜單
	    cout<<"當前算法編號:"<<ma_algorithm<<endl;
	    fflush(stdin);
	    choice=getchar();	//獲取用戶輸入
	    switch(choice)
		{
	        case '1': set_mem_size(); break; 	//設置內存大小   //開機只能設置一次,且只能在分配之前設置 
	        case '2': set_algorithm();flag=1; break;//設置算法
	        case '3': new_process(); flag=1; break;//創(chuàng)建新進程
	        case '4': kill_process(); flag=1;   break;//刪除進程
	        case '5': display_mem_usage();    flag=1; break;	//顯示內存使用
	        case '0': do_exit(); exit(0);	//釋放鏈表并退出
	        default: break;      
		}
	} 
}

六、應對內存碎片的策略

6.1內存池技術

內存池(Memory Pool)是一種內存分配方式。 通常我們習慣直接使用new、malloc等API申請分配內存,這樣做的缺點在于:由于所申請內存塊的大小不定,當頻繁使用時會造成大量的內存碎片并進而降低性能。

內存池則是在真正使用內存之前,先申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠再繼續(xù)申請新的內存。這樣做的一個顯著優(yōu)點是盡量避免了內存碎片,使得內存分配效率得到提升。

在c++程序設計中,一般在沒有什么特殊要求時,都是使用new 與 delete 來分配和釋放動態(tài)內存,這幾乎時直接在與操作系統的內存管理系統進行交互。當應用程序需要的動態(tài)內存很小的時候,這不是什么問題,但如果應用程序需要的動態(tài)內存不僅量很大,而且對動態(tài)內存的申請和釋放又極其頻繁,那么問題就很大。

操作系統對系統內存的管理是一件極其繁瑣的事,用戶對動態(tài)內存的每一次請求都要經過一道繁瑣的手續(xù)才能獲得成功,就是銷毀內存也是一件不太容易的事,因為每次系統的內存管理系統都要對內存的使用情況進行整理和記錄。簡單的說就是,每次系統的內存管理系統來使用內存,既費時又費力。

那么解決這個這個問題的方法就是“批發(fā)”,即一次申請一大塊內存,把這大塊內存作為應用程序的內存儲備,每當需要一小塊內存的時候就到這個儲備的內存中來取用,不用了就歸還給這個內存,程序就快捷多了。總有一些意外會使初始申請的這塊內存儲備也不夠,那么就再申請一塊內存,然后把它與原有的內存塊用鏈表連接起來組成更大的內存儲備。這種由應用程序儲備的內存就叫做內存池。

使用內存池另一個好處就是能夠有效的防治內存碎片華,因為他總是大塊大塊的申請內存,因此對于系統內存管理系統來講,這是個省時省力的好事。

以游戲開發(fā)中頻繁創(chuàng)建和銷毀子彈對象為例,假設每個子彈對象大小為 32 字節(jié)。如果使用傳統的內存分配方式,每次創(chuàng)建子彈都調用new,銷毀時調用delete,隨著游戲中子彈數量的增多,內存碎片會迅速增加。而使用內存池技術,在游戲初始化時,就預先分配一塊大內存,比如 1024 字節(jié)。將這塊大內存按照 32 字節(jié)的大小劃分成 32 個內存塊,組成一個內存池。

當需要創(chuàng)建子彈對象時,直接從內存池中取出一個空閑的內存塊來使用,而不需要向操作系統申請新的內存。當子彈對象銷毀時,將其占用的內存塊放回內存池,而不是直接歸還給操作系統。這樣,因為內存池中的內存塊大小通常是固定的,所以可以避免內部碎片的產生。同時,當內存塊被釋放時,可以直接放回內存池中,而不需要進行復雜的內存合并操作,從而減少外部碎片的產生 。

6.2選擇合適的內存分配策略

不同的內存分配策略對內存碎片的產生有不同的影響 。在 C++ 中,我們可以根據程序的特點選擇合適的內存分配策略。例如,對于需要頻繁分配和釋放小對象的程序,可以考慮使用基于對象池的分配策略;對于需要分配大量連續(xù)內存的程序,可以考慮使用線性分配器等。此外,還可以使用一些智能的內存分配算法,如伙伴系統(buddy system)、slab 分配器等。這些算法可以根據內存需求的大小自動選擇合適的內存塊進行分配,從而減少內存碎片的產生 。

在一個網絡通信程序中,會頻繁地接收和發(fā)送小的數據包,每個數據包大小可能在幾十到幾百字節(jié)不等。這時使用基于對象池的分配策略就很合適。預先創(chuàng)建一個包含多個固定大小內存塊的對象池,每個內存塊的大小剛好能容納一個最大尺寸的數據包。當有數據包到來時,直接從對象池中獲取一個空閑內存塊來存儲數據包,處理完后再將內存塊放回對象池。

而在一個 3D 建模軟件中,可能需要分配大量連續(xù)內存來存儲模型的頂點數據、紋理數據等。這種情況下,使用線性分配器,一次性從內存中分配一大塊連續(xù)的內存區(qū)域,然后按照需求在這塊區(qū)域內進行分配和管理,就可以滿足對大量連續(xù)內存的需求,同時減少內存碎片的產生 。

6.3及時釋放不再使用的內存

及時釋放不再使用的內存是減少內存碎片的重要方法之一 。在 C++ 中,我們可以使用智能指針等技術來自動管理內存的分配和釋放,避免內存泄漏問題。同時,在程序中要注意及時清理不再使用的對象,釋放其占用的內存。例如,在使用std::vector等容器時,如果不再需要容器中的元素,可以使用clear()方法清空容器,釋放內存。對于動態(tài)分配的內存,可以在使用完畢后及時使用delete或delete[]運算符釋放內存 。

在一個圖像編輯程序中,當用戶關閉一個圖像文件時,程序應該及時釋放用于存儲該圖像數據的內存。如果使用智能指針來管理圖像數據的內存,當智能指針超出作用域時,它會自動釋放所指向的內存。比如std::unique_ptr<ImageData> image(new ImageData());,當image變量離開其作用域時,ImageData對象所占用的內存會被自動釋放,這樣就避免了內存泄漏,也減少了內存碎片的產生 。

6.4避免頻繁的內存分配和釋放操作

頻繁的內存分配和釋放操作是導致內存碎片產生的主要原因之一 。在編寫程序時,可以盡量減少不必要的內存分配和釋放操作。例如,可以在程序啟動時一次性分配足夠的內存,然后在程序運行過程中根據需要進行復用,而不是每次需要時都進行新的內存分配。此外,還可以使用對象緩存等技術,將不再使用的對象緩存起來,以便下次需要時直接復用,而不需要進行新的內存分配 。

在一個數據庫管理系統中,連接數據庫是一個比較耗時的操作,同時也會涉及到內存的分配和釋放。如果每次執(zhí)行數據庫查詢都重新建立連接,就會頻繁地進行內存分配和釋放操作。可以在系統啟動時建立一個數據庫連接池,一次性分配好多個數據庫連接對象所需的內存。

當有查詢請求時,從連接池中獲取一個空閑的連接,使用完畢后再放回連接池,而不是每次都重新分配和釋放連接對象的內存。這樣不僅減少了內存分配和釋放的次數,降低了內存碎片產生的可能性,還提高了系統的性能 。

七、解決內存碎片方法

7.1內部碎片的解決

在操作系統的內存管理體系中,內部碎片就像是一個如影隨形的 “頑疾”。目前,大多數操作系統采用以頁(通常為 4KB)為單位的內存分配策略,這種固定粒度的分配方式,從根源上決定了內部碎片無法被徹底消除。

打個比方,內存分配就像在超市購物后用固定規(guī)格的塑料袋裝物品。當程序向系統請求 3KB 的內存時,就好比你只買了少量物品,系統卻必須給你一個能裝 4KB “貨物” 的塑料袋 —— 它無法拆分出一個剛好容納 3KB 內存的 “小袋子”,只能分配一整頁 4KB 的內存空間。如此一來,就有 1KB 的內存空間被閑置,這 “多出來” 的 1KB,便是內部碎片。

由于程序對內存的需求千差萬別,有的程序需要幾十 KB,有的則需要幾百 MB,無論操作系統如何優(yōu)化,都很難讓每次分配的內存都能被程序精準 “填滿”。這就如同用標準大小的塑料袋去裝各式各樣的物品,總會出現袋子裝不滿的情況。所以在當前以頁為單位的內存分配機制下,內部碎片的存在幾乎是必然的,它就像內存管理領域的 “黑洞”,默默吞噬著部分寶貴的內存資源。

圖片

盡管內部碎片如同內存管理領域的 “幽靈”,無法被徹底驅逐,但我們仍能通過巧妙的策略和技術,將其帶來的負面影響降至最低。在 C/C++ 編程世界中,經典的malloc內存分配函數與強大的 STL(標準模板庫),就像兩位 “內存管理大師”,各自施展著獨特的 “功夫”,在減少內存碎片的戰(zhàn)場上大展身手。接下來,就讓我們深入探究它們是如何與內存碎片 “過招”,守護程序的內存健康的。

第一種解決方法——malloc內存分配函數。

想要理解malloc如何優(yōu)化內存碎片問題,首先需要對它的內存分配機制有個基礎認知。雖然malloc在不同的系統和庫實現中存在多種版本(如 glibc 中的 ptmalloc、jemalloc 等,各自有復雜精妙的實現邏輯),但我們可以通過一個簡化模型,快速抓住它的核心原理。

malloc就像一位精明的 “內存管家”,在程序內部精心維護著一座專屬的 “內存?zhèn)}庫”—— 內存池。當程序啟動,或是第一次調用malloc函數時,這位 “管家” 就會主動出擊,向操作系統申請一塊面積可觀的連續(xù)內存區(qū)域。此后,程序中所有的內存分配請求,都將從這座 “內存?zhèn)}庫” 中按需提取,而不再頻繁地與操作系統 “打交道”。

圖片

程序運行時,頻繁的內存分配與釋放會把內存池 “切割” 成一塊塊不連續(xù)的碎片。為了管理這些碎片,malloc會在內存池里維護一張 “狀態(tài)清單”,用隱式鏈表法記錄每個內存塊是已被占用還是空閑可用。這種方法就像在圖書館里標記每本書的借閱狀態(tài)一樣,讓malloc能快速找到合適的內存塊分配給程序。

圖片圖片

malloc管理的內存塊就像用鏈條串起來的珠子,已分配和空閑的內存塊都通過指針首尾相連。每個內存塊的具體結構是這樣的:

圖片圖片

每個內存塊里除了存放用戶要用的數據,還 “藏” 著一些關鍵信息,比如塊的大小、有沒有被占用。另外,每個塊都帶一個 “小尾巴”(指針),指向下一個內存塊。靠著這些設計,不管內存塊是閑著還是正在被用,都能像火車車廂一樣串聯起來。

圖片

當程序調用malloc申請內存時,它會優(yōu)先在自己的內存池里 “翻箱倒柜”,找一塊大小合適的空閑內存塊分配出去,而不是每次都麻煩操作系統重新劃撥內存。

為了找到大小合適的空閑內存塊,malloc有幾種常用的 “尋寶策略”:

  • 首次適應算法(First Fit):從空閑分區(qū)表的第一個表目起查找該表,把最先能夠滿足要求的空閑區(qū)分配給作業(yè),這種方法的目的在于減少查找時間。為適應這種算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)要按地址由低到高進行排序。該算法優(yōu)先使用低址部分空閑區(qū),在低址空間造成許多小的空閑區(qū),在高地址空間保留大的空閑區(qū)。
  • 最佳適應算法(Best Fit):從全部空閑區(qū)中找出能滿足作業(yè)要求的、且大小最小的空閑分區(qū),這種方法能使碎片盡量小。為適應此算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)要按從小到大進行排序,自表頭開始查找到第一個滿足要求的自由分區(qū)分配。該算法保留大的空閑區(qū),但造成許多小的空閑區(qū)。
  • 最差適應算法(Worst Fit):從全部空閑區(qū)中找出能滿足作業(yè)要求的、且大小最大的空閑分區(qū),從而使鏈表中的結點大小趨于均勻,適用于請求分配的內存大小范圍較窄的系統。為適應此算法,空閑分區(qū)表(空閑區(qū)鏈)中的空閑分區(qū)按大小從大到小進行排序,自表頭開始查找到第一個滿足要求的自由分區(qū)分配。該算法保留小的空閑區(qū),盡量減少小的碎片產生。

要是內存池里的 “存貨” 不夠用了,malloc就會去找操作系統 “補貨”。這時候,它會通過調用brk或者mmap這兩個 “采購員”,向操作系統申請更多內存資源 。

  • brk系統調用:brk用于將進程的數據段(堆)的邊界向上擴展(增加)或向下收縮(減少)。它通過設置進程的break指針來實現內存的分配和釋放。當調用brk時,內核會調整break指針的位置,將堆空間擴大或縮小。如果是擴大堆空間,新分配的內存就在原來堆的頂部連續(xù)增加;如果是縮小堆空間,內核會釋放break指針以下的部分內存。
  • mmap系統調用:mmap用于將一個文件或設備的內存區(qū)域映射到進程的虛擬地址空間中。它可以將磁盤文件的內容直接映射到進程的地址空間,使得進程可以像訪問內存一樣訪問文件內容,也可以用于分配匿名內存(即不與任何文件關聯的內存)。當使用mmap分配內存時,內核會在進程的虛擬地址空間中找到一段合適的空閑區(qū)域,并將其與物理內存建立映射關系。

為什么malloc對大塊內存(≥128KB)和小塊內存(<128KB)采用不同的分配策略?

主要有兩個原因:

①效率權衡

  • brk:通過移動堆頂指針分配內存,操作輕量但只能釋放堆頂內存(類似棧結構)。小塊內存頻繁分配釋放時,這種方式更高效。
  • mmap:每次創(chuàng)建獨立的內存映射,釋放時可立即回收,但創(chuàng)建映射的開銷較大。大塊內存使用 mmap 避免長期占用堆空間,導致碎片累積。

②碎片控制

  • 大塊內存若用 brk 分配,會導致堆頂長期被占用,后續(xù)釋放時無法回收中間的空閑空間,形成外部碎片。
  • mmap 分配的內存獨立于堆,釋放后可完全回收,避免碎片影響堆的連續(xù)性。

簡單說:小塊用 brk(快),大塊用 mmap(避免碎片),這是性能與內存利用率的平衡之道。

第二種解決方法——STL二級空間配置器。

STL(標準模板庫)為了更高效地管理內存,對new運算符進行了 “魔改”,引入了二級空間配置器(__default_alloc_template)。這個配置器有兩種 “工作模式”:

  • 第一級空間配置器:直接使用 malloc/free 分配內存。
  • 第二級空間配置器:當要分配內存大于 128 字節(jié)由第一級空間分配器分配。當要分配內存小于 128 字節(jié),從內存池中分配。

第二級空間配置器分為兩部分:空閑鏈表(free_list)與內存池。

圖片圖片

第二級空間配置器就像是一個精密的 “內存便利店”,內部維護著 16 個 “貨架”(空閑鏈表),每個貨架專門存放一種固定尺寸的 “商品”(內存塊)。這些 “商品” 的規(guī)格從 8 字節(jié)開始,以 8 字節(jié)為一檔遞增,最大到 128 字節(jié)。每個 “貨架” 上的內存塊都通過指針串成了一條 “鏈子”,就像超市里掛著的一排掛鉤,方便快速取用和歸還。

查找合適的空閑鏈表:根據所需分配的內存大小,計算出對應的空閑鏈表編號。例如,若要分配 20 字節(jié)的內存,會將其調整為 24 字節(jié)(8 的倍數),然后找到對應 24 字節(jié)的空閑鏈表。

檢查空閑鏈表是否為空:

  • 不為空:從鏈表頭部取出一個節(jié)點,將其從鏈表中移除,然后返回該節(jié)點的地址給用戶。
  • 為空:調用 refill 函數重新填充該空閑鏈表。refill 函數會調用 chunk_alloc 從內存池分配一大塊內存,接著將其分割成多個小塊,插入到空閑鏈表中,最后返回其中一個小塊給用戶。

refill和chunk_alloc就像是內存池的 “搬運工”,專門負責從系統申請內存。其中refill函數的工作流程是這樣的:

  • 調用 chunk_alloc 分配內存:嘗試從內存池分配一定數量(通常為 20 個)的指定大小的內存塊。
  • 分割內存塊:將分配到的大塊內存分割成多個小塊。
  • 插入空閑鏈表:把除第一個小塊之外的其他小塊插入到對應的空閑鏈表中,第一個小塊返回給用戶。

chunk_alloc函數是內存池的 “材料供應商”,它的工作流程主要包括:

  • 檢查內存池剩余空間:
  • 足夠:直接從內存池分配所需數量的內存塊。
  • 部分足夠:分配盡可能多的內存塊,調整剩余空間。
  • 不足:嘗試從堆中分配更多內存,更新內存池的起始和結束指針。如果堆內存不足,會嘗試從其他空閑鏈表中尋找可用的內存塊。

圖片圖片

當程序釋放內存時,二級空間配置器會像圖書管理員整理書架一樣,執(zhí)行以下操作:

  • 查找合適的空閑鏈表:根據釋放的內存塊大小,找到對應的空閑鏈表。
  • 將內存塊插入鏈表頭部:把釋放的內存塊轉換為空閑內存塊類型,將其空閑鏈表頭指針指向當前鏈表頭部,然后更新鏈表頭部指針為該內存塊。

STL 二級空間配置器通過空閑鏈表管理小內存塊,將內存按 8 字節(jié)對齊分類存儲(8B~128B 共 16 類)。這種設計就像圖書館的書架系統,不同大小的 “書籍”(內存塊)存放在專屬區(qū)域,分配 / 回收時直接從對應鏈表取 / 還,無需頻繁向操作系統申請內存,既減少了系統調用開銷,又避免了內存碎片,顯著提升了小對象的內存管理效率。

7.2外部碎片的解決

外部碎片的治理主要依賴操作系統的物理內存管理策略。以 Linux 為例,它運用伙伴系統算法對物理頁進行管理,通過將內存塊按 2 的冪次大小劃分,在內存分配與回收時合并相鄰空閑塊,以此有效減少外部碎片問題。

第一種解決方法——伙伴系統算法。

在 Linux 系統里,物理內存以頁為最小單位分配,由伙伴系統算法統一管理。系統維護著 12 條空閑塊鏈表,專門用來記錄所有可用的物理頁。

Linux 伙伴系統的 12 個空閑塊鏈表,按內存塊大小由小到大排列:

  • list[0]存放 1 頁(2^0)大小的空閑塊
  • list[1]存放 2 頁(2^1)大小的空閑塊
  • 依此類推,每個鏈表的內存塊大小都是前一個的 2 倍

圖片圖片

當系統收到內存分配請求時,會按這樣的步驟快速響應:

  1. 計算頁數:將請求大小(如 16KB)除以頁框大小(如 4KB),得到所需頁數(4 頁)
  2. 確定鏈表索引:通過公式log2(頁數)找到對應鏈表索引(log2 (4)=2,即list[2])
  3. 分配內存:直接從list[2]提取一個 4 頁的空閑塊

找到對應空閑鏈表后,操作系統就開始 “取貨”:如果鏈表中有存貨(存在空閑內存塊),就直接拿走一塊分配出去,同時更新鏈表狀態(tài),標記這塊內存已被占用。

要是對應鏈表沒 “貨”(為空),系統就從 “大倉庫” 里拆內存:去下一個更大的空閑鏈表找內存塊,找到后一分為二,一半用來滿足當前需求,另一半放回對應大小的鏈表。這新分出的兩塊內存就像 “孿生兄弟”,被稱為伙伴塊。

圖片圖片

分配內存時,如果當前鏈表沒空閑塊,系統會 “層層上找”,直到找到有空閑塊的鏈表,或者找遍所有鏈表。要是最大的鏈表都沒可用內存,那就只能提示內存不足、分配失敗了。

釋放內存時,系統先把內存塊標記為空閑,再減少其引用計數。當計數歸零,這塊內存就能被回收。回收時,系統會根據內存塊的地址和大小,算出它的 “伙伴” 地址 —— 所謂 “伙伴”,就是當初一起從大內存塊拆分出來、大小相同且緊挨著的另一塊內存。

圖片圖片

回收內存時,系統會先查看目標內存塊的 “伙伴” 狀態(tài):

  • 伙伴空閑:立即將兩者合并成更大的塊,放入對應鏈表,接著檢查新塊的伙伴是否也空閑,循環(huán)合并,直到沒有可合并的伙伴。
  • 伙伴被占用:直接把釋放的內存塊放進對應大小的空閑鏈表。

伙伴系統這種 “找搭檔 - 合并” 的設計,讓內存分配又快又準,回收時還能把相鄰的空閑塊 “拼” 起來,減少碎片。不管是分配小頁還是大連續(xù)區(qū)域,都能高效搞定,大幅提升內存使用效率。

第二種解決方法——slab分配器。

在以 “頁” 為單位的大尺度內存管理上,Linux 依靠伙伴系統避免碎片化;但面對一頁內存內部的 “小空間”,伙伴系統就力不從心了。這時,Linux 會啟用 slab 分配器,專門處理頁內內存,進一步減少碎片問題。

slab 分配器就像一個 “小型內存管家”,專門負責管理小尺寸的內存對象。它的核心策略是:把內核常用的對象預先 “存” 在 slab 緩存里,隨時待命。當程序需要內存時,直接從緩存拿對象,不用每次都找伙伴系統 “要新頁”。slab 分配器由緩存、slab 和對象三層結構組成:

  • 緩存:針對不同類型對象(如文件描述符結構體),建立專屬 “倉庫”;
  • slab:每個 “倉庫” 里有多個 “貨架”,存放固定大小的對象;
  • 對象:具體的數據結構,像整齊碼放在 “貨架” 上的商品,隨取隨用 。

圖片圖片

slab 就像不同狀態(tài)的 “內存小倉庫”,主要有這三種情況:

  • FULL(滿):所有對象已被分配,無法再分配新對象。
  • PARTIAL(部分空):部分對象在用,部分是空閑的,優(yōu)先從此分配內存。
  • EMPTY(空):所有對象均未使用,系統內存不足時可回收釋放。

圖片圖片

在 Linux 內核里,slab 分配器堪稱 “小對象管家”,專門負責管理進程控制塊(PCB)、文件描述符、內存頁結構體這類 “常客”。這些對象尺寸固定,還經常被反復創(chuàng)建、銷毀,用 slab 分配器管理,既能快速響應需求,又能避免內存浪費。

當內核模塊需要分配對象時,slab 分配器分三步完成任務:

  • 快速檢索:根據對象類型定位專屬緩存,在緩存內的多個 slab 中查找是否有空閑對象。由于分類存儲,能迅速鎖定目標。
  • 補充資源:若緩存無可用對象,檢查是否存在空閑 slab。若有,直接從中取出一個對象交付;若沒有,則向內存申請新的 slab,新 slab 由連續(xù)內存頁構成,并分割成固定大小的對象單元。
  • 交付標記:無論從現有空閑資源還是新分配的 slab 中獲取對象,都會立即分配給請求模塊,并將對象標記為已使用狀態(tài)。

對象使用完釋放時,slab 分配器會這樣處理:

  • 回收對象:將對象標記為空閑,放回原本緩存的 slab 里,就像把用過的工具放回專屬抽屜。
  • 檢查抽屜狀態(tài):釋放后,查看這個 slab 里還有多少空閑對象。如果所有對象都空出來了,就把整個 slab 標記為空閑。這些空閑 slab 后續(xù)可能會被重新拿出來用;要是內存緊張,還會被歸還給內存池 “二次利用”。

slab 分配器解決內存碎片的核心思路可以概括為:“預分配 + 固定規(guī)格 + 循環(huán)利用”。它提前在連續(xù)內存上建立對象緩存,劃分成統一大小的對象單元。當內核需要頻繁創(chuàng)建、銷毀小對象時,直接從緩存拿取或歸還,避免零散分配釋放造成外部碎片;固定尺寸的對象分配模式,又防止了按需切割引發(fā)的內部碎片。而且釋放的對象會被復用回原緩存,減少內存頻繁操作,讓內存空間始終保持整齊高效。

八、附錄:其他內存技術

8.1垃圾回收機制

程序在創(chuàng)建對象或者數組等引用類型實體的時候,系統會在堆內存上為之分配一段內存區(qū),用來保存這些對象,當這些對象永久地失去引用后,就會變成垃圾,等待系統垃圾回收機制進行回收。

垃圾回收機制只會回收堆內存中的對象,不會回收物理資源(網絡io)

垃圾回收機制是由系統控制的,程序是無法精準控制垃圾回收機制的時機的,程序只能指導對象什么時候不再被引用,當一個對象永久性地失去引用后,會由可達狀態(tài)變?yōu)榭苫謴蜖顟B(tài),程序會通知系統進行垃圾回收,但只是通知,最終是否回收是不確定的。

因為系統進行垃圾回收之前,會調用finalize()方法,這個 方法可能會使對象被重新引用,變?yōu)榭蛇_狀態(tài),此時垃圾回收就會取消,

當系統調用了所有finalize()方法后,該對象仍然是可恢復狀態(tài),也就是說此時的對象真正的失去了作用,這個時候該對象就會由可恢復狀態(tài)轉變位不可達狀態(tài),最終會被系統作為垃圾回收掉該對象所占用的資源。

finalize()方法時系統進行垃圾回收之前調用的方法。當系統要對某個不再被引用的對象所占用的資源進行回收時,會要求程序調用適當的方法來進行資源的清理,在java中提供了默認的機制來進行該資源的清理,也就是finalize()方法;finalize()方法時由系統的垃圾回收機制來調用的,這個方法的調用會使恢復狀態(tài)的對象重新被引用,所以它具有不確定性,最好不要人為的調用它來清理某個系統資源。

對象在垃圾回收機制有三種狀態(tài):可達性、可恢復性、不可達性。

  1. 當一個對象被創(chuàng)建后,只要有一個以上的引用變量引用它,這個狀態(tài)就處于可達狀態(tài);
  2. 當一個對象失去了所有的引用之后,它就進入了可恢復狀態(tài),這個狀態(tài)下,系統的垃圾回收機制會隨時調用finalize()方法對它占用的資源進行回收,此時只要由引用變量引用對該對象,這個對象又變成可達狀態(tài),否則進入不可達狀態(tài)。
  3. 處于不可達狀態(tài)的對象,就會等待系統弄隨時對它所占的資源進行回收。

8.2內存壓縮與緊縮

為節(jié)省存儲空間或傳輸帶寬,人們已經在計算機系統中廣泛地使用了數據壓縮技術。在磁介質存儲數據或網絡傳輸數據時,人們使用基于硬件或軟件的各種壓縮技術。當壓縮技術在各個領域都很流行時,內存壓縮技術卻由于其復雜性而一直未得到廣泛使用。近年來,由于在并行壓縮一解壓算法以及在硅密度及速度方面取得的進展,使得內存壓縮技術變得可行。

內存壓縮技術的主要思想是將數據按照一定的算法壓縮后存入壓縮內存中,系統從壓縮內存中找到壓縮過的數據,將其解壓后即可以供系統使用。這樣既可以增加實際可用的內存空間,又可以減少頁面置換所帶來的開銷,從而以較小的成本提高系統的整體性能。

內存壓縮機制是在系統的存儲層次中邏輯地加入一層——壓縮內存層。系統在該層中以壓縮的格式保存物理頁面,當頁面再次被系統引用時,解壓該壓縮頁后,即可使用。我們將管理這一壓縮內存層的相關硬件及軟件的集合統稱為內存壓縮系統。內存壓縮系統對于CPU、I/O設備、設備驅動以及應用軟件來說是透明的,但是操作系統必須具有管理內存大小變化以及壓縮比率變化的功能。

對于大多數的操作系統而言,要實現內存壓縮,大部分體系結構都不需要改動。在標準的操作系統中,內存都是通過固定數目的物理頁框(page frame)來描述的,由操作系統的VMM來管理。要支持內存壓縮,OS要管理的實際內存大小和頁框數目是基于內存的壓縮比率來確定的。

這里的實現內存是指操作系統可的內存大小,它與物理內存的關系如下:假設PM是物理內存,RM(t)是系統在t時刻的實際內存,而CR(t)是壓縮比率,在給定時刻t可支持的最大實際內存為RM(t)=CR1(t)×PM。然而,由于應用程序的數據壓縮率是不依賴于OS而動態(tài)變化的,未壓縮的數據可能會耗盡物理內存,因此當物理內存接近耗盡時,操作系統必須采取行動來解決這個問題。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2024-12-05 15:33:50

Python列表元組

2009-06-15 09:47:12

Java程序內存溢出

2010-08-10 13:58:00

Flex性能測試

2025-02-10 03:00:00

2010-06-11 10:19:22

systemd

2019-02-01 09:50:00

提升Python程序性能

2010-02-04 09:41:03

Android應用程序

2018-07-06 16:26:11

編程語言Python程序性能

2010-11-15 16:20:33

Oracle系統優(yōu)化

2024-05-16 11:04:06

C#異步編程編程

2011-09-20 10:41:45

Web

2022-10-08 13:13:14

Python程序性能

2024-04-29 08:16:18

2020-12-29 15:00:46

PerfVTune工具

2018-11-20 10:50:00

Java性能優(yōu)化編程技巧

2013-12-17 17:05:20

iOS性能優(yōu)化

2009-07-01 18:24:59

JSP應用程序JMeter

2019-10-17 10:10:23

優(yōu)化Web前端

2025-06-09 02:10:00

2020-07-07 07:57:39

Linux內存碎片化
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美人妖网站 | 小川阿佐美pgd-606在线 | 亚洲在线一区二区 | 亚洲一区二区三区观看 | 国产精品国产a级 | 国产精品久久久久一区二区三区 | 精品中文字幕在线 | 国产中文字幕在线观看 | 波多野吉衣在线播放 | 国产精品成人一区二区三区 | 精品国产18久久久久久二百 | 国产一区二区 | 国产精久久久 | 久久人 | 狠狠干av | 欧美精品久久 | 国产特级毛片aaaaaa喷潮 | 午夜免费在线观看 | 韩日av在线 | 国产ts人妖另类 | 自拍偷拍中文字幕 | 亚洲在线日韩 | 亚洲国产高清高潮精品美女 | 播放一级黄色片 | 国产精品1区2区 | 精品久久久久久亚洲综合网站 | 黄色免费在线观看网站 | 日韩视频一区二区在线 | 久久四虎 | 91豆花视频 | 日韩欧美一区二区在线播放 | 91免费看片| 国产精品揄拍一区二区 | 91精品国产综合久久久久久丝袜 | www.亚洲区| 国产成人精品av | 在线观看www高清视频 | 欧美午夜视频 | 成人影院在线 | 嫩草视频网 | 亚洲在线久久 |