说明: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,如果愿意,其它线程也还是可以访问到的,于是一定要注意。