目录

1.知识回顾

2.线程求和代码

3.C++11的thread线程库

C++11的thread线程库和Linux的pthread原生线程库有什么关系?

结论

反思: 为什么thread线程库底层用了pthread线程库?

5.了解clone系统调用

验证代码

6.pthread线程库需要维护线程

一般情况下,用户级线程的属性存储在线程库内部

7.Linux的线程=用户级线程+内核的LWP

8.创建一批线程的方法

创建一批线程的代码

证明: 每个线程都有自己独立的栈结构

python脚本处理RSP数据

从相邻RSP寄存器值作差结果来推测每个线程栈的近似大小

用系统调用获取每个线程栈的大小

结论


1.知识回顾

参见OS68.【Linux】pthread线程库的使用文章

2.线程求和代码

新建如下文件:

test_thread/
├── makefile
└── test_thread.cpp

makefile写入:

test_thread.out:test_thread.cpp
	g++ -o $@ $^ -g -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f test_thread.out

主线程创建一个新线程,新线程负责运算,主线程负责取得运算的结果

这里用两个类: Request类和Response类

Request类实例化后的对象存储相加的范围[_start,_end]和线程名称

class Request
{
public:
    Request(int start,int end,const std::string& thread_name)
    :_start(start)
    ,_end(end)
    ,_thread_name(thread_name)
    {}

    int _start;
    int _end;
    std::string _thread_name;
};

Response类实例化后的对象存储计算结果

class Response
{
public:
    Response(int result=0)
    :_result(result)
    {}
    int _result;
};

线程函数:

void* sum_routine(void* arg)
{
    Request* request=static_cast<Request*>(arg);
    Response* response=new Response;
    int cnt=0;
    for (int i=request->_start;i<=request->_end;i++)
    {
        cnt+=i;
        std::cout<<request->_thread_name<<" : "<<"i="<<i<<std::endl;
    }
        
    response->_result=cnt;
    delete request;
    return response;
}

注意: static_cast用于类型安全的转换

main函数:

int main() 
{   
    pthread_t tid;
    Request* request=new Request(1,100,std::string("new thread"));
    pthread_create(&tid, nullptr,sum_routine, request);
    void* tmp;
    pthread_join(tid,&tmp);
    Response* retval=static_cast<Response*>(tmp);
    std::cout<<"main thread : "<<retval->_result<<std::endl;
    delete retval;
    return 0;
}

上方代码中,主线程new的对象在新线程中也可以用,新线程new的对象在主线程中也可以用,说明堆空间是主线程和新线程共享的

注: static_cast是C++的

运行结果:

结论: 线程函数参数类型是void*,但是也可以传入对象指针

3.C++11的thread线程库

Linux的原生线程库是pthread,C++11有thread线程库

test_thread.cpp写入:

#include <iostream>
#include <thread>
#include <unistd.h>
void thread_func()
{
    int cnt=3;
    while(cnt--)
    {
        std::cout<<"新线程正在运行..."<<std::endl;
        sleep(1);
    }
}
int main()
{
    std::thread new_t(thread_func);
    new_t.join();//阻塞等待
    std::cout<<"新线程已经退出"<<std::endl;
    return 0;
}

运行结果:

C++11的thread线程库和Linux的pthread原生线程库有什么关系?

运行以下命令:

nm -D /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep pthread

注: nm 是一个符号表查看工具,可用于分析目标文件、可执行文件、共享库里的符号, -D是只显示参与动态链接的符号

运行结果:

结论

结论: Linux系统,C++11的thread线程库底层用了Linux的pthread原生线程库

反思: 为什么thread线程库底层用了pthread线程库?

Linux系统中已经有成熟的pthread原生线程库,那么C++的thread线程库不需要自己造轮子

C++的线程库具有跨平台性, 推荐用C++上的线程库,这样有可移植性

5.了解clone系统调用

之前在OS68.【Linux】pthread线程库的使用文章说过"Linux内核中没有很明确的线程的概念,只有轻量级进程的概念那么Linux就没有直接提供线程的系统调用,只会给程序员提供轻量级进程的系统调用",例如clone系统调用
这里只看其中一些参数:

参数fn: 函数指针,新的执行流执行的就是fn指向的函数
参数stack: 创建新线程自定义的栈(线程的栈是线程自己私有的)

可以肯定的是: pthread的底层封装了clone系统调用

验证代码

这里不考虑pthread_create和pthread_join执行失败的情况

#include <pthread.h>
#include <string.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
void *start_routine(void*)
{
    return nullptr;
}

int main() 
{   
    pthread_t tid;
    pthread_create(&tid, nullptr,start_routine, nullptr);
    pthread_join(tid,nullptr);  
    return 0;
}

使用strace跟踪代码执行时调用系统调用和信号:
用了clone3系统调用(是clone系统调用的加强版)

clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|
CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|
CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, 
child_tid=0x7fd4bd019910, parent_tid=0x7fd4bd019910, 
exit_signal=0, stack=0x7fd4bc819000, stack_size=0x7fff00, 
tls=0x7fd4bd019640} => {parent_tid=[181007]}, 88) = 181007

6.pthread线程库需要维护线程

程序员使用pthread线程库的各个函数的前提是: pthread线程库需要加载到物理内存中! 之后由操作系统映射到进程地址空间的共享区

Linux操作系统没有线程的概念, 线程id、栈空间(主线程和新线程都有自己私有的栈空间)、线程的回调函数......这些都属于线程的属性Linux操作系统不关心,这些用户级线程的属性由pthread线程库维护,注: pthread线程库不维护线程的执行流

显然pthread线程库需要管理这些线程: 先描述再组织
        描述线程可以用线程控制块tcb
        在glibc-2.42的/nptl/descr.h定义了线程控制块tcb: struct pthread
        限于篇幅,这里只摘取一部分:

struct pthread
{
  union
  {
#if !TLS_DTV_AT_TP
    /* This overlaps the TCB as used for TLS without threads (see tls.h).  */
    tcbhead_t header;
#else
    struct
    {
      int multiple_threads;
      int gscope_flag;
    } header;
#endif
    void *__padding[24];
  };
  //......
};

其实x86_64下,pthread_self()的返回值tid就是线程控制块struct pthread的地址,因为地址是唯一的,所以满足Tid是唯一的这个要求证明过程参见OS71.【Linux】证明: x86_64,glibc的pthread库中,线程的tid就是线程控制块tcb的起始地址文章

注: 其它架构下,pthread_self()的返回值tid不一定是线程控制块struct pthread的地址

一般情况下,用户级线程的属性存储在线程库内部

如下图:

举例线程的私有部分 1.线程的上下文 2.线程的栈

除了主线程(线程库自己是不维护主线程的栈的),所有其他线程的独立栈,都在共享区中,具体来讲是在pthread库中,即tid指向的用户tcb中

7.Linux的线程=用户级线程+内核的LWP

Linux的线程==用户级线程(pthread库)+内核的LWP(轻量级进程)

而且用户级线程:内核的LWP==1:1

8.创建一批线程的方法

之前在OS68.【Linux】pthread线程库的使用文章演示过创建一个线程,这次创建一批线程的方法

主线程需要创建一批线程,主线程等待每个新线程,主线程是最后退出的

创建一批线程的代码

主线程可以将每个新线程的tid暂存起来,方便主线程等待每个新线程

#include <pthread.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#define NUM 10
void* thread_routine(void*)
{
    sleep(1);
    return nullptr;
}
int main()
{
    std::vector<pthread_t> tid_arr;
    for (int i=1;i<=NUM;i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr,thread_routine, nullptr);
        tid_arr.push_back(tid);
    }
    for (auto tid:tid_arr)
    {
        if (!pthread_join(tid,nullptr))
            std::cout<<"主线程成功等待tid为"<<tid<<"的新线程"<<std::endl;
    }
    return 0;
}

注意: 不能写成

运行结果:

证明: 每个线程都有自己独立的栈结构

要证明每个线程有独立的栈,最直接的办法就是打印它们的RSP寄存器值进行对比,RSP寄存器指向当前栈的栈顶

修改以上代码,让每个线程执行thread_routine时,都打印当前RSP寄存器的值

要获取RSP寄存器的值需要使用内联汇编

#include <pthread.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#include <string>
#define NUM 10

void* thread_routine(void* args)
{
    unsigned long long rsp_val;
    __asm__ volatile ("movq %%rsp, %0" : "=r" (rsp_val));
    printf("新线程RSP寄存器的值:0x%llx\n",rsp_val);
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tid_arr;
    std::vector<std::string*> thread_name_arr;
    for (int i=1;i<=NUM;i++)
    {
        pthread_t tid;
        std::string* thread_name=new std::string("thread_"+std::to_string(i));
        thread_name_arr.push_back(thread_name);
        pthread_create(&tid, nullptr,thread_routine, thread_name);
        tid_arr.push_back(tid);
    }
    for (auto tid:tid_arr)
    {
        if(pthread_join(tid,nullptr))
            std::cout<<"主线程等待失败"<<std::endl;
    }
    for (auto thread_name:thread_name_arr)
        delete thread_name;
    return 0;
}

运行结果:每个新线程打印的RSP寄存器的值都不一样

python脚本处理RSP数据

addresses = 
[
    "0x7fc470db2e30",
    "0x7fc4705b1e30",
    "0x7fc46fdb0e30",
    "0x7fc46f5afe30",
    "0x7fc46edaee30",
    "0x7fc46d5abe30",
    "0x7fc467ffee30",
    "0x7fc46cdaae30",
    "0x7fc46e5ade30",
    "0x7fc46ddace30"
]

sorted_addresses = sorted(addresses, key=lambda x: int(x, 16))
for addr in sorted_addresses:
    print(addr)

int_addresses = [int(addr, 16) for addr in sorted_addresses]

for i in range(1,len(int_addresses)):
    print(hex(int_addresses[i]-int_addresses[i-1]))

RSP排序后:

0x7fc46cdaae30
0x7fc46d5abe30
0x7fc46ddace30
0x7fc46e5ade30
0x7fc46edaee30
0x7fc46f5afe30
0x7fc46fdb0e30
0x7fc4705b1e30
0x7fc470db2e30

从相邻RSP寄存器值作差结果来推测每个线程栈的近似大小

python脚本运行结果:

0x4dac000
0x801000
0x801000
0x801000
0x801000
0x801000
0x801000
0x801000
0x801000

除了第一个0x4dac000,其它都是0x801000,如果各个线程栈在内存中是连续存放的话,那么0x801000(8MB+4KB,按1B=1024KB,1024KB=1MB算)极有可能是每个线程栈的大小

用系统调用获取每个线程栈的大小

线程栈的大小属于线程的属性,需要通过pthread_attr_getstack获取

#include <pthread.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#include <string>
#define NUM 10

void* thread_routine(void* args)
{
    pthread_attr_t attr;
    void *stackaddr;
    size_t stacksize;
    pthread_getattr_np(pthread_self(), &attr);
    pthread_attr_getstack(&attr, &stackaddr, &stacksize);
    printf("当前%s线程的栈大小: 0x%lx 字节\n", (*((std::string*)args)).c_str(),stacksize);
    pthread_attr_destroy(&attr);
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tid_arr;
    std::vector<std::string*> thread_name_arr;
    for (int i=1;i<=NUM;i++)
    {
        pthread_t tid;
        std::string* thread_name=new std::string("thread_"+std::to_string(i));
        thread_name_arr.push_back(thread_name);
        pthread_create(&tid, nullptr,thread_routine, thread_name);
        tid_arr.push_back(tid);
    }
    for (auto tid:tid_arr)
    {
        if(pthread_join(tid,nullptr))
            std::cout<<"主线程等待失败"<<std::endl;
    }
    for (auto thread_name:thread_name_arr)
        delete thread_name;
    return 0;
}

运行结果:

发现估计的0x801000(8MB+4KB)和系统给出的0x800000(8MB)非常接近,仅差一个4KB页的大小,这个差的一个页是glibc库设置的保护页,用于保护相邻线程栈之间的安全,防止栈溢出导致越界访问

在glibc-2.42的/nptl/allocatestack.c中定义了分配线程栈的函数allocate_thread_stack,其中需要提供保护页的大小guardsize:

static void *
allocate_thread_stack (size_t size, size_t guardsize)
{
  /* MADV_ADVISE_GUARD does not require an additional PROT_NONE mapping.  */
  int prot = stack_prot ();

  if (atomic_load_relaxed (&allocate_stack_mode) == ALLOCATE_GUARD_PROT_NONE)
    /* If a guard page is required, avoid committing memory by first allocate
       with PROT_NONE and then reserve with required permission excluding the
       guard page.  */
    prot = guardsize == 0 ? prot : PROT_NONE;

  return __mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1,
		 0);
}

结论

每个线程都有自己独立的栈结构,且各个线程栈在内存中近似连续存放的,"近似"的原因是相邻线程栈之间有保护页,防止栈溢出导致越界访问

→其实线程和线程之间,几乎没有秘密,线程的栈上的数据,也是可以被其他线程看到并访问的(读者可以自己验证),因为进程的地址空间是共享给所有线程的

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐