Linux中NPTL线程库的线程ID、内存布局与独立上下文
目录
一、线程ID的生成与存储
1、内核线程标识(LWP)
2、pthread_create生成的线程ID
二、线程ID的获取
三、pthread_t类型的本质
1、Linux的线程实现背景
2、关于动态链接、动态库加载和地址重定位(回顾)
动态链接与动态库加载
动态链接的优势
3、线程库的动态库特性
四、进程地址空间与线程资源
1、线程栈
2、struct pthread
3、线程局部存储(TLS)
五、线程描述与内存管理
六、示例代码与结果分析
七、线程的内存布局和动态库加载
知识点
1、内核空间与用户空间
2、内存布局
3、动态库(pthread.so)
4、struct pthread
工作流程
1、线程创建(pthread_create)
2、线程维护
3、线程终止(pthread_join)
总结
八、独立上下文和独立栈
1、独立上下文
PCB(Process Control Block,进程控制块)
TCP(Thread Control Block,线程控制块,或在用户层由pthread库维护的结构)
2、独立栈
栈的作用
栈的来源
栈的管理
3、工作流程
线程创建
线程执行
线程终止
4、总结
一、线程ID的生成与存储
pthread_create函数在创建新线程时,会产生一个线程ID,并将该ID存储在第一个参数所指向的内存地址中。需要特别注意的是,这里产生的线程ID与内核层面的线程标识(LWP,Light Weight Process)并非同一概念。
1、内核线程标识(LWP)
-
在操作系统层面,线程作为轻量级进程,是调度器进行任务调度的最小单位。
-
为了唯一标识和管理这些线程,内核需要为每个线程分配一个数值型的标识符,即LWP。
-
这个标识符用于进程调度、资源分配等核心操作。
2、pthread_create生成的线程ID
-
该线程ID属于NPTL(Native POSIX Threads Library)线程库的范畴。
-
pthread_create函数的第一个参数指向一个虚拟内存单元,这个内存单元的地址就被用作新创建线程的线程ID。 -
线程库在后续的线程操作中,如线程的启动、终止、同步等,都是基于这个线程ID来进行的。
二、线程ID的获取

线程库NPTL提供了pthread_self函数,用于获取线程自身的ID。该函数的原型如下:
pthread_t pthread_self(void);
-
调用
pthread_self函数返回的线程ID,与通过pthread_create函数第一个参数获取的线程ID是相同的。 -
这意味着,无论是创建线程时获取的ID,还是线程自身通过
pthread_self获取的ID,都是用于标识同一个线程的。

三、pthread_t类型的本质
pthread_t类型的具体定义取决于线程库的实现。在Linux系统目前广泛使用的NPTL线程库实现中,pthread_t类型的线程ID本质上是一个进程地址空间共享区上的虚拟地址。
1、Linux的线程实现背景
-
Linux内核本身并不提供真正的线程概念,而是提供了轻量级进程(LWP)作为调度的基本单位。
-
这意味着,操作系统只需要对内核执行流LWP进行管理,而用户层面的线程接口、数据结构等则由线程库自行管理。
-
这种“先描述,再组织”的管理方式在线程库内部实现。
2、关于动态链接、动态库加载和地址重定位(回顾)
在Linux系统中,当一个可执行程序是动态链接的ELF(Executable and Linkable Format)文件时,它依赖于动态库(共享对象,如.so文件)来提供运行时所需的功能。以下是关于动态链接、动态库加载和地址重定位的详细解释:
动态链接与动态库加载
ELF可执行程序
-
ELF是一种通用的文件格式,用于可执行文件、目标代码、共享库和核心转储。
-
动态链接的ELF可执行程序在编译时不包含所有需要的代码,而是依赖于运行时加载的共享库。
动态库(Shared Libraries)
-
动态库(
.so文件)包含可以被多个程序共享的代码和数据。 -
使用动态库可以减少可执行文件的大小,并允许库的更新而不必重新编译依赖它的程序。
动态链接过程
-
程序启动:当运行一个动态链接的可执行程序时,操作系统创建进程,并开始执行其入口点(通常是
_start函数)。 -
动态链接器介入:在程序启动的早期阶段,动态链接器(如
ld-linux.so)被调用。 -
加载动态库:动态链接器读取可执行文件的动态段(
.dynamic),确定所需的共享库,并将这些库加载到内存中。 -
地址重定位:
-
动态链接器执行地址重定位,将库中的符号地址解析为实际的内存地址。
-
这一过程可能包括修正代码中的绝对地址引用,以适应库在内存中的实际加载位置。
-
内存映射
-
加载的共享库被映射到进程的地址空间中,通常是在
mmap区域或共享区。 -
每个库可能被映射到不同的虚拟地址,但通过地址重定位,所有引用都能正确解析。
符号解析
-
动态链接器解析可执行程序和共享库之间的符号引用。
-
符号可以是函数或全局变量,动态链接器确保每个符号引用指向正确的定义。
初始化
-
某些共享库可能需要执行初始化代码,动态链接器负责调用这些初始化例程。
动态链接的优势
-
节省内存:多个进程可以共享同一个库的代码段,减少物理内存的使用。
-
易于更新:共享库可以独立于可执行程序进行更新,只要接口保持不变,可执行程序无需重新编译。
-
减少磁盘空间:由于库是共享的,不需要为每个可执行程序存储一份库的副本。
3、线程库的动态库特性
-
通过
ldd命令可以查看,我们使用的线程库实际上是一个动态库。
-
在进程运行时,这个动态库会被加载到内存中,并通过页表映射到进程地址空间的共享区。
-
这样,进程内的所有线程都可以访问这个动态库,从而使用线程库提供的各种功能。

四、进程地址空间与线程资源
1、线程栈
-
每个线程都有自己私有的栈空间。
-
其中,主线程使用的栈是进程地址空间中原生的栈,而其他通过
pthread_create创建的线程,其栈空间则是在共享区中开辟的。
2、struct pthread
-
每个线程都有自己的
struct pthread结构体,这个结构体中包含了对应线程的各种属性,如线程状态、优先级、调度策略等。
3、线程局部存储(TLS)
-
每个线程还有自己的线程局部存储区域,用于存储线程被切换时的上下文数据,如寄存器状态、栈指针等。
-
这些数据在线程切换时会被保存和恢复,以确保线程能够正确继续执行。

五、线程描述与内存管理
-
每一个新线程在共享区都有一块对应的内存区域,用于描述该线程的各种属性和状态。
-
因此,要找到一个用户级线程,只需要找到该线程内存块的起始地址,就可以获取到该线程的各种信息。
-
上面提到的各种线程函数,本质上都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行。也就是说,线程数据的管理本质是在共享区的。
六、示例代码与结果分析
由于pthread_t在NPTL线程库中本质上是一个虚拟地址,我们可以通过打印这个地址来验证这一点。以下是一个简单的示例代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* Routine(void* arg) {while (1) {printf("new thread tid: %p\n", pthread_self());sleep(1);}
}int main() {pthread_t tid;pthread_create(&tid, NULL, Routine, NULL);while (1) {printf("main thread tid: %p\n", pthread_self());sleep(2);}return 0;
}
这个警告是关于格式字符串与参数类型不匹配的问题,但是我们这里直接就是想转化为地址的格式来观察,也就是16进制的格式,,所以这里的报错不用关心:

运行上述代码后,我们会看到主线程和新创建的线程分别打印出自己的线程ID。从打印结果可以看出,这些线程ID看起来很像内存地址。

而现在我们可以明确地说,在NPTL线程库的实现中,这些线程ID本质上就是进程地址空间共享区上的虚拟地址。由于同一个进程中的所有虚拟地址都是不同的,因此可以用这些地址来唯一区分每一个线程。
七、线程的内存布局和动态库加载

这两张图展示了在Linux系统中使用NPTL(Native POSIX Threads Library)实现线程的内存布局和动态库如何与线程结构交互的过程。
知识点
1、内核空间与用户空间
-
内核空间是操作系统内核运行的空间,用户空间是应用程序运行的空间。
-
线程的创建和管理在用户空间由线程库(如NPTL)处理,而调度和执行由内核处理。
2、内存布局
-
主线程栈:每个线程都有自己独立的栈空间,主线程栈是进程启动时创建的。
-
mmap区域:用于动态内存映射的区域,可以用于共享内存或线程栈的分配。
-
堆:动态分配内存的区域,所有线程共享。
-
代码段/数据段:存储程序代码和全局/静态变量。
3、动态库(pthread.so)
-
提供线程相关的函数,如
pthread_create和pthread_join。 -
维护
struct pthread,其中包含线程局部存储、线程栈等信息。
4、struct pthread
-
包含线程的标识符(
pthread_t tid)、线程局部存储和线程栈等信息。 -
每个线程在动态库中都有一个对应的
struct pthread。

工作流程
1、线程创建(pthread_create)
-
加载动态库:当程序启动时,
pthread.so库被加载到内存中。 -
库映射:库的代码和数据被映射到每个进程的地址空间,通常是在
mmap动态映射区域或共享区。 -
初始化线程结构:调用
pthread_create时,动态库会在堆或共享区中分配一个struct pthread,并初始化其中的字段,包括线程ID、栈空间等。 -
设置线程栈:为新线程分配栈空间,可能是在
mmap区域。 -
内核介入:线程库通过系统调用(如
clone)请求内核创建一个新的执行上下文(LWP)。
2、线程维护
-
线程局部存储:每个线程有自己的局部存储,用于保存线程特定的数据。
-
线程栈管理:线程栈用于函数调用和局部变量存储,线程库确保每个线程有独立的栈空间。
-
调度与执行:内核调度器负责线程的实际执行,线程库提供同步和通信机制(如互斥锁、条件变量)。
3、线程终止(pthread_join)
-
线程执行完毕后,线程库负责回收资源,如释放栈空间和
struct pthread。 -
pthread_join用于等待特定线程终止,并获取其退出状态。
总结
-
线程创建涉及用户空间的线程库和内核的协作,动态库负责管理线程的数据结构,而内核负责实际的调度和执行。
-
内存布局确保每个线程有独立的执行环境,避免资源冲突。
-
线程维护包括资源管理、同步和通信,确保线程高效、安全地运行。
这种设计使得线程的创建和管理高效且灵活,充分利用了用户空间和内核的优势。
八、独立上下文和独立栈
在多线程编程中,每个线程都有其独立的上下文和栈空间,这是实现并发执行的基础。以下是对独立上下文和独立栈的详细解释:
1、独立上下文
PCB(Process Control Block,进程控制块)
-
内核层面:在操作系统内核中,每个线程(或轻量级进程,LWP)都有一个对应的PCB或类似的数据结构。PCB包含了线程的调度信息(如优先级、状态)、寄存器状态、内存映射信息等。
-
作用:内核通过PCB来管理和调度线程,确保每个线程能够按照预期执行。
TCP(Thread Control Block,线程控制块,或在用户层由pthread库维护的结构)
-
用户层面:在用户空间的线程库(如NPTL)中,每个线程也有一个对应的控制结构,通常称为
struct pthread或类似的名称。这个结构包含了线程的用户级信息,如线程ID、线程局部存储、栈地址、错误码等。 -
作用:线程库通过这个结构来管理线程的创建、销毁、同步等操作,而不必每次都与内核交互。
2、独立栈
栈的作用
-
栈是线程执行函数时用来存储局部变量、函数参数、返回地址等信息的内存区域。
-
每个线程都需要有自己独立的栈空间,以避免不同线程之间的数据干扰。
栈的来源
-
进程原有的栈:主线程通常使用进程创建时分配的栈。
-
动态申请的栈:对于新创建的线程,线程库(如通过
pthread_create)会在堆或通过mmap系统调用动态申请一块内存区域作为新线程的栈。
栈的管理
-
线程库负责为新线程分配和初始化栈空间。
-
栈的大小可以在创建线程时指定,或者使用默认值。
-
线程执行完毕后,线程库负责回收栈空间。
3、工作流程
线程创建
-
当调用
pthread_create时,线程库在用户空间分配一个struct pthread,并初始化其中的字段。 -
线程库通过系统调用(如
clone)请求内核创建一个新的执行上下文(LWP),并关联到新线程的struct pthread。 -
线程库为新线程分配栈空间,并设置栈指针等寄存器状态。
线程执行
-
内核调度器负责选择哪个线程(LWP)来运行。
-
当线程获得CPU时间片时,其上下文(包括寄存器状态、栈指针等)被加载到CPU中,开始执行线程的代码。
线程终止
-
线程执行完毕后,线程库负责回收其栈空间和
struct pthread等资源。 -
如果调用了
pthread_join,则等待线程终止的线程会继续执行,并可能获取终止线程的退出状态。
4、总结
每个线程都有独立的上下文(包括内核层面的PCB和用户层面的struct pthread)和独立的栈空间。这种设计使得线程能够并发执行,而不会相互干扰。线程库负责管理线程的创建、销毁、同步等操作,并与内核协作实现高效的线程调度。
