關於Linux執行緒的執行緒棧以及TLS

2021-08-30 19:25:00 字數 4637 閱讀 3765

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!

a.本文描述linux nptl的執行緒棧簡要實現以及執行緒本地儲存的原理,實驗環境中linux核心版本為2.6.32,glibc版本是2.12.1,linux發行版為ubuntu,硬體平台為x86的32位系統。

b.對於linux nptl執行緒,有很多話題。本文挑選了原則上是每執行緒私有的位址空間來討論,分別是執行緒棧和tls。原則山私有並不是真的私有,因為大家都知道執行緒的特點就是共享位址空間,原則私有空間就是一般而言通過正常手段其它執行緒不會觸及這些空間的資料。

雖然linux將執行緒和程序不加區分的統一到了task_struct,但是對待其位址空間的stack還是有些區別的。對於linux程序或者說主線程,其stack是在fork的時候生成的,實際上就是複製了父親的stack空間位址,然後寫時拷貝(cow)以及動態增長,這可從sys_fork呼叫do_fork的引數中看出來:

int sys_fork(struct pt_regs *regs)
何謂動態增長呢?可以看到子程序初始的size為0,然後由於複製了父親的sp以及稍後在dup_mm中複製的所有vma,因此子程序stack的flags仍然包含:

#define vm_stack_flags    (vm_growsdown | vm_stack_default_flags | vm_account)
這就說針對帶有這個flags的vma(stack也在乙個vma中!)可以動態增加其大小了,這可從do_page_fault中看到:

if (likely(vma->vm_start <= address))    goto good_area;if (unlikely(!(vma->vm_flags & vm_growsdown)))
很清晰。

然而對於主線程生成的子執行緒而言,其stack將不再是這樣的了,而是事先固定下來的,使用mmap系統呼叫,它不帶有vm_stack_flags    標記(估計以後的核心會支援!)。這個可以從glibc的nptl/allocatestack.c中的allocate_stack函式中看到:

mem = mmap (null, size, prot,        map_private | map_anonymous | map_stack, -1, 0);
此呼叫中的size引數的獲取很是複雜,你可以手工傳入stack的大小,也可以使用預設的,一般而言就是預設的。這些都不重要,重要的是,這種stack不能動態增長,一旦用盡就沒了,這是和生成程序的fork不同的地方。在glibc中通過mmap得到了stack之後,底層將呼叫sys_clone系統呼叫:

int sys_clone(struct pt_regs *regs)
因此,對於子執行緒的stack,它其實是在程序的位址空間中map出來的一塊記憶體區域,原則上是執行緒私有的,但是同乙個程序的所有執行緒生成的時候淺拷貝生成者的task_struct的很多字段,其中包括所有的vma,如果願意,其它執行緒也還是可以訪問到的,於是一定要注意。

asmlinkage int sys_set_thread_area(struct user_desc __user *u_info)int do_set_thread_area(struct task_struct *p, int idx,               struct user_desc __user *u_info,               int can_allocate)    if (idx < gdt_entry_tls_min || idx > gdt_entry_tls_max)        return -einval;    set_tls_desc(p, idx, &info, 1);    return 0;}
fill_ldt設定gdt中第6個段描述符的基址和段限以及dpl等資訊,這些資訊都是從sys_set_thread_area系統呼叫的u_info引數中得來的。本質上,最終gdt的第6個段中描述的資訊其實就是一塊記憶體,這塊記憶體用於儲存tls節,這塊記憶體其實也是使用brk,mmap之類呼叫在主線程的堆空間申請的,只是後來呼叫sys_set_thread_area將其設定成了本執行緒的私有空間罷了,主線程或者其它執行緒如果願意,也是可以通過其它手段訪問到這塊空間的。

我們發現是第六個段用於記錄tls資料,我了證實一下,寫乙個最簡單的程式,用gdb看一下gs暫存器的值,到此我們已經知道gs暫存器表示的段描述子指向的段記錄tls資料,如下圖所示:

可以看到紅色圈住的部分,gs的值是0x33,這個0x33如何解釋呢?見下圖分解:

這就證實了確實是gs指向的段來表示tls資料了,在glibc中,初始化的時候會將gs暫存器指向第六個段:

既然如此,我們是不是可以直接通過gs暫存器來訪問tls資料呢?答案當然是肯定的,glibc其實就是這麼做的,無非經過封裝,使用更加方便了。但是如果想明白其所以然,還是自己折騰一下比較妥當,我的環境是ubuntu glibc-2.12.1,值得注意的是,每乙個glibc的版本的tls header都可能不一樣,一定要對照自己除錯的那個版本的原始碼來看,否則一定會發瘋的。我將上面的那個test_gs.c修改了一下,成為下面的**:

#include #include #include #include #include int main(int argc, char **argv)
這個**的含義在於,我可以通過gs暫存器訪問到tls變數,為了方便,我就沒有寫**,而是通過gdb來證實,其實通過寫**取出tls變數和通過gdb檢視記憶體的方式效果是一樣的,個人認為通過除錯的方法對於理解還更好些。

當除錯的時候,在取出gs之後,我們得到了tls的位址,然後根據該版本的tls結構體分析**儲存的是tls變數,然後檢視tls位址附近的記憶體,證實那裡確實存著乙個tls變數,這可以通過比較位址得出結論。當然在實際操作之前,我們首先看一下glibc-2.12.1版本的tls資料結構,如下圖所示:

注意,由於我們並無意深度hack tls,因此僅僅知道在何處能取到變數即可,因此我們只需要知道一些欄位的大小就可以了,暫且不必理解其含義與設計思想。

我們發現,應該是從第35*4個位元組開始就是tls變數的區域了,是不是這樣呢?我們來看一下除錯結果,注意我們要把斷點設定在asm之後,這樣才能打出b的值,當然你也可以調整上述**,把asm內嵌彙編放在**最前面也是可以的。gdb命令就不多說了,都是些簡單的,如下展示出結果:

結果很明了了。最終還有乙個小問題,那就是關於執行緒切換的問題。

load_tls(next, cpu);
每個task_struct都有thread_struct,而該執行緒tls的元資料資訊就儲存在thread_struct結構體的tls_array陣列中:

static inline void native_load_tls(struct thread_struct *t, unsigned int cpu)
除了我們使用pthread的api在執行時建立的tls變數之外,還有一部分tls稱為靜態tls變數,這些tls元素是在編譯期間預先生成的,常見的有:

1.自定義_thread修飾符修飾的變數;

2.一些庫級別預定義的變數,比如errno

那麼這些變數儲存在**呢?設計者很明智的將其放在了動態tls臨接的空間內,就是gs暫存器指示的位址下面,其實要是我設計也會這麼設計的,你也一樣。這樣設計的好處在於可以很方便對不管是動態tls變數還是靜態tls變數的訪問,並且對於動態tls的管理也很方便。

_thread int test = 123;

那麼除錯顯示的結果,它處於gs暫存器指示tls段位址的緊接著下方4個位元組的偏移處,而errno處於_thread變數下方14*4位元組的位置。具體這些空間到底怎麼安排的,可以看glibc的dl-reloc.c,dl-tls.c等檔案,然而本人認為這沒有什麼意義,由於這涉及到很多關於編譯,鏈結,重定向,elf等知識,如果不想深度優先的迷失在這裡面的化,理解原理也就夠了,本人真的是沒有時間再寫了,回到家就要看孩子,購物,做家務....。最後給出一幅圖,重定向後總的示意圖如下:

給我老師的人工智慧教程打call!

關於Linux執行緒的執行緒棧以及TLS

a.本文描述linux nptl的執行緒棧簡要實現以及執行緒本地儲存的原理,實驗環境中linux核心版本為2.6.32,glibc版本是2.12.1,linux發行版為ubuntu,硬體平台為x86的32位系統。b.對於linux nptl執行緒,有很多話題。本文挑選了原則上是每執行緒私有的位址空間...

linux核心 程序棧執行緒棧

程序使用者空間的管理在task struct 的mm struct mm成員中體現,mm中的成員定義了使用者空間的布局情況如圖一。使用者空間的棧起始於stack top,如果設定了pf randomize,則起始點會減少乙個小的隨機量,每個體系結構都必須定義stack top,大多數都設定為task...

Java執行緒 執行緒棧模型與執行緒的變數

scjp5學習筆記 要理解執行緒排程的原理,以及執行緒執行過程,必須理解執行緒棧模型。執行緒棧是指某時刻時記憶體中線程排程的棧資訊,當前呼叫的方法總是位於棧頂。執行緒棧的內容是隨著程式的執行動態變化的,因此研究執行緒棧必須選擇乙個執行的時刻 實際上指 執行到什麼地方 下面通過乙個示例性的 說明執行緒...