泉缘泉 发表于 2025-2-18 22:46:00

Linux线程库与线程库封装

Linux线程库与线程库封装

何为线程id

在线程操作部分提到了利用pthread_self()接口获取到当火线程的id,比方下面的代码:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>

void *routine(void *arg)
{
    printf("%ld\n", pthread_self());

    return NULL;
}

int main()
{
    pthread_t thid;

    // 创建线程
    int n = pthread_create(&thid, NULL, routine, (void *)"thread-1");

    int ret = pthread_join(thid, NULL);

    return 0;
}
运行后结果如下:
123897116624576
这个结果不敷直观,利用下面的代码将该数值转换为16进制输出:
// ...
std::string toHex(pthread_t num)
{
    char buffer;
    snprintf(buffer, sizeof(buffer), "0x%lx", num);

    return buffer;
}

void *routine(void *arg)
{
    std::cout << toHex(pthread_self()) << std::endl;

    return NULL;
}
// ...
此时再运行上面的代码即可看到下面的结果:
0x7df688c006c0
这个结果实际上就是一个地址值,所以所谓的线程id实际上就是一个地址值而并不是LWP值,具体缘故原由在接下来先容线程库时即可表明
线程库

前置知识

在前面提到,由于Linux本身只有轻量级历程的概念,所以只有操作轻量级历程的接口,为了用户更加方便创建操作系统概念层面的线程,存在着一个用户级线程库libpthread.so,而由于是动态库,所以在编译链接时需要加上-lpthread,利用ldd下令查看对应的运行程序可以看到链接的动态库
   某些版本的操作系统利用ldd下令不会表现动态链接的libpthread.so,假如线程运行是正常的,就不需要再思量
当程序运行时,加载动态库到内存,建立假造地址和物理地址的映射,此时当前历程中的全部线程就都可以看到链接的pthread动态库并利用该动态库
既然线程可以被用户创建,线程在Linux下又是轻量级历程,那么肯定也有其对应的属性,假如用户想要获取一些线程属性,而不是轻量级历程的属性,那么用户级线程库依旧需要对这些属性举行封装,所以实际上,线程库除了封装操作线程的接口外,还需要封装一些线程的属性,此时就需要一个结构体来维护这些属性。在glibc中,这个结构叫struct pthread,其中一些属性,比方线程id,线程栈大小,线程局部存储
   在前面文件部分也提到过语言库对系统底层的文件属性举行封装,即struct FILE
在创建线程时,操作系统会创建对应的物理内存空间用于动态库,并将该空间通过mmap接口映射到当前历程的假造地址空间,接着就会在这个动态库所在的空间开发对应的线程结构以及线程需要的空间,比方线程栈和线程局部存储,此时线程就拥有了本身独立的线程栈,如下图所示:
https://i-blog.csdnimg.cn/direct/392d6ec60afe48ad86d6d942e9344066.png
团体示意图如下:
https://i-blog.csdnimg.cn/direct/0c80ec9edd5345a2817f9705f68ae7e5.png
而所谓的线程id就是在动态库空间中的struct pthread结构的起始地址,即:
struct pthread* pd = ...;
pthread_t tid = (pthread_t)pd;
此时,在历程地址空间中,从逻辑上看,存在两种栈:一种是历程启动时创建的,用于支持main函数实验的栈,可以称之为主线程栈。另一种是在libpthread.so动态库加载后,由线程库为每个新创建的线程分配的栈,这些栈位于动态库管理的内存区域内,可以称之为线程栈
有了上面的理解,下面就可以进入用户级线程库源代码理解创建线程所经历的过程
线程结构体pthread

在glibc源代码中,下面是对应的线程结构:
/* Thread descriptor data structure.*/
struct pthread
{
    // ...
    /* This descriptor's link on the `stack_used' or `__stack_user' list.*/
    list_t list;

    // 线程id
    pid_t tid;

    // 当前进程pid
    pid_t pid;

    // ...

    // 判断用户是否传入自定义栈
    bool user_stack;

    // ...

    // 存储线程执行函数的返回值
    void *result;

    // 线程执行函数和参数
    void *(*start_routine) (void *);
    void *arg;

    // 线程栈指针
    void *stackblock;
    // 线程栈大小
    size_t stackblock_size;
   
    // ...
} __attribute ((aligned (TCB_ALIGNMENT)));
其中的result就是用于一些需要获取到线程实验函数返回值的位置,比方前面提到的pthread_join获取到函数返回值就是通过这个result变量,别的前面提到过clone函数,这个函数的原型如下:
int clone(int (*fn)(void *), void *stack, int flags,
            void * arg, .../* pid_t * parent_tid, void * tls,
                              pid_t * child_tid */ );
其中,第一个参数就是需要实验的函数,第二个参数表现创建的栈空间,第三个参数表现标志位,用于标志是创建子历程还是轻量级历程,第四个参数就是需要实验的函数的指针,背面的参数可以不用思量,但是其中的void* tls就是表现线程局部存储的空间
在上面的pthread的结构中,一些属性就是传递到clone接口中,比方线程栈指针stackblock
线程创建函数pthread_create

int __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
                      void *(*start_routine) (void *), void *arg)
{
    // ...

    // 线程属性,就是pthread_create函数的第二个参数,默认情况下传递NULL
    const struct pthread_attr *iattr = (struct pthread_attr *) attr;
    // 创建默认属性
    struct pthread_attr default_attr;

    // ...

    // 如果iattr为NULL,就使用默认属性
    if (iattr == NULL)
    {
      // ...

      // 使用默认属性
      iattr = &default_attr;
    }

    // 创建线程结构体
    struct pthread *pd = NULL;

    // 调用ALLOCATE_STACK申请struct pthread对象
    int err = ALLOCATE_STACK(iattr, &pd);

    // ...

    // 设置线程执行函数和对应的参数到线程结构体对象中
    pd->start_routine = start_routine;
    pd->arg = arg;

    // ...

    // 设置参数pthread* newthread为线程结构体对象的地址,即设置线程id为结构体对象的地址
    *newthread = (pthread_t) pd;

    // ...

    if (__glibc_unlikely (report_thread_creation (pd)))
    {
      // ...

      // 创建线程
      retval = create_thread(pd, iattr, STACK_VARIABLES_ARGS);
      
      // ...
    }
   
    // ...
}
// 版本确认信息,如果用的库是GLIBC_2_1,则使用__pthread_create_2_1
versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1);
在上面的函数中,首先该函数会接收四个参数,分别表现新线程的id值、新线程的属性、新线程的实验函数和实验函数的参数
在函数内部,首先会判定用户是否传递了自界说的线程属性,假如没有就利用默认的线程属性,所以对于pthread_create函数来说,第二个参数设置为NULL就可以包管线程利用默认属性,线程属性结构体如下:
struct pthread_attr
{
    // ...

    // 线程栈空间指针
    void *stackaddr;
    // 线程栈大小
    size_t stacksize;

    // ...
};
接着就是描述线程,创建线程结构体指针,调用相干的函数为结构体指针创建对象,对应的ALLOCATE_STACK函数如下:
// 创建一个宏,通过属性和struct pthread指针调用allocate_stack函数
# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)

static int allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
                ALLOCATE_STACK_PARMS)
{
    struct pthread *pd;
    size_t size;

    // ...

    // 设置线程栈大小,如果用户已经指定,则使用用户指定的大小,否则使用默认大小
    if (attr->stacksize != 0)
      size = attr->stacksize;
    else
    {
      // ...
      size = __default_pthread_attr.stacksize;
      // ...
    }

    if (__glibc_unlikely (attr->flags & ATTR_FLAG_STACKADDR))
    {
      // ...
    }
    else
    {
      // ...

      // 先从pthread缓存中申请空间
      reqsize = size;
      pd = get_cached_stack (&size, &mem);
      if (pd == NULL)
      {
            // ...

            // 缓存申请失败,就到堆空间中申请私有的匿名内存空间
            mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

            // ...
      
            /* Remember the stack-related values.*/
            // 使用线程对象记录创建的空间和大小
            pd->stackblock = mem;
            pd->stackblock_size = size;
            
            // ...
      }
    }

    // 二级指针,用于返回struct pthread的地址
    *pdp = pd;

    // ...

    return 0;
}
从这个函数中可以看出,线程栈虽然是动态申请的,但是其大小在申请的那一刻就已经固定好了,所以对于新线程来说,其栈大小是不会改变的,一旦用完就不会再扩容
回到__pthread_create_2_1,申请完线程栈后,就开始设置线程实验函数和对应的参数到线程结构体对象中,便于后续读取和调用,接着设置线程id为线程结构体对象的地址,这也就表明了为什么线程id是地址值
当全下属性设置完毕后,开始创建线程,调用create_thread接口,代码如下:
static int 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
   
    // ...

    if (__builtin_expect(THREAD_GETMEM(THREAD_SELF, report_events), 0))
    {
      // ...
      int res = do_clone(pd, attr, clone_flags, start_thread, STACK_VARIABLES_ARGS, 1);
      
      // ...
    }
   
    // ...
    return res;
}
在create_thread接口中,调用do_clone接口创建线程:
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,在glibc中对应的是一段汇编封装的调用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 thechild 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 bewrong. */
cfi_endproc;
syscall // 陷⼊内核(x86_32是int 80),要求内核创建轻量级进程
testq   %rax,%rax
jl      SYSCALL_ERROR_LABEL
jz      L(thread_start)
在上面的汇编中$SYS_ify(clone)就是调用系统调用clone
至此,创建线程的整个过程就已经根本结束了,所以本质pthread_create函数就是在做空间分配、属性添补和调用clone系统调用创建线程
利用指令查看创建线程时利用的系统调用

阅读源码之后可以知道pthread_create底层调用了clone接口,除了这种方式以外,还可以利用指令的方式查看一个程序中调用的系统接口,以下面的代码为例:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>

void *routine(void *arg)
{
    return NULL;
}

int main()
{
    pthread_t thid;

    // 创建线程
    int n = pthread_create(&thid, NULL, routine, (void *)"thread-1");

    return 0;
}
利用strace下令查看当前程序利用的系统调用可以看到:
// ...

clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7627b1200990, parent_tid=0x7627b1200990, exit_signal=0, stack=0x7627b0a00000, stack_size=0x7fff80, tls=0x7627b12006c0} => {parent_tid=}, 88) = 6010

// ...
从上面的结果可以看到调用了系统调用clone3(clone的升级版),在参数上可以传递一个结构体取代原来clone分别传递参数的情势,并且支持传递更多的属性,函数的作用一致
strace 是一个 Linux 下的下令行工具,用于跟踪历程实验过程中的系统调用和信号。 简单来说,它可以让你看到程序在运行时都做了哪些“变乱”,比方:


[*]打开了哪些文件
[*]读取了哪些数据
[*]发送了哪些网络请求
[*]利用了哪些内存
[*]调用了哪些系统函数
这对于调试程序、理解程序举动、以及排查性能问题非常有帮助。
比方,假如想知道程序thread在运行时都调用了哪些系统调用,你可以在终端中运行 strace ./thread。 strace 会输出大量的文本,每一行都代表一个系统调用,以及它的参数和返回值。
一些常用的 strace 选项包罗:


[*]-p <pid>: 跟踪指定的历程 ID。
[*]-o <file>: 将 strace 的输出写入到指定的文件中。
[*]-f: 跟踪由 fork 或 clone 创建的子历程。
[*]-c: 统计每个系统调用的耗时和调用次数。
在线程的上下文中,strace -f ./thread 可以用来观察线程的创建 (通常通过 clone 系统调用) 和线程相干的操作。
线程库封装

直接利用glibc的线程操作接口会略显麻烦,所以思量利用C++面向对象的方式封装本身的线程库,本质就是对一些操作举行封装
首先思量线程对象需要有哪些属性,本次设计重要思量下面的属性:

[*]线程的名称(string范例)
[*]线程的id(pthread_t范例)
[*]线程的实验函数(无参函数)
[*]线程的状态(枚举类范例)
[*]线程是否是分离的,默认不是分离(布尔范例)
接着思量本次设计的线程库需要包罗的操作:

[*]创建线程操作(利用pthread_create函数)
[*]等候线程操作(利用pthread_join函数)
[*]分离线程操作(利用pthread_detach函数)
[*]停止线程操作(利用pthread_cancel函数)
[*]获取线程名称
别的,思量利用一个枚举类来表现当火线程的运行状态:
// 线程状态
enum class ThreadStatus
{
    isNew,
    isRunning,
    isStopped,
    isDetached,
    isJoinable
};
根本实现如下:
#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <functional>

namespace ThreadModule
{
    // 线程执行的任务类型
    using func_t = std::function<void()>;
    // 计数器
    static int count = 0;

    // 线程状态
    enum class ThreadStatus
    {
      isNew,
      isRunning,
      isStopped,
      isDetached,
      isJoinable
    };

    class Thread
    {
    private:
      static void *routine(void *arg) // 线程执行函数
      {
            // 获取到this指针
            Thread *_t = static_cast<Thread *>(arg);
            // 更改线程状态为运行状态
            _t->_thStatus = ThreadStatus::isRunning;
            // 执行任务函数
            _t->_func();

            return NULL;
      }

    public:
      Thread(func_t func)
            : _func(func), _thStatus(ThreadStatus::isNew), _joinable(true)
      {
            _name = "Thread" + std::to_string(count++);
      }

      // 创建线程
      bool start()
      {
            if (_thStatus != ThreadStatus::isRunning)
            {
                int ret = pthread_create(&_tid, NULL, routine, (void *)this);

                if (!ret)
                  return false;

                return true;
            }

            return false;
      }

      // 终止线程
      bool cancel()
      {
            if (_thStatus == ThreadStatus::isRunning)
            {
                int ret = pthread_cancel(_tid);
                if (!ret)
                  return false;

                _thStatus = ThreadStatus::isStopped;

                return true;
            }

            return false;
      }

      // 等待线程
      bool join()
      {
            if (_thStatus != ThreadStatus::isDetached || _joinable)
            {
                int ret = pthread_join(_tid, NULL);
                if (ret)
                  return false;

                _thStatus = ThreadStatus::isStopped;

                return true;
            }
            return false;
      }

      // 分离线程
      bool detach()
      {
            if (_joinable)
            {
                pthread_detach(_tid);
                _joinable = false;
                return true;
            }

            return false;
      }

      // 获取线程名称
      std::string getName()
      {
            return _name;
      }

      ~Thread()
      {
      }

    private:
      std::string _name;
      pthread_t _tid;
      ThreadStatus _thStatus;
      bool _joinable;
      func_t _func;
    };
}
   上面的代码需要注意,routine函数假如放在类内部,需要思量到隐藏的this,可以思量放置在类外,也可以思量利用静态方法,但是为了能够调用任务函数,还是思量放在类内,并且传递参数时显式传递this
测试运行代码如下:
#include "thread.hpp"

int main()
{
    ThreadModule::Thread t([&]()
                           {
      while (true)
      {
            std::cout << "我是新线程:" << t.getName() << ",我的线程id为:" << pthread_self() << std::endl;
            sleep(1);
      } });

    t.start();

    t.join();
    return 0;
}
输出结果如下:
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
我是新线程:Thread0,我的线程id为:128731471414976
...
假如需要创建多线程,此时就会非常方便:
#define NUM 10

using thread_ptr = std::shared_ptr<ThreadModule::Thread>;

int main()
{
    // 使用哈希表建立线程名称和线程对象的映射
    std::unordered_map<std::string, thread_ptr> threads;

    for (int i = 0; i < NUM; i++)
    {
      thread_ptr t = std::make_shared<ThreadModule::Thread>([]()
                                                            {
      while (true)
      {
            std::cout << "我是新线程" << ",我的线程id为:" << pthread_self() << std::endl;
            sleep(1);
      } });

      threads = t;
    }

    for (auto pair : threads)
      pair.second->start();

    for (auto pair : threads)
      pair.second->join();

    return 0;
}

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Linux线程库与线程库封装