一.线程栈
尽管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 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,假如乐意,其它线程也还是可以访问到的,于是一定要注意。
须要C/C++Linux服务器构架师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,解释器,DPDK,ffmpeg等),免费分享
二.线程本地储存-TLS
Linux的glibc使用GS寄存器来访问TLS,也就是说,GS寄存器指示的段指向本线程的TEB(Windows的术语),也就是TLS,那么做有个益处,那就是可以高效的访问TLS上面储存的信息而不用一次次的调用系统调用,其实使用系统调用的方法也是可以的。之所以可以如此做,是由于Intel对各个寄存器的作用的规范规定的比较松散,因而你可以拿GS,FS等段寄存器来做几乎任何事linux 分区,其实也就可以做TLS直接访问了,最终glibc在线程启动的时侯首先将GS寄存器指向GDT的第6个段,完全使用段机制来支持针对TLS的轮询访问,后续的访问TLS信息就和访问用户态的信息一样高效了。
在线程启动的时侯,可以通过sys_set_thread_area来设置该线程的TLS信息,所有的信息都得glibc来提供:
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 entry_number))
return -EFAULT;
}
if (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将其设置成了本线程的私有空间罢了,主线程或则其它线程假如乐意,也是可以通过其它手段访问到这块空间的。
明白了大致原理以后,我们来看一下一切是怎样关联上去的。首先看一下Linux内核关于GDT的段定义,如右图所示:
我们发觉是第六个段用于记录TLS数据,我了否认一下,写一个最简单的程序,用gdb看一下GS寄存器的值,到此我们早已晓得GS寄存器表示的段描述子指向的段记录TLS数据,如右图所示:
可以看见蓝色圈住的部份,GS的值是0x33,这个0x33怎么解释呢?见右图分解:
这就否认了确实是GS指向的段来表示TLS数据了,在glibc中,初始化的时侯会将GS寄存器指向第六个段:
既然这么,我们是不是可以直接通过GS寄存器来访问TLS数据呢?答案其实是肯定的,glibc虽然就是如此做的,无非经过封装,使用愈加便捷了。而且假如想明白其所以然,还是自己折腾一下比较妥当,我的环境是ubuntuglibc-2.12.1,值得注意的是,每一个glibc的版本的TLSheader都可能不一样,一定要对照自己调试的哪个版本的源码来看,否则一定会发狂的。我将里面的那种test_gs.c更改了一下,成为下边的代码:
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int a=10, b = 0; //b保存GS寄存器表示的段的地址
//设置三个TLS变量,其中前两个使用堆内存,最后一个不使用
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的地址,这个需要用内嵌汇编来做
asm volatile("movl %%gs:0, %0;"
:"=r"(b) /* output */
);
printf("okn");
}
这个代码的涵义在于,我可以通过GS寄存器访问到TLS变量,为了便捷,我就没有写代码,而是通过gdb来否认,虽然通过写代码取出TLS变量和通过gdb查看显存的形式疗效是一样的,个人觉得通过调试的方式对于理解还更好些。
当调试的时侯,在取出GS以后,我们得到了TLS的地址,之后按照该版本的TLS结构体剖析那里储存的是TLS变量,之后查看TLS地址附近的显存,否认那儿确实存着一个TLS变量,这可以通过比较地址得出推论。其实在实际操作之前linux 安装gdb 设置环境变量,我们首先看一下glibc-2.12.1版本的TLS数据结构,如右图所示:
注意,因为我们并无意深度hackTLS,因而仅仅晓得在何处能取到变量即可,因而我们只须要晓得一些数组的大小就可以了,姑且毋须理解其涵义与设计思想。
我们发觉,应当是从第35*4个字节开始就是TLS变量的区域了,是不是这样呢?我们来看一下调试结果linux 安装gdb 设置环境变量,注意我们要把断点设置在asm以后,这样就能打出b的值,其实你也可以调整上述代码linux怎么查看系统版本,把asm内嵌汇编置于代码最上面也是可以的。gdb命令就不多说了,都是些简单的,如下展示出结果:
结果很明了了。最终还有一个小问题,那就是关于线程切换的问题。
对于Windows而言,线程的TEB几乎是固定的,而对于Linux,它同样也是这样子,只须要得到GS寄存器,才能得到当前线程的TCB,换句话说,GS仍然是不变化的,仍然是0x33,仍旧指向GDT的第6个段,变化的是GDT的第6个段的内容,每每进程或则线程切换的时侯,第6个段的内容都须要重新加载,载入即将运行线程的TLSinfo中的信息,这是在切换时switch_to宏中完成的:
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)
{
unsigned int i;
struct desc_struct *gdt = get_cpu_gdt_table(cpu);
for (i = 0; i tls_array[i];
}
注意:关于TLS另外须要说的
不仅我们使用pthread的API在运行时创建的TLS变量之外,还有一部份TLS称为静态TLS变量,这种TLS元素是在编译期间预先生成的,常见的有:
1.自定义_thread修饰符修饰的变量;
2.一些库级别预定义的变量,例如errno
这么这种变量储存在那儿呢?设计者很明智的将其置于了动态TLS临接的空间内,就是GS寄存器指示的地址下边,虽然要是我设计也会如此设计的,你也一样。这样设计的用处在于可以很便捷对不管是动态TLS变量还是静态TLS变量的访问,但是对于动态TLS的管理也很便捷。
这种数据处于“initializeddatasection”,但是在链接或则线程初始化的时侯被动态重定向到了静态TLS空间内,在我的实验环境中,假如我定义了一个变量:
_threadinttest=123;
这么调试显示的结果,它处于GS寄存器指示tls段地址的紧接着下方4个字节的偏斜处,而errno处于_thread变量下方14*4字节的位置。具体这种空间究竟如何安排的,可以看glibc的dl-reloc.c,dl-tls.c等文件,但是本人觉得这没有哪些意义,因为这涉及到好多关于编译,链接,重定向,ELF等知识,假若不想深度优先的迷失在这儿面的话,理解原理也就够了。最后给出一幅图,重定向后总的示意图如下: