Linux —— 理解pthread库和pthread_t
理解pthread库
pthread库是一个动态库
使用下面指令可以查找的系统目录下的库信息
ls /lib/x86_64-linux-gnu/libpthread.so.0 -l
可以看到,libpthread.so.0
是一个动态库,而动态库是在运行时由程序加载。
pthread
库实现了POSIX标准下的线程管理功能,它提供了**创建、管理、同步和销毁线程的函数。**这些函数允许程序在一个进程中同时执行多个任务,即多线程操作。 这些函数都是对系统调用的封装,提供高效、标准化的多线程编程接口,它通过动态加载和链接机制,不仅节省了系统资源,还提供了灵活的升级路径。在使用pthread库时,程序可以享受标准线程操作接口的便利,而不必关心底层复杂的线程管理和调度细节。
底层逻辑
1.线程创建的目标
pthread_create
的主要任务就是在一个进程中创建一个新的线程,新的线程共享地址空间和部分资源。
2.使用clone()
系统调用
在Linux中,线程的创建的核心就是使用clone()
。clone()
与fork()
相似,但是比后者更加的灵活,其可以控制新线程和父进程共享的资源和属性。
clone() 系统调用的签名为:
pid_t clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...
pid_t *ptid, void *tls, pid_t *ctid);
参数:
- fn: 新线程的执行函数指针。
- child_stack: 指向新线程的栈顶,用于在新线程中执行函数。
- flags: 指定新线程与父线程共享哪些资源(如内存、文件描述符等)。
- arg: 传递给 fn 的参数。
- 还有一些可选的参数(如 tls, ptid, ctid),用于更细粒度的控制新线程的特性。
线程创建的流程:
线程创建的大体流程如下:
- 准备线程属性:
pthread_create
首先会根据传入的线程属性(如栈大小、调度策略)准备必要的资源。如果没有指定,使用默认值。- 例如,默认情况下,新线程会继承进程的地址空间、文件描述符表、信号处理器等。
- 设置栈空间:
- 为新线程分配栈空间,通常通过映射新的内存区域来完成(使用 mmap() 或现有栈空间)。
- 栈空间是线程私有的,即每个线程都有自己的栈,以避免不同线程之间的数据混淆。
- 调用 clone():
pthread_create
通过clone()
系统调用创建新线程。clone()
创建一个新的轻量级进程(LWP),它与调用者共享资源(如地址空间),但有自己的栈和执行上下文。- 通过
flags
参数指定哪些资源要共享,常见的标志包括:CLONE_VM
: 共享内存空间。CLONE_FS
: 共享文件系统信息。CLONE_FILES
: 共享文件描述符表。CLONE_SIGHAND
: 共享信号处理。
- 在新线程中执行函数:
- clone() 成功后,新线程开始执行 fn 函数,并将传递的 arg 作为参数。
- 新线程执行函数 fn,直到它返回或显式调用 pthread_exit。
- 线程 ID 的管理:
pthread_create
返回时,会将新创建线程的 ID (pthread_t) 传给调用者。这通常是一个整数(或指针),在系统中唯一标识该线程。
内核会跟踪这个线程 ID 以及它的相关信息(如状态、栈指针等)。
LWP
LWP是Linux系统中一个重要的概念,一般用来描述系统内核的线程的实现,也是一种轻量化的进程。
轻量级进程(LWP) 是内核调度的最小单位,LWP通常与用户线程(如使用pthread库创建的线程)一一对应。每个用户线程对应一个LWP,LWP由内核调度和管理。
通过 clone() 系统调用可以创建LWP。clone()允许指定与父进程共享的资源,创建出的子进程就是一个LWP。
LWP的实现是通过 task_struct 结构体。每个LWP都有一个 task_struct 实例,该结构体包含了调度所需的所有信息,如进程ID(PID)、状态、调度策略等。
pthread_t
pthread_t的概念
pthread_t 是 Pthreads库中的一个数据类型,用来标识一个线程。
当调用pthread_create()
时,Pthreads库会为新线程分配一个pthread_t标识符。这个标识符通常在内部对应一个轻量级进程的ID(LWP ID)。
pthread_t 是一个用户级别的标识符。
pthread_t 的实现
pthread_t
的具体实现取决于操作系统和平台。在 Linux 上,通常 pthread_t
被定义为一个 unsigned long int
,即一个无符号长整型。这种类型能够唯一标识一个线程。
实际上,pthread_t 在内部可能是一个线程控制块(TCB)的指针,或者是一个可以唯一标识线程的 ID。在 Linux 上,它可能会被直接映射到轻量级进程(LWP)的 ID,或者映射到某种形式的索引结构。
pthread_t 与 LWP 的关系
pthread_t
是用户空间的标识符,用户通过它操作线程。每个pthread_t
对应一个内核中的轻量级进程(LWP)。- 当用户调用 pthread 库的函数操作
pthread_t
时,底层会通过系统调用与内核交互,对应操作LWP
。
独立的栈空间
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>
int g_val = 100;
std::string ToHex(pthread_t tid)
{
char ch[128];
snprintf(ch, sizeof(ch), "0x%lx", tid);
return ch;
}
void *threadRun(void *arg)
{
std::string name = static_cast<const char *>(arg);
while (1)
{
std::string tid = ToHex(pthread_self());
std::cout << "I am new thread, my id = " << tid << " val = " << g_val << " &val = " << &g_val << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void *)"thread-1");
while (1)
{
std::cout << "I am main thread, my val = " << g_val << " &val = " << &g_val << std::endl;
sleep(1);
}
return 0;
}
声明全局变量,分别通过主新线程打印变量值和地址。
主新线程的g_val值和地址都一样。
那如果在声明变量的时候加上__thread
呢?
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>
__thread int g_val = 100; //加上了__thread
std::string ToHex(pthread_t tid)
{
char ch[128];
snprintf(ch, sizeof(ch), "0x%lx", tid);
return ch;
}
void *threadRun(void *arg)
{
std::string name = static_cast<const char *>(arg);
while (1)
{
std::string tid = ToHex(pthread_self());
std::cout << "I am new thread, my id = " << tid << " val = " << g_val++ << " &val = " << &g_val << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void *)"thread-1");
while (1)
{
std::cout << "I am main thread, my val = " << g_val << " &val = " << &g_val << std::endl;
sleep(1);
}
return 0;
}
主新线程的g_val值和地址都不一样,证明不太线程有其独立的栈空间。
管理
pthread_t 是一个用户级别的标识符,而LWP是内核级别的标识符。
- 内核管理:
无论是进程还是线程,内核都使用 task_struct 结构体来进行管理。task_struct 是进程和线程在内核中的核心数据结构,包含了所有的管理信息。 - 用户空间管理:
pthread 库在用户空间使用自己的数据结构(如 pthread_t 和 TCB)来抽象和管理线程。这些结构为用户提供了线程操作的接口,同时通过系统调用将操作委托给内核。最终,内核中的 task_struct 结构体管理实际的线程调度和执行。
&emsp因此,pthread
库确实在用户空间维护了一套数据结构来管理线程,而内核则继续使用 task_struct
来维护和管理进程与线程的执行。两者之间通过系统调用进行交互,用户空间的数据结构(如 pthread_t
)在本质上是对内核线程的一个抽象。