Linux應用程式設計:用一種討巧方式,來獲取執行緒棧的使用資訊

sewain發表於2021-05-30

面對的問題

對於執行緒的棧空間,相信各位小夥伴都不陌生。它有下面的這幾項特性:

> 1. 由作業系統分配固定的空間;
>
> 2. 使用一個棧暫存器來儲存實時位置;
>
> 3. 後進先出。

今天,我們不聊作業系統層面對棧的管理,只從應用程式的角度,來看一下如何實時獲取棧的使用情況

在一般的微控制器/嵌入式程式開發過程中,在建立一個執行緒(或者稱作任務)的時候,是可以指定給該執行緒分配多少棧空間的。

然後在除錯的時候呢,週期性的列印出棧區的使用情況:消耗了多少空間,還剩餘多少空間。

這樣的話,跑完每一個測試用例之後,就能得到一個大致的統計資料,從而最終決定:需要給這個執行緒分配多少棧空間

例如:在 ucOS 系統中,提供了函式 NT8U OSTaskStkChk(INT8U prio, OS_STK_DATA *p_stk_data),來獲取一個任務的棧使用資訊

但是在 Linux 系統中,並沒有這樣類似的函式,來直接獲取棧使用資訊。

因此,為了得到此執行緒的已使用空閒棧空間,必須通過其他的方式來獲取。

下面,就提供 2 種解決方案:正規軍方式和雜牌軍方式


正規軍方式

Linux 系統中,在建立一個執行緒的時候,是可以通過執行緒屬性來設定:為這個執行緒分配多少的棧(stack)空間的。

如果應用程式不指定的話,作業系統就設定為一個預設的值。

執行緒建立完畢之後,作業系統在核心空間,記錄了這個執行緒的一切資訊,當然也就包括給它分配的棧空間資訊。

為了讓應用層能夠獲取到這個資訊,作業系統也提供了相應的系統函式。程式碼如下:

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);

從上面這段程式碼中可以看到,它只能獲取棧空間的地址開始以及總的空間大小,仍然不知道當前棧空間的實際使用情況!

我找了一下相關的系統呼叫,Linux 似乎沒有提供相關的函式。

怎麼辦?只能迂迴操作。

我們知道,在 Linux x86 平臺上,暫存器 ESP 就是來儲存棧指標的。對於一個滿遞減型別的棧,這個暫存器裡的值,就代表了當前棧中最後背使用的、那個棧空間的地址。

因此,只要我們能夠獲取到 ESP 暫存器裡的值,就相當於知道了當前這個棧有多少空間被使用了。

那麼怎樣來獲取 ESP 暫存器的值呢? 既然是暫存器,那就肯定是使用彙編程式碼了。

很簡單,就 1 行:

size_t esp_val;
asm("movl %%esp, %0" : "=m"(esp_val) :);

對不起,我錯了!應該是 2 行程式碼,忘記變數定義了。

對於彙編程式碼不熟悉的小夥伴,可以參考之前總結的一篇文章:內聯彙編很可怕嗎?看完這篇文章,終結它!

找到第 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 的當前值
    size_t esp_val;
    asm("movl %%esp, %0" : "=m"(esp_val) :);

    // 通過執行緒屬性,獲取棧區的起始地址和空間總大小
    memset(&amp;attr, 0, sizeof(pthread_attr_t));
    pthread_getattr_np(pthread_self(), &amp;attr);
    pthread_attr_getstack(&amp;attr, &amp;stack_addr, &amp;stack_size);
    pthread_attr_destroy(&amp;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;
}


雜牌軍方式

上面的正規軍方法,主要是通過系統函式獲取了執行緒的屬性資訊,從而獲取了棧區的開始地址和棧的總空間大小。

為了獲取這兩個值,呼叫了 3 個函式,有點笨重!

不知各位小夥伴是否想起:Linux 作業系統會為一個應用程式,都提供了一些關於 limit 的資訊,這其中就包括堆疊的相關資訊。

這樣的話,我們就能拿到一個執行緒的棧空間總大小了。

此時,還剩下最後一個變數不知道:棧區的開始地址

我們來分析一下哈:當一個執行緒剛剛開始執行的時候,棧區裡可以認為是空的,也就是說此時 ESP 暫存器裡的值就可以認為是指向棧區的開始地址

是不是有豁然開朗的感覺?!

但是,這仍然需要呼叫匯編程式碼來獲取。

再想一步,既然此時棧區裡可以認為是空的,那麼如果線上程的第一個函式中,定義一個區域性變數,然後通過獲取這個區域性變數的地址,不就相當於是獲取到了棧區的開始地址了嗎?

如下圖所示:

我們可以把這個區域性變數的地址,記錄在一個全域性變數中。然後在應用程式的其他程式碼處,就可以用它來代表棧的起始地址

知道了 3 個必需的變數,就可以計算棧空間的使用情況了:

// 用來儲存棧區的起始地址
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, &amp;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;
    // 記錄棧區的起始地址(近似值)
    top_stack = (size_t)&amp;x; 
    print_stack2();
    return 0;
}

更討巧的方式

在上面的兩種方法中,獲取棧的當前指標位置的方式,都是通過彙編程式碼,來獲取暫存器 ESP 中的值。

是否可以繼續利用剛才的技巧:通過定義一個區域性變數的方式,來間接地獲取 ESP 暫存器的值?

void print_stack3()
{
    int x = 0;
    size_t used, avail;
    // 區域性變數的地址,可以近似認為是 ESP 暫存器的值
    size_t tmp = (size_t)&amp;x;
    used =  top_stack - tmp;

    struct rlimit limit;
    getrlimit(RLIMIT_STACK, &amp;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)&amp;x;
    print_stack3();
    return 0;
}

總結

以上的幾種方式,各有優缺點。

我們把以上 3 個列印堆疊使用情況的函式放在一起,然後在 main 函式中,按順序呼叫 3 個測試函式,每個函式中都定義一個整型陣列(消耗 4K 的棧空間),然後看一下這幾種方式的列印輸出資訊:

// 測試程式碼(3個列印函式就不貼出來了)
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)&amp;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 

------ End ------

讓知識流動起來,越分享越幸運!


Hi~,我是道哥,嵌入式開發老兵。


星標公眾號,能更快找到我!


推薦閱讀

【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現物件導向程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!
【5】都說軟體架構要分層、分模組,具體應該怎麼做

</pthread.h></string.h></stdlib.h></stdio.h></unistd.h>

相關文章