说明:

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的参数中看出来:

[plain] view plaincopy
  1. int sys_fork(struct pt_regs *regs)
  2. {
  3. return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
  4. }

何谓动态增长呢?可以看到子进程初始的size为0,然后由于复制了父亲的sp以及稍后在dup_mm中复制的所有vma,因此子进程stack的flags仍然包含:

[plain] view plaincopy
  1. #define VM_STACK_FLAGS    (VM_GROWSDOWN | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT)

这就说针对带有这个flags的vma(stack也在一个vma中!)可以动态增加其大小了,这可从do_page_fault中看到:

[plain] view plaincopy
  1. if (likely(vma->vm_start <= address))
  2. goto good_area;
  3. if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
  4. bad_area(regs, error_code, address);
  5. return;
  6. }

很清晰。
        然而对于主线程生成的子线程而言,其stack将不再是这样的了,而是事先固定下来的,使用mmap系统调用,它不带有VM_STACK_FLAGS    标记(估计以后的内核会支持!)。这个可以从glibc的nptl/allocatestack.c中的allocate_stack函数中看到:

[plain] view plaincopy
  1. mem = mmap (NULL, size, prot,
  2. MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

此调用中的size参数的获取很是复杂,你可以手工传入stack的大小,也可以使用默认的,一般而言就是默认的。这些都不重要,重要的是,这种stack不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。在glibc中通过mmap得到了stack之后,底层将调用sys_clone系统调用:

[plain] view plaincopy
  1. int sys_clone(struct pt_regs *regs)
  2. {
  3. unsigned long clone_flags;
  4. unsigned long newsp;
  5. int __user *parent_tidptr, *child_tidptr;
  6. clone_flags = regs->bx;
  7. //获取了mmap得到的线程的stack指针
  8. newsp = regs->cx;
  9. parent_tidptr = (int __user *)regs->dx;
  10. child_tidptr = (int __user *)regs->di;
  11. if (!newsp)
  12. newsp = regs->sp;
  13. return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
  14. }

因此,对于子线程的stack,它其实是在进程的地址空间中map出来的一块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候浅拷贝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

二.线程本地存储-TLS

Linux的glibc使用GS寄存器来访问TLS,也就是说,GS寄存器指示的段指向本线程的TEB(Windows的术语),也就是TLS,这么做有个好处,那就是可以高效的访问TLS里面存储的信息而不用一次次的调用系统调用,当然使用系统调用的方式也是可以的。之所以可以这么做,是因为Intel对各个寄存器的作用的规范规定的比较松散,因此你可以拿GS,FS等段寄存器来做几乎任何事,当然也就可以做TLS直接访问了,最终glibc在线程启动的时候首先将GS寄存器指向GDT的第6个段,完全使用段机制来支持针对TLS的寻址访问,后续的访问TLS信息就和访问用户态的信息一样高效了。
        在线程启动的时候,可以通过sys_set_thread_area来设置该线程的TLS信息,所有的信息都得glibc来提供:

[plain] view plaincopy
  1. asmlinkage int sys_set_thread_area(struct user_desc __user *u_info)
  2. {
  3. int ret = do_set_thread_area(current, -1, u_info, 1);
  4. asmlinkage_protect(1, ret, u_info);
  5. return ret;
  6. }
  7. int do_set_thread_area(struct task_struct *p, int idx,
  8. struct user_desc __user *u_info,
  9. int can_allocate)
  10. {
  11. struct user_desc info;
  12. if (copy_from_user(&info, u_info, sizeof(info)))
  13. return -EFAULT;
  14. if (idx == -1)
  15. idx = info.entry_number;
  16. /*
  17. * index -1 means the kernel should try to find and
  18. * allocate an empty descriptor:
  19. */
  20. if (idx == -1 && can_allocate) {
  21. idx = get_free_idx();
  22. if (idx < 0)
  23. return idx;
  24. if (put_user(idx, &u_info->entry_number))
  25. return -EFAULT;
  26. }
  27. if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
  28. return -EINVAL;
  29. set_tls_desc(p, idx, &info, 1);
  30. return 0;
  31. }

fill_ldt设置GDT中第6个段描述符的基址和段限以及DPL等信息,这些信息都是从sys_set_thread_area系统调用的u_info参数中得来的。本质上,最终GDT的第6个段中描述的信息其实就是一块内存,这块内存用于存储TLS节,这块内存其实也是使用brk,mmap之类调用在主线程的堆空间申请的,只是后来调用sys_set_thread_area将其设置成了本线程的私有空间罢了,主线程或者其它线程如果愿意,也是可以通过其它手段访问到这块空间的。

明白了大致原理之后,我们来看一下一切是如何关联起来的。首先看一下Linux内核关于GDT的段定义,如下图所示:

我们发现是第六个段用于记录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修改了一下,成为下面的代码:

[plain] view plaincopy
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <malloc.h>
  4. #include <string.h>
  5. #include <pthread.h>
  6. int main(int argc, char **argv)
  7. {
  8. int a=10, b = 0;  //b保存GS寄存器表示的段的地址
  9. //设置三个TLS变量,其中前两个使用堆内存,最后一个不使用
  10. static pthread_key_t thread_key1;
  11. static pthread_key_t thread_key2;
  12. static pthread_key_t thread_key3;
  13. char *addr1 = (char *)malloc(5);
  14. char *addr2 = (char *)malloc(5);
  15. memset(addr1, 0, 5);
  16. memset(addr2, 0, 5);
  17. strcpy(addr1, "aaaa");
  18. strcpy(addr2, "bbbb");
  19. pthread_key_create (&thread_key1, NULL);
  20. pthread_key_create (&thread_key2, NULL);
  21. pthread_key_create (&thread_key3, NULL);
  22. pthread_setspecific (thread_key1, addr1);
  23. pthread_setspecific (thread_key2, addr2);
  24. pthread_setspecific (thread_key3, "1111111111");
  25. //得到GS指示的段,也就是TLS的地址,这个需要用内嵌汇编来做
  26. asm volatile("movl %%gs:0, %0;"
  27. :"=r"(b)  /* output */
  28. );
  29. printf("ok\n");
  30. }

这个代码的含义在于,我可以通过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命令就不多说了,都是些简单的,如下展示出结果:

结果很明了了。最终还有一个小问题,那就是关于线程切换的问题。
        对于Windows而言,线程的TEB几乎是固定的,而对于Linux,它同样也是这样子,只需要得到GS寄存器,就能得到当前线程的TCB,换句话说,GS始终是不变化的,始终是0x33,始终指向GDT的第6个段,变化的是GDT的第6个段的内容,每当进程或者线程切换的时候,第6个段的内容都需要重新加载,载入将要运行线程的TLS info中的信息,这是在切换时switch_to宏中完成的:

[plain] view plaincopy
  1. load_TLS(next, cpu);

每个task_struct都有thread_struct,而该线程TLS的元数据信息就保存在thread_struct结构体的tls_array数组中:

[plain] view plaincopy
  1. static inline void native_load_tls(struct thread_struct *t, unsigned int cpu)
  2. {
  3. unsigned int i;
  4. struct desc_struct *gdt = get_cpu_gdt_table(cpu);
  5. for (i = 0; i < GDT_ENTRY_TLS_ENTRIES; i++)
  6. gdt[GDT_ENTRY_TLS_MIN + i] = t->tls_array[i];
  7. }

注意:关于TLS另外需要说的

除了我们使用pthread的API在运行时创建的TLS变量之外,还有一部分TLS称为静态TLS变量,这些TLS元素是在编译期间预先生成的,常见的有:
1.自定义_thread修饰符修饰的变量;
2.一些库级别预定义的变量,比如errno
那么这些变量存储在哪里呢?设计者很明智的将其放在了动态TLS临接的空间内,就是GS寄存器指示的地址下面,其实要是我设计也会这么设计的,你也一样。这样设计的好处在于可以很方便对不管是动态TLS变量还是静态TLS变量的访问,并且对于动态TLS的管理也很方便。
        这些数据处于“initialized data section”,然而在链接或者线程初始化的时候被动态重定向到了静态TLS空间内,在我的实验环境中,如果我定义了一个变量:
_thread int test = 123;

那么调试显示的结果,它处于GS寄存器指示tls段地址的紧接着下方4个字节的偏移处,而errno处于_thread变量下方14*4字节的位置。具体这些空间到底怎么安排的,可以看glibc的dl-reloc.c,dl-tls.c等文件,然而本人认为这没有什么意义,由于这涉及到很多关于编译,链接,重定向,ELF等知识,如果不想深度优先的迷失在这里面的化,理解原理也就够了,本人真的是没有时间再写了,回到家就要看孩子,购物,做家务....。最后给出一幅图,重定向后总的示意图如下:

关于Linux线程的线程栈以及TLS相关推荐

  1. Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    栈是什么?栈有什么作用? 首先,栈 (stack) 是一种串列形式的 数据结构.这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 ...

  2. 一文读懂 | Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    点击蓝字 关注我们 因公众号更改推送规则,请点"在看"并加"星标"第一时间获取精彩技术分享 来源于网络,侵删 栈是什么?栈有什么作用? 首先,栈 (stack) ...

  3. Linux中的各种栈:进程栈 线程栈 内核栈 中断栈

    Linux中的各种栈:进程栈 线程栈 内核栈 中断栈 栈的作用 1. 函数调用 2. 多任务支持 Linux 中有几种栈?各种栈的内存位置? 1. 进程栈 2. 线程栈 3. 进程内核栈 4. 中断栈 ...

  4. 吉林大学操作系统上机(实验一:Linux进程与线程通讯)

    前言 我希望找到这里的学弟学妹能基于我的内容完成的更好,这里的代码和思路应该是你们的下限!! 我分享这些笔记的初衷是因为我觉得csdn上与之相关的不少博客都收费,但是我当时做的时候,我觉得就那么一点东 ...

  5. linux thread model . Linux上posix线程库实现原理讨论

    http://blog.csdn.net/fanbird2008/article/details/6752853 Linux上posix线程库实现原理讨论 说明一下,这个问题困扰我好长时间,因为正如使 ...

  6. Linux进程与线程的区别 详细总结(面试经验总结)

    首先,简要了解一下进程和线程.对于操作系统而言,进程是核心之核心,整个现代操作系统的根本,就是以进程为单位在执行任务.系统的管理架构也是基于进程层面的.在按下电源键之后,计算机就开始了复杂的启动过程, ...

  7. Linux查看进程线程个数

    1.根据进程号进行查询: # pstree -p 进程号 # top -Hp 进程号 2.根据进程名字进行查询: # pstree -p `ps -e | grep server | awk '{pr ...

  8. linux 线程--内核线程、用户线程实现方法

    Linux上进程分3种,内核线程(或者叫核心进程).用户进程.用户线程 内核线程拥有 进程描述符.PID.进程正文段.核心堆栈 当和用户进程拥有相同的static_prio 时,内核线程有机会得到更多 ...

  9. Linux进程与线程的区别

    2019独角兽企业重金招聘Python工程师标准>>> Linux进程与线程的区别 cnyinlinux 本文较长,耐心阅读,必有收获! 进程与线程的区别,早已经成为了经典问题.自线 ...

最新文章

  1. php文本分割成csv,怎么在php中将文本文件转换为csv文件并输出
  2. 智慧树python程序设计基础山东联盟期末答案_智慧树Python程序设计基础(山东联盟)期末答案...
  3. MR案例:Reduce-Join
  4. python 对excel文件进行分词并进行词频统计_python 词频分析
  5. java 对象重写tostring
  6. Python面向对象高级编程
  7. 面试官:我们只想要这样的C++工程师
  8. 结构设计模式 - 适配器设计模式
  9. 0057-简单的累加
  10. 【技术解读】大赛TOP团队方案技巧大揭秘!
  11. Oracle PL/SQL之NEXT_DAY - 取得下一个星期几所在的日期
  12. oppoa1计算机记录删了怎么办,捡到oppoA1怎么解锁
  13. oracle中锁表语句,oracle查询锁表解锁语句
  14. Python绘制计算机CPU占有率变化的折线图
  15. 全面了解ADSL,让你上网更轻松
  16. 三丁基-巯基膦烷「tBuBrettPhos Pd(allyl)」OTf),1798782-17-8
  17. 欧美相关出口企业要注意了!
  18. android 轮换实现动画,怎么在Android中实现一个首页无限轮播功能
  19. Linux GccGcc-c++安装
  20. orangepi——学习python02

热门文章

  1. 导致定时器失效_C# 因IIS回收导致定时器失效的解决方案
  2. python模拟抛硬币_python实现简单随机模拟——抛呀抛硬币
  3. DR5白金版 for mac(PS一键磨皮插件Delicious Retouch)支持ps 2022
  4. 如何使用iToolab FixGo for mac修复iPhone/iPad的系统问题
  5. 1.6 编程基础之一维数组 03 计算书费 python
  6. python内置方法怎么使用_python中的常用内置方法
  7. 【ES6(2015)】Set
  8. uni app map 地图 漂浮问题及方案
  9. tail -f 查找关键字_C语言九种查找算法 | 总有一款适合你
  10. Java笔记-腾讯验证码平台使用实例