生活随笔
收集整理的這篇文章主要介紹了
关于Linux线程的线程栈以及TLS
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
說明:
a.
本文描述Linux NPTL的線程棧簡要實現(xiàn)以及線程本地存儲的原理,實驗環(huán)境中Linux內(nèi)核版本為2.6.32,glibc版本是2.12.1,Linux發(fā)行版為ubuntu,硬件平臺為x86的32位系統(tǒng)。
b.
對于Linux NPTL線程,有很多話題。本文挑選了原則上是每線程私有的地址空間來討論,分別是線程棧和TLS。原則山私有并不是真的私有,因為大家都知道線程的特點就是共享地址空間,原則私有空間就是一般而言通過正常手段其它線程不會觸及這些空間的數(shù)據(jù)。
一.線程棧
雖然Linux將線程和進程不加區(qū)分的統(tǒng)一到了task_struct,但是對待其地址空間的stack還是有些區(qū)別的。對于Linux進程或者說主線程,其stack是在fork的時候生成的,實際上就是復(fù)制了父親的stack空間地址,然后寫時拷貝(cow)以及動態(tài)增長,這可從sys_fork調(diào)用do_fork的參數(shù)中看出來:
[plain] ?view plaincopy
int?sys_fork(struct?pt_regs?*regs)?? {?? ????return?do_fork(SIGCHLD,?regs->sp,?regs,?0,?NULL,?NULL);?? }??
何謂動態(tài)增長呢?可以看到子進程初始的size為0,然后由于復(fù)制了父親的sp以及稍后在dup_mm中復(fù)制的所有vma,因此子進程stack的flags仍然包含:
[plain] ?view plaincopy
#define?VM_STACK_FLAGS????(VM_GROWSDOWN?|?VM_STACK_DEFAULT_FLAGS?|?VM_ACCOUNT)??
這就說針對帶有這個flags的vma(stack也在一個vma中!)可以動態(tài)增加其大小了,這可從do_page_fault中看到:
[plain] ?view plaincopy
if?(likely(vma->vm_start?<=?address))?? ????goto?good_area;?? if?(unlikely(!(vma->vm_flags?&?VM_GROWSDOWN)))?{?? ????bad_area(regs,?error_code,?address);?? ????return;?? }??
很清晰。
? ?? ?? 然而對于主線程生成的子線程而言,其stack將不再是這樣的了,而是事先固定下來的,使用mmap系統(tǒng)調(diào)用,它不帶有VM_STACK_FLAGS?? ?標(biāo)記(估計以后的內(nèi)核會支持!)。這個可以從glibc的nptl/allocatestack.c中的allocate_stack函數(shù)中看到:
[plain] ?view plaincopy
mem?=?mmap?(NULL,?size,?prot,?? ????????MAP_PRIVATE?|?MAP_ANONYMOUS?|?MAP_STACK,?-1,?0);??
此調(diào)用中的size參數(shù)的獲取很是復(fù)雜,你可以手工傳入stack的大小,也可以使用默認的,一般而言就是默認的。這些都不重要,重要的是,這種stack不能動態(tài)增長,一旦用盡就沒了,這是和生成進程的fork不同的地方。在glibc中通過mmap得到了stack之后,底層將調(diào)用sys_clone系統(tǒng)調(diào)用:
[plain] ?view plaincopy
int?sys_clone(struct?pt_regs?*regs)?? {?? ????unsigned?long?clone_flags;?? ????unsigned?long?newsp;?? ????int?__user?*parent_tidptr,?*child_tidptr;?? ?? ????clone_flags?=?regs->bx;?? ????//獲取了mmap得到的線程的stack指針?? ????newsp?=?regs->cx;?? ????parent_tidptr?=?(int?__user?*)regs->dx;?? ????child_tidptr?=?(int?__user?*)regs->di;?? ????if?(!newsp)?? ????????newsp?=?regs->sp;?? ????return?do_fork(clone_flags,?newsp,?regs,?0,?parent_tidptr,?child_tidptr);?? }??
因此,對于子線程的stack,它其實是在進程的地址空間中map出來的一塊內(nèi)存區(qū)域,原則上是線程私有的,但是同一個進程的所有線程生成的時候淺拷貝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它線程也還是可以訪問到的,于是一定要注意。
二.線程本地存儲-TLS
Linux的glibc使用GS寄存器來訪問TLS,也就是說,GS寄存器指示的段指向本線程的TEB(Windows的術(shù)語),也就是TLS,這么做有個好處,那就是可以高效的訪問TLS里面存儲的信息而不用一次次的調(diào)用系統(tǒng)調(diào)用,當(dāng)然使用系統(tǒng)調(diào)用的方式也是可以的。之所以可以這么做,是因為Intel對各個寄存器的作用的規(guī)范規(guī)定的比較松散,因此你可以拿GS,FS等段寄存器來做幾乎任何事,當(dāng)然也就可以做TLS直接訪問了,最終glibc在線程啟動的時候首先將GS寄存器指向GDT的第6個段,完全使用段機制來支持針對TLS的尋址訪問,后續(xù)的訪問TLS信息就和訪問用戶態(tài)的信息一樣高效了。
??????? 在線程啟動的時候,可以通過sys_set_thread_area來設(shè)置該線程的TLS信息,所有的信息都得glibc來提供:
[plain] ?view plaincopy
asmlinkage?int?sys_set_thread_area(struct?user_desc?__user?*u_info)?? {?? ????int?ret?=?do_set_thread_area(current,?-1,?u_info,?1);?? ????asmlinkage_protect(1,?ret,?u_info);?? ????return?ret;?? }?? int?do_set_thread_area(struct?task_struct?*p,?int?idx,?? ???????????????struct?user_desc?__user?*u_info,?? ???????????????int?can_allocate)?? {?? ????struct?user_desc?info;?? ?? ????if?(copy_from_user(&info,?u_info,?sizeof(info)))?? ????????return?-EFAULT;?? ?? ????if?(idx?==?-1)?? ????????idx?=?info.entry_number;?? ?? ????/*?? ?????*?index?-1?means?the?kernel?should?try?to?find?and?? ?????*?allocate?an?empty?descriptor:?? ?????*/?? ????if?(idx?==?-1?&&?can_allocate)?{?? ????????idx?=?get_free_idx();?? ????????if?(idx?<?0)?? ????????????return?idx;?? ????????if?(put_user(idx,?&u_info->entry_number))?? ????????????return?-EFAULT;?? ????}?? ?? ????if?(idx?<?GDT_ENTRY_TLS_MIN?||?idx?>?GDT_ENTRY_TLS_MAX)?? ????????return?-EINVAL;?? ?? ????set_tls_desc(p,?idx,?&info,?1);?? ?? ????return?0;?? }??
fill_ldt設(shè)置GDT中第6個段描述符的基址和段限以及DPL等信息,這些信息都是從sys_set_thread_area系統(tǒng)調(diào)用的u_info參數(shù)中得來的。本質(zhì)上,最終GDT的第6個段中描述的信息其實就是一塊內(nèi)存,這塊內(nèi)存用于存儲TLS節(jié),這塊內(nèi)存其實也是使用brk,mmap之類調(diào)用在主線程的堆空間申請的,只是后來調(diào)用sys_set_thread_area將其設(shè)置成了本線程的私有空間罷了,主線程或者其它線程如果愿意,也是可以通過其它手段訪問到這塊空間的。
??????? 明白了大致原理之后,我們來看一下一切是如何關(guān)聯(lián)起來的。首先看一下Linux內(nèi)核關(guān)于GDT的段定義,如下圖所示:
我們發(fā)現(xiàn)是第六個段用于記錄TLS數(shù)據(jù),我了證實一下,寫一個最簡單的程序,用gdb看一下GS寄存器的值,到此我們已經(jīng)知道GS寄存器表示的段描述子指向的段記錄TLS數(shù)據(jù),如下圖所示:
可以看到紅色圈住的部分,GS的值是0x33,這個0x33如何解釋呢?見下圖分解:
這就證實了確實是GS指向的段來表示TLS數(shù)據(jù)了,在glibc中,初始化的時候會將GS寄存器指向第六個段:
既然如此,我們是不是可以直接通過GS寄存器來訪問TLS數(shù)據(jù)呢?答案當(dāng)然是肯定的,glibc其實就是這么做的,無非經(jīng)過封裝,使用更加方便了。但是如果想明白其所以然,還是自己折騰一下比較妥當(dāng),我的環(huán)境是ubuntu glibc-2.12.1,值得注意的是,每一個glibc的版本的TLS header都可能不一樣,一定要對照自己調(diào)試的那個版本的源碼來看,否則一定會發(fā)瘋的。我將上面的那個test_gs.c修改了一下,成為下面的代碼:
[plain] ?view plaincopy
#include?<stdlib.h>?? #include?<stdio.h>?? #include?<malloc.h>?? #include?<string.h>?? #include?<pthread.h>?? int?main(int?argc,?char?**argv)?? {?? ????int?a=10,?b?=?0;??//b保存GS寄存器表示的段的地址?? ????//設(shè)置三個TLS變量,其中前兩個使用堆內(nèi)存,最后一個不使用?? ????????static?pthread_key_t?thread_key1;?? ????????static?pthread_key_t?thread_key2;?? ????????static?pthread_key_t?thread_key3;?? ????????char?*addr1?=?(char?*)malloc(5);?? ????????char?*addr2?=?(char?*)malloc(5);?? ????????memset(addr1,?0,?5);?? ????????memset(addr2,?0,?5);?? ????????strcpy(addr1,?"aaaa");?? ????????strcpy(addr2,?"bbbb");?? ????????pthread_key_create?(&thread_key1,?NULL);?? ????????pthread_key_create?(&thread_key2,?NULL);?? ????????pthread_key_create?(&thread_key3,?NULL);?? ????????pthread_setspecific?(thread_key1,?addr1);?? ????????pthread_setspecific?(thread_key2,?addr2);?? ????????pthread_setspecific?(thread_key3,?"1111111111");?? ????//得到GS指示的段,也就是TLS的地址,這個需要用內(nèi)嵌匯編來做?? ????????asm?volatile("movl?%%gs:0,?%0;"?? ????????????????:"=r"(b)??/*?output?*/?????? ????????);?? ?? ????????printf("ok\n");?? }??
這個代碼的含義在于,我可以通過GS寄存器訪問到TLS變量,為了方便,我就沒有寫代碼,而是通過gdb來證實,其實通過寫代碼取出TLS變量和通過gdb查看內(nèi)存的方式效果是一樣的,個人認為通過調(diào)試的方法對于理解還更好些。
??????? 當(dāng)調(diào)試的時候,在取出GS之后,我們得到了TLS的地址,然后根據(jù)該版本的TLS結(jié)構(gòu)體分析哪里存儲的是TLS變量,然后查看TLS地址附近的內(nèi)存,證實那里確實存著一個TLS變量,這可以通過比較地址得出結(jié)論。當(dāng)然在實際操作之前,我們首先看一下glibc-2.12.1版本的TLS數(shù)據(jù)結(jié)構(gòu),如下圖所示:
注意,由于我們并無意深度hack TLS,因此僅僅知道在何處能取到變量即可,因此我們只需要知道一些字段的大小就可以了,暫且不必理解其含義與設(shè)計思想。
??????? 我們發(fā)現(xiàn),應(yīng)該是從第35*4個字節(jié)開始就是TLS變量的區(qū)域了,是不是這樣呢?我們來看一下調(diào)試結(jié)果,注意我們要把斷點設(shè)置在asm之后,這樣才能打出b的值,當(dāng)然你也可以調(diào)整上述代碼,把asm內(nèi)嵌匯編放在代碼最前面也是可以的。gdb命令就不多說了,都是些簡單的,如下展示出結(jié)果:
結(jié)果很明了了。最終還有一個小問題,那就是關(guān)于線程切換的問題。
??????? 對于Windows而言,線程的TEB幾乎是固定的,而對于Linux,它同樣也是這樣子,只需要得到GS寄存器,就能得到當(dāng)前線程的TCB,換句話說,GS始終是不變化的,始終是0x33,始終指向GDT的第6個段,變化的是GDT的第6個段的內(nèi)容,每當(dāng)進程或者線程切換的時候,第6個段的內(nèi)容都需要重新加載,載入將要運行線程的TLS info中的信息,這是在切換時switch_to宏中完成的:
[plain] ?view plaincopy
load_TLS(next,?cpu);??
每個task_struct都有thread_struct,而該線程TLS的元數(shù)據(jù)信息就保存在thread_struct結(jié)構(gòu)體的tls_array數(shù)組中:
[plain] ?view plaincopy
static?inline?void?native_load_tls(struct?thread_struct?*t,?unsigned?int?cpu)?? {?? ????unsigned?int?i;?? ????struct?desc_struct?*gdt?=?get_cpu_gdt_table(cpu);?? ?? ????for?(i?=?0;?i?<?GDT_ENTRY_TLS_ENTRIES;?i++)?? ????????gdt[GDT_ENTRY_TLS_MIN?+?i]?=?t->tls_array[i];?? }??
注意:關(guān)于TLS另外需要說的
除了我們使用pthread的API在運行時創(chuàng)建的TLS變量之外,還有一部分TLS稱為靜態(tài)TLS變量,這些TLS元素是在編譯期間預(yù)先生成的,常見的有:
1.自定義_thread修飾符修飾的變量;
2.一些庫級別預(yù)定義的變量,比如errno
那么這些變量存儲在哪里呢?設(shè)計者很明智的將其放在了動態(tài)TLS臨接的空間內(nèi),就是GS寄存器指示的地址下面,其實要是我設(shè)計也會這么設(shè)計的,你也一樣。這樣設(shè)計的好處在于可以很方便對不管是動態(tài)TLS變量還是靜態(tài)TLS變量的訪問,并且對于動態(tài)TLS的管理也很方便。
??????? 這些數(shù)據(jù)處于“initialized data section”,然而在鏈接或者線程初始化的時候被動態(tài)重定向到了靜態(tài)TLS空間內(nèi),在我的實驗環(huán)境中,如果我定義了一個變量:
_thread int test = 123;
那么調(diào)試顯示的結(jié)果,它處于GS寄存器指示tls段地址的緊接著下方4個字節(jié)的偏移處,而errno處于_thread變量下方14*4字節(jié)的位置。具體這些空間到底怎么安排的,可以看glibc的dl-reloc.c,dl-tls.c等文件,然而本人認為這沒有什么意義,由于這涉及到很多關(guān)于編譯,鏈接,重定向,ELF等知識,如果不想深度優(yōu)先的迷失在這里面的化,理解原理也就夠了,本人真的是沒有時間再寫了,回到家就要看孩子,購物,做家務(wù)....。最后給出一幅圖,重定向后總的示意圖如下:
總結(jié)
以上是生活随笔 為你收集整理的关于Linux线程的线程栈以及TLS 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。