聊聊內存那些事(基於單片機系統)

單片機的RAM和ROM

單片機的ROM,叫只讀程序存儲器,是FLASH存儲器構成的,如U盤就是FLASH存儲器。所以,FLASH和ROM是同義的。單片機的程序,就是寫到FLASH中了。

而RAM是隨機讀/寫存儲器,用作數據存儲器,是在運行程序時,存放數據的。

內存區

內存主要分爲:代碼區、常量區、靜態區(全局區)、堆區、棧區這幾個區域。

代碼區:存放程序的代碼,即CPU執行的機器指令,並且是隻讀的。

常量區:存放常量(程序在運行的期間不能夠被改變的量,例如: 25,字符串常量”dongxiaodong”, 數組的名字等)

靜態區(全局區)靜態變量和全局變量的存儲區域是一起的,一旦靜態區的內存被分配, 靜態區的內存直到程序全部結束之後纔會被釋放

堆區:由程序員調用malloc()函數來主動申請的,需使用free()函數來釋放內存,若申請了堆區內存,之後忘記釋放內存,很容易造成內存泄漏

棧區:存放函數內的局部變量,形參和函數返回值。棧區之中的數據的作用範圍過了之後,系統就會回收自動管理棧區的內存(分配內存 , 回收內存),不需要開發人員來手動管理。棧區就像是一家客棧,裏面有很多房間,客人來了之後自動分配房間,房間裏的客人可以變動,是一種動態的數據變動。

STM32F103C8T6中

ROM起始地址爲:0x8000000, 大小爲:0x10000 (64K)

只讀的,存放着代碼區和常量區

RAM起始地址爲:0x20000000,大小爲:0x5000  (20K)

可讀可寫的,存放着靜態區、棧區和堆區

STM32各區詳細介紹:

代碼區:

l  代碼區存放着程序編譯後的CPU指令

l  函數名稱是一個指針,可以通過查詢函數名稱所處的內存地址,查詢函數存放的區域

 1 //函數聲明
 2 void dong();
 3 //主函數定義
 4 int main(void)
 5 { 
 6   //串口初始化
 7     Uart1_Init(115200);
 8     //函數調用
 9   dong();
10     //輸出test函數地址
11     printf("dong() addrs : 0x%p\n",dong);
12     
13     while(1);
14 }
15 void dong(){
16      //輸出main函數地址
17      printf("mian() addrs : 0x%p\n",main);
18 }

輸出:

可見0x08000c2d和0x08000be9都在ROM裏的代碼區

常量區

指針可以指向常量也可以指向變量的區域,通過指針(char *p)來測試一下常量與變量去的地址變化。

 1 void dongxiaodong_fun(){
 2    char *p=NULL;//定義一個指針變量
 3      //常量
 4      p="2020dongxiaodong";//指針指向一個常量
 5    printf("addr1:0x%p\r\n",p);//輸出常量地址
 6      //變量
 7      char data[]={"dong1234"};
 8      p=data;//指針指向一個變量
 9    printf("addr2:0x%p\r\n",p);//輸出變量地址
10 }

輸出:

可見常量的地址在ROM裏的常量區,局部變量在RAM的棧空間下

靜態區

靜態區包括靜態變量和全局變量,靜態變量通過static修飾,一旦初始化則一直佔用RAM空間

 1 int global_a;//全局變量,默認值爲0
 2 static int global_b;//靜態全局變量,默認值爲0
 3 void fun(){
 4   static int c;//靜態變量,默認值爲0
 5     printf("static int c add:0x%p , val:%d \r\n",&c,c);
 6     c++;
 7 }
 8 void dongxiaodong_fun(){
 9   //輸出全局變量
10     printf("       int a add:0x%p , val:%d \r\n",&global_a,global_a);
11     printf("static int b add:0x%p , val:%d \r\n",&global_b,global_b);
12     //調用函數查看靜態變量
13     for(int i=0;i<3;i++){
14          fun();
15     }
16 }

輸出:

其中global_a爲全局變量、global_b爲全局靜態變量、c爲局部靜態變量,他們如果沒有賦初值都會被系統自動賦值爲0,靜態變量初始化則一直有效,並不會因爲多次調用了初始化語句而出現多次初始化的問題。代碼中雖然看似初始化了c變量三次,其實實際只有第一次有效。

堆區

堆區是調用malloc函數來申請的內存空間,這部分空間使用完後要調用free()函數來釋放申請的空間。Void * malloc(size_t);函數的參數是需要分配的空間字節大小,返回是一個void*類型的指針,該指針指向分配空間的首地址,void*類型指針可以轉換爲任意的其它類型指針。

l  堆是向上增長,即首地址遞增的方向增長

l  通過malloc()申請的空間必須通過free()進行釋放,如果申請的內存未釋放則可能造成內存泄露

l  malloc()內存申請失敗將返回NULL

l  malloc分配的內存空間在邏輯上是連續的,而在物理上可以不連續。

l  釋放只能釋放一次,如果釋放兩次及兩次以上會出現錯誤(但是釋放空指針例外,釋放空指針其實也等於什麼都沒有做,所以,釋放多少次都是可以的),free()釋放空間後可以將指針指向“NULL”確保指針不會成爲野指針。

STM32C8T6:

標準庫中定義了默認堆的大小爲0x200=512字節,其可以認爲程序同一時間的malloc分配大小不可大於512字節數據。

堆空間默認不常駐RAM空間,但當代碼出現malloc關鍵字後,堆空間將分配設置的整體大小(512字節)佔用RAM空間。

void dongxiaodong_fun(){
  //申請
    printf("-----malloc-----\r\n");
    char *p1=malloc(100);
    if(p1==NULL) printf("p1 malloc fail \r\n");
    char *p2=malloc(1024);
    if(p2==NULL) printf("p2 malloc fail \r\n");
    
    //賦值  
    memcpy(p1,"dongxiaodong123456",strlen("dongxiaodong123456"));
    
    printf("p1 addr:%p  ,val:%s \r\n",p1,p1);
    printf("p2 addr:%p\r\n",p2);
    
    
    //釋放
    printf("-----free-----\r\n");
    free(p1);
    free(p2);
    
    printf("p1 addr:%p  ,val:%s \r\n",p1,p1);

    
    p1=NULL;
    printf("p1 addr:%p \r\n",p1);
    
}

輸出:

可見堆空間分配內存失敗則會返回NULL,並且地址指向0x00,釋放時只是通過free(),僅是把指向的內容變成了空值,但地址還是存在的,所以標準的做法是賦上“NULL”值。內存釋放後(使用free函數之後指針變量p本身保存的地址並沒有改變),需要將p的值賦值爲NULL(拴住野指針)。

分配空間不能達到所規定的最大值:

void dongxiaodong_fun(){
       char *d=malloc(512);
       //char *d=malloc(500); //可行
       if(d==NULL) printf("512 malloc fail\r\n");
}

輸出:

查看解釋:

如果用malloc(n)來分配堆內存,那麼分配的內存比n大,爲什麼呢?

0.malloc分配的內存不一定連續,所以需要header指針來鏈接各部分

1.實際分配的堆內存是Header + n結構。返回給用戶的是n部分的首地址  所以他還有一部分內存是用來存header的,所以比原始的大

2.由於內存對齊值8,內存對其機制,實際分配的堆內存大於等於sizeof(Header) + n

棧區

棧區由編譯器自動分配和是釋放,存放函數中定義的參數值、返回值和局部變量,在程序運行過程實時分配和釋放,棧區由操作系統自動管理,無須手動管理。棧區是先進後出原則。

l  棧是向下增長,即首地址遞減的方向增長

l  編譯器不會給未初始化的局部變量賦初始值0,所以未初始化的局部變量通常是一個混亂值,所以定義局部變量時賦初值是最穩妥的。

STM32C8T6:

標準庫中定義了默認棧的大小爲0x400=1024字節,其可以認爲程序同一時間的局部變量不可大於1024字節數據。

棧空間的字節數是常駐空間,一經初始化將分配設置的整體大小(1024字節)佔用RAM空間。

 1 //主函數定義
 2 int main(void)
 3 { 
 4     //串口初始化
 5     Uart1_Init(115200);
 6     printf("start SYS 1\r\n");
 7     char data1[1024]={0};   //1024字節
 8     printf("start SYS 2\r\n");
 9     char data2[100]={0};    //100字節
10     printf("start SYS 3\r\n");
11     char data3[100]={0};     //100字節,10字節可以正常運行
12     printf("start SYS 4\r\n");
13     while(1);
14 }

實測發現棧空間的大小到1024+100+10字節都是可以正常運行的,這個難道是STM32做了棧空間的預留嗎?1024並不是做了完全的強制限制。

地址測試

void dongxiaodong_fun(){
int a=100;
    int b;
    printf("a addr:0x%p val:%d\r\n",&a,a);
    printf("b addr:0x%p val:%d\r\n",&b,b);
}

輸出:

可見b的地址小於a的地址,其是向首地址遞減的方向增長(向下增長),b的值沒有賦初值,其值是混亂的,建議賦初值使用。

注意:

const修飾的數據

l  const修飾的是變量名,之所以叫const常量,意思是不可以更改,權限爲只讀,但是它的本質是變量,只不過是不可修改的變量

l  const修飾局部變量則存放在棧區,如果修飾全局變量就存放在靜態區(全局區)

數據存儲(大小端模式)

數據在內存中存放,分爲大端模式和小端模式

大端模式:低位字節存在高地址上,高位字節存在低地址上。

小端模式:低位字節存在低地址上,高位字節存在高地址上。

網絡字節序:TCP/IP各層協議將字節序列定義爲大端模式,因此在TCP/IP協議中使用的大端模式通常稱爲網絡字節序。

void dongxiaodong_fun(){
    int data=0x12345678;
    char *p=(char*)&data;
    printf("p+0:0x%p-->0x%02X\r\n",p,*(p+0));
    printf("p+1:0x%p-->0x%02X\r\n",p,*(p+1));
    printf("p+2:0x%p-->0x%02X\r\n",p,*(p+2));
    printf("p+3:0x%p-->0x%02X\r\n",p,*(p+3));
}

輸出:

可見其值的高位存儲在地址的低位上,所以STM32的變量存儲是小端模式

動態內存申請的碎片化問題

標準的內存動態分配是動態鏈表進行管理。由於malloc返回的是一個指針再加上單片機沒有MMU,使得分配的指針就像一個個釘子一樣在內存中了,直到被釋放。這就會導致內存管理非常困難,從而導致內存碎片化。

這是一個理想的極端例子

單片機的堆空間分配有1KB的空間,其爲1024字節,爲了說明和計算方便我們忽略掉鏈表佔用的空間,只計算實際存儲空間大小。

第一步:申請64塊內存空間,每塊16字節,那麼就會分配完1K字節的空間。

char *p[64]={NULL};
for(int i=0; i<64; i++){
    ptr[i] = malloc(16);
}

第二步:釋放掉偶數塊內存空間

for(int i=0; i<64; i+=2){
    free(ptr[i] );
    ptr[i]=NULL;
}

第三步:

我們釋放掉的空間達到了堆的一半大小,512字節了,但都是不連續的。32塊16字節的非連續空間,所以要分配出大於16字節的內存塊是分配不出來的。有512字節的空間但只能分配小於16字節的連續空間,在某些場合原本單片機RAM的堆空間資源就很緊張,再加上這種不充分的使用使得程序穩定性大打折扣。

STM32C8T6真實案例:

內存碎片化可以通過如下列子進行驗證:

 1 void dongxiaodong_fun(){
 2     char *p[8]={NULL};
 3     //512字節的堆空間,似乎只能分配8*50=400字節
 4     for(int i=0;i<8;i++){
 5         p[i]=malloc(50);
 6         if(p[i]==NULL) printf("p[%d] malloc fail\r\n",i);
 7     }
 8    //輸出其中一個數的地址
 9     printf("%p\r\n",p[2]);
10     printf("%p\r\n",p[3]);
11     //釋放偶數下標空間
12     for(int i=0;i<8;i+=2){
13         free(p[i]);
14         p[i]=NULL;
15     }
16     //分配失敗,內存碎片化
17     char *d1=malloc(100); //可行
18     if(d1==NULL) printf("d1 100 malloc fail\r\n");
19     
20     //釋放一個奇數位空間
21     free(p[3]);
22     //分配成功,分配的空間在p[2]和p[3]的空間上,和多了10個字節的額外空間
23     char *d2=malloc(160);
24     if(d2==NULL) printf("d2 100 malloc fail\r\n");
25     printf("%p\r\n",d2);
26 }

輸出:

這個例子大體上還是體現出了內存碎片化的問題所在,因爲總共有8個空間快,申請後釋放奇數塊理論上有50*4=200字節,但分配100字節卻行不通,重要原因在於釋放的偶數塊每塊大小爲50,並且其地址是不連續的。當釋放其中一個奇數塊後,內存就可以達到需要分配的連續塊大小了,所以分配的空間使用了p[2]、 p[3]、p[4]的空間。

 

存在幾個問題:

Malloc分配的空間總共可以有512,但分1包也只能是500左右的有效空間,分8包是400左右的有效空間,利用率爲什麼這麼低?

碎片化測試時,p[2]、p[3]、p[4]的大小應該是3*50=150,結果最大可以是160左右。

 

查看解釋:

如果用malloc(n)來分配堆內存,那麼分配的內存比n大,爲什麼呢?

0.malloc分配的內存不一定連續,所以需要header指針來鏈接各部分

1.實際分配的堆內存是Header + n結構。返回給用戶的是n部分的首地址  所以他還有一部分內存是用來存header的,所以比原始的大

2.由於內存對齊值8,內存對其機制,實際分配的堆內存大於等於sizeof(Header) + n

 

內存碎片化的主要解決方法:

將間隔的小內存移動到一起並排,釋放連續空間

現在普遍採用的段頁式內存分配方式就是將進程的內存區域分爲不同的段,然後將每一段由多個固定大小的頁組成。通過頁表機制,使段內的頁可以不必連續處於同一內存區域,從而減少了外部碎片,然而同一頁內仍然可能存在少量的內部碎片,只是一頁的內存空間本就較小,從而使可能存在的內部碎片也較少。

 

正點原子的mymalloc() 函數

問題1:Malloc函數標準庫有爲什麼又出現這個?

問題2:內存碎片化處理?

總結:

l  可以進行多種RAM的內存管理,比如外部的SRAM,方便管理多個RAM空間

l  可以查看到內存的使用率

l  沒有進行內存碎片化處理

STM32查看FLASH空間和RAM空間使用量

打開顯示:

 編譯後輸出:

 

Program Size: Code=38356 RO-data=6676 RW-data=400 ZI-data=47544

 

Code:代碼佔用的空間

RO-data:其中RO表示Read Only ,只讀常亮的大小

RW-data:其中RW表示Read Write,可讀可寫的變量大小,初始化已經付了初值

ZI-data:其中ZI表示Zero Initialize,可讀可寫的變量大小,沒有賦初值而被系統賦值爲0的字節數

 

RAM的大小:

RAM=【RW-data】+【ZI-data】

 

ROM的大小:

ROM =【Code】+【RO-data】+【RW-data】,ROM的大小即爲程序所下載到ROM Flash中的大小。爲什麼Rom中還要有RW,因爲掉電後RAM中所有的數據都丟失了,每次上電RAM中的數據是被程序賦值的,每次這些固定的值就是存儲在ROM中的,爲什麼不包含ZI段呢,是因爲ZI數據都是0,沒必要包含,只要查詢運行之前將ZI數據所在的區域一律清零即可。包含進去反而浪費存儲空間。

 

程序運行:

燒錄到ROM中的image文件與實際運行時的ARM程序之間並不是完全一樣的。MCU執行過程是先將RW從ROM中搬到RAM中,因爲RW是變量,變量不能存在ROM中。然後將ZI所在的RAM區域全部清零,因爲ZI區域並不在Image中,所以需要程序根據編譯器給出的ZI地址及大小來將相應的RAM區域清零。ZI中也是變量,同理:變量不能存儲在ROM中,在程序運行的最初階段,RO中的指令完成了這兩項工作後C程序才能正常訪問變量。否則只能運行不含變量的代碼。

 

參考:

內存碎片化:

https://blog.csdn.net/chenyefei/article/details/82534058

STM32的編譯內存信息:

https://blog.csdn.net/qq_37858386/article/details/79541451

程序分區:

https://blog.csdn.net/u014470361/article/details/79297601

正點原子malloc:

http://www.openedv.com/forum.php?mod=viewthread&tid=954&extra=&highlight=malloc&page=1

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