歡迎光臨
每天分享高質量文章

行程概述和記憶體分配

行程概述和記憶體分配

本文是作者閱讀TLPI(The Linux Programer Interface的總結),為了突出重點,避免一刀砍,我不會過多的去介紹基本的概念和用法,我重點會去介紹原理和細節。因此對於本文的讀者,至少要求讀過APUE,或者是實際有寫過相關程式碼的程式員,因為知識有點零散,所以我會盡可能以FAQ的形式呈現給讀者。

行程

一個行程的記憶體佈局是什麼樣的?

每個行程所所分配的記憶體由很多部分組成,通常我們稱之為段,一般會有如下段:

  • 文字段 包含了行程執行的程式機器語言指令,文字段具有隻讀屬性,以防止行程透過錯誤指標意外修改自身的指令。

  • 初始化資料段包含了顯示初始化的全域性變數和靜態變數,當程式載入到記憶體時,從可執行檔案中讀取這些變數的值

  • 未初始化資料段包含了未進行顯式初始化的全域性變數和靜態變數,程式啟動之前,系統將本段內所有的記憶體初始化為0。

  • 棧段是一個動態增長和收縮的段,由棧幀組成,系統會為每個當前呼叫的函式分配一個棧幀,棧幀中儲存了函式的具備變數,引數,和傳回值。

  • 堆段是可在執行時動態行程記憶體分配的一塊區域,堆頂端稱作program break

註: 為什麼要區分初始化資料段,和未初始化資料段呢?,未初始化資料段簡稱為BSS段,有何含義BSS全稱為Block Started by Symbol,其主要原因在於程式在磁碟上儲存時,沒有必要為未經初始化的變數分配儲存空間,相反,可執行檔案只需要記錄未初始化資料段的位置和所需大小即可。直到執行時才分配記憶體空間。透過size命令可以顯示可執行檔案的文字段,初始化資料段,未初始化資料段的段大小資訊。

如何知道行程的文字段,初始化資料段和非初始化資料段的結束位置?

   大多數UNIX實現中C語言程式設計環境提供了三個全域性符號:etext,edata,end,可在程式內使用這些符號獲取文字段,初始化資料段,未初始化資料段結尾處下一位元組的地址。程式碼如下:

#include 
#include 
#include 
extern char etext,edata,end;
int main()
{
printf("%p\n",&etext;);
printf("%p\n",&edata;);
printf("%p\n",&end;);
}

如何獲取虛擬記憶體的頁面大小?

#include 
#include 

int main()
{
printf("page-size:%d\n",sysconf(_SC_PAGESIZE));
}

如何讀取任一行程的命令列引數和程式名?

   透過讀取proc/PID/cmdline可以得到任一行程的命令列引數資訊,如果想獲取程式本身的命令列引數,可以使用proc/self/cmdline來讀取,對於如何獲取行程的程式名有如下兩種方法:

  • 讀取/proc/self/exe的符號連結內容,這個檔案會透過符號連結到真正的可執行檔案路徑,是絕對路徑,透過readlink可以讀取其中連結的絕對路徑名稱

#include 
#include 
#include 
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 
#include 

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 
   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 
#include 
#include 

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命令是用來分析mtrace()的輸出,預設是沒有安裝的,它是glibc提供的,所以需要額外安裝glibc-utils。

mcheck和mproe函式允許對已分配的記憶體塊進行一致性檢查。例如對已分配記憶體之外進行了寫操作。

[root@localhost test]# cat mcheck.c 
#include 
#include 
#include 

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 
#include 
#include 

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 
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 
#include 
#include 
#include 

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 
   int posix_memalign(void **memptr, size_t alignment, size_t size);
   #include 
   void *memalign(size_t alignment, size_t size);

如何在棧上分配記憶體?

   我們知道malloc是在堆上進行記憶體分配的,但是你有聽過在棧上也可以分配記憶體的嘛,的確是可以的alloca就可以在棧上進行記憶體的分配,因為當前函式的棧幀是位於堆疊的頂部。幀的上方是存在可擴充套件空間,只需要改堆疊指標值即可,其函式原型如下:

   #include 
   void *alloca(size_t size);

   透過alloca分配的記憶體不需要進行釋放,因為函式執行結束後自動釋放對應的棧幀,修改器堆疊指標為前一個棧幀的末尾地址。alloca是不是很神奇,筆者很想知道其實現原理。儘管上文中已經說到了,其實就是利用棧上的擴充套件空間,擴充套件了棧的空間,使用棧上的擴充套件空間來進行記憶體的分配。下麵是其實現程式碼的彙編表示.

#include 
#include 
#include 

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的初衷相同,考慮到位元組對齊吧。

贊(0)

分享創造快樂