UNIX再学习 -- 内存管理
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- 内存管理
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
C 語言部分,就一再的講內存管理,參看:C語言再學習 -- 再論內存管理??UNIX、Linux 部分還是要講,足見其重要。
一、存儲空間布局
1、我們先了解一個命令 size,繼而引出我們今天要講的內容.
詳細可自行 man size 查看功能:
顯示二進制文件中部分的大小,如果沒有指定輸入文件,則假定 a.out。size 支持的目標: elf32-i386 a.out-i386-linux pei-i386 elf32-little elf32-big elf64-x86-64 elf32-x86-64 pei-x86-64 elf64-l1om elf64-k1om elf64-little elf64-big plugin srec symbolsrec verilog tekhex binary ihex trad-core
選項:
-A | -B --format = {sysv | berkeley}選擇輸出樣式(默認為berkeley) -o | -d | -x --radix = {8 | 10 | 16}以八進制,十進制或十六進制顯示數字 -t -- totals 顯示總大小(僅限伯克利)-- common 顯示* COM *符號的總大小--target = <bfdname> 設置二進制文件格式@ <file>從<file>讀取選項 -h --help顯示此信息 -v --version顯示程序的版本示例:
# size text data bss dec hex filename1621 272 8 1901 76d a.out講解:
前 3 列分別為 正文段(text段)、數據段、bbs段(未初始數據段)的長度(以字節為單位)。 第 4 列 和第 5 列分別以十進制和十六進制表示的 3 段的總長度。 最后一列表示文件名,如果沒有指定輸出,則假定為 a.out。2、存儲空間布局
歷史沿襲至今,C 程序一直由下列幾部分組成:(1)正文段( text段/代碼段)
這是由 CPU 執行的機器指令的部分。通常,正文段是可共享的,所以即使是頻繁執行的程序(如文本編輯器,C 編譯器和 shell 等)在存儲器中也只需有一個副本,另外正文段常常是只讀的,以防止程序由于意外而修改其指令。 正文段是用來存放可執行文件的操作指令,也就是說它是可執行程序在內存中的鏡像。(2)初始化數據段(數據段)
數據段用來存放可執行文件中已經初始化的全局變量,換句話說就是存放程序靜態分配的變量和全局變量。 例如,C 程序中任何函數之外的聲明: int num = 10;使此變量以其初值存放在初始化數據段中。(3)未初始化數據段(bbs段)
bbs段包含了程序中未初始化的全局變量,在程序開始執行之前,內核將此段中的數據初始化為 0 或空指針。 例如:函數外的聲明: long sum[100];使此變量存放在非初始化數據段中。(4)堆
堆是用于存放進程進行中被動態分配的內存段,它大小并不固定,可動態擴張或縮減。當進程調用 malloc 等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用 free 等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。由于歷史上形成的慣例,堆位于未初始化數據段和棧之間。(5)棧
棧是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static 聲明的變量,static 意味著在數據段中存放變量)。除此之外在函數調用結束后,函數的返回值也會被存放回棧中。由于棧的后進先出(LIFO)特點,所以棧特別方便用來保存/恢復調用現場。從這個意義上講我們可以把堆棧看成一個臨時數據寄存,交換的內存區。(6)參數和環境區
命令行參數和環境變量。命令行參數: 上篇文章已講,參看:UNIX再學習 -- 進程環境 每個C語言程序都必須有一個稱為main()的函數,作為程序啟動的起點。當執行程序時,命令行參數(command-line argument)(由shell逐一解析)通過兩個入參提供給 main() 函數。第一個參數int argc,表示命令行參數的個數。第二個參數char *argv[],是一個指向命令行參數的指針數組,每一參數又都是以空字符(null) 結尾的字符串。第一個字符串,亦即argv[0]指向的,(通常)是該程序的名稱。argv中的指針列表以NULL指針結尾(即argv[argc]為NULL)。
環境變量: 參看:UNIX再學習 -- 環境變量
3、內容回顧
下圖清楚的總結了存儲空間布局:看了一篇文章居然用 size 命令分析 linux 程序內存段的分布,厲害了我的哥。 參看:用size命令分析linux程序內存段的分布
(1)示例一
參考對象,依次與之比較 text、data、bss,查看下面的定義保存在哪個區域內。?int main (void) {return 0; }# size text data bss dec hex filename1056 252 8 1316 524 a.out
(2)示例二
定義可執行指令 、字面值常量 、具有常屬性且被 初始化的全局、靜態全局 和 靜態局部變量,經比較其保存在代碼段參看:字面值常量參看:什么是可執行語句const int i = 10; const static j = 12; int main (void) { const int n = 8; int a = 9; //變量 a 在棧區;字面值常量 9 在代碼段 a = 10; //可執行指令,在代碼段 const static int num = 10;return 0; } # size text data bss dec hex filename1100 252 8 1360 550 a.out
(3)示例三
定義?不具常屬性且被初始化的 全局、靜態全局 和 靜態局部變量,經比較其保存在數據段?static int n = 10; int num = 10; int main (void) {static int i = 8;return 0; } # sizetext data bss dec hex filename1056 264 8 1328 530 a.out(4)示例四
定義?未初始化全局、靜態全局?和 靜態局部變量?,經比較其保存在 bbs段 static int n; int num; int main (void) {static int i;return 0; } # sizetext data bss dec hex filename1056 252 20 1328 530 a.out 我們之前講 C 語言再學習部分,涉及到這部分的內容也有不少處。 如:內存區域劃分舉例說明;段錯誤、值傳遞、址傳遞里的堆棧內容;關鍵字 static、const;存儲類、鏈接。 看到下面如此多的參看鏈接,你還覺的內存管理部分簡單嗎 參看:C語言再學習 -- 內存管理 參看:C語言再學習 -- 段錯誤(核心已轉儲) 參看:C語言再學習 -- 值傳遞,址傳遞,引用傳遞 參看:C語言再學習 -- 存儲類型關鍵字 參看:C語言再學習 -- 關鍵字const 參看:C語言再學習 -- 存儲類、鏈接二、存儲空間分配
ISO C 說明了 3 個用于存儲空間動態分配的函數。 #include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size); 三個函數返回:若成功則為非空指針,若出錯則為NULL void free(void *ptr);1、函數比較
(1)malloc,分配指定字節數的存儲區。此存儲區中的初始值不確定。 (2)calloc,為指定數量指定長度的對象分配存儲空間。該空間中的每一位(bit)都初始化為 0. (3)realloc,增加或減少以前分配區的長度。當增加長度時,可能需將以前分配區的內容移到另一個足都大的區域,以便在尾端提供增加的存儲區,而新增區域內的初始值則不確定。2、函數解析
參看:淺談malloc,calloc,realloc函數之間的區別(1)malloc 函數
功能:
malloc函數可以從堆上獲得指定字節的內存空間。返回值:
如果函數執行成功,malloc返回獲得內存空間的首地址;如果函數執行失敗,那么返回值為NULL。 由于 malloc函數值的類型為void型指針,因此,可以將其值類型轉換后賦給任意類型指針,這樣就可以通過操作該類型指針來操作從堆上獲得的內存空間。用法:
需要注意的是,malloc函數分配得到的內存空間是未初始化的。因此,一般在使用該內存空間時,要調用另一個函數 memset 來將其初始化為全 0。memset 函數的聲明如下:void * memset (void * p,int c,int n) ;該函數可以將指定的內存空間按字節單位置為指定的字符 c。其中,p 為要清零的內存空間的首地址,c 為要設定的值,n 為被操作的內存空間的字節長度。如果要用memset清 0,變量 c 實參要為 0。
示例: #include <stdio.h> #include <stdlib.h> #include <string.h>int main (void) {int *p = (int *)malloc (sizeof (int));if (NULL == p)perror ("fail to malloc"), exit (1);printf ("%d\n", *p);memset (p, 0, sizeof (int));printf ("%d\n", *p);*p = 2;printf ("%d\n", *p);free (p);p = NULL;return 0; } 輸出結果: 0 0 2
(2)calloc 函數
功能:
calloc函數的功能與malloc函數的功能相似,都是從堆分配內存。 int *p = (int *)calloc (SIZE, sizeof (int)); 等同于 int *p = (int *)malloc (SIZE * sizeof (int));返回值:
函數返回值為 void 型指針。如果執行成功,函數從堆上獲得size * n的字節空間,并返回該空間的首地址。如果執行失敗,函數返回NULL。用法:
該函數與malloc函數的一個顯著不同時是,calloc函數得到的內存空間是經過初始化的,其內容全為0。calloc函數適合為數組申請空間,可以將 size 設置為數組元素的空間長度,將 n 設置為數組的容量。示例:
#include <stdio.h> #include <stdlib.h> #define SIZE 5 int main (void) {int i = 0;int *p = (int *)calloc (SIZE, sizeof (int));if (NULL == p)perror ("fail to calloc"), exit (1);for (i = 0; i < SIZE; i++)p[i] = i;for (i = 0; i < SIZE; i++)printf ("p[%d] = %d\n", i, p[i]);free (p);p = NULL;return 0; } 輸出結果: p[0] = 0 p[1] = 1 p[2] = 2 p[3] = 3 p[4] = 4(3)realloc 函數
功能:
realloc函數的功能比 malloc 函數和 calloc 函數的功能更為豐富,可以實現內存分配和內存釋放。返回值:
成功返回非空指針,失敗返回 NULL用法:
realloc 函數將指針 ptr 指向的內存塊的大小改變為 size 字節。如果 size 小于或等于 ptr 之前指向的空間大小,那么。保持原有狀態不變。如果 size 大于原來 ptr 之前指向的空間大小,那么,系統將 重新為 ptr 從堆上分配一塊大小為 size 的內存空間,同時,將原來指向空間的內容依次復制到新的內存空間上,ptr 之前指向的空間被釋放。relloc 函數分配的空間也是未初始化的。示例:
#include<stdio.h> #include<stdlib.h> int main (void) {int i = 0;int* pn = (int*)malloc(5*sizeof(int));printf ("malloc %p\n",pn);for(i = 0;i < 5; i++)pn[i]=i;pn = (int*)realloc(pn, 10*sizeof(int));printf("realloc %p\n",pn);for(i = 5;i < 10; i++)pn[i]=i;for(i=0;i<10;i++)printf("%d ",pn[i]);printf ("\n");free(pn);return 0; } 輸出結果: malloc 0x8d59008 realloc 0x8d59008 0 1 2 3 4 5 6 7 8 9(4)free 函數
功能:
free函數可以實現釋放內存的功能。用法:
從堆上獲得的內存空間在程序結束以后,系統不會將其自動釋放,需要程序員來自己管理。一個程序結束時,必須保證所有從堆上獲得的內存空間已被安全釋放,否則,會導致內存泄露。由于形參為void指針,free函數可以接受任意類型的指針實參。但是,free函數只是釋放指針指向的內容,而該指針仍然指向原來指向的地方,此時,指針為野指針,如果此時操作該指針會導致不可預期的錯誤。 安全做法是:在使用free函數釋放指針指向的空間之后,將指針的值置為NULL。因此,對于上面的demo,需要在return?語句前加入以下兩行語句:
free(p);
p=NULL;
注意:使用 malloc 函數分配的堆空間在程序結束之前必須釋放。
3、new、delete函數
這部分現在不求掌握,只要了解。
new/delete 為?C++ 中動態內存分配函數,那么它和 C 中的有何不同呢,簡單介紹下。(1)動態內存的分配和釋放的基本語句,我們應該都比較熟悉的,如:
int* p = new int; delete p;(2)C++ 的 new 操作符允許在動態分配內存時對其做初始化。
int* p = new int; int* p = new int (); int* p = new int (100);(3)以數組方式 new 的,也要以數組方式 delete。
int* p = new int[4] {10, 20, 30, 40}; delete[] p;某些 C++ 實現,用 new 操作符動態分配數組時,會在數組首元素前面多分配 4 或 8 個字節,用以存放數組的長度。new 操作符所返回的地址是數組首元素的地址,而非所分配內存的起始地址。如果將 new 操作符返回的地址直接交給 delete 操作符,將導致無效指針(invalidate pointer)異常。delete[] 操作符會將交給它的地址向低地址方向偏移 4 或 ?8個字節,避免了無效指針異常的發生。
delete使用需要注意的地方:
(1)不能通過delete操作符釋放已釋放過的內存
int* p = new int; delete p; delete p; // 核心轉儲(2)delete野指針后果未定義,delete空指針安全
int* p = new int; delete p; p = NULL; delete p; // 什么也不做內存分配失敗操作:
(1)malloc函數
char* p = (char*)malloc (0xFFFFFFFF); if (p == NULL) { cerr << "內存分配失敗!" << endl; exit (-1); }(2)new 函數
try { char* p = new char[0xFFFFFFFF]; } catch (bad_alloc& ex) { cerr << "內存分配失敗!" << endl; exit (-1); }示例:
#include <iostream> #include <cstdio> using namespace std; struct Student {string name;int age; };int main (void) {int *p1 = new int;*p1 = 1234;++*p1;cout << *p1 << endl;delete p1;p1 = new int ();cout << *p1 << endl;delete p1;p1 = new int (1234); cout << *p1 << endl;delete p1;p1 = new int[4] {1, 2, 3, 4};for (size_t i = 0; i < 4; i++)cout << p1[i] << ' ';cout << endl;delete[] p1;try {p1 = new int [3];delete[] p1;p1 = NULL;}catch (exception& ex) {cout << ex.what() << endl;perror ("fail to new");return -1;}int (*p2)[4] = new int[3][4];for (int i = 0; i < 3; i++)for (int j = 0; j < 4; j++)p2[i][j] = (i + 1) * 10 + j + 1;for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++)cout << p2[i][j] << ' ';cout << endl;}delete[] p2;int (*p3)[4][5] = new int[3][4][5];for (int i = 0; i < 3; ++i)for (int j = 0; j < 4; ++j)for (int k = 0; k < 5; ++k)p3[i][j][k] = (i+1)*100+(j+1)*10+k+1;for (int i = 0; i < 3; ++i) {for (int j = 0; j < 4; ++j) {for (int k = 0; k < 5; ++k)cout << p3[i][j][k] << ' ';cout << endl;}cout << endl;}delete[] p3;string *p4 = new string;cout << '[' << *p4 << ']' << endl;delete p4;p4 = new string ("string");cout << *p4 << endl;delete p4;p4 = new string[3] {"beijing", "shanghai", "shenzhen"};for (int i = 0; i < 3; i++)cout << p4[i] << ' ';cout << endl;delete[] p4;Student *p5 = new Student;p5->name = "mayun";p5->age = 56;cout << p5->name << "," << p5->age << endl;delete p5;return 0; } 輸出結果: 1235 0 1234 1 2 3 4 11 12 13 14 21 22 23 24 31 32 33 34 111 112 113 114 115 121 122 123 124 125 131 132 133 134 135 141 142 143 144 145 211 212 213 214 215 221 222 223 224 225 231 232 233 234 235 241 242 243 244 245 311 312 313 314 315 321 322 323 324 325 331 332 333 334 335 341 342 343 344 345 [] string beijing shanghai shenzhen mayun,56三、虛擬內存
1、這部分現在只做了解,Linux 部分會詳細介紹的。
Linux 操作系統采用虛擬內存管理技術,使得每個進程都有各自互不干涉的進程地址空間。該空間是塊大小為 4 G的線性虛擬空間,用戶所看到和接觸的都是該虛擬地址,無法看到實際的物理內存地址。利用這種虛擬地址不但能起到保護操作系統的效果(用戶不能直接訪問物理內存),而且更重要的是用戶程序可使用比實際物理內存更大的地址空間。 4G的進程地址空間被認為的分成兩部分 -- 用戶空間和內核空間。 用戶空間從 0 到 3G (0c0000000),內核空間占據3G到4G。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內核空間虛擬地址。例外情況只有用戶進程進行系統調用(代表用戶進程在內核態執行)等時刻可以訪問到內核空間。 用戶空間對應進程,所以每當進程切換,用戶空間就會跟著變化;而內核空間是由內核負責映射,它并不會跟著進程改變,是固定的。內核空間地址有自己對應的頁表(init_mm.pgd),用戶進程各自有不同的頁表。 每個進程的用戶空間都是完全獨立、互不相干的。你可以把程序同時運行 10 次(當然為了同時運行,讓它們在返回前一同睡眠100秒吧),你會看到 10 個進程占用的線性地址一模一樣。四、物理內存管理(頁管理)
Linux 內核管理物理內存是通過分頁機制實現的,它將整個內存劃分成無數 4K *(在i386體系結構中)大小頁,從而分配和回收內存的基本單位便是內存頁了。利用分頁管理有助于靈活分配內存地址,因為分配時不必要求必須有大塊的連續內存,系統可以東一頁、西一頁的湊出所需要的內存共進程使用。雖然如此,但是實際上系統使用內存還是傾向于分配連續的內存塊,因為分配連續內存時,頁表不需要更改,因此能降低刷新率(頻繁刷新會很大增加訪問速度)。虛擬內存到物理內存的映射以頁(4K = 4096字節)為單位。涉及到一個函數 getpagesize
#include <unistd.h> int getpagesize(void);(1)函數功能
主要用于獲取當前系統中一個內存頁的大小,一般為4Kb(2)示例說明
#include <stdio.h> #include <unistd.h>int main (void) {printf ("page size = %d Byte\n", getpagesize ());return 0; } 輸出結果: page size = 4096 Byte五、虛擬內存管理
1、函數 sbrk
#include <unistd.h> void *sbrk(intptr_t increment);(1)函數功能
主要用于按照參數指定的大小來調整內存塊的大小。(2)返回值
成功返回上次調用 sbrk/brk 后的堆尾指針,失敗返回 -1.(3)參數解析
increment:虛擬內存增量 (以字節為單位) 大于 0,分配虛擬內存 小于 0,釋放虛擬內存 等于 0,當前堆尾指針(4)函數解析
系統內部維護一個指針,指向當前堆尾,即堆區最后一個字節的下一個位置,sbrk 函數根據增量參數調整該指針的位置,同時返回該指針在調整前的位置,期間若發現內存也耗盡或空閑,則自動追加或取消內存頁的映射。 規則:申請比較小的內存時,一般會默認分配 1 個內存頁,申請的內存超過 1 個內存頁時,會再次分配 1 個內存頁。釋放內存時,釋放完畢后剩余的內存如果在一個內存頁內,則一次性釋放 1 個內存頁。(5)示例說明
//sbrk函數的使用 #include <stdio.h> #include <stdlib.h> #include <unistd.h>int main(void) {//申請4個字節的動態內存void* p1 = sbrk(4);void* p2 = sbrk(4);void* p3 = sbrk(4);printf("p1 = %p,p2 = %p,p3 = %p\n",p1,p2,p3);printf("------------------\n");//獲取內存塊當前的末尾位置+1void* cur = sbrk(0);printf("cur = %p\n",cur);//p3+4//釋放4個字節的內存空間void* p4 = sbrk(-4);printf("p4 = %p\n",p4);//p3+4//獲取內存塊的當前位置cur = sbrk(0);printf("cur = %p\n",cur);//p3printf("--------------\n");//再次釋放4個字節的內存p4 = sbrk(-4);printf("p4 = %p\n",p4);//p3cur = sbrk(0);printf("cur = %p\n",cur);//p2printf("--------------\n");//目前就剩下4個字節printf("當前進程PID:%d\n",getpid());printf("目前進程擁有4個字節\n");getchar();sbrk(4093);printf("申請了4093個字節的內存,恰好超過了1個內存頁\n");getchar();sbrk(-1);printf("釋放了1個字節的內存,回到了1個內存頁的范圍\n");getchar();return 0; } 輸出結果: p1 = 0x8141000,p2 = 0x8141004,p3 = 0x8141008 ------------------ cur = 0x814100c p4 = 0x814100c cur = 0x8141008 -------------- p4 = 0x8141008 cur = 0x8141004 -------------- 當前進程PID:4787 目前進程擁有4個字節 1 申請了4093個字節的內存,恰好超過了1個內存頁 釋放了1個字節的內存,回到了1個內存頁的范圍 2(6)示例總結
通過示例可以看出,用 sbrk 分配內存比較方便,用多少內存就傳多少增量參數,同時返回指向新分配內存區域的指針,但用 sbrk 做一次性內存釋放比較麻煩,因為必須將所有的既往增量進行累加。2、函數 brk
#include <unistd.h> int brk(void *addr);(1)函數功能
表示操作內存的末尾地址到參數指定的位置,如果參數指定的位置大于當前的末尾位置,則申請內存。如果參數指定的地址小于當前的末尾位置,則釋放內存。(2)返回值
成功返回上次調用 sbrk/brk 后的堆尾指針,失敗返回 -1.(3)參數解析
addr:堆尾指針的新位置 大于堆尾指針的原位置:分配虛擬內存 小于堆尾指針的原位置:釋放虛擬內存 等于堆尾指針的原位置:什么也沒有做(4)函數解析
系統內部維護一個指針,指向當前堆尾,即堆區最后一個字節的下一個位置,brk函數根據指針參數設置該指針的位置,期間若發現內存頁耗盡或空閑,則自動追加或取消內存頁的映射。(5)示例說明
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main (void) {//獲取一個有效位置void *p = sbrk (0);printf ("p = %p\n", p);//使用 brk 函數申請 4 個字節內存int res = brk (p + 4);if (-1 == res)perror ("fail to brk"), exit (1);void *cur = sbrk (0);printf ("cur = %p\n", cur);//申請 4 個字節brk (p + 8);cur = sbrk (0);printf ("cur = %p\n", cur);//釋放了 4 個字節brk (p + 4);cur = sbrk (0);printf ("cur = %p\n", cur);//釋放了所有字節brk (p);cur = sbrk (0);printf ("cur = %p\n", cur);return 0; } 輸出結果: p = 0x98cc000 cur = 0x98cc004 cur = 0x98cc008 cur = 0x98cc004 cur = 0x98cc000(3)示例總結
通過示例可以看出,用 brk 釋放內存比價方便,只需將堆尾指針設到一開始的位置即可一次性釋放掉之前分多次分配的內存,但用 brk 分配內存比較麻煩,因為必須根據所需要的內存大小計算出堆尾指針的絕對位置。3、函數 sbrk 和 brk 搭配使用 ?(重點)
事實上,sbrk 和 brk 不過是移動堆尾指針的兩種不同方法,移動過程中還要兼顧虛擬內存和物理內存之間映射關系的建立和解除(以頁為單位)。 上面講到了: 使用sbrk函數申請內存比較方便,釋放內存不太方便;使用brk ?函數釋放內存比較方便,申請內存不太方便;所以一般使用sbrk函數和brk函數搭配使用,sbrk函數專門申請,brk函數專門釋放。
(1)示例說明
//使用sbrk函數和brk函數操作內存 #include <stdio.h> #include <unistd.h> #include <string.h> int main() {void* cur=sbrk(0); //動態分配內存 和malloc一樣的用法printf("cur=%p\n",cur);int* pi=(int*)sbrk(sizeof(int));*pi=100;double* pd=(double*)sbrk(sizeof(double));*pd=3.1415926;char* pc=(char*)sbrk(sizeof(char));strcpy(pc,"hello");printf("*pi=%d,*pd=%lf,pc=%s\n",*pi,*pd,pc);//釋放所有的動態內存brk(pi);//申請開始的cur=sbrk(0);printf("cur=%p\n",cur);return 0; } 輸出結果: cur=0x9b4c000 *pi=100,*pd=3.141593,pc=hello cur=0x9b4c0004、詳解 sbrk/brk
(1)man sbrk 查看對于sbrk/brk 描述:
DESCRIPTIONbrk() and sbrk() change the location of the program break, which defines the end of the process's data segment (i.e., the programbreak is the first location after the end of the uninitialized data segment). Increasing the program break has the effect of allo‐cating memory to the process; decreasing the break deallocates memory.brk() sets the end of the data segment to the value specified by addr, when that value is reasonable, the system has enough memory,and the process does not exceed its maximum data size (see setrlimit(2)).sbrk() increments the program's data space by increment bytes. Calling sbrk() with an increment of 0 can be used to find the cur‐rent location of the program break.簡單翻譯下: brk()和sbrk()更改 program break 的位置,該位置定義進程?data segment?的結束(即program break 是?uninitialized data segment?結束后的第一個位置)。 根據下圖看出,program break?指向當前堆尾,即堆區最后一個字節的下一個位置。
sbrk/brk 分配虛擬內存示意圖,如下:
(2)上圖分析
參看:Linux下進程內存管理之malloc和sbrk Linux 下每個進程所分配的虛擬內存空間是 3G,主要包括正文段、數據段、bbs段、堆、棧。而 malloc 所申請的內存空間就是從堆中分配的。 值得注意的地方就是 program break,這是進程堆尾地址。當用戶通過 malloc 函數申請空間的時候,實際就是利用 sbrk 函數移動 program break,使其向上增長,以獲得更大的堆空間。所以看起來很神秘的內存申請只不過是移動一個指針而已。(3)示例說明
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main (void) {void *cur = sbrk (0);printf ("cur = %p\n", cur);void *ptr = malloc (100);cur = sbrk (0);printf ("cur = %p\n", cur);printf ("ptr = %p\n", ptr);free (ptr); // ptr = NULL;cur = sbrk (0);printf ("cur = %p\n", cur);printf ("ptr = %p\n", ptr);return 0; } 輸出結果: cur = 0x9d9b000 cur = 0x9dbc000 ptr = 0x9d9b008 cur = 0x9dbc000 ptr = 0x9d9b008(4)示例總結
程序講解:程序首先用 sbrk (0) 得到堆尾地址,然后利用 malloc 申請了一個長 100 字節長度的空間,這個時候再看堆尾地址即申請空間的地址。最后,再釋放申請的空間然后再看堆尾地址和申請空間地址。 從輸出結果可到: 堆尾地址開始是 0x9d9b000 ,利用 malloc 申請完 100 字節空間之后,堆尾地址變為 0x9dbc000,增加了 0x21000。而 malloc 所申請的空間的起始地址是 0x9d9b008,比一開始的堆尾地址后移了 8 個字節。 到這里,參看文章結束,開始自己的理解內容。 工具:進制間轉換工具重點來了,十六進制 0x21000 轉換成 十進制為?135168 ?= 33 * 4096?上面講到當前系統內存頁的大小為 4Kb 得到結論: malloc 申請內存,系統會一次映射 33 個內存頁。如果超出申請內存不報錯,但如果超出 33 個內存頁的空間大小,則會出現段錯誤的。 舉個例子: #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() {void *cur = sbrk (0);printf ("cur=%p\n", cur);int* pi=(int*)malloc(4);printf("pi=%p\n",pi);//故意越界使用一下內存*(pi+10000)=250;printf("越界存放的數據是:%d\n",*(pi+10000));//250//故意超出33個內存頁的范圍*(pi+1025*33)=250;//*(pi+33789)=250;printf("越界存放的數據是:%d\n",*(pi+1025*33));//int類型 加1等于4個字節//十六進制的21000就是33個內存頁也就是十六進制的1000就是1個內存頁while(1);return 0; } 輸出結果: cur=0x8966000 pi=0x8966008 越界存放的數據是:250 段錯誤 (核心已轉儲) 示例總結: 用 dmesg 查看錯誤:
參看:C語言再學習 -- 段錯誤(核心已轉儲)?得到: #dmesg [131882.277528] test[5977]: segfault at 94ee08c ip 08048476 sp bf8dfcd0 error 6 in test[8048000+1000]錯誤代碼: error 6 參看:UNIX再學習 -- 錯誤和警告? 得到: #define ENXIO 6 /* No such device or address */ 即,沒有驅動或地址。 總結: 一般來說,使用 malloc 申請比較小的動態內存時,操作系統會一次性分配 33 個內存頁,從而提高效率。 使用malloc申請的動態內存,千萬不要越界訪問,極有可能破壞管理信息,從而引發段錯誤。
5、查看內存結構
(1)通過?cat /proc/進程ID/maps??查看內存的分配情況:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() {void *cur = sbrk (0);printf ("cur = %p\n", cur);//獲取當前進程的進程號printf("進程號是:%d\n",getpid());int* pi=(int*)malloc(4);printf("pi=%p\n",pi);//故意越界使用一下內存*(pi+10000)=250;printf("越界存放的數據是:%d\n",*(pi+10000));//250//故意超出33個內存頁的范圍*(pi+33789)=250;printf("越界存放的數據是:%d\n",*(pi+33789));//int類型 加1等于4個字節//十六進制的21000就是33個內存頁也就是十六進制的1000就是1個內存頁while (1);return 0; } 輸出結果: cur = 0x9e6c000 進程號是:6393 pi=0x9e6c008 越界存放的數據是:250 越界存放的數據是:250//在另一個終端查看: # cat /proc/6393/maps 08048000-08049000 r-xp 00000000 08:01 2102158 /home/tarena/project/c_test/a.out 08049000-0804a000 r--p 00000000 08:01 2102158 /home/tarena/project/c_test/a.out 0804a000-0804b000 rw-p 00001000 08:01 2102158 /home/tarena/project/c_test/a.out 09e6c000-09e8d000 rw-p 00000000 00:00 0 [heap] b755c000-b755d000 rw-p 00000000 00:00 0 b755d000-b76fc000 r-xp 00000000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b76fc000-b76fe000 r--p 0019f000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b76fe000-b76ff000 rw-p 001a1000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b76ff000-b7702000 rw-p 00000000 00:00 0 b7713000-b7716000 rw-p 00000000 00:00 0 b7716000-b7717000 r-xp 00000000 00:00 0 [vdso] b7717000-b7737000 r-xp 00000000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so b7737000-b7738000 r--p 0001f000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so b7738000-b7739000 rw-p 00020000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so bfe48000-bfe69000 rw-p 00000000 00:00 0 [stack] 通過,09e6c000-09e8d000 rw-p 00000000 00:00 0 ? ? ? ? ?[heap] ?(堆) 也可以看出該進程,映射了 33 個內存頁。(2)講解 maps 文件
參看:Linux下 /proc/maps 文件分析如:08048000-08049000 r-xp 00000000 08:01 2102158 ? ?/home/tarena/project/c_test/a.out該文件有6列,分別為:地址:庫在進程里地址范圍
權限:虛擬內存的權限,r=讀,w=寫,x=,s=共享,p=私有;
偏移量:庫在進程里地址范圍
設備:映像文件的主設備號和次設備號;
節點:映像文件的節點號;
路徑:?映像文件的路徑
每項都與一個vm_area_struct結構成員對應,
六、建立/解除到內存的映射
參看:mmap詳解1、mmap 函數
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);(1)參數解析
第一個參數:映射區內存起始地址,NULL 系統自動選定后返回? 第二個參數:映射區字節長度,自動按頁(4K)調整 第三個參數:映射區訪問權限,可取以下值: ? ? PROT_READ? --映射區可讀 ? ? PROT_WRITE--映射區可寫 ? ? PROT_EXEC ? --映射區可執行 ? ? PROT_NONE --映射區不可訪問 第四個參數:映射的模式,可取以下值: ? ? MAP_ANONYMOUS ? ?匿名映射,將虛擬內存映射到物理內存,而非文件,忽略 fd 和 offset 參數 ? ? MAP_PRIVATE ? ? ? ? ? ? ?對映射區的寫操作值反映搭配緩沖區中,并不會真正寫入文件? ? MAP_SHARED ? ? ? ? ? ? ?對映射區的寫操作直接反映到文件中
? ? MAP_DENYWRITE ? ? ? 拒絕其它對文件的寫操作 ? ? MAP_FIXED ? ? ? ? ? ? ? ? ?若在 addr 上無法創建映射,則失敗(無此標志系統會自動調整) ? ? MAP_LOCKED ? ? ? ? ? ? ?鎖定映射區,保證其不被換出 第五個參數:文件描述符,映射物理內存,給 0 即可 第六個參數:文件偏移量,自動按頁(4K)對齊,映射物理內存,給 0 即可
(2)返回值
成功返回映射的地址,失敗返回 MAP_FAILED(-1)(3)函數功能
創建虛擬內存到物理內存或文件的映射2、munmap 函數
#include <sys/mman.h> int munmap(void *addr, size_t length);(1)參數解析
第一個參數:映射區內存起始地址必須是頁的首地址 第二個參數:映射區字節長度,自動按頁(4K)調整(2)返回值
成功返回 0,失敗返回 -1(3)函數功能
解除虛擬內存到物理內存或文件的映射(4)示例說明
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <stdio.h> #include <stdlib.h>int main (void) {int fd;struct stat sb;fd = open("/etc/passwd", O_RDONLY); /*打開/etc/passwd */char *start = (char*)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);if(start == MAP_FAILED) /* 判斷是否映射成功 */perror ("mmap"), exit (1);printf("%s", start); munmap(start, sb.st_size); /* 解除映射 */close (fd);return 0; } 輸出結果: 同 cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh ....七、系統調用
UNIX/Linux 系統的大部分功能都是通過系統調用實現的,如 open、close 等。 UNIX/Linux 的系統調用已被封裝成 C 函數的形式,但它們并不是 C 語言標準庫的一部分。 標準庫函數大部分時間運行在用戶態,但部分函數偶爾也會調用系統調用,進入內核態,如 malloc、free 等。 程序員自己編寫的代碼也可以跳過標準庫,直接使用系統調用,如 brk、sbrk、mmap和 munmap 等,與操作系統內核交互,進入內核態。 系統調用在內核中實現,其外部結構定義在 C 庫中,該接口的實現借助軟中斷進入內核。 在學習內核驅動的時候,印象最深的一句話就是: 用戶空間切換到內核空間利用系統調用,就是由USR模式切換到SVC模式,說明系統調用本質上靠軟中斷來實現。 (這部分在講 Linux 時會重點講的,現在只做了解) 從應用程序到操作系統內核需要經歷如下調用鏈:然后介紹兩個用于系統調用的指令:
1、time
參看:time 指令(1)功能
用于統計給定命令所花費的總時間。(2)示例
# time ./a.out real 0m0.004s user 0m0.000s sys 0m0.000s(3)解析
輸出的信息分別顯示了該命令所花費的real時間、user時間和sys時間。real時間 ?是指掛鐘時間,也就是命令開始執行到結束的時間。這個短時間包括其他進程所占用的時間片,和進程被阻塞時所花費的時間。
user時間 ?是指進程花費在用戶模式中的CPU時間,這是唯一真正用于執行進程所花費的時間,其他進程和花費阻塞狀態中的時間沒有計算在內。
sys時間 ?是指花費在內核模式中的CPU時間,代表在內核中執系統調用所花費的時間,這也是真正由進程使用的CPU時間。
2、strace
參看:strace 指令(1)功能
我們可以使用strace對應用的系統調用和信號傳遞的跟蹤結果來對應用進行分析,以達到解決問題或者是了解應用工作過程的目的。(2)參數
-c 統計每一系統調用的所執行的時間,次數和出錯的次數等. -d 輸出strace關于標準錯誤的調試信息. -f 跟蹤由fork調用所產生的子進程. -ff 如果提供-o filename,則所有進程的跟蹤結果輸出到相應的filename.pid中,pid是各進程的進程號. -F 嘗試跟蹤vfork調用.在-f時,vfork不被跟蹤. -h 輸出簡要的幫助信息. -i 輸出系統調用的入口指針. -q 禁止輸出關于脫離的消息. -r 打印出相對時間關于,,每一個系統調用. -t 在輸出中的每一行前加上時間信息. -tt 在輸出中的每一行前加上時間信息,微秒級. -ttt 微秒級輸出,以秒了表示時間. -T 顯示每一調用所耗的時間. -v 輸出所有的系統調用.一些調用關于環境變量,狀態,輸入輸出等調用由于使用頻繁,默認不輸出. -V 輸出strace的版本信息. -x 以十六進制形式輸出非標準字符串 -xx 所有字符串以十六進制形式輸出. -a column 設置返回值的輸出位置.默認 為40. -e expr 指定一個表達式,用來控制如何跟蹤.格式如下: [qualifier=][!]value1[,value2]... qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用來限定的符號或數字.默認的 qualifier是 trace.感嘆號是否定符號.例如: -eopen等價于 -e trace=open,表示只跟蹤open調用.而-etrace!=open表示跟蹤除了open以外的其他調用.有兩個特殊的符號 all 和 none. 注意有些shell使用!來執行歷史記錄里的命令,所以要使用\\. -e trace=set 只跟蹤指定的系統 調用.例如:-e trace=open,close,rean,write表示只跟蹤這四個系統調用.默認的為set=all. -e trace=file 只跟蹤有關文件操作的系統調用. -e trace=process 只跟蹤有關進程控制的系統調用. -e trace=network 跟蹤與網絡有關的所有系統調用. -e strace=signal 跟蹤所有與系統信號有關的 系統調用 -e trace=ipc 跟蹤所有與進程通訊有關的系統調用 -e abbrev=set 設定 strace輸出的系統調用的結果集.-v 等與 abbrev=none.默認為abbrev=all. -e raw=set 將指 定的系統調用的參數以十六進制顯示. -e signal=set 指定跟蹤的系統信號.默認為all.如 signal=!SIGIO(或者signal=!io),表示不跟蹤SIGIO信號. -e read=set 輸出從指定文件中讀出 的數據.例如: -e read=3,5 -e write=set 輸出寫入到指定文件中的數據. -o filename 將strace的輸出寫入文件filename -p pid 跟蹤指定的進程pid. -s strsize 指定輸出的字符串的最大長度.默認為32.文件名一直全部輸出. -u username 以username 的UID和GID執行被跟蹤的命令(3)示例
主要用法,跟蹤程序系統調用使用情況: ?starce ./a.out 示例太多,自行嘗試。? 參看:strace 使用 參看:strace 指令八、未講部分
虛擬內存映射部分,我沒有往深了去講,就這已經寫了很多內容。 malloc 和 sbrk 關系未講,malloc/free 源代碼未講,另起一篇文章繼續研究內存管理。總結
以上是生活随笔為你收集整理的UNIX再学习 -- 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos7 RPM命令安装操作
- 下一篇: Redis-benchmark测试Red