Linux應(yīng)用程序設(shè)計:用一種討巧方式,來獲取線程棧的使用信息
面對的問題
對于線程的??臻g,相信各位小伙伴都不陌生。它有下面的這幾項特性:
- 由操作系統(tǒng)分配固定的空間;
- 使用一個棧寄存器來保存實時位置;
- 后進(jìn)先出。

今天,我們不聊操作系統(tǒng)層面對棧的管理,只從應(yīng)用程序的角度,來看一下如何實時獲取棧的使用情況。
在一般的單片機(jī)/嵌入式程序開發(fā)過程中,在創(chuàng)建一個線程(或者稱作任務(wù))的時候,是可以指定給該線程分配多少??臻g的。
然后在調(diào)試的時候呢,周期性的打印出棧區(qū)的使用情況:消耗了多少空間,還剩余多少空間。
這樣的話,跑完每一個測試用例之后,就能得到一個大致的統(tǒng)計數(shù)據(jù),從而最終決定:需要給這個線程分配多少??臻g。
例如:在 ucOS 系統(tǒng)中,提供了函數(shù) NT8U OSTaskStkChk(INT8U prio, OS_STK_DATA *p_stk_data),來獲取一個任務(wù)的棧使用信息。
但是在 Linux 系統(tǒng)中,并沒有這樣類似的函數(shù),來直接獲取棧使用信息。
因此,為了得到此線程的已使用和空閑??臻g,必須通過其他的方式來獲取。
下面,就提供 2 種解決方案:正規(guī)軍方式和雜牌軍方式!
正規(guī)軍方式
在 Linux 系統(tǒng)中,在創(chuàng)建一個線程的時候,是可以通過線程屬性來設(shè)置:為這個線程分配多少的棧(stack)空間的。
如果應(yīng)用程序不指定的話,操作系統(tǒng)就設(shè)置為一個默認(rèn)的值。
線程創(chuàng)建完畢之后,操作系統(tǒng)在內(nèi)核空間,記錄了這個線程的一切信息,當(dāng)然也就包括給它分配的棧空間信息。
為了讓應(yīng)用層能夠獲取到這個信息,操作系統(tǒng)也提供了相應(yīng)的系統(tǒng)函數(shù)。代碼如下:
- pthread_attr_t attr;
- void *stack_addr;
- int stack_size;
- memset(&attr, 0, sizeof(pthread_attr_t));
- pthread_getattr_np(pthread_self(), &attr);
- pthread_attr_getstack(&attr, &stack_addr, &stack_size);
- pthread_attr_destroy(&attr);
- printf("statck top = %p \n", stack_addr);
- printf("stack bottom = %p \n", stack_addr + stack_size);
從上面這段代碼中可以看到,它只能獲取??臻g的地址開始以及總的空間大小,仍然不知道當(dāng)前??臻g的實際使用情況!
我找了一下相關(guān)的系統(tǒng)調(diào)用,Linux 似乎沒有提供相關(guān)的函數(shù)。
怎么辦?只能迂回操作。
我們知道,在 Linux x86 平臺上,寄存器 ESP 就是來存儲棧指針的。對于一個滿遞減類型的棧,這個寄存器里的值,就代表了當(dāng)前棧中最后背使用的、那個??臻g的地址。
因此,只要我們能夠獲取到 ESP 寄存器里的值,就相當(dāng)于知道了當(dāng)前這個棧有多少空間被使用了。
那么怎樣來獲取 ESP 寄存器的值呢?既然是寄存器,那就肯定是使用匯編代碼了。
很簡單,就 1 行:
- size_t esp_val;
- asm("movl %%esp, %0" : "=m"(esp_val) :);
對不起,我錯了!應(yīng)該是 2 行代碼,忘記變量定義了。
對于匯編代碼不熟悉的小伙伴,可以參考之前總結(jié)的一篇文章:內(nèi)聯(lián)匯編很可怕嗎?看完這篇文章,終結(jié)它!
找到第 4 個示例,直接抄過來就行。
好了,拿到了以上的所有信息,就可以計算出棧的已使用和空閑空間的大小了:
把以上代碼放在一起:
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <pthread.h>
- #include <sys/resource.h>
- void print_stack1()
- {
- size_t used, avail;
- pthread_attr_t attr;
- void *stack_addr;
- int stack_size;
- // 獲取棧寄存器 ESP 的當(dāng)前值
- size_t esp_val;
- asm("movl %%esp, %0" : "=m"(esp_val) :);
- // 通過線程屬性,獲取棧區(qū)的起始地址和空間總大小
- memset(&attr, 0, sizeof(pthread_attr_t));
- pthread_getattr_np(pthread_self(), &attr);
- pthread_attr_getstack(&attr, &stack_addr, &stack_size);
- pthread_attr_destroy(&attr);
- printf("espVal = %p \n", esp_val);
- printf("statck top = %p \n", stack_addr);
- printf("stack bottom = %p \n", stack_addr + stack_size);
- avail = esp_val - (size_t)stack_addr;
- used = stack_size - avail;
- printf("print_stack1: used = %d, avail = %d, total = %d \n",
- used, avail, stack_size);
- }
- int main(int argc, char *agv[])
- {
- print_stack1();
- return 0;
- }
雜牌軍方式
上面的正規(guī)軍方法,主要是通過系統(tǒng)函數(shù)獲取了線程的屬性信息,從而獲取了棧區(qū)的開始地址和棧的總空間大小。
為了獲取這兩個值,調(diào)用了 3 個函數(shù),有點笨重!
不知各位小伙伴是否想起:Linux 操作系統(tǒng)會為一個應(yīng)用程序,都提供了一些關(guān)于 limit 的信息,這其中就包括堆棧的相關(guān)信息。
這樣的話,我們就能拿到一個線程的??臻g總大小了。
此時,還剩下最后一個變量不知道:棧區(qū)的開始地址!
我們來分析一下哈:當(dāng)一個線程剛剛開始執(zhí)行的時候,棧區(qū)里可以認(rèn)為是空的,也就是說此時 ESP 寄存器里的值就可以認(rèn)為是指向棧區(qū)的開始地址!
是不是有豁然開朗的感覺?!
但是,這仍然需要調(diào)用匯編代碼來獲取。
再想一步,既然此時棧區(qū)里可以認(rèn)為是空的,那么如果在線程的第一個函數(shù)中,定義一個局部變量,然后通過獲取這個局部變量的地址,不就相當(dāng)于是獲取到了棧區(qū)的開始地址了嗎?
如下圖所示:
我們可以把這個局部變量的地址,記錄在一個全局變量中。然后在應(yīng)用程序的其他代碼處,就可以用它來代表棧的起始地址。
知道了 3 個必需的變量,就可以計算??臻g的使用情況了:
- // 用來存儲棧區(qū)的起始地址
- size_t top_stack;
- void print_stack2()
- {
- size_t used, avail;
- size_t esp_val;
- asm("movl %%esp, %0" : "=m"(esp_val) :);
- printf("esp_val = %p \n", esp_val);
- used = top_stack - esp_val;
- struct rlimit limit;
- getrlimit(RLIMIT_STACK, &limit);
- avail = limit.rlim_cur - used;
- printf("print_stack2: used = %d, avail = %d, total = %d \n",
- used, avail, used + avail);
- }
- int main(int argc, char *agv[])
- {
- int x = 0;
- // 記錄棧區(qū)的起始地址(近似值)
- top_stack = (size_t)&x;
- print_stack2();
- return 0;
- }
更討巧的方式
在上面的兩種方法中,獲取棧的當(dāng)前指針位置的方式,都是通過匯編代碼,來獲取寄存器 ESP 中的值。
是否可以繼續(xù)利用剛才的技巧:通過定義一個局部變量的方式,來間接地獲取 ESP 寄存器的值?
- void print_stack3()
- {
- int x = 0;
- size_t used, avail;
- // 局部變量的地址,可以近似認(rèn)為是 ESP 寄存器的值
- size_t tmp = (size_t)&x;
- used = top_stack - tmp;
- struct rlimit limit;
- getrlimit(RLIMIT_STACK, &limit);
- avail = limit.rlim_cur - used;
- printf("print_stack3: used = %d, avail = %d, total = %d \n",
- used, avail, used + avail);
- }
- int main(int argc, char *agv[])
- {
- int x = 0;
- top_stack = (size_t)&x;
- print_stack3();
- return 0;
- }
總結(jié)
以上的幾種方式,各有優(yōu)缺點。
我們把以上 3 個打印堆棧使用情況的函數(shù)放在一起,然后在 main 函數(shù)中,按順序調(diào)用 3 個測試函數(shù),每個函數(shù)中都定義一個整型數(shù)組(消耗 4K 的??臻g),然后看一下這幾種方式的打印輸出信息:
- // 測試代碼(3個打印函數(shù)就不貼出來了)
- void print_stack1()
- {
- ...
- }
- void print_stack2()
- {
- ...
- }
- void print_stack3()
- {
- ...
- }
- void func3()
- {
- int num[1024];
- print_stack1();
- printf("\n\n ********* \n");
- print_stack2();
- printf("\n\n ********* \n");
- print_stack3();
- }
- void func2()
- {
- int num[1024];
- func3();
- }
- void func1()
- {
- int num[1024];
- func2();
- }
- int main(int argc, char *agv[])
- {
- int x = 0;
- top_stack = (size_t)&x;
- func1();
- return 0;
- }
打印輸出信息:
- espVal = 0xffe8c980
- statck top = 0xff693000
- stack bottom = 0xffe90000
- print_stack1: used = 13952, avail = 8362368, total = 8376320
- *********
- esp_val = 0xffe8c9a0
- print_stack2: used = 12456, avail = 8376152, total = 8388608
- *********
- print_stack3: used = 12452, avail = 8376156, total = 8388608
本文轉(zhuǎn)載自微信公眾號「 IOT物聯(lián)網(wǎng)小鎮(zhèn)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 IOT物聯(lián)網(wǎng)小鎮(zhèn)公眾號。