当前位置: 首页 > news >正文

【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是模板类的指针,所以在强转的时候就会比较恶心:

        

        

相关文章:

  • Qwen2.5模型结构
  • 【前端笔记】CSS 选择器的常见用法
  • C++ 析构函数
  • goland无法debug
  • 源雀SCRM开源·AI企微客服|RAG知识中枢+自训练闭环
  • 屏蔽力 | 在复杂世界中从内耗到成长的转变之道
  • 在shell中运行RDD程序
  • layui下拉框输入关键字才出数据
  • c++中“”符号代表引用还是取内存地址?
  • 手写 Vue 源码 === 完善依赖追踪与触发更新
  • 数组和集合
  • 【CSS】Grid 的 auto-fill 和 auto-fit 内容自适应
  • NHANES指标推荐:AISI
  • Qwen2-VL详解
  • cocos中加入protobuf和编译protobuf的方法
  • 软件设计师2025
  • SecureCRT SFTP命令详解与实战
  • Unity3D 游戏内存优化策略
  • 模拟设计中如何减小失配
  • 淘宝按图搜索商品(拍立淘)Java 爬虫实战指南
  • 中信银行:拟出资100亿元全资设立信银金融资产投资有限公司
  • 司法部:持续规范行政执法行为,加快制定行政执法监督条例
  • 波音公司计划于2027年交付新版“空军一号”飞机
  • 戴维·珀杜宣誓就任美国驻华大使
  • 中国经济新动能|警惕数字时代下经济的“四大极化”效应
  • 全国铁路五一假期累计发送1.51亿人次,多项运输指标创历史新高