原文地址:https://blog.csdn.net/zhangyifei216/article/details/51423580
本文是作者閱讀TLPI(The Linux Programer Interface的總結),爲了突出重點,避免一刀砍,我不會過多的去介紹基本的概念和用法,我重點會去介紹原理和細節。因此對於本文的讀者,至少要求讀過APUE,或者是實際有寫過相關代碼的程序員,因爲知識有點零散,所以我會盡可能以FAQ的形式呈現給讀者。
進程
一個進程的內存佈局是什麼樣的?
每個進程所所分配的內存由很多部分組成,通常我們稱之爲段,一般會有如下段:
- 文本段 包含了進程執行的程序機器語言指令,文本段具有隻讀屬性,以防止進程通過錯誤指針意外修改自身的指令。
- 初始化數據段包含了顯示初始化的全局變量和靜態變量,當程序加載到內存時,從可執行文件中讀取這些變量的值
- 未初始化數據段包含了未進行顯式初始化的全局變量和靜態變量,程序啓動之前,系統將本段內所有的內存初始化爲0。
- 棧段是一個動態增長和收縮的段,由棧幀組成,系統會爲每個當前調用的函數分配一個棧幀,棧幀中存儲了函數的具備變量,實參,和返回值。
- 堆段是可在運行時動態進程內存分配的一塊區域,堆頂端稱作program break
注: 爲什麼要區分初始化數據段,和未初始化數據段呢?,未初始化數據段簡稱爲BSS段,有何含義BSS全稱爲Block Started by Symbol,其主要原因在於程序在磁盤上存儲時,沒有必要爲未經初始化的變量分配存儲空間,相反,可執行文件只需要記錄未初始化數據段的位置和所需大小即可。直到運行時才分配內存空間。通過size命令可以顯示可執行文件的文本段,初始化數據段,未初始化數據段的段大小信息。
如何知道進程的文本段,初始化數據段和非初始化數據段的結束位置?
大多數UNIX實現中C語言編程環境提供了三個全局符號:etext,edata,end,可在程序內使用這些符號獲取文本段,初始化數據段,未初始化數據段結尾處下一字節的地址。代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char etext,edata,end;
int main()
{
printf("%p\n",&etext);
printf("%p\n",&edata);
printf("%p\n",&end);
}
如何獲取虛擬內存的頁面大小?
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("page-size:%d\n",sysconf(_SC_PAGESIZE));
}
如何讀取任一進程的命令行參數和程序名?
通過讀取proc/PID/cmdline
可以得到任一進程的命令行參數信息,如果想獲取程序本身的命令行參數,可以使用proc/self/cmdline
來讀取,對於如何獲取進程的程序名有如下兩種方法:
- 讀取
/proc/self/exe
的符號鏈接內容,這個文件會通過符號鏈接到真正的可執行文件路徑,是絕對路徑,通過readlink可以讀取其中鏈接的絕對路徑名稱 #include <stdio.h> #include <unistd.h> #include <string.h> char * get_program_path(char *buf,int count); char * get_program_name(char *buf,int count); int main() { //程序測試 char buf[1024]; bzero(buf,sizeof(buf)); //打印路徑名稱 printf("%s\n",get_program_path(buf,sizeof(buf))); bzero(buf,sizeof(buf)); //打印程序名稱 printf("%s\n",get_program_name(buf,sizeof(buf))); } /* * 獲取程序的路徑名稱 */ char * get_program_path(char *buf,int count) { int i=0; int retval = readlink("/proc/self/exe",buf,count-1); if((retval < 0 || retval >= count - 1)) { return NULL; } //添加末尾結束符號 buf[retval] = '\0'; char *end = strrchr(buf,'/'); if(NULL == end) buf[0] = '\0'; else *end = '\0'; return buf; } /* * 獲取這個程序的文件名,其實這有點多餘,argv[0] * 就代表這個執行的程序文件名 */ char * get_program_name(char *buf,int count) { int retval = readlink("/proc/self/exe",buf,count-1); if((retval < 0 || retval >= count - 1)) { return NULL; } buf[retval] = '\0'; //獲取指向最後一次出現'/'字符的指針 return strrchr(buf,'/'); }
- 通過GNU C語言提供的兩個全局變量來實現
#define _GNU_SOURCE #include <stdio.h> #include <errno.h> extern char * program_invocation_name; extern char * program_invocation_short_name; int main() { printf("%s\n",program_invocation_name); printf("%s\n",program_invocation_short_name); }
volatile關鍵字的作用?
將變量聲明爲volatile是告訴優化器不要對其進行優化,從而避免了代碼重組。例如下面這段程序:
int a = 10;
int main()
{
a = a + 1;
while(a == 2);
}
對上面的代碼使用gcc -O -S
進行優化編譯,查看其彙編代碼。關鍵代碼如下:
movl a(%rip), %eax
addl $1, %eax //a = a + 1
movl %eax, a(%rip) //寫回內存
cmpl $2, %eax //while(a == 2)
編譯器對齊進行優化,發現a = a + 1
和while(a == 2)
中間沒有對a進行修改,因此根據代碼的上下文分析後進行優化,直接拿%eax
進行比較。但是編譯器的優化僅僅只能根據當前的代碼上下文來優化,如果在多線程場景下另外一個函數中對a進行了修改,但是這裏卻使用的是a的舊值,這就會導致代碼邏輯上出現了問題,很難debug。我們來看看加了volatile關鍵字後情況變成什麼樣了。下面是加了volatile後的彙編代碼:
movl a(%rip), %eax
addl $1, %eax
movl %eax, a(%rip)
movl a(%rip), %eax //在比較之前重新讀取了a的值
cmpl $2, %eax
volatile關鍵字遠遠在比我這裏描述的更加複雜,這裏有篇文章建議大家閱讀一下,深刻了解下這個關鍵字的作用。C/C++ Volatile關鍵詞深度剖析。
內存分配
brk 和 sbrk的作用是很什麼?
brk和sbrk是linux提供給我們的兩個用於分配內存的系統調用,內存的分配其實就是將堆區的內存空間進行隱射和物理內存頁進行關聯。我們的程序會大量的調用這兩個系統調用,這導致一個問題就是大量的系統調用開銷的產生,爲此malloc和free封裝了這兩個函數,通過brk和sbrk預先分配一段比較大的內存空間,然後一點點的分配出去,通過維護內部的一個數據結構記錄分配出去的內存塊信息,方便後面的回收和合並這樣就避免了大量系統調用的調用開銷。下面是這兩個函數的函數原型:
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
brk
可以調整program break
的位置,program break
是堆的頂端,也就是堆最大可以增長到的地址,而sbrk則是設置program break
爲原有地址加上increment後的位置。sbrk(0)
返回當前的program break
位置。
有哪些malloc的調試工具和庫?
glibc提供了一些malloc調試庫分別如下:
- mtrace和muntrace函數分別在程序中打開和關閉對內存分配調用進行跟蹤的功能
[root@localhost test]# cat mtrace.c #include <mcheck.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char *argv[]) { int j; mtrace(); for (j = 0; j < 2; j++) malloc(100); /* Never freed--a memory leak */ calloc(16, 16); /* Never freed--a memory leak */ exit(EXIT_SUCCESS); } 進行編譯,生成mtrace調試信息,因爲調試信息叫複雜,glibc提供了mtrace命令用戶分析調試信息 [root@localhost test]# gcc -g mtrace.c [root@localhost test]# export MALLOC_TRACE="/tmp/t" //設置這個環境變量,mtrace會讀取這個環境變量,把調試信息輸出到這個環境變量所指向的文件 [root@localhost test]# ./a.out [root@localhost test]# cat /tmp/t = Start @ ./a.out:[0x400637] + 0x132a460 0x64 @ ./a.out:[0x400637] + 0x132a4d0 0x64 @ ./a.out:[0x400650] + 0x132a540 0x100 [root@localhost test]# mtrace ./a.out $MALLOC_TRACE Memory not freed: ----------------- Address Size Caller //在12行進行了二次內存分配,大小是0x64,在16行進行了一次內存分配,分配的大小是0x100 0x000000000132a460 0x64 at /root/test/mtrace.c:12 (discriminator 2) 0x000000000132a4d0 0x64 at /root/test/mtrace.c:12 (discriminator 2) 0x000000000132a540 0x100 at /root/test/mtrace.c:16
mtrace命令是用來分析mtrace()的輸出,默認是沒有安裝的,它是glibc提供的,所以需要額外安裝glibc-utils。
- mcheck和mproe函數允許對已分配的內存塊進行一致性檢查。例如對已分配內存之外進行了寫操作。
[root@localhost test]# cat mcheck.c #include <stdlib.h> #include <stdio.h> #include <mcheck.h> int main(int argc, char *argv[]) { char *p; if (mcheck(NULL) != 0) { //需要在第一次調用malloc前調用。 fprintf(stderr, "mcheck() failed\n"); exit(EXIT_FAILURE); } p = malloc(1000); free(p); free(p); //doubel free exit(EXIT_SUCCESS); } [root@localhost test]# ./a.out block freed twice Aborted
上面只是簡單的演示了其基本用法,更詳細的用法參見man 文檔。
MALLOC_CHECK_
環境變量 提供了類似於mcheck
的功能和mprobe的功能,但是MALLOC_CHECK_
這種方式無需進行修改和重新編譯,通過設置不同的值來控制對內存分配錯誤的響應方式下面是一個使用MALLOC_CHECK_
環境變量的實現方式mcheck
的功能的例子:#include <stdlib.h> #include <stdio.h> #include <mcheck.h> int main(int argc, char *argv[]) { char *p; p = malloc(1000); ++p; free(p); //非法釋放 free(p); //double free exit(EXIT_SUCCESS); } 編譯上面得到代碼之前先導出下MALLOC_CHECK_環境變量 [root@localhost test]# export MALLOC_CHECK_=1 //其值應該是一個單數字,具體的含義可以參考man 3 mallopt [root@localhost test]# gcc mcheck.c -lmcheck //編譯的時候鏈接mcheck即可 [root@localhost test]# ./a.out memory clobbered before allocated block Aborted
上面的三種方式都是通過函數庫的形式給程序添加了內存分配的檢測,和追蹤功能,我們也可以使用一些第三方的工具來完成這些功能,比較流行的有Valgrind,Insure++等。
如何控制和監測malloc函數包?
linux提供了mallopt用來修改各選參數,以控制malloc所採用的算法,例如:何時進行sbrk進行堆收縮。規定從堆中分配內存塊的上限,超出上限的內存塊則使用mmap系統調用,此外還提供了mallinfo函數,這個函數會返回一個結構包含了malloc分配內存的各種統計數據。下面是mallinfo的接口聲明和基本使用。
#include <malloc.h>
struct mallinfo mallinfo(void);
struct mallinfo {
int arena; /* Non-mmapped space allocated (bytes) */
int ordblks; /* Number of free chunks */
int smblks; /* Number of free fastbin blocks */
int hblks; /* Number of mmapped regions */
int hblkhd; /* Space allocated in mmapped regions (bytes) */
int usmblks; /* Maximum total allocated space (bytes) */
int fsmblks; /* Space in freed fastbin blocks (bytes) */
int uordblks; /* Total allocated space (bytes) */
int fordblks; /* Total free space (bytes) */
int keepcost; /* Top-most, releasable space (bytes) */
};
下面是一段代碼顯示了當前進程的malloc分配內存信息
#include <stdlib.h>
#include <stdio.h>
#include <mcheck.h>
#include <malloc.h>
static void display_mallinfo(void)
{
struct mallinfo mi;
mi = mallinfo();
printf("Total non-mmapped bytes (arena): %d\n", mi.arena);
printf("# of free chunks (ordblks): %d\n", mi.ordblks);
printf("# of free fastbin blocks (smblks): %d\n", mi.smblks);
printf("# of mapped regions (hblks): %d\n", mi.hblks);
printf("Bytes in mapped regions (hblkhd): %d\n", mi.hblkhd);
printf("Max. total allocated space (usmblks): %d\n", mi.usmblks);
printf("Free bytes held in fastbins (fsmblks): %d\n", mi.fsmblks);
printf("Total allocated space (uordblks): %d\n", mi.uordblks);
printf("Total free space (fordblks): %d\n", mi.fordblks);
printf("Topmost releasable block (keepcost): %d\n", mi.keepcost);
}
int main(int argc, char *argv[])
{
char *p;
p = malloc(1000);
display_mallinfo();
free(p);
printf("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
display_mallinfo();
exit(EXIT_SUCCESS);
}
下面是運行後的結果:
[root@localhost test]# ./a.out
Total non-mmapped bytes (arena): 135168
# of free chunks (ordblks): 1
# of free fastbin blocks (smblks): 0
# of mapped regions (hblks): 0
Bytes in mapped regions (hblkhd): 0
Max. total allocated space (usmblks): 0
Free bytes held in fastbins (fsmblks): 0
Total allocated space (uordblks): 1024 //這是分配的內存,我的代碼中分配的是1000,因爲malloc會字節對齊,因此變成了1024
Total free space (fordblks): 134144
Topmost releasable block (keepcost): 134144
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Total non-mmapped bytes (arena): 135168
# of free chunks (ordblks): 1
# of free fastbin blocks (smblks): 0
# of mapped regions (hblks): 0
Bytes in mapped regions (hblkhd): 0
Max. total allocated space (usmblks): 0
Free bytes held in fastbins (fsmblks): 0
Total allocated space (uordblks): 0
Total free space (fordblks): 135168
Topmost releasable block (keepcost): 135168
關於mallopt的使用這裏就略過了,因爲這東西較複雜,筆者自己也沒認真看過。如果你希望瞭解,我給你推薦的第一手資料絕對是man 3 mallopt
。
爲什麼要內存對齊,如何內存對齊?
關於爲什麼要內存對齊,我推薦給大家一篇文章Data alignment: Straighten up and fly right,通常我們在討論內存的時候常常會使用byte來作爲內存的最小分配單位,於是乎大家就認爲內存是一個字節一個字節的進行讀取的……,但其實這是一個誤區,byte做內存的基本單位這是從程序員的角度來看待內存的,如果是CPU的話,它不會也這樣看待,畢竟一次只讀一個字節似乎有點太慢,的確,對於CPU來說,內存是一個個內存塊來讀取,內存塊的大小通常是2的整數次冪。不同的硬件架構不同,一般是4或8個字節,如果字節不對齊會有什麼後果呢?最直接的後果就是會導致你的程序變慢。具體分析如下:
對於單字節對齊的系統來說(這也正是程序員看到的內存狀態)從地址0開始讀取4個字節和從地址1開始讀取4個字節沒有任何區別,所以也不存在字節對齊的概念,對不對齊其實都一樣。對於4字節對齊的系統來說,CPU一次要讀取4個字節的內容,從地址0開始讀取4個字節0~3,只需要讀取一次就ok了。如果從1開始讀取的話,需要讀二次,第一次讀0~3,第二次讀4~7,然後截取這兩個內存塊的1~4這個區域,就是讀取到的四個字節的內容了。因爲CPU只會一個個內存塊的邊界開始讀取一個內存塊,地址1並不是內存塊的邊界,因此CPU會從0開始讀取。就是這樣的一個簡單操作導致了CPU多進行了一次讀操作,可想而知內存對齊該有多重要。關於內存對齊的更多分析請看我給大家推薦的文章。linux提供了posix_memalign和memalign兩個函數用於分配字節對齊的內存地址,其函數原型如下:
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
#include <malloc.h>
void *memalign(size_t alignment, size_t size);
如何在棧上分配內存?
我們知道malloc是在堆上進行內存分配的,但是你有聽過在棧上也可以分配內存的嘛,的確是可以的alloca就可以在棧上進行內存的分配,因爲當前函數的棧幀是位於堆棧的頂部。幀的上方是存在可擴展空間,只需要改堆棧指針值即可,其函數原型如下:
#include <alloca.h>
void *alloca(size_t size);
通過alloca分配的內存不需要進行釋放,因爲函數運行結束後自動釋放對應的棧幀,修改器堆棧指針爲前一個棧幀的末尾地址。alloca是不是很神奇,筆者很想知道其實現原理。儘管上文中已經說到了,其實就是利用棧上的擴展空間,擴展了棧的空間,使用棧上的擴展空間來進行內存的分配。下面是其實現代碼的彙編表示.
#include <stdio.h>
#include <alloca.h>
#include <stdlib.h>
int main()
{
void *y = NULL;
y = alloca(4);
}
pushq %rbp //保存上一個棧幀的基址寄存器
movq %rsp, %rbp //設置當前棧幀的基址寄存器
subq $16, %rsp //開閉16個字節的空間,因爲是向下增長,所以是subq
movq $0, -8(%rbp) //void *y = NULL
movl $16, %eax //下面是一連串的地址大小計算,現在可以不用管這些細節
subq $1, %rax
addq $19, %rax
movl $16, %ecx
movl $0, %edx
divq %rcx
imulq $16, %rax, %rax
subq %rax, %rsp //修改rsp的地址,也就是棧頂地址
movq %rsp, %rax
addq $15, %rax
shrq $4, %rax
salq $4, %rax
movq %rax, -8(%rbp) //將分配的地址賦值給y 也就是y = alloca(4)
leave
ret
打印下y本身的地址和分配的地址如下:
y的地址:0x7ffd366b7fd8
分配的地址:0x7ffd366b7fb0
根據y
的地址結合彙編代碼可知,棧頂的地址是0x7ffd366b7fd8 - 8 =0x7ffd366b7fd0
分配的地址是0x7ffd366b7fb0
, 兩者相差0x20
。也就是說雖然分配的是4個字節,但是棧頂卻減少了0x20
個字節,那麼現在的棧頂就是0x7ffd366b7fb0
,之前的棧頂是0x7ffd366b7fd0
,這中間的區域就是分配的空間,至於爲什麼是0x20
這一應該是和malloc
的初衷相同,考慮到字節對齊吧。