目录
解析tid
线程的地址空间布局
线程栈
我们来学习线程控制与线程库
解析tid
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<thread>
using namespace std;int shared_value = 100;
string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}void* start(void* args)
{string name = static_cast<const char*>(args);sleep(1);while (true){cout << "I am a new thread, name: " << name << ", thread is: " << toHex(pthread_self()) << ", shared_value: " << shared_value << ", &shared_value: " << &shared_value << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start, (void*)"thread-1");cout << "I am a new thread, name: main, " << toHex(pthread_self())<< ", NEW thread id: " << toHex(tid) << endl;while (true){cout << "main thread, " << " shared_value: " << shared_value << ", &shared_value: " << &shared_value << endl;shared_value+=10;sleep(1);}pthread_join(tid, nullptr);return 0;
}
通过上面这个代码的执行结果我们发现,两个线程都可以访问同一个全局变量的空间,重点不是这个,他们的id通过16进制打印出来有点像地址呀,没错线程的id不是一个单纯的数字串,而是线程属性集合在用户级线程库中的地址。
果然是地址,怎么证明呢?,线程属性的地址具有唯一性对吧,线程的tid也具有唯一性,所以二者的具有相同的特点。
线程的地址空间布局
pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类 型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。
对于Linux来说,其内核只有lwp,用户想要创建线程就需要调用用软件层封装的线程接口,那如果我们要得到线程的其他属性比如id,优先级,状态,栈大小,用户要调用线程库pthread.so就需要先将在用户内存的pthread.so线程库的地址通过页表映射到虚拟地址的共享区,在内存中线程库的内部维护着所有线程的属性集合,哦线程的属性存在这里,也就是线程的属性不是操作系统管理的,是线程库自己管理的,库管理线程属性集合依照先描述再组织的原则,对于比如线程大小,id状态什么的就装在一个struct结构体里面,struct pthread
是 线程控制块(Thread Control Block, TCB),用于存储线程的所有运行时信息。每个线程有独立的存储空间和线程栈,所以线程的属性=struct pthread(线程控制块)+线程局部存储+线程栈,这三个和起来就是线程的全部属性,这个线程栈是被动态申请出来的。tcb中维护描述库的控制结构
一个一个线程属性集合会依次排在库里面,此时库就相当于数组了,每个线程属性集合按照数组存放,此时线程id就是每个线程属性集合在线程库中的首地址,库里就这么设计的。
__thread
(或 C11 的 _Thread_local
)是 GCC/Clang 提供的 线程局部存储(Thread-Local Storage, TLS) 扩展,用于定义线程私有变量。每个线程拥有该变量的独立副本,互不干扰。__thread只能修饰内置类型,经由__thread修饰的变量相当于将这个变量在每个线程属性集的局部存储中各自拷贝一份。相当于__thread修饰的变量在各个线程中相互独立了。
__thread int shared_value = 100;string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}
我们可以看到不仅子线程中的全局变量不再随着主线程++了,就连两个线程的同一个名字的全局变量的地址都不一样了。
之前有一个mmap区域,mmap
在 Linux 线程(尤其是 pthread
线程)的实现中扮演重要角色,但 不直接创建线程,而是用于 分配线程栈和线程局部存储(TLS),mmap创建线程的物理空间,mmap是你们malloc的底层实现,mmap首先会先向物理内存申请一块空间,然后通过页表映射到库里面,建立完映射,然后创建一个虚拟地址空间让虚拟内存中的一个指针指向这个物理内存空间,然后允许用户使用的部分返回。这个我们之前讲过,是靠着缺页中断完成最后的合法完整映射的。
线程的tcb属性控制结构体在调用用户级创建线程函数pthread_create就已经创建好了,对于clone函数操作系统内核创建线程通过syscall/int 80中断时CPU陷入内核执行中断方法促使操作系统去创建线程。在创建的过程中线程的属性是会动态变化的,在上层可以查看。
最后结论:每一个线程都有自己的栈结构,新线程的栈结构是在共享区重新申请空间,然后将起始地址写入tcb中的stack指针里面(所以指向线程栈的地址在tcb里面)最后通过clone函数传进内核。
线程栈
虽然Linux将线程和进程不加区分的统⼀到了task_struct ,但是对待其地址空间的 stack 还是 有些区别的。
这个线程栈的大小是固定的,size存在线程的tcb里面,对于Linux进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复 制了⽗亲的 stack 空间地址,然后写时拷⻉(cow)以及动态增⻓。如果扩充超出该上限则栈溢出 会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错 误⸺⸺超出扩充上限才报。
然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack 将不再是向下⽣⻓的,⽽是事先固定下来的。线 程栈⼀般是调⽤glibc/uclibc等的 pthread 库接⼝ pthread_create 创建的线程,在⽂件映 射区(或称之为共享区)。
这种stack不能动态增⻓,⼀旦⽤尽就没了,这是和 ⽣成进程的fork不同的地⽅。因此,对于⼦线程的 stack ,它其实是在进程的地址空间中mmap出来的⼀块内存区域,原则上是 线程私有的,但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct 的很多 字段,如果愿意,其它线程也还是可以访问到的,于是⼀定要注意。
根据clone的参数,调用clone的时候,clone会使用 mmap
分配栈的大小和空间。