說明:
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)
- {
- return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
- }
何謂動態增長呢?可以看到子進程初始的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))) {
- bad_area(regs, error_code, address);
- return;
- }
很清晰。
然而對於主線程生成的子線程而言,其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)
- {
- 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出來的一塊內存區域,原則上是線程私有的,但是同一個進程的所有線程生成的時候淺拷貝生成者的task_struct的很多字段,其中包括所有的vma,如果願意,其它線程也還是可以訪問到的,於是一定要注意。