1. 线程ID及地址空间布局
结论1: pthread库也是库,要被映射到当前进程的虚拟地址空间以支持线程控制!!!
结论2:线程ID(pthread_t)其实是一个地址!
1.1 TCB与线程库
线程(库中的真线程,不是内核的轻量级进程,下面的线程也是这个意思)可以有多个!我们可以创建、终止、等待甚至分离线程,说明线程这个概念其实是真实存在的! 所以线程本身也要被管理!
换句话说 Linux内核中的轻量级进程只能属于线程基本属性的一部分! 所以在我们对应的PCB中,只是关于执行流调度执行的相关信息,但是线程的相关属性并没有在PCB中!
即 必须要有个 描述线程的结构体!!这个结构体被我们维护在了pthread库中!
那么如何 组织呢?

当进程运行时 线程库已经映射到了共享区了 我们每创建一个线程,库中就会为我们 在共享区中创建对应的TCB(struct pthread)、线程局部存储和线程栈这样的属性集!即 我每创建一个线程 就创建这么一坨:

而为了让我们上层用户能快速查到对应的属性!而其起始地址 我们就把它称为tid!,即我们拿到的那个 pthread_t这个类型的参数!
有了这个tid,我们就能用类似数组方式将其组织管理起来!!
⚠️:进程代码区加载线程库时,其首地址被加载在进程虚拟地址空间的共享区!需要注意的是,虽然子线程的私有资源也位于共享区,但它们与线程库本身的加载地址是相互独立的!即 线程库的首地址是共享区中一个固定的映射位置,而子线程的 属性首地址则是共享区中动态分配的内存地址,两者虽然都在共享区,但用途和分配方式完全不同!!
而这些 创建的对应属性 也可以说是创建在线程库内部,由该库统一管理(类比成:PCB创建在内核中,由内核统一管理 内核 类比 线程库!)中!由所有进程共享!通过页表在库中找到属于自己的TCB!!!

补充: 我们知道,和加载普通程序以及OS内核一样,动态库加载到内存时,在虚拟内存中是连续的,但在物理内存中不需要连续,所以
TCB在线程在库中,和PCB在内核中一样 有“逻辑”上在里面的感觉(自己体会体会)
1.2 源码阅读,理解线程的创建过程!!
以下是 glibc-2.4 中 pthread 源码相关内容 路径nptl/pthread_create.c(pthread_create的底层)(截一些重点)
1. 线程属性
关于线程属性:

struct pthread_attr
{
/* 调度器参数和优先级 */
struct sched_param schedparam;
/* 调度策略 */
int schedpolicy;
/* 各种标志位,如分离状态(detachstate)、作用域(scope)等 */
int flags;
/* 栈溢出保护区(Guard Area)的大小 */
size_t guardsize;
/* 栈的处理(栈地址) */
void *stackaddr;
/* 栈的大小 */
size_t stacksize;
/* CPU 亲和性掩码 */
cpu_set_t *cpuset;
/* CPU 亲和性掩码的大小 */
size_t cpusetsize;
};
上面属性 我们主要需要关注的就是 栈大小和栈的地址!!
2. struct phread

这一步就是创建 struct pthread!不过其具体的内存和里面的值 是交给ALLOCATE_STACK完成的 类似int * a=NULL;
线程控制块的一部分代码(
TCB):
里面有个这个属性:
分别表示了 TCB可以通过链表连接,里面的pid_t tid就是我们当时ps -aL看到的LWP!!
这个属性表示 当前创建的线程的栈 是否由用户提供!
这是因为 主线程的栈是创建在栈区 而其他新线程的栈是其实是创建在哦共享区的!
下面就是用于保存 线程运行完毕后的返回值(void*)
下面的参数 就是用户调用
pthread_create往里面传递的函数指针和参数!!!
具体来说,TCB 中维护栈信息的核心字段包括:
stackblock(或stack_base):记录线程栈的起始虚拟地址(低地址)。stackblock_size(或stack_size):记录线程栈的总大小(默认通常为 8MB)。guardsize(或guard_page):记录栈底防溢出保护区(Guard Page)的大小或地址。注意:TCB是存在新线程的栈中的 最高地址(栈底)
3. ALLOCATE_STACK

了解即可:
int err = ALLOCATE_STACK(iattr, &pd);是 Linux 线程库在创建新线程时,在用户态分配线程栈和线程控制块(TCB)的核心步骤。ALLOCATE_STACK本质上是一个宏,其底层调用了allocate_stack 函数,当这行代码执行完毕后,新线程所需的栈空间和控制块就已经在用户态准备就绪了。
完成了如下工作:
这里就是创建struct pthread的结构体时,它一开始会尝试直接从缓存里找对应空间!!
原理是: 当一个线程结束运行时,glibc(“用户态”的 C 标准库,它是用户程序与内核之间的“桥梁”或“中间层”) 并不会立刻调用munmap把这块栈内存还给操作系统。相反,它会将这块内存从 stack_used 链表中移除,并放入stack_cache链表中“缓存”起来,等待下一个新线程创建时复用。
这是为了防止频繁调用系统调用!!!
如果缓存区没有空余空间,则会调用mmap(c库的)系统调用,向内核申请一块虚拟地址空间
这个接口是用来 创建共享内存的(因为要在共享区申请栈空间),但是如果只给自己用不给别人共享 不相当于是进程自己的嘛~
然后我们就会把申请到地址的起始地址传给输出型参数pd,同时往strcut pthread里面填入对应的属性
至此 就把线程对应信息管理起来了!!!
⚠️:当线程库在用户态通过
ALLOCATE_STACK分配好栈空间后,会立即将这块内存的详细信息写入TCB中!
可以这么理解 : pthread_attr 只是创建前的“临时需求单”,而 TCB 里的栈信息才是线程运行期间永久持有的“真实房产证”!即 pthread_attr是创建的时候用的!而TCB才是维护管理线程用的!
随后,线程库会继续调用
create_thread函数,最终通过clone系统调用进入内核态,完成真正的线程创建!
4. 设置要执⾏的⽅法的地址和参数
这段代码就是把pthread_create创建线程后其需要执行的函数和参数的地址传到TCB中保存!!

5. 获得tid
在pthread_create中 会有个 thread:输出型参数,返回线程ID 最后就会以这种方式返回:

而这个就是我们当时传入的那个&tid的参数,它会以整形的形式接收线程id 然后给我们获取!!
int __pthread_create_2_1(newthread, attr, start_routine, arg)
pthread_t *newthread;//这个!!
const pthread_attr_t *attr;
void *(*start_routine)(void *);
此时 我们用户层就能获取线程ID 从而可以与TCB建立联系了!!
6. 判断线程是否分离

关于is_detached是什么(这个定义在了TCB中):

那么joinid又是什么?(也定义在了TCB中)

然后这个(pd)-> joinid == (pd)又是什么意思?
意思是你自己的joinid指向了你自己! 即 如果joinid默认指向主线程 那么你就不是被分离的 如果指向你自己 那么你就是被分离的!!
7. 创建线程
最后调用create_thread 把你 填充好的线程控制信息(tid,即pd的起始地址)pd 和 属性 以及栈大小相关信息 调用进去整合( 即前面的步骤(也就是库的本职工作)都是在创建具体的线程组织管理信息【理解成正在画施工图】,到这一步才真刀真枪的通过前面给的信息【可以理解成施工图】在内存里创建线程本体(即 轻量级进程)【开始施工】!! )进行通过 clone 系统调用进入内核态,完成真正的线程创建!

8. create_thread
create_thread(struct pthread *pd, const struct pthread_attr *attr,
STACK_VARIABLES_PARMS)
{
...
int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL |
CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
//这里是设置标志位 判断你是要创建一个完整的进程还是轻量级线程
//------------------------------------------------
int res = do_clone(pd, attr, clone_flags, start_thread,
STACK_VARIABLES_ARGS, 1);
//然后将pd 属性 标志位(用于判断是创建线程还是进程) 栈大小等信息传递给 do_clone 这个系统调用开始在内存中真正创建线程!!! do_clone 本质上是一个封装了 clone 系统调用的内部函数
9. do_clone
在do_clone里面有个 这个 也是该函数最关键的部分:

它会调用 ARCH_CLONE这个宏:
#define ARCH_CLONE __clone
__clone是glibc⽤汇编封装的⼀个调⽤clone系统调⽤的函数,所以
__clone的实现就是汇编,贴⼀份代码(sysdeps/unix/sysv/linux/x86_64):
ENTRY (BP_SYM (__clone))
/* Sanity check arguments. */
movq $-EINVAL,%rax
testq %rdi,%rdi /* no NULL function pointers */
jz SYSCALL_ERROR_LABEL
testq %rsi,%rsi /* no NULL stack pointers */
jz SYSCALL_ERROR_LABEL
/* Insert the argument onto the new stack. */
subq $16,%rsi
movq %rcx,8(%rsi)
/* Save the function pointer. It will be popped off in the
child in the ebx frobbing below. */
movq %rdi,0(%rsi)
/* Do the system call. */
movq %rdx, %rdi
movq %r8, %rdx
movq %r9, %r8
movq 8(%rsp), %r10
movl $SYS_ify(clone),%eax // 获取系统调⽤号
/* End FDE now, because in the child the unwind info will be
wrong. */
cfi_endproc;
syscall // 陷⼊内核(x86_32是int 80),要求内核创建轻量级进程
testq %rax,%rax
jl SYSCALL_ERROR_LABEL
jz L(thread_start)
触发软中断 陷入内核 创建线程!!!而这个宏 就是 OS内核里的 系统调用本体!!这部分代码了解即可。
10. 总结
所以不管 pthread_create多么复杂 底层其实都是在调用clone !创建完新线程后,主线程继续向下执行,而新线程基于你传入的函数指针 执行 回调函数!
在创建线程中 库的左右是创建描述和组织线程的相关结构体 而底层则是通过clone创建轻量级进程!
补充: 如果加载了线程库,进程启动的初始化阶段,主线程也会创建对应的用户级TCB,存储位置为 数据区!!
1.3 线程局部存储的理解
来一段代码
pid_t id =0;
void *routine(void* args)
{
std::string name=static_cast<const char *>(args);
while (1)
{
std::cout<< "id:"<<id<<std::endl;
id++;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
while (1)
{
std::cout<<"main thread id:"<<id<<std::endl;
}
pthread_join(tid,nullptr);
return 0;
}
效果:
我们发现id值 主、新线程都是一样的!
如果给这个id 加上如下修饰(注意是两条_):
__thread pid_t id = 0;
此时我们发现一个变 一个不变
我们把这个用__thread 修饰的值 叫做 线程局部存储!!作用就是 你定义的变量在申请新线程的时候 会在 线程局部存储那里也开辟一份 然后新线程虽然用的是同一个名字 其实早就为该值申请了一个新的虚拟地址!
证明也很容易 把虚拟地址打印看看就行:

细节: 这个只能存储一些整数,是没办法存储类结构体之类的!!
2. LWP的获取(加餐)
由于 glibc 标准库没有直接封装获取 LWP ID 的函数,所以需要通过系统调用来获取。
用 syscall 系统调用:
#include <sys/syscall.h>
#include <unistd.h>
pid_t lwp_id = syscall(SYS_gettid); // 获取当前线程的内核LWP ID
部分较新的系统或 glibc 版本可能提供了非标准的 gettid() 函数,可以直接调用,但为了代码的可移植性,建议优先使用 syscall 方式!!!
3. C++多线程(库的兼容问题)
在C语言环境下 因为不同平台的pthread库是不同的 Linux线程库的接口pthread_create就没办法在windows下跑!!
所以就衍生出了C++线程库 其完美的解决了跨平台的问题!!
这是因为 C++多线程的本质,是对不同平台pthread库的封装!!
关于 C++多线程接口的讲解 敬请期待!!












3271

被折叠的 条评论
为什么被折叠?



