【Linux笔记】线程概念与控制

一、线程概念

1.1 理解线程

通俗理解:线程是进程中的一个执行分支 (执行流),CPU 实际在跑的那条指令序列

本质理解 :Linux 内核中进程和线程使用同一个数据结构 task_struct 来表示,所以线程本质上就是共享部分资源的"轻量级进程"

打个比方:

  • 进程像一家工厂,拥有厂房、设备、原料 (内存、文件等资源)

  • 线程像工厂里的工人,每个工人各自干活,共享工厂的一切资源

  • 工厂至少要有一个工人,所以一个进程至少有一个线程 (主线程)

  • 多招几个工人(线程),他们共用厂房原料,沟通成本极低,但一个工人操作失误弄坏原料(写坏共享内存),全厂遭殃。

1.2 进程与线程共享的资源

从内核和资源的角度:在Linux中,进程是资源分配的单位(负责管理内存和文件),线程是调度的单位(负责占用CPU)。

"线程是轻量级进程"------站在Linux内核视角里根本没有进程和线程两种东西,只有一群 task_struct,谁和谁共享同一套资源对象,谁和谁就是"同一个进程里的线程"。

复制代码
在Linux下“资源”具体指以下三大类
​
1. 内存资源(地址空间)
​
- 进程独有:每个进程都有一张独立的页表(Page Table),映射到不同的物理内存。
​
- 线程共享:同一个进程里的线程,共用同一张页表。(因此,线程A malloc 出一块内存,线程B 直接能用指针访问)
​
​
2.I/O 资源(文件描述符表)
​
- 进程独有:每个进程维护一张打开文件描述符表,记录着当前打开了哪些文件、socket(网络套接字)、管道。
​
- 线程共享:线程间共享这张表。( 所以线程A打开 fd= 3,线程B可以直接往 fd=3 写数据)
​
​
3. 身份与状态资源(上下文)
​
- 进程ID(PID/PPID)、用户ID(UID/GID)、工作组(Session ID)。
​
- 信号处置表(Signal Handlers):比如你对 SIGINT 设置了自定义回调函数。
​
  线程共享这张表,但对于信号的状态表(Pending)线程是独立的。
​
- 当前工作目录(cwd)和环境变量(environ)。

线程共享进程资源的核心:资源被做成独立的内核对象,线程通过"指针"指向它们,而不是把资源复制一份。

多个线程的 task_struct 里,指向资源的指针,通过引用计数的方式指向同一个对象。

  • 共享的(资源) :mm、files、sighand ...指向同一对象

  • 私有的(执行上下文) :寄存器、程序计数器、用户栈、内核栈、TLS、errno、TID、调度状态......每个线程一份

图示理解线程资源的指向:

复制代码
线程A的 task_struct 和 线程B的 task_struct
 |
 ├─ mm ────────┐           
 │             └─────→  同一个 mm_struct(引用计数 = 2) (同一份页表)
 │                      
 ├─ files ─────┐          
 │             └─────→  同一个 files_struct(引用计数 = 2) (同一张文件描述符表)
 │                      
 └─ sighand ───┐          
               └─────→  同一个 sighand_struct(引用计数 = 2) (同一张信号处理表)           

1.3 线程中私有的资源

1.3.1 CPU 寄存器 ---硬件上下文

当线程在运行时:这些硬件上下文数据放在 CPU 中;

当线程被切换时:内核把它们保存到该线程的内核栈(用PCB进行维护) 中,切回来时进行恢复。

复制代码
- 程序计数器 PC:我执行到哪条指令了
​
- 栈指针 SP:指向我的用户栈
​
- 通用寄存器:计算中间的结果
​
- 标志寄存器、浮点/向量寄存器

1.3.2 虚拟内存 ---栈

复制代码
- 用户栈:局部变量、函数参数、返回地址全在这里,局部变量。  
  
  ·对于主线程的用户栈,位于虚拟地址空间的栈区,能够动态增长,在可执行程序加载时进行分配。
  ·对于子线程的用户栈,位于虚拟地址空间的共享区,大小固定,由mmap进行申请
  ·用户栈通过task_struct中的mm_struct结构体和vm_area_struct进行管理。
  
- 内核栈:线程通过系统调用进入内核时,走自己独立的内核栈。 
  
  ·在内核空间中都有的固定大小的栈。
  ·内核栈通过task_struct中的stack指针进行管理
​
- TLS(线程局部存储): 通过__thread/ thread_local 修饰的变量 和 errno 变量。
​
   ·每个线程的 fs 寄存器指向自己那一块 TLS,所以同一行代码里写 errno,各线程读写的是各自的副本。

1.3.3 task_struct ---部分字段

复制代码
- LWP :线程身份ID
​
- 调度状态(运行/睡眠)、优先级、nice 值、CPU 亲和性、调度策略
​
- 信号屏蔽字 blocked:线程选择挡住哪些信号
​
- 线程私有的 pending 信号

1.3.4 总结

复制代码
线程拥有独立的:
​
1. 寄存器
​
2. 程序计数器 PC
​
3. 线程栈 Stack
​
4. 线程局部存储 TLS
​
5. 调度状态
复制代码
线程共享进程的:
​
1. 代码段
​
2. 全局变量
​
3. 静态变量
​
4. 堆 Heap
​
5. 文件描述符
​
6. Socket
​
7. 进程资源

二、Linux 线程控制

2.2 pthread_create - 创建线程

2.2.1 基础使用要求

复制代码
1.头文件包含: #include <pthread.h>
​
2.函数原型:
​
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
​
3.函数功能:创建出一个新的线程,产生一个新的执行流。

注:

A. 编译链接参数:pthread 不是 Linux 默认链接的库,编译时需要显式添加 -lpthread 或 -pthread 参数,否则会报 undefined reference to pthread_create 的链接错误。

B. 错误处理规则:与常规系统调用不同,pthread_create 失败时不会设置全局 errno,而是直接返回对应的错误码,需要用strerror(ret) 将错误码转换为可读的错误信息,不能使用 perror()。

2.2.1 参数说明

复制代码
1.参数一:pthread_t *thread

- 指向 pthread_t 类型变量的指针,用于接收新创建线程的唯一标识符(线程 ID)。

- 后续对该线程的所有操作(如等待结束、分离、取消等)都需要通过这个 ID 进行。


2.参数二:const pthread_attr_t *attr (一般可忽略)

- 栈大小:默认在 x86_64 架构下为 2MB,可通过 pthread_attr_setstacksize() 修改

- 分离状态:默认可连接(joinable),需要调用 pthread_join() 回收资源;也可设置为分离(detached),线程结束后自动释放资源

- 调度策略:如 SCHED_FIFO(实时先进先出)、SCHED_RR(实时轮转)等,默认为普通分时调度 SCHED_OTHER


3.参数三:start_routine(线程入口函数)

函数头必须按照该格式设计:void* 函数名 (void * arg) ,线程执行的入口部分。 


4.参数四: void *arg (传参给线程入口函数)

- 传递给 start_routine 的唯一参数,类型为 void*。

- 如果需要传递多个参数,必须将所有参数封装到结构体中,传递结构体的地址。

2.2.2 函数返回值

复制代码
成功:返回 0,此时 thread 指向的变量会被设置为新线程的 ID。

失败:返回非零的错误码,thread 指向的内容未定义,不会创建新线程。

注:需要通过strerror进行输出错误信息。

函数的错误码:

错误码 含义
EAGAIN 系统资源不足,无法创建新线程(如超过线程数上限、内存不足等)
EINVAL attr 属性配置无效,或 start_routine 为 NULL
EPERM 没有权限设置 attr 中指定的调度策略或参数(如实时调度需要 root 权限)

2.2.3 代码演示

复制代码
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt--)
    {
        printf("%s starts running...\n",name);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, thread_run, (void *)"thread -1");

    if (ret != 0)
    {
        strerror(ret);
    }

    int cnt = 5;
    while (cnt--)
    {
        printf("Main thread running...\n");
    }
    
    return 0;
}

运行结果如下所示:

复制代码
Main thread running...
Main thread running...
Main thread running...
Main thread running...
Main thread running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...

2.3 pthread_join -线程等待

2.3.1 基础使用条件

复制代码
1.头文件包含 #include <pthread.h>

2.函数原型:int pthread_join(pthread_t thread, void **retval);

3.函数功能:同步和回收可连接(joinable)的线程资源

2.3.2 参数说明

复制代码
1.参数一:pthread_t thread

- 目标线程的 ID,即 pthread_create 第一个输出参数获得的值。

- 必须是当前进程中有效的、尚未被 join 且未分离的线程 ID。


2.参数二:void **retval

- 指向 void* 指针的二级指针,用于接收目标线程通过 return 或 pthread_exit() 传递的返回值。

- 如果不需要获取返回值,传 NULL 即可。

- 如果目标线程是被 pthread_cancel() 取消的,*retval 会被设置为特殊常量 PTHREAD_CANCELED。

注意:绝对不要将局部变量的地址作为返回值传出!因为线程结束后其栈帧已被销毁,该地址变为悬空指针,访问它将导致未定义行为,应使用堆内存、全局变量或静态变量来传递复杂数据。

复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

void* worker(void* arg) 
{
    int input = *(int*)arg;
    int* result = malloc(sizeof(int));  // 堆上分配,安全
    *result = input * input;
    return result;
}

int main() 
{
    pthread_t tid;
    int val = 7;

    pthread_create(&tid, NULL, worker, &val);

    void* ret;
    int rc = pthread_join(tid, &ret);
    if (rc != 0) 
    {
        printf("join failed: %s\n", strerror(rc));
        return 1;
    }

    printf("7^2 = %d\n", *(int*)ret);
    free(ret);  // 记得释放堆内存
    return 0;
}

2.4 pthread_detach -线程分离

2.4.1 基础使用条件

复制代码
1.头文件包含: #include <pthread.h>

2.函数原型:int pthread_detach(pthread_t thread);

3.函数功能:将指定线程标记为"分离状态(detached)",分离线程终止时,系统自动回收其资源(栈、线程描述符等),无需也不允许其他线程调用 pthread_join。

2.4.2 detach后的线程状态

复制代码
操作系统detach线程的执行过程

1. 立即释放线程栈内存(通常几 MB)

2. 释放线程控制块(TCB)

3. 丢弃返回值(即使线程函数 return 了非 NULL 值,也无处接收)

4. 不保留任何可供 join 的状态

2.4.3 detach的两种时机

A. 运行时 detach
复制代码
pthread_t tid;
pthread_create(&tid, NULL, worker, arg);
pthread_detach(tid);  // 创建后分离
B. 创建前 detach
复制代码
pthread_attr_t attr;//线程属性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_t tid;
pthread_create(&tid, &attr, worker, arg);  // 出生即分离

pthread_attr_destroy(&attr);  //  属性对象用完必须销毁,否则泄漏
C. detach 时间线分析
复制代码
方式A的时间线:

  create ──────────────── detach
         	↑ 危险窗口 ↑
         线程可能在此区间内已经终止!


如果线程在 pthread_create 和 pthread_detach 之间就执行完毕并退出了,而系统又恰好立即回收了其资源,那pthread_detach(tid) 操作的就是一个无效的线程标识符,属于未定义行为。
复制代码
方式B的时间线:

  create(DETACHED) ───→ 线程从第一刻起就是分离状态 不存在竞态窗口

2.4.4 detach 的约束

A. 绝不能对分离线程调用 join
复制代码
pthread_detach(tid);
pthread_join(tid, NULL);  //  明确规定这是未定义行为
B. 不能获取返回值
复制代码
void* worker(void* arg) 
{
    return some_result;  //  如果线程是 detached,这个返回值被丢弃
}
C. 参数生命周期必须自给自足
复制代码
// 经典错误

void * worker(void * args)
{
	//...执行任务
	return NULL;
}

void spawn_task(int id) 
{
    int local_id = id;
    pthread_create(&tid, &attr, worker, &local_id);  // 传栈变量地址
    //  spawn_task 返回后 local_id 销毁
    //  分离线程可能还在运行,此时读到悬空指针
}

在 pthread_join模式下,主线程通过等待隐式延长了参数的生命周期。

detach 模式下没有这个安全机制,spawn_task函数结束后,局部变量销毁,导致线程中的指针悬空。

2.5 线程终止

线程终止:是线程生命周期的最后一个环节,也是资源回收的前置条件。

POSIX 提供了三种终止方式,它们的语义、副作用和适用场景各不相同。

2.5.1 线程执行 return

基本语义:线程入口函数void*(*)(void*)执行 return 语句,是最自然的终止方式。

复制代码
void* worker(void* arg) 
{
    int id = *(int*)arg;
    printf("Thread %d doing work...\n", id);

    // 正常执行完毕,return 一个值
    return (void*)(long)id;  // 将整数转为 void* 返回
}

关键细节

细节一:return 的值就是 pthread_join 能取到的值

复制代码
void* worker(void* arg) 
{
    int id = *(int*)arg;
    printf("Thread %d doing work...\n", id);

    // 正常执行完毕,return 一个值
    return (void*)(long)id;  // 将整数转为 void* 返回
}

void* retval;
pthread_join(tid, &retval); //二级指针进行获取
printf("Thread returned: %ld\n", (long)retval);

细节 2:return NULL 表示"无返回值",不是"失败"

复制代码
void* worker(void* arg) 
{
    // 做一些事情...
    return NULL;  // 正常终止,没有需要传递的结果
}

NULL 和 PTHREAD_CANCELED 是不同的,join 可以通过这个区分判断线程是"自然死亡"还是"被杀死"。

  • NULL 表示线程自行决定正常结束;

  • PTHREAD_CANCELED 是系统标记,表示线程被外部取消。

细节 3:绝不能返回局部变量的地址

复制代码
//错误做法:
void* worker(void* arg) 
{
    char buf[128];
    snprintf(buf, sizeof(buf), "result_%d", *(int*)arg);
    return buf;  //  致命错误!buf 在 return 后栈帧销毁
}


//正确做法:

// 方案A:堆分配
char* buf = malloc(128);
snprintf(buf, 128, "result_%d", *(int*)arg);
return buf;  //  调用方负责 free

// 方案B:静态/全局变量(不推荐,非线程安全)
static char buf[128];  // 多线程同时 return 会覆盖

// 方案C:返回整数值(最安全)
return (void*)(long)some_int;

细节 4:return 只终止当前线程,不影响其他线程

复制代码
void* worker(void* arg) 
{
    return NULL;  // 仅本线程结束
    // main 线程和其他子线程继续运行
}

细节 5:C++ 中 return 会触发栈上对象的析构

复制代码
void* worker(void* arg) 
{
    std::string s = "hello";
    std::lock_guard<std::mutex> lock(mtx);
    // ... 做一些事情 ...
    return NULL;
    //  s 的析构函数被调用
    //  lock 的析构函数被调用(mutex 被解锁)
    //  所有 RAII 对象正常清理
}

2.5.2 线程执行 pthread_exit

函数原型:

复制代码
头文件包含:#include <pthread.h>

函数原型:void pthread_exit(void *retval);  

核心用法:显式终止调用它的线程。

retval 的含义与 return 完全一致------都是传递给 pthread_join 的返回值。

复制代码
void* worker(void* arg) 
{
    int id = *(int*)arg;

    if (some_error_condition) {
        fprintf(stderr, "Thread %d encountered error, exiting early.\n", id);
        pthread_exit((void*)-1);  // 提前终止,不执行后续代码
    }

    // 正常逻辑...
    return (void*)(long)id;
}

细节分析:

POSIX 标准的规定 :pthread_exit 应当触发 C++ 栈展开(stack unwinding),即调用析构函数。但实际行为依赖编译器和平台:

  • Linux glibc + g++ :通常正确触发析构(通过内部 __cleanup 机制)

  • 某些嵌入式/旧编译器 :可能直接 longjmp,跳过析构。

  • C++ 标准本身 :不承认 pthread_exit 的存在,行为是实现定义的。

复制代码
void* worker(void* arg) 
{
    std::string s = "important data";
    std::lock_guard<std::mutex> lock(mtx);

    helper_function();  // 内部调用了 pthread_exit

    //  问题:s 和 lock 的析构函数是否被调用?
}

void helper_function()
{
    // ... 一些逻辑 ...
    pthread_exit(NULL);  // 从辅助函数中终止线程
}

2.6 pthread库

2.6.1 线程库的本质定位

线程库的本质:Linux 内核本身没有"线程"这个概念,线程是用户态库(pthread)对内核"轻量级进程"机制的封装。

内核提供"底层结构"(轻量级进程task_struct + futex),pthread 库负责"封装"成标准化的线程接口。

复制代码
			
┌─────────────────────────────────┐
│           pthread 	       	  │ 用户空间:
│  ┌──────────┐  ┌──────────┐     │
│  │ struct   │  │ struct   │     │  ← 线程控制块(TCB):管理线程属性、栈地址等
│  │ pthread  │  │ pthread  │     │     
│  │ (TCB)    │  │ (TCB)    │     │
│  └────┬─────┘  └────┬─────┘     │
│       │ 线程栈       │ 线程栈     │
│       ▼             ▼           │
├───────┼─────────────┼───────────┤
│       │ clone()     │ clone()   │  ← 系统调用边界
├───────┼─────────────┼───────────┤
│       ▼             ▼           │  内核空间:   
│  ┌───────────┐ ┌────────────┐   │
│  │task_struct│ │task_struct │   │  ← 内核调度实体(LWP):cpu的调度单位
│  │  (LWP)    │ │   (LWP)    │   │     
│  └───────────┘ └────────────┘   │
│                                 │
└─────────────────────────────────┘

2.6.2 线程地址空间布局

线程地址空间布局图:

复制代码
┌─────────────────────┐  Highest addresses
│      				  │
|	Kernel space 	  |
|					  |
┌─────────────────────┐
|      Stack		  |
|    				  |
│  Main thread stack  │  ← 主线程栈
├─────────────────────┤
│   mmap shared area  │  ← TCBs + thread stacks for other threads are here
│  ┌───────────────┐  │    其他线程的 TCB + 线程栈都在这里
│  │ TCB (Thread 2)│  │
│  │ Stack of Thd 2│  │
│  ├───────────────┤  │
│  │ TCB (Thread 3)│  │
│  │ Stack of Thd 3│  │
│  └───────────────┘  │
|       ...			  │
├─────────────────────┤
│   Heap (shared)     │  ←堆区
├─────────────────────┤
│   Data segment 	  │  ←数据段
├─────────────────────┤
|   Code segment      |  ←代码段
└─────────────────────┘  Low addresses 低地址

2.6.3 内核空间管理线程

Linux 内核对进程和线程都用 task_struct 表示,每个线程在内核中都有一个独立的 task_struct,这就是一个轻量级进(LWP)。

task_struct 中的关键字段:

复制代码
struct task_struct
{
    //线程的ID
    pid_t pid;              // 内核线程 ID(每个 LWP 唯一)
    pid_t tgid;             // 线程组 ID(同进程所有线程共享)
    
    // 管理线程
    struct task_struct *group_leader;  // 指向主线程
    struct list_head thread_group;     // 线程组链表
    struct mm_struct *mm;   // 地址空间描述符(同组线程共享)
    
    // 线程的上下文
    // ... 寄存器、调度优先级、状态等  
};
  • pid :内核视角的"进程 ID",对 LWP 来说就是线程 ID(通过 gettid() 获取)

  • tgid :线程组 ID,等于主线程的 pid(getpid() 返回的就是 tgid)

  • mm :指向虚拟内存描述符,同组线程共享同一个 mm,因此共享地址空间

2.6.4 用户空间管理线程

复制代码
┌──────────────────────────────┐  高地址
│    struct pthread(TCB)      │  ← pthread_t 就是这个结构体的首地址
├──────────────────────────────┤
│    线程局部存储(TLS)          │  ← __thread 变量的副本
├──────────────────────────────┤
│                              │
│    线程栈(默认 8MB)          │  ← 向下增长
│                              │
└──────────────────────────────┘  低地址


								High addresses 高地址	
┌──────────────────────────────┐  
│    struct pthread (TCB)      │  ← 线程描述表:pthread_t 就是这个结构体的首地址
├──────────────────────────────┤
│   		TLS				   │  ← 线程局部存储: 存储__thread 变量的副本
├──────────────────────────────┤
│                              │
│      Thread stack			   │  ← 线程栈: 向下增长
│      (default 8MB)           │
└──────────────────────────────┘ 
							     Low addresses 低地址
A.struct pthread -TCB
复制代码
struct pthread 
{
    /* ---- 标识信息 ---- */
    struct pthread *self;         // ★指向自身(用于 %fs:0 快速定位)
    pid_t tid;                    // ★内核分配的 LWP ID(gettid 的值)
    pid_t pid;                    // ★进程 ID(tgid)

    /* ---- 线程栈信息 ---- */
    void *stackblock;             // ★栈内存的起始地址
    size_t stackblock_size;       // 栈大小(默认 8MB)
    void *guardaddr;              // 栈底保护页地址(防止栈溢出)
    size_t guardsize;             // 保护页大小(默认 4KB)

    /* ---- 生命周期状态 ---- */
    int cancel_state;             // 取消状态(是否允许被取消)
    int cancel_type;              // 取消类型(延迟/异步)
    int canceled;                 // 是否已被取消
    int detached;                 // 是否已 detach
    int joinid;                   // 谁在 join 我

    /* ---- 同步相关 ---- */
    struct pthread_mutex *mutex_list;  // 持有的互斥锁链表
    futex_t *robust_list;         // 健壮互斥锁列表

    /* ---- 清理与退出 ---- */
    struct _pthread_cleanup_buffer *cleanup;  // 清理处理器链表
    void *(*start_routine)(void *);           // 线程入口函数
    void *arg;                    // 入口函数参数
    void *result;                 // ★线程返回值(pthread_exit 的参数)

    /* ---- 信号相关 ---- */
    sigset_t sigmask;             // 线程信号掩码
    sigset_t sigpending;          // 挂起信号集

    /* ---- TLS 相关 ---- */
    void *specific[LEVEL1_SIZE];  // pthread_key_create 的动态 TLS 数组
    dtv_t *dtv;                   // 动态线程向量(Dynamic Thread Vector)

    /* ---- errno ---- */
    int err_no;                   // 本线程的 errno(通过 TLS 访问)

    /* ---- 调度信息 ---- */
    int schedpolicy;              // 调度策略
    int schedparam;               // 调度参数
    int flags;                    // 线程标志位

    /* ... 更多字段 ... */
};

关键字段:

字段 作用
self 放在 TCB 的第一个位置,%fs:0 直接读到 TCB 自身地址。
tid 创建时由内核通过 CLONE_CHILD_SETTID 写入,是用户态访问内核 LWP 的唯一凭证
stackblock 记录栈的起始地址,线程退出时据此释放内存
dtv 动态线程向量,管理动态链接库中声明的 __thread 变量
specific[] pthread_key_create() 创建的动态 TLS 槽位
cleanup pthread_cleanup_push() 注册的清理函数链表
result pthread_exit() 或线程函数返回值,供 pthread_join() 获取
B. 线程局部存储 -TLS

TLS解决的问题:全局变量和静态变量在所有线程间共享,但是很多场景需要"看起来像全局变量,实际上每个线程各有一份"的数据。

例如: errno、日志缓冲区、线程私有缓存等。

关键字:__thread 和 thread_local

复制代码
// C 语言(GCC 扩展)
__thread int counter = 0; //每个线程都能看见,但是y独立的一份

// C++11 标准
thread_local std::string buffer;
C. 线程栈 -Thread stack

线程栈存放的内容如下所示:

复制代码
┌─────────────────────────────────────┐  Higher addresses
│  Function arguments 				  │  ←  函数参数
├─────────────────────────────────────┤
│  Return address (pushed by call)    │  ← 返回地址(call 指令压入) 
├─────────────────────────────────────┤
│  Saved registers (RBP, RBX, etc.)   │  ← 保存的寄存器(RBP、RBX 等) 
├─────────────────────────────────────┤
│  Local variables (arrays, structs)  │  ← 局部变量(数组、结构体等)  
├─────────────────────────────────────┤
│  Temporary values (intermediate)    │  ←  临时值(函数调用的中间结果)   
└─────────────────────────────────────┘  

线程栈的介绍:

复制代码
- 用户栈:局部变量、函数参数、返回地址全在这里,局部变量。  
  
  ·对于主线程的用户栈,位于虚拟地址空间的栈区,能够动态增长,在可执行程序加载时进行分配。
  ·对于子线程的用户栈,位于虚拟地址空间的共享区,大小固定,由mmap进行申请
  ·用户栈通过task_struct中的mm_struct结构体和vm_area_struct进行管理。
  
- 内核栈:线程通过系统调用进入内核时,走自己独立的内核栈。 
  
  ·在内核空间中都有的固定大小的栈。
  ·内核栈通过task_struct中的stack指针进行管理

2.6.5 线程的完整流程

A.线程创建的完整流程
复制代码
用户空间                              内核空间
   │                                    │
   │  1. mmap() 分配线程栈 + TLS          │
   │  2. 初始化 TCB (struct pthread)     │
   │  3. 调用 clone() 系统调用 ──────────→│
   │     flags: 						|
   |     CLONE_VM   					|  4. 创建 task_struct
   │     CLONE_FILES     				│  5. 根据 flags 共享资源:
   │     CLONE_SIGHAND       			│     - CLONE_VM → 共享 mm(地址空间)
   │      ...             			    │     - CLONE_FILES → 共享 files_struct(文件描述符表)
   │                                    │     - CLONE_SIGHAND → 共享 signal_struct(信号处理)
   |                                    │     ...
   |								    │
   │                                    │  6. 设置 tgid = 父线程 tgid
   │                                    │  7. 设置 group_leader
   │                                    │  8. 加入调度队列
   │  ←──────── 返回新线程的 LWP ID ────  │
   │  9. 记录 TCB 与 LWP 的映射           │
   │                                    │  
   │                                    │ 10. 新线程被调度后开始执行 start_routine
   │                                    │ 
B.线程销毁的完整流程
复制代码
用户空间                              内核空间
   │                                    │
   │  1. 线程函数返回,调用pthread_exit()   │
   │                				    │
   │  2. 执行清理处理器                   │
   │  3. 调用 exit() 系统调用 ──────────→ │
   │                                    │  4. 内核释放 task_struct
   │                                    │  5. 如果设置了 CLONE_CHILD_CLEARTID,
   │                                    │     清零 TCB 中的 tid 字段
   │                                    │ 
   │  ←──────────────────────────────── │  6. futex 唤醒等待 join 的线程
   |  7. join 线程被唤醒,获取返回值  	     |
   │  8. 回收线程栈和 TCB(mmap 释放)      │

2.7 thread的封装

复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>
#include <cstdlib>

// 基于 C 接口 (pthread) 的 C++ 线程封装
namespace ThreadModule
{
    // 已绑参的可调用对象类型: 无参无返回
    using func_t = std::function<void()>;

    // 内部上下文: 承载可调用对象 + 线程名
    // 独立分配在堆上, 是为了把生命周期与 Thread 对象解耦
    // (线程可能在 Thread 对象析构后仍在运行, 不能让回调引用悬空)
    class ThreadData
    {
        friend class Thread; // 须带 class: 否则 Thread 尚未声明, 会被当成友元函数声明

    public:
        ThreadData(const func_t &func, const std::string &name)
            : _func(func),
              _name(name)
        {
        }

    private:
        func_t _func;
        std::string _name;
    };

    // 线程状态: 防止非法操作 (重复 join / join 已 detach 的线程等)
    enum class ThreadStatus
    {
        NEW,      // 已构造, 未启动
        RUNNING,  // 运行中
        DETACHED, // 已分离
        JOINED    // 已回收
    };

    // 线程自动编号: inline 函数 + static 局部变量
    // C++11 下 inline 函数的 static 局部量跨编译单元共享, 头文件安全
    inline int NextThreadId()
    {
        static int id = 1;
        return id++;
    }

    class Thread
    {
    public:
        // 接受任意可调用对象 + 参数, 用 std::bind 绑参后转为 std::function<void()>
        template <typename F, typename... Args>
        Thread(const std::string &name, F &&f, Args &&...args)
        {
            _name = name + "-" + std::to_string(NextThreadId());
            _status = ThreadStatus::NEW;
            _td = new ThreadData(
                std::bind(std::forward<F>(f), std::forward<Args>(args)...),
                _name);
        }

        ~Thread()
        {
            // 析构策略 B : 仍在运行就自动 detach, 保证不泄漏
            if (_status == ThreadStatus::RUNNING)
            {
                pthread_detach(_tid); // 不断言, 容错 (线程可能已结束)
            }
            // 从未启动: 跳板不会运行, 自己清理 _td
            if (_status == ThreadStatus::NEW && _td != nullptr)
            {
                delete _td;
                _td = nullptr;
            }
            // 已启动 (RUNNING/DETACHED/JOINED): _td 由跳板结束时释放, 此处不动
        }

        // 禁用拷贝: 一个 pthread_t 不能被两个对象管理 (double join / double free)
        Thread(const Thread &) = delete;
        Thread &operator=(const Thread &) = delete;

        // 启动线程 (构造与启动分离, 便于统一管理一批线程)
        void start()
        {
            if (_status != ThreadStatus::NEW)
            {
                std::cerr << "[Thread:" << _name << "] already started\n";
                return;
            }
            int n = pthread_create(&_tid, nullptr, start_routine, _td);
            if (n != 0)
            {
                std::cerr << "pthread_create error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::RUNNING;
        }

        // 等待回收
        void join()
        {
            if (_status != ThreadStatus::RUNNING)
            {
                std::cerr << "[Thread:" << _name << "] cannot join (status not RUNNING)\n";
                return;
            }
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                std::cerr << "pthread_join error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::JOINED;
            _td = nullptr; // _td 已被跳板释放, 仅置空标记
        }

        // 分离
        void detach()
        {
            if (_status != ThreadStatus::RUNNING)
            {
                std::cerr << "[Thread:" << _name << "] cannot detach (status not RUNNING)\n";
                return;
            }
            int n = pthread_detach(_tid);
            if (n != 0)
            {
                std::cerr << "pthread_detach error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::DETACHED;
            _td = nullptr; // _td 由跳板结束时释放
        }

        const std::string &name() const { return _name; }
        bool isRunning() const { return _status == ThreadStatus::RUNNING; }

    private:
        // 跳板: 签名必须匹配 pthread 要求的 void*(*)(void*)
        // 由于非 static 成员函数隐含 this, 签名不匹配, 因此必须用 static
        static void *start_routine(void *arg)
        {
            ThreadData *td = static_cast<ThreadData *>(arg);
            td->_func(); // 执行用户回调
            delete td;   // 跳板作为最终消费者, 释放上下文
            return nullptr;
        }

    private:
        pthread_t _tid;
        std::string _name; // 自己保留一份, name() 在 join/detach 后仍可用
        ThreadStatus _status;
        ThreadData *_td = nullptr;
    };

} // namespace ThreadModule
相关推荐
阳光九叶草LXGZXJ42 分钟前
达梦数据库-学习-69-DM9主备集群部署
linux·运维·服务器·数据库·sql·学习
Sun 32851 小时前
Linux 常用命令梳理
linux·运维·服务器·开发·常用命令
一条破秋裤1 小时前
Linux 线程同步:条件变量
java·linux·jvm
LRL_1 小时前
【K8s 存储】Longhorn v1.12.1 离线安装全流程:外网制作离线包 + 内网部署(附踩坑实录)
linux·windows·node.js
星恒随风1 小时前
Linux开发工具详解(一):软件包管理、Vim、GCC/G++、Makefile与进度条实战
linux·运维·笔记·学习·vim
小小、码农1 小时前
Ubuntu / Linux 软件安装完整笔记
linux·笔记·ubuntu
yunwei371 小时前
eBPF 开发实践:使用 sockops 加速网络请求转发
linux·后端·性能优化
rest10241 小时前
做io测试
linux·服务器·网络
一条破秋裤1 小时前
Linux 网络编程基础:网络分类、分层模型与数据封装
linux·运维·网络