【LINUX操作系统】线程库与线程库封装
1. 认识线程库
在前文的学习中,我们了解了线程库中各个接口的用法,现在来了解一些线程库的底层
【LINUX操作系统】线程操作-CSDN博客
pthread_ create函数会产⽣⼀个线程ID,存放在第⼀个参数指向的地址中。
该线程ID和前面用ps -aL看到的LWP不是⼀回事,在上一文中有提到,这个线程ID更像是一个 地址 。
因为Linux本身只有轻量级进程的概念,所以只有操作轻量级进程的接口,为了用户更加方便创建操作系统概念层面的线程,存在着一个用户级线程库
libpthread.so
,而因为是动态库,所以在编译链接时需要加上-lpthread
【LINUX操作系统】 动静态库的链接原理-CSDN博客
而我们能通过ldd指令(ldd ELF_NAME)查看一个ELF文件所依赖的.so(动态库)
我们的libthread.so库通过页表从内存映射到虚拟地址空间中去,每一个线程(执行流)都能去找到这个lib->库是共享的。
某些版本的操作系统使用
ldd
命令不会显示动态链接的libpthread.so
,如果线程运行是正常的,就不需要再考虑
当程序启动并运行时,系统会将动态库加载到内存中,并建立起虚拟地址与物理地址之间的映射关系。此时,当前进程中的所有线程都能够访问并使用所链接的pthread动态库。
在Linux环境下,线程被视为轻量级进程,这意味着它们虽然与进程有相似之处,但又有其独特的属性。
对于用户而言,如果想要获取线程的一些特定属性,而这些属性并非轻量级进程所共有的,那么就需要依靠用户级线程库来提供支持。用户级线程库不仅封装了对线程接口的操作,还封装了线程的一些属性。在glibc中,用于维护这些属性的结构体是struct pthread。这个结构体包含了诸如线程ID、线程栈大小以及线程局部存储等重要属性。通过这样的设计,用户级线程库能够更好地管理和控制线程的行为和特性,从而满足不同应用场景的需求。struct pthread是被放在动态库中维护起来的
注意,所有的当下系统中的线程(进程A的所有线程、进程B的所有线程.....)都由一个动态库维护。
struct pthread在库中的存储位置
struct pthread存储在虚拟地址的mmap区域。
在OS的mmap区域中,我们可以想象每一个struct pthread就是像一个vector\数组的元素一样
pthread_create返回的“线程ID”其实就是在这个“数组中”对应的‘‘元素’’的起始地址,也就是pthread_t tid。所以,对一个线程的所有操作的前提就是获得对应的tid,才能join、detach等等。。。。。并且,每一个结构体里面一定都有一个lwp(类似于在文件系统中,FILE结构体中一定有一个fd值),可以将lwp和这个tid对应的属性给联系起来。
每个线程一定还有自己的上下文数据,一定也是保存到自己对应的struct pthread里面
每一个线程都有自己的栈,但是主线程栈是提前就存在于虚拟地址中的(不同于其他的线程栈)。其他新线程的栈更类似于是从共享区动态分配的,创建一个新线程,再动态分配一个新的栈空间过去(有点像malloc的感觉),最后只需要通过找到虚拟地址就能使用这些线程栈了。
任何一个栈区的数据,如果想被访问,都可以被访问(想办法获得地址就行),但是一般不会这么写(存在Bug风险)
让一个线程内的变量不和其他变量起冲突。但是,所有的“看不到、不冲突”都是假的,你想强行通过地址访问,是可以的,属于是强行写bug行为。
errno本质就是被__thread修饰过的,每一个线程都单独有一个。
线程局部存储
上一文提到的用__thread修饰内置类型,这个变量的副本也是存在这个结构体中的。
(变量的副本意味着,会直接用一个新的地址重新拷一份。)
进一步理解作为众多接口的底层(fork,vfork,pthread_create)clone:
lwp如何运行指定的方法?lwp怎么创建出来的?
其实fork的底层、pthread_create的底层都是clone,clone第一个参数就传需要被执行的函数,第二个就把线程栈传进去,设置flag(上一文有提到)
clone所以要申请空间,传递参数等等。。。。其实clone很忙^-^
2. 阅读线程库重点源码(了解与扩展)
1. attr
attr即attribute,表示一个线程的属性,在pthread_create的第二个参数中我们一般都传的是nullptr,
pthread_attr_t attr;pthread_t tid2;pthread_create(&tid2,&attr,run,nullptr);
但是这个属性是内部设置的自解释内容,所以作为了解就好
如果没有显示传attr,就会自动设计成default
里面的属性包括但不限制于该线程的栈的大小等
2. struct pthread
struct pthread有一个变量专门用于存储线程的任务函数的返回值。
/* The result of the thread function. */ // 线程运⾏完毕,返回值就是void*, 最后的返回值就放在tcb中的该变量⾥⾯ // 所以我们⽤pthread_join获取线程退出信息的时候,就是读取该结构体 // 另外,要能理解线程执⾏流可以退出,但是tcb可以暂时保留,这句话 void *result;
所以该进程join值其实就是从这里拿到的。
HERE: void *(*start_routine) (void *); void *arg;/* Debug state. */ td_eventbuf_t eventbuf; /* Next descriptor with a pending event. */ struct pthread *nextevent; #ifdef HAVE_FORCED_UNWIND /* Machine-specific unwind info. */ struct _Unwind_Exception exc; #endif /* If nonzero pointer to area allocated for the stack and its size. */HERE: // 线程⾃⼰的栈和⼤⼩ void *stackblock; size_t stackblock_size;
两处HERE标记出来的代码,分别是线程的任务函数及其参数(一个void* (*func_name)(void*)的函数),线程栈的起始地址及其大小
3. ALLOCATE_STACK
用iattr去给pd传属性,用于创建对应大小的栈
iattr中有栈大小的信息,再来追一下这个ALLOCATE_STACK
具体的对pd申请空间的办法:
先在缓存中申请,缓存申请失败就去mmap里申请。
4.mmap
mmap就是malloc的底层实现方法
mmap只开辟虚拟地址空间,如果要使用的时候会触发缺页中断,缺页中断的时候就会去物理内存中开辟物理空间。填好页表项,页框的起始地址不填。
在Linux系统中,
mmap
映射的区域通常既不位于传统的堆区(heap),也不属于共享区(shared memory segment),而是由内核在进程的虚拟地址空间中单独分配的一块内存区域。
5. 回到线程库
pd指向的struct thread获得相关的任务和参数
// 重点5:把pd(就是线程控制块地址)作为ID,传递出去,所以上层拿到的就是⼀个虚拟地址 *newthread = (pthread_t)pd;
他们两个都来自于:
再一次证明线程id就是pthread的起始地址,用一个newthread就把这个id带出来了
完成了这些准备工作,就能开始调clone_create了
static int do_clone(struct pthread *pd, const struct pthread_attr *attr, int clone_flags, int (*fct)(void *), STACK_VARIABLES_PARMS,int stopped) {// ...// 调用当前操作系统下的`clone`函数if (ARCH_CLONE(fct, STACK_VARIABLES_ARGS, clone_flags, pd, &pd->tid, TLS_VALUE, &pd->tid) == -1){// ...}//...return 0; }
ARCH_CLONE是一个宏,直接用汇编实现,也会直接调clone
以上所有的代码都属于:用户级线程
3. 以类和对象的方法封装一个Thread库
考虑使用C++面向对象的方式封装自己的线程库,本质就是对一些操作进行封装
首先考虑线程对象需要有哪些属性,本次设计主要考虑下面的属性以及操作:
小Tips:设置接口的时候没有想好返回值的参数,可以先都写成void的返回,在使用的时候再进一步确认。
大概框架:
为了描述每一个线程的工作状态,还需要一个变量STATUS来获得每个线程的简易状态,我们使用C++的强枚举功能(enum后面接一个class)
enum class TSTATUS{NEW,RUNNING,STOP};
强类型枚举:枚举值的作用域限定在枚举类型内部,必须通过类型名访问。
现在的整体代码如下:(语法上,重点看start中强枚举类型的使用)
#ifndef __THREAD_HPP_ #define __THREAD_HPP_#include <iostream> #include <string> #include <cstring> #include <functional> #include <sys/types.h> #include <unistd.h>using func_t = std::function<void()>;namespace ThreadModule {enum class TSTATUS{NEW,RUNNING,STOP};class Thread{static int num_of_name;public:Thread(func_t func):_IsJoined(true),_pid(getpid()),_func(func){_name = "THREAD-"+std::to_string(num_of_name++);//THREAD-1 THREAD-2 THREAD -3........}static void* Routine(void* arg){}bool Start(){if(_STATUS!=TSTATUS::RUNNING)//留下被stop的线程再启动的可能性{int n = pthread_create(&(this->_tid),nullptr,Routine,nullptr);if(n<0){std::cerr<<"pthread ERROR "<<strerror(errno)<<std::endl;}//...._STATUS=TSTATUS::RUNNING;}}void Stop(){}void Join(){}void EnableDetach(){}private:std::string _name;pthread_t _tid;pid_t _pid;bool _IsJoined;func_t _func;TSTATUS _STATUS;};static int num_of_name = 1; }#endif
关于Routine的两个问题:
1.为什么要用Routine封装一层func,而不是直接调用?
因为func中可能需要传各种参数(只不过现在我们暂且把func_t给using成了一个不需要传参也不需要返回的,而pthread_create的第三个参数必须是传void*,也返回void*的函数名)
2. Routine为什么要用static修饰?
因为如果不用static修饰,就会导致一个this指针是routine第一个参数,routine不能作为pthread_create的第三个参数
3.被static修饰的routine如何通过this找到自己的unc?
pthread_create还有一个参数arg,我们可以传一个this进去。
完整的hpp代码:
#ifndef __THREAD_HPP_ #define __THREAD_HPP_#include <iostream> #include <string> #include <cstring> #include <functional> #include <sys/types.h> #include <unistd.h>using func_t = std::function<void()>;namespace ThreadModule {enum class TSTATUS{NEW,RUNNING,STOP};class Thread{static int num_of_name;public:Thread(func_t func): _IsJoined(true), _pid(getpid()), _func(func),_STATUS(TSTATUS::NEW){_name = "THREAD-" + std::to_string(num_of_name++); // THREAD-1 THREAD-2 THREAD -3........}static void *Routine(void *arg){Thread* p_thread = static_cast<Thread*>(arg); p_thread->_func();p_thread->_STATUS=TSTATUS::RUNNING;return nullptr;}bool Start(){if (_STATUS != TSTATUS::RUNNING) // 留下被stop的线程再启动的可能性{int n = pthread_create(&(this->_tid), nullptr, Routine, (void*)this);if (n != 0) // 构建代码的健壮性{std::cerr << "pthread ERROR " << strerror(errno) << std::endl;return false;}_STATUS = TSTATUS::RUNNING;return true;}return false;}bool Stop(){if (_STATUS == TSTATUS::RUNNING){int n = pthread_cancel(_tid);if (n != 0)return false;_STATUS = TSTATUS::STOP;return true;}return false;}bool Join(){if(_STATUS!=TSTATUS::RUNNING){if(_IsJoined){int n = pthread_join(_tid,nullptr);if(n!=0) return false;_STATUS = TSTATUS::STOP;return true;}return false;}return false;}void EnableDetach(){_IsJoined=false;}bool Detach(){EnableDetach();pthread_detach(_tid);return true;}private:std::string _name;pthread_t _tid;pid_t _pid;bool _IsJoined;func_t _func;TSTATUS _STATUS;};static int num_of_name = 1; }#endif
下面写一点main函数和指令用于实验:
利用智能指针+lambda表达式来创建多线程:
用一个哈希表来存储所有的Thread结构。
如果要给func传一个单参数可以用一个模板,数据变量名字叫data:
如果要多参数的话建议直接传一个类
由于这个时候的args是模板类的指针,所以在强转的时候就会比较恶心: