UNIX再学习 -- 内存管理
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- 内存管理
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
C 語言部分,就一再的講內(nèi)存管理,參看:C語言再學習 -- 再論內(nèi)存管理??UNIX、Linux 部分還是要講,足見其重要。
一、存儲空間布局
1、我們先了解一個命令 size,繼而引出我們今天要講的內(nèi)容.
詳細可自行 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}以八進制,十進制或十六進制顯示數(shù)字 -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段)、數(shù)據(jù)段、bbs段(未初始數(shù)據(jù)段)的長度(以字節(jié)為單位)。 第 4 列 和第 5 列分別以十進制和十六進制表示的 3 段的總長度。 最后一列表示文件名,如果沒有指定輸出,則假定為 a.out。2、存儲空間布局
歷史沿襲至今,C 程序一直由下列幾部分組成:(1)正文段( text段/代碼段)
這是由 CPU 執(zhí)行的機器指令的部分。通常,正文段是可共享的,所以即使是頻繁執(zhí)行的程序(如文本編輯器,C 編譯器和 shell 等)在存儲器中也只需有一個副本,另外正文段常常是只讀的,以防止程序由于意外而修改其指令。 正文段是用來存放可執(zhí)行文件的操作指令,也就是說它是可執(zhí)行程序在內(nèi)存中的鏡像。(2)初始化數(shù)據(jù)段(數(shù)據(jù)段)
數(shù)據(jù)段用來存放可執(zhí)行文件中已經(jīng)初始化的全局變量,換句話說就是存放程序靜態(tài)分配的變量和全局變量。 例如,C 程序中任何函數(shù)之外的聲明: int num = 10;使此變量以其初值存放在初始化數(shù)據(jù)段中。(3)未初始化數(shù)據(jù)段(bbs段)
bbs段包含了程序中未初始化的全局變量,在程序開始執(zhí)行之前,內(nèi)核將此段中的數(shù)據(jù)初始化為 0 或空指針。 例如:函數(shù)外的聲明: long sum[100];使此變量存放在非初始化數(shù)據(jù)段中。(4)堆
堆是用于存放進程進行中被動態(tài)分配的內(nèi)存段,它大小并不固定,可動態(tài)擴張或縮減。當進程調(diào)用 malloc 等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴張);當利用 free 等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除(堆被縮減)。由于歷史上形成的慣例,堆位于未初始化數(shù)據(jù)段和棧之間。(5)棧
棧是用戶存放程序臨時創(chuàng)建的局部變量,也就是說我們函數(shù)括弧“{}”中定義的變量(但不包括static 聲明的變量,static 意味著在數(shù)據(jù)段中存放變量)。除此之外在函數(shù)調(diào)用結(jié)束后,函數(shù)的返回值也會被存放回棧中。由于棧的后進先出(LIFO)特點,所以棧特別方便用來保存/恢復調(diào)用現(xiàn)場。從這個意義上講我們可以把堆棧看成一個臨時數(shù)據(jù)寄存,交換的內(nèi)存區(qū)。(6)參數(shù)和環(huán)境區(qū)
命令行參數(shù)和環(huán)境變量。命令行參數(shù): 上篇文章已講,參看:UNIX再學習 -- 進程環(huán)境 每個C語言程序都必須有一個稱為main()的函數(shù),作為程序啟動的起點。當執(zhí)行程序時,命令行參數(shù)(command-line argument)(由shell逐一解析)通過兩個入?yún)⑻峁┙o main() 函數(shù)。第一個參數(shù)int argc,表示命令行參數(shù)的個數(shù)。第二個參數(shù)char *argv[],是一個指向命令行參數(shù)的指針數(shù)組,每一參數(shù)又都是以空字符(null) 結(jié)尾的字符串。第一個字符串,亦即argv[0]指向的,(通常)是該程序的名稱。argv中的指針列表以NULL指針結(jié)尾(即argv[argc]為NULL)。
環(huán)境變量: 參看:UNIX再學習 -- 環(huán)境變量
3、內(nèi)容回顧
下圖清楚的總結(jié)了存儲空間布局:看了一篇文章居然用 size 命令分析 linux 程序內(nèi)存段的分布,厲害了我的哥。 參看:用size命令分析linux程序內(nèi)存段的分布
(1)示例一
參考對象,依次與之比較 text、data、bss,查看下面的定義保存在哪個區(qū)域內(nèi)。?int main (void) {return 0; }# size text data bss dec hex filename1056 252 8 1316 524 a.out
(2)示例二
定義可執(zhí)行指令 、字面值常量 、具有常屬性且被 初始化的全局、靜態(tài)全局 和 靜態(tài)局部變量,經(jīng)比較其保存在代碼段參看:字面值常量參看:什么是可執(zhí)行語句const int i = 10; const static j = 12; int main (void) { const int n = 8; int a = 9; //變量 a 在棧區(qū);字面值常量 9 在代碼段 a = 10; //可執(zhí)行指令,在代碼段 const static int num = 10;return 0; } # size text data bss dec hex filename1100 252 8 1360 550 a.out
(3)示例三
定義?不具常屬性且被初始化的 全局、靜態(tài)全局 和 靜態(tài)局部變量,經(jīng)比較其保存在數(shù)據(jù)段?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)示例四
定義?未初始化全局、靜態(tài)全局?和 靜態(tài)局部變量?,經(jīng)比較其保存在 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 語言再學習部分,涉及到這部分的內(nèi)容也有不少處。 如:內(nèi)存區(qū)域劃分舉例說明;段錯誤、值傳遞、址傳遞里的堆棧內(nèi)容;關(guān)鍵字 static、const;存儲類、鏈接。 看到下面如此多的參看鏈接,你還覺的內(nèi)存管理部分簡單嗎 參看:C語言再學習 -- 內(nèi)存管理 參看:C語言再學習 -- 段錯誤(核心已轉(zhuǎn)儲) 參看:C語言再學習 -- 值傳遞,址傳遞,引用傳遞 參看:C語言再學習 -- 存儲類型關(guān)鍵字 參看:C語言再學習 -- 關(guān)鍵字const 參看:C語言再學習 -- 存儲類、鏈接二、存儲空間分配
ISO C 說明了 3 個用于存儲空間動態(tài)分配的函數(shù)。 #include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size); 三個函數(shù)返回:若成功則為非空指針,若出錯則為NULL void free(void *ptr);1、函數(shù)比較
(1)malloc,分配指定字節(jié)數(shù)的存儲區(qū)。此存儲區(qū)中的初始值不確定。 (2)calloc,為指定數(shù)量指定長度的對象分配存儲空間。該空間中的每一位(bit)都初始化為 0. (3)realloc,增加或減少以前分配區(qū)的長度。當增加長度時,可能需將以前分配區(qū)的內(nèi)容移到另一個足都大的區(qū)域,以便在尾端提供增加的存儲區(qū),而新增區(qū)域內(nèi)的初始值則不確定。2、函數(shù)解析
參看:淺談malloc,calloc,realloc函數(shù)之間的區(qū)別(1)malloc 函數(shù)
功能:
malloc函數(shù)可以從堆上獲得指定字節(jié)的內(nèi)存空間。返回值:
如果函數(shù)執(zhí)行成功,malloc返回獲得內(nèi)存空間的首地址;如果函數(shù)執(zhí)行失敗,那么返回值為NULL。 由于 malloc函數(shù)值的類型為void型指針,因此,可以將其值類型轉(zhuǎn)換后賦給任意類型指針,這樣就可以通過操作該類型指針來操作從堆上獲得的內(nèi)存空間。用法:
需要注意的是,malloc函數(shù)分配得到的內(nèi)存空間是未初始化的。因此,一般在使用該內(nèi)存空間時,要調(diào)用另一個函數(shù) memset 來將其初始化為全 0。memset 函數(shù)的聲明如下:void * memset (void * p,int c,int n) ;該函數(shù)可以將指定的內(nèi)存空間按字節(jié)單位置為指定的字符 c。其中,p 為要清零的內(nèi)存空間的首地址,c 為要設定的值,n 為被操作的內(nèi)存空間的字節(jié)長度。如果要用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; } 輸出結(jié)果: 0 0 2
(2)calloc 函數(shù)
功能:
calloc函數(shù)的功能與malloc函數(shù)的功能相似,都是從堆分配內(nèi)存。 int *p = (int *)calloc (SIZE, sizeof (int)); 等同于 int *p = (int *)malloc (SIZE * sizeof (int));返回值:
函數(shù)返回值為 void 型指針。如果執(zhí)行成功,函數(shù)從堆上獲得size * n的字節(jié)空間,并返回該空間的首地址。如果執(zhí)行失敗,函數(shù)返回NULL。用法:
該函數(shù)與malloc函數(shù)的一個顯著不同時是,calloc函數(shù)得到的內(nèi)存空間是經(jīng)過初始化的,其內(nèi)容全為0。calloc函數(shù)適合為數(shù)組申請空間,可以將 size 設置為數(shù)組元素的空間長度,將 n 設置為數(shù)組的容量。示例:
#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; } 輸出結(jié)果: p[0] = 0 p[1] = 1 p[2] = 2 p[3] = 3 p[4] = 4(3)realloc 函數(shù)
功能:
realloc函數(shù)的功能比 malloc 函數(shù)和 calloc 函數(shù)的功能更為豐富,可以實現(xiàn)內(nèi)存分配和內(nèi)存釋放。返回值:
成功返回非空指針,失敗返回 NULL用法:
realloc 函數(shù)將指針 ptr 指向的內(nèi)存塊的大小改變?yōu)?size 字節(jié)。如果 size 小于或等于 ptr 之前指向的空間大小,那么。保持原有狀態(tài)不變。如果 size 大于原來 ptr 之前指向的空間大小,那么,系統(tǒng)將 重新為 ptr 從堆上分配一塊大小為 size 的內(nèi)存空間,同時,將原來指向空間的內(nèi)容依次復制到新的內(nèi)存空間上,ptr 之前指向的空間被釋放。relloc 函數(shù)分配的空間也是未初始化的。示例:
#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; } 輸出結(jié)果: malloc 0x8d59008 realloc 0x8d59008 0 1 2 3 4 5 6 7 8 9(4)free 函數(shù)
功能:
free函數(shù)可以實現(xiàn)釋放內(nèi)存的功能。用法:
從堆上獲得的內(nèi)存空間在程序結(jié)束以后,系統(tǒng)不會將其自動釋放,需要程序員來自己管理。一個程序結(jié)束時,必須保證所有從堆上獲得的內(nèi)存空間已被安全釋放,否則,會導致內(nèi)存泄露。由于形參為void指針,free函數(shù)可以接受任意類型的指針實參。但是,free函數(shù)只是釋放指針指向的內(nèi)容,而該指針仍然指向原來指向的地方,此時,指針為野指針,如果此時操作該指針會導致不可預期的錯誤。 安全做法是:在使用free函數(shù)釋放指針指向的空間之后,將指針的值置為NULL。因此,對于上面的demo,需要在return?語句前加入以下兩行語句:
free(p);
p=NULL;
注意:使用 malloc 函數(shù)分配的堆空間在程序結(jié)束之前必須釋放。
3、new、delete函數(shù)
這部分現(xiàn)在不求掌握,只要了解。
new/delete 為?C++ 中動態(tài)內(nèi)存分配函數(shù),那么它和 C 中的有何不同呢,簡單介紹下。(1)動態(tài)內(nèi)存的分配和釋放的基本語句,我們應該都比較熟悉的,如:
int* p = new int; delete p;(2)C++ 的 new 操作符允許在動態(tài)分配內(nèi)存時對其做初始化。
int* p = new int; int* p = new int (); int* p = new int (100);(3)以數(shù)組方式 new 的,也要以數(shù)組方式 delete。
int* p = new int[4] {10, 20, 30, 40}; delete[] p;某些 C++ 實現(xiàn),用 new 操作符動態(tài)分配數(shù)組時,會在數(shù)組首元素前面多分配 4 或 8 個字節(jié),用以存放數(shù)組的長度。new 操作符所返回的地址是數(shù)組首元素的地址,而非所分配內(nèi)存的起始地址。如果將 new 操作符返回的地址直接交給 delete 操作符,將導致無效指針(invalidate pointer)異常。delete[] 操作符會將交給它的地址向低地址方向偏移 4 或 ?8個字節(jié),避免了無效指針異常的發(fā)生。
delete使用需要注意的地方:
(1)不能通過delete操作符釋放已釋放過的內(nèi)存
int* p = new int; delete p; delete p; // 核心轉(zhuǎn)儲(2)delete野指針后果未定義,delete空指針安全
int* p = new int; delete p; p = NULL; delete p; // 什么也不做內(nèi)存分配失敗操作:
(1)malloc函數(shù)
char* p = (char*)malloc (0xFFFFFFFF); if (p == NULL) { cerr << "內(nèi)存分配失敗!" << endl; exit (-1); }(2)new 函數(shù)
try { char* p = new char[0xFFFFFFFF]; } catch (bad_alloc& ex) { cerr << "內(nèi)存分配失敗!" << 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; } 輸出結(jié)果: 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三、虛擬內(nèi)存
1、這部分現(xiàn)在只做了解,Linux 部分會詳細介紹的。
Linux 操作系統(tǒng)采用虛擬內(nèi)存管理技術(shù),使得每個進程都有各自互不干涉的進程地址空間。該空間是塊大小為 4 G的線性虛擬空間,用戶所看到和接觸的都是該虛擬地址,無法看到實際的物理內(nèi)存地址。利用這種虛擬地址不但能起到保護操作系統(tǒng)的效果(用戶不能直接訪問物理內(nèi)存),而且更重要的是用戶程序可使用比實際物理內(nèi)存更大的地址空間。 4G的進程地址空間被認為的分成兩部分 -- 用戶空間和內(nèi)核空間。 用戶空間從 0 到 3G (0c0000000),內(nèi)核空間占據(jù)3G到4G。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內(nèi)核空間虛擬地址。例外情況只有用戶進程進行系統(tǒng)調(diào)用(代表用戶進程在內(nèi)核態(tài)執(zhí)行)等時刻可以訪問到內(nèi)核空間。 用戶空間對應進程,所以每當進程切換,用戶空間就會跟著變化;而內(nèi)核空間是由內(nèi)核負責映射,它并不會跟著進程改變,是固定的。內(nèi)核空間地址有自己對應的頁表(init_mm.pgd),用戶進程各自有不同的頁表。 每個進程的用戶空間都是完全獨立、互不相干的。你可以把程序同時運行 10 次(當然為了同時運行,讓它們在返回前一同睡眠100秒吧),你會看到 10 個進程占用的線性地址一模一樣。四、物理內(nèi)存管理(頁管理)
Linux 內(nèi)核管理物理內(nèi)存是通過分頁機制實現(xiàn)的,它將整個內(nèi)存劃分成無數(shù) 4K *(在i386體系結(jié)構(gòu)中)大小頁,從而分配和回收內(nèi)存的基本單位便是內(nèi)存頁了。利用分頁管理有助于靈活分配內(nèi)存地址,因為分配時不必要求必須有大塊的連續(xù)內(nèi)存,系統(tǒng)可以東一頁、西一頁的湊出所需要的內(nèi)存共進程使用。雖然如此,但是實際上系統(tǒng)使用內(nèi)存還是傾向于分配連續(xù)的內(nèi)存塊,因為分配連續(xù)內(nèi)存時,頁表不需要更改,因此能降低刷新率(頻繁刷新會很大增加訪問速度)。虛擬內(nèi)存到物理內(nèi)存的映射以頁(4K = 4096字節(jié))為單位。涉及到一個函數(shù) getpagesize
#include <unistd.h> int getpagesize(void);(1)函數(shù)功能
主要用于獲取當前系統(tǒng)中一個內(nèi)存頁的大小,一般為4Kb(2)示例說明
#include <stdio.h> #include <unistd.h>int main (void) {printf ("page size = %d Byte\n", getpagesize ());return 0; } 輸出結(jié)果: page size = 4096 Byte五、虛擬內(nèi)存管理
1、函數(shù) sbrk
#include <unistd.h> void *sbrk(intptr_t increment);(1)函數(shù)功能
主要用于按照參數(shù)指定的大小來調(diào)整內(nèi)存塊的大小。(2)返回值
成功返回上次調(diào)用 sbrk/brk 后的堆尾指針,失敗返回 -1.(3)參數(shù)解析
increment:虛擬內(nèi)存增量 (以字節(jié)為單位) 大于 0,分配虛擬內(nèi)存 小于 0,釋放虛擬內(nèi)存 等于 0,當前堆尾指針(4)函數(shù)解析
系統(tǒng)內(nèi)部維護一個指針,指向當前堆尾,即堆區(qū)最后一個字節(jié)的下一個位置,sbrk 函數(shù)根據(jù)增量參數(shù)調(diào)整該指針的位置,同時返回該指針在調(diào)整前的位置,期間若發(fā)現(xiàn)內(nèi)存也耗盡或空閑,則自動追加或取消內(nèi)存頁的映射。 規(guī)則:申請比較小的內(nèi)存時,一般會默認分配 1 個內(nèi)存頁,申請的內(nèi)存超過 1 個內(nèi)存頁時,會再次分配 1 個內(nèi)存頁。釋放內(nèi)存時,釋放完畢后剩余的內(nèi)存如果在一個內(nèi)存頁內(nèi),則一次性釋放 1 個內(nèi)存頁。(5)示例說明
//sbrk函數(shù)的使用 #include <stdio.h> #include <stdlib.h> #include <unistd.h>int main(void) {//申請4個字節(jié)的動態(tài)內(nèi)存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");//獲取內(nèi)存塊當前的末尾位置+1void* cur = sbrk(0);printf("cur = %p\n",cur);//p3+4//釋放4個字節(jié)的內(nèi)存空間void* p4 = sbrk(-4);printf("p4 = %p\n",p4);//p3+4//獲取內(nèi)存塊的當前位置cur = sbrk(0);printf("cur = %p\n",cur);//p3printf("--------------\n");//再次釋放4個字節(jié)的內(nèi)存p4 = sbrk(-4);printf("p4 = %p\n",p4);//p3cur = sbrk(0);printf("cur = %p\n",cur);//p2printf("--------------\n");//目前就剩下4個字節(jié)printf("當前進程PID:%d\n",getpid());printf("目前進程擁有4個字節(jié)\n");getchar();sbrk(4093);printf("申請了4093個字節(jié)的內(nèi)存,恰好超過了1個內(nèi)存頁\n");getchar();sbrk(-1);printf("釋放了1個字節(jié)的內(nèi)存,回到了1個內(nèi)存頁的范圍\n");getchar();return 0; } 輸出結(jié)果: p1 = 0x8141000,p2 = 0x8141004,p3 = 0x8141008 ------------------ cur = 0x814100c p4 = 0x814100c cur = 0x8141008 -------------- p4 = 0x8141008 cur = 0x8141004 -------------- 當前進程PID:4787 目前進程擁有4個字節(jié) 1 申請了4093個字節(jié)的內(nèi)存,恰好超過了1個內(nèi)存頁 釋放了1個字節(jié)的內(nèi)存,回到了1個內(nèi)存頁的范圍 2(6)示例總結(jié)
通過示例可以看出,用 sbrk 分配內(nèi)存比較方便,用多少內(nèi)存就傳多少增量參數(shù),同時返回指向新分配內(nèi)存區(qū)域的指針,但用 sbrk 做一次性內(nèi)存釋放比較麻煩,因為必須將所有的既往增量進行累加。2、函數(shù) brk
#include <unistd.h> int brk(void *addr);(1)函數(shù)功能
表示操作內(nèi)存的末尾地址到參數(shù)指定的位置,如果參數(shù)指定的位置大于當前的末尾位置,則申請內(nèi)存。如果參數(shù)指定的地址小于當前的末尾位置,則釋放內(nèi)存。(2)返回值
成功返回上次調(diào)用 sbrk/brk 后的堆尾指針,失敗返回 -1.(3)參數(shù)解析
addr:堆尾指針的新位置 大于堆尾指針的原位置:分配虛擬內(nèi)存 小于堆尾指針的原位置:釋放虛擬內(nèi)存 等于堆尾指針的原位置:什么也沒有做(4)函數(shù)解析
系統(tǒng)內(nèi)部維護一個指針,指向當前堆尾,即堆區(qū)最后一個字節(jié)的下一個位置,brk函數(shù)根據(jù)指針參數(shù)設置該指針的位置,期間若發(fā)現(xiàn)內(nèi)存頁耗盡或空閑,則自動追加或取消內(nèi)存頁的映射。(5)示例說明
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main (void) {//獲取一個有效位置void *p = sbrk (0);printf ("p = %p\n", p);//使用 brk 函數(shù)申請 4 個字節(jié)內(nèi)存int res = brk (p + 4);if (-1 == res)perror ("fail to brk"), exit (1);void *cur = sbrk (0);printf ("cur = %p\n", cur);//申請 4 個字節(jié)brk (p + 8);cur = sbrk (0);printf ("cur = %p\n", cur);//釋放了 4 個字節(jié)brk (p + 4);cur = sbrk (0);printf ("cur = %p\n", cur);//釋放了所有字節(jié)brk (p);cur = sbrk (0);printf ("cur = %p\n", cur);return 0; } 輸出結(jié)果: p = 0x98cc000 cur = 0x98cc004 cur = 0x98cc008 cur = 0x98cc004 cur = 0x98cc000(3)示例總結(jié)
通過示例可以看出,用 brk 釋放內(nèi)存比價方便,只需將堆尾指針設到一開始的位置即可一次性釋放掉之前分多次分配的內(nèi)存,但用 brk 分配內(nèi)存比較麻煩,因為必須根據(jù)所需要的內(nèi)存大小計算出堆尾指針的絕對位置。3、函數(shù) sbrk 和 brk 搭配使用 ?(重點)
事實上,sbrk 和 brk 不過是移動堆尾指針的兩種不同方法,移動過程中還要兼顧虛擬內(nèi)存和物理內(nèi)存之間映射關(guān)系的建立和解除(以頁為單位)。 上面講到了: 使用sbrk函數(shù)申請內(nèi)存比較方便,釋放內(nèi)存不太方便;使用brk ?函數(shù)釋放內(nèi)存比較方便,申請內(nèi)存不太方便;所以一般使用sbrk函數(shù)和brk函數(shù)搭配使用,sbrk函數(shù)專門申請,brk函數(shù)專門釋放。
(1)示例說明
//使用sbrk函數(shù)和brk函數(shù)操作內(nèi)存 #include <stdio.h> #include <unistd.h> #include <string.h> int main() {void* cur=sbrk(0); //動態(tài)分配內(nèi)存 和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);//釋放所有的動態(tài)內(nèi)存brk(pi);//申請開始的cur=sbrk(0);printf("cur=%p\n",cur);return 0; } 輸出結(jié)果: 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?的結(jié)束(即program break 是?uninitialized data segment?結(jié)束后的第一個位置)。 根據(jù)下圖看出,program break?指向當前堆尾,即堆區(qū)最后一個字節(jié)的下一個位置。
sbrk/brk 分配虛擬內(nèi)存示意圖,如下:
(2)上圖分析
參看:Linux下進程內(nèi)存管理之malloc和sbrk Linux 下每個進程所分配的虛擬內(nèi)存空間是 3G,主要包括正文段、數(shù)據(jù)段、bbs段、堆、棧。而 malloc 所申請的內(nèi)存空間就是從堆中分配的。 值得注意的地方就是 program break,這是進程堆尾地址。當用戶通過 malloc 函數(shù)申請空間的時候,實際就是利用 sbrk 函數(shù)移動 program break,使其向上增長,以獲得更大的堆空間。所以看起來很神秘的內(nèi)存申請只不過是移動一個指針而已。(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; } 輸出結(jié)果: cur = 0x9d9b000 cur = 0x9dbc000 ptr = 0x9d9b008 cur = 0x9dbc000 ptr = 0x9d9b008(4)示例總結(jié)
程序講解:程序首先用 sbrk (0) 得到堆尾地址,然后利用 malloc 申請了一個長 100 字節(jié)長度的空間,這個時候再看堆尾地址即申請空間的地址。最后,再釋放申請的空間然后再看堆尾地址和申請空間地址。 從輸出結(jié)果可到: 堆尾地址開始是 0x9d9b000 ,利用 malloc 申請完 100 字節(jié)空間之后,堆尾地址變?yōu)?0x9dbc000,增加了 0x21000。而 malloc 所申請的空間的起始地址是 0x9d9b008,比一開始的堆尾地址后移了 8 個字節(jié)。 到這里,參看文章結(jié)束,開始自己的理解內(nèi)容。 工具:進制間轉(zhuǎn)換工具重點來了,十六進制 0x21000 轉(zhuǎn)換成 十進制為?135168 ?= 33 * 4096?上面講到當前系統(tǒng)內(nèi)存頁的大小為 4Kb 得到結(jié)論: malloc 申請內(nèi)存,系統(tǒng)會一次映射 33 個內(nèi)存頁。如果超出申請內(nèi)存不報錯,但如果超出 33 個內(nèi)存頁的空間大小,則會出現(xiàn)段錯誤的。 舉個例子: #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);//故意越界使用一下內(nèi)存*(pi+10000)=250;printf("越界存放的數(shù)據(jù)是:%d\n",*(pi+10000));//250//故意超出33個內(nèi)存頁的范圍*(pi+1025*33)=250;//*(pi+33789)=250;printf("越界存放的數(shù)據(jù)是:%d\n",*(pi+1025*33));//int類型 加1等于4個字節(jié)//十六進制的21000就是33個內(nèi)存頁也就是十六進制的1000就是1個內(nèi)存頁while(1);return 0; } 輸出結(jié)果: cur=0x8966000 pi=0x8966008 越界存放的數(shù)據(jù)是:250 段錯誤 (核心已轉(zhuǎn)儲) 示例總結(jié): 用 dmesg 查看錯誤:
參看:C語言再學習 -- 段錯誤(核心已轉(zhuǎn)儲)?得到: #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 */ 即,沒有驅(qū)動或地址。 總結(jié): 一般來說,使用 malloc 申請比較小的動態(tài)內(nèi)存時,操作系統(tǒng)會一次性分配 33 個內(nèi)存頁,從而提高效率。 使用malloc申請的動態(tài)內(nèi)存,千萬不要越界訪問,極有可能破壞管理信息,從而引發(fā)段錯誤。
5、查看內(nèi)存結(jié)構(gòu)
(1)通過?cat /proc/進程ID/maps??查看內(nèi)存的分配情況:
#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);//故意越界使用一下內(nèi)存*(pi+10000)=250;printf("越界存放的數(shù)據(jù)是:%d\n",*(pi+10000));//250//故意超出33個內(nèi)存頁的范圍*(pi+33789)=250;printf("越界存放的數(shù)據(jù)是:%d\n",*(pi+33789));//int類型 加1等于4個字節(jié)//十六進制的21000就是33個內(nèi)存頁也就是十六進制的1000就是1個內(nèi)存頁while (1);return 0; } 輸出結(jié)果: cur = 0x9e6c000 進程號是:6393 pi=0x9e6c008 越界存放的數(shù)據(jù)是:250 越界存放的數(shù)據(jù)是: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 個內(nèi)存頁。(2)講解 maps 文件
參看:Linux下 /proc/maps 文件分析如:08048000-08049000 r-xp 00000000 08:01 2102158 ? ?/home/tarena/project/c_test/a.out該文件有6列,分別為:地址:庫在進程里地址范圍
權(quán)限:虛擬內(nèi)存的權(quán)限,r=讀,w=寫,x=,s=共享,p=私有;
偏移量:庫在進程里地址范圍
設備:映像文件的主設備號和次設備號;
節(jié)點:映像文件的節(jié)點號;
路徑:?映像文件的路徑
每項都與一個vm_area_struct結(jié)構(gòu)成員對應,
六、建立/解除到內(nèi)存的映射
參看:mmap詳解1、mmap 函數(shù)
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);(1)參數(shù)解析
第一個參數(shù):映射區(qū)內(nèi)存起始地址,NULL 系統(tǒng)自動選定后返回? 第二個參數(shù):映射區(qū)字節(jié)長度,自動按頁(4K)調(diào)整 第三個參數(shù):映射區(qū)訪問權(quán)限,可取以下值: ? ? PROT_READ? --映射區(qū)可讀 ? ? PROT_WRITE--映射區(qū)可寫 ? ? PROT_EXEC ? --映射區(qū)可執(zhí)行 ? ? PROT_NONE --映射區(qū)不可訪問 第四個參數(shù):映射的模式,可取以下值: ? ? MAP_ANONYMOUS ? ?匿名映射,將虛擬內(nèi)存映射到物理內(nèi)存,而非文件,忽略 fd 和 offset 參數(shù) ? ? MAP_PRIVATE ? ? ? ? ? ? ?對映射區(qū)的寫操作值反映搭配緩沖區(qū)中,并不會真正寫入文件? ? MAP_SHARED ? ? ? ? ? ? ?對映射區(qū)的寫操作直接反映到文件中
? ? MAP_DENYWRITE ? ? ? 拒絕其它對文件的寫操作 ? ? MAP_FIXED ? ? ? ? ? ? ? ? ?若在 addr 上無法創(chuàng)建映射,則失敗(無此標志系統(tǒng)會自動調(diào)整) ? ? MAP_LOCKED ? ? ? ? ? ? ?鎖定映射區(qū),保證其不被換出 第五個參數(shù):文件描述符,映射物理內(nèi)存,給 0 即可 第六個參數(shù):文件偏移量,自動按頁(4K)對齊,映射物理內(nèi)存,給 0 即可
(2)返回值
成功返回映射的地址,失敗返回 MAP_FAILED(-1)(3)函數(shù)功能
創(chuàng)建虛擬內(nèi)存到物理內(nèi)存或文件的映射2、munmap 函數(shù)
#include <sys/mman.h> int munmap(void *addr, size_t length);(1)參數(shù)解析
第一個參數(shù):映射區(qū)內(nèi)存起始地址必須是頁的首地址 第二個參數(shù):映射區(qū)字節(jié)長度,自動按頁(4K)調(diào)整(2)返回值
成功返回 0,失敗返回 -1(3)函數(shù)功能
解除虛擬內(nèi)存到物理內(nèi)存或文件的映射(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; } 輸出結(jié)果: 同 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 ....七、系統(tǒng)調(diào)用
UNIX/Linux 系統(tǒng)的大部分功能都是通過系統(tǒng)調(diào)用實現(xiàn)的,如 open、close 等。 UNIX/Linux 的系統(tǒng)調(diào)用已被封裝成 C 函數(shù)的形式,但它們并不是 C 語言標準庫的一部分。 標準庫函數(shù)大部分時間運行在用戶態(tài),但部分函數(shù)偶爾也會調(diào)用系統(tǒng)調(diào)用,進入內(nèi)核態(tài),如 malloc、free 等。 程序員自己編寫的代碼也可以跳過標準庫,直接使用系統(tǒng)調(diào)用,如 brk、sbrk、mmap和 munmap 等,與操作系統(tǒng)內(nèi)核交互,進入內(nèi)核態(tài)。 系統(tǒng)調(diào)用在內(nèi)核中實現(xiàn),其外部結(jié)構(gòu)定義在 C 庫中,該接口的實現(xiàn)借助軟中斷進入內(nèi)核。 在學習內(nèi)核驅(qū)動的時候,印象最深的一句話就是: 用戶空間切換到內(nèi)核空間利用系統(tǒng)調(diào)用,就是由USR模式切換到SVC模式,說明系統(tǒng)調(diào)用本質(zhì)上靠軟中斷來實現(xiàn)。 (這部分在講 Linux 時會重點講的,現(xiàn)在只做了解) 從應用程序到操作系統(tǒng)內(nèi)核需要經(jīng)歷如下調(diào)用鏈:然后介紹兩個用于系統(tǒng)調(diào)用的指令:
1、time
參看:time 指令(1)功能
用于統(tǒng)計給定命令所花費的總時間。(2)示例
# time ./a.out real 0m0.004s user 0m0.000s sys 0m0.000s(3)解析
輸出的信息分別顯示了該命令所花費的real時間、user時間和sys時間。real時間 ?是指掛鐘時間,也就是命令開始執(zhí)行到結(jié)束的時間。這個短時間包括其他進程所占用的時間片,和進程被阻塞時所花費的時間。
user時間 ?是指進程花費在用戶模式中的CPU時間,這是唯一真正用于執(zhí)行進程所花費的時間,其他進程和花費阻塞狀態(tài)中的時間沒有計算在內(nèi)。
sys時間 ?是指花費在內(nèi)核模式中的CPU時間,代表在內(nèi)核中執(zhí)系統(tǒng)調(diào)用所花費的時間,這也是真正由進程使用的CPU時間。
2、strace
參看:strace 指令(1)功能
我們可以使用strace對應用的系統(tǒng)調(diào)用和信號傳遞的跟蹤結(jié)果來對應用進行分析,以達到解決問題或者是了解應用工作過程的目的。(2)參數(shù)
-c 統(tǒng)計每一系統(tǒng)調(diào)用的所執(zhí)行的時間,次數(shù)和出錯的次數(shù)等. -d 輸出strace關(guān)于標準錯誤的調(diào)試信息. -f 跟蹤由fork調(diào)用所產(chǎn)生的子進程. -ff 如果提供-o filename,則所有進程的跟蹤結(jié)果輸出到相應的filename.pid中,pid是各進程的進程號. -F 嘗試跟蹤vfork調(diào)用.在-f時,vfork不被跟蹤. -h 輸出簡要的幫助信息. -i 輸出系統(tǒng)調(diào)用的入口指針. -q 禁止輸出關(guān)于脫離的消息. -r 打印出相對時間關(guān)于,,每一個系統(tǒng)調(diào)用. -t 在輸出中的每一行前加上時間信息. -tt 在輸出中的每一行前加上時間信息,微秒級. -ttt 微秒級輸出,以秒了表示時間. -T 顯示每一調(diào)用所耗的時間. -v 輸出所有的系統(tǒng)調(diào)用.一些調(diào)用關(guān)于環(huán)境變量,狀態(tài),輸入輸出等調(diào)用由于使用頻繁,默認不輸出. -V 輸出strace的版本信息. -x 以十六進制形式輸出非標準字符串 -xx 所有字符串以十六進制形式輸出. -a column 設置返回值的輸出位置.默認 為40. -e expr 指定一個表達式,用來控制如何跟蹤.格式如下: [qualifier=][!]value1[,value2]... qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用來限定的符號或數(shù)字.默認的 qualifier是 trace.感嘆號是否定符號.例如: -eopen等價于 -e trace=open,表示只跟蹤open調(diào)用.而-etrace!=open表示跟蹤除了open以外的其他調(diào)用.有兩個特殊的符號 all 和 none. 注意有些shell使用!來執(zhí)行歷史記錄里的命令,所以要使用\\. -e trace=set 只跟蹤指定的系統(tǒng) 調(diào)用.例如:-e trace=open,close,rean,write表示只跟蹤這四個系統(tǒng)調(diào)用.默認的為set=all. -e trace=file 只跟蹤有關(guān)文件操作的系統(tǒng)調(diào)用. -e trace=process 只跟蹤有關(guān)進程控制的系統(tǒng)調(diào)用. -e trace=network 跟蹤與網(wǎng)絡有關(guān)的所有系統(tǒng)調(diào)用. -e strace=signal 跟蹤所有與系統(tǒng)信號有關(guān)的 系統(tǒng)調(diào)用 -e trace=ipc 跟蹤所有與進程通訊有關(guān)的系統(tǒng)調(diào)用 -e abbrev=set 設定 strace輸出的系統(tǒng)調(diào)用的結(jié)果集.-v 等與 abbrev=none.默認為abbrev=all. -e raw=set 將指 定的系統(tǒng)調(diào)用的參數(shù)以十六進制顯示. -e signal=set 指定跟蹤的系統(tǒng)信號.默認為all.如 signal=!SIGIO(或者signal=!io),表示不跟蹤SIGIO信號. -e read=set 輸出從指定文件中讀出 的數(shù)據(jù).例如: -e read=3,5 -e write=set 輸出寫入到指定文件中的數(shù)據(jù). -o filename 將strace的輸出寫入文件filename -p pid 跟蹤指定的進程pid. -s strsize 指定輸出的字符串的最大長度.默認為32.文件名一直全部輸出. -u username 以username 的UID和GID執(zhí)行被跟蹤的命令(3)示例
主要用法,跟蹤程序系統(tǒng)調(diào)用使用情況: ?starce ./a.out 示例太多,自行嘗試。? 參看:strace 使用 參看:strace 指令八、未講部分
虛擬內(nèi)存映射部分,我沒有往深了去講,就這已經(jīng)寫了很多內(nèi)容。 malloc 和 sbrk 關(guān)系未講,malloc/free 源代碼未講,另起一篇文章繼續(xù)研究內(nèi)存管理。總結(jié)
以上是生活随笔為你收集整理的UNIX再学习 -- 内存管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos7 RPM命令安装操作
- 下一篇: UNIX再学习 -- 死磕内存管理