Linux 线程(1)

1.线程概念

线程是进程内的最小执行单元,一个进程可以包含多个线程,所有线程共享进程的资源(内存、文件句柄等),但有自己独立的执行栈和程序计数器。

结合进程的核心区别可以这样理解:

进程是资源分配的基本单位

系统给进程分配内存、CPU 时间片等资源,进程像是一个 "独立的工作车间"。

线程是 CPU 调度的基本单位

线程是车间里的 "工人",一个车间可以有多个工人,他们共享车间的工具和材料(进程资源),但各自干不同的活,且切换工人的开销远小于切换车间。

要是想使用线程,就得自己重新定义线程的结构体,相当于从头重新弄一套线程相关的实现;而 Linux 那边好像是直接用了原本就有的线程那一套机制,把它叫做轻量级进程

wiindows 系统就算重新弄了一套相关的实现

我们可以简单理解成

线程:是进程内的一个执行分支

如何理解我们以前的进程???

操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个线程(主线程)!

Linux 实现方案

在 Linux 中,线程在进程 "内部" 执行,线程在进程的地址空间内运行

任何执行流要执行,都要有资源!地址空间是进程的资源窗口

2.线程的性质

同一进程内的多个线程:共享内核空间的数据页表(且共享用户空间页表,因线程共享进程的页表根目录 PGD,内核空间映射全局统一);

不同进程的线程:逻辑上共享内核空间页表(所有进程的内核空间页表映射完全一致,系统启动时初始化一次,全局复用),但用户空间页表相互独立。

我们还是用以前虚拟地址的图片

|--------|----------|---|---|---|
| | 相同进程不同线程 | 不同进程不同线程 |||
| 内核空间页表 | 相同 | 相同 |||
| 用户空间页表 | 相同 | 不同 |||

假设我们创建了n个进程

内核空间页表只有1份

用户空间页表有n份

无论一个进程里面有几个线程 一个进程里面只有一份独立的用户空间页表

和一份和其他进程共享的一份内核空间页表

线程切换比进程快 为什么???

线程共享进程的地址空间、文件等资源,切换时不用换页表、刷新缓存(这些是进程切换的大开销);

仅需保存 / 恢复 CPU 执行状态(比如寄存器、程序计数器),不用处理进程级的资源上下文。

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

3.虚拟地址到物理地址的转换方式

高 10 位(PDE 索引)= 楼栋号

小区太大,先按 "楼栋" 分组(页目录就是小区的「楼栋列表」,CR3 寄存器是小区大门的指引牌,告诉你楼栋列表放在小区哪个角落)。

高 10 位就是你快递上的 "楼栋号",用它查楼栋列表,就能找到目标快递所在的楼栋(对应 "找到目标页表的物理地址")。

中 10 位(PTE 索引)= 楼层号

找到楼栋后,还要找具体楼层(页表就是这栋楼的「楼层列表」)。

中 10 位是 "楼层号",用它查楼层列表,就能找到目标快递所在的那一层(对应 "找到目标物理页帧的地址")。

低 12 位(页内偏移)= 房间号

每一层的户型大小都一样(都是 4KB,因为 2^12=4096 字节),房间号范围固定(0-4095)。

低 12 位直接就是 "房间号",不用再查列表,直接顺着楼层找到对应的房间(对应 "物理页帧内的具体字节位置")。

如果不拆分,直接用 32 位虚拟地址对应物理地址,会有两个大问题:

浪费内存:

32 位系统最大虚拟地址是 4GB,按 4KB 一页算,需要 100 万个 "页表项"(4GB/4KB=1024*1024),每个页表项占 4 字节,一个进程的页表就占 4MB。如果有 1000 个进程,光页表就占 4GB,内存直接炸了!

拆分后(10+10+12):

页目录只需要 1024 个项(10 位),占 4KB(1024*4 字节);

每个页表也只需要 1024 个项,占 4KB;

只有进程用到的页表才会加载到内存,不用一次性加载所有页表,比如一个进程只用到 10 个页,总页表占用才 4KB(页目录)+10*4KB(页表)=44KB,比 4MB 省了近 100 倍!

查找更快:

拆分后是 "三级索引"(页目录→页表→页内偏移),每级都是固定长度的索引(10 位、10 位、12 位),CPU 的 MMU(内存管理单元)能像 "查字典按部首→页码→行数" 一样,硬件级快速计算地址,比线性查找快得多。

4.线程接口

线程函数的接口统一的一个头文件

cpp 复制代码
#include <pthread.h>

1.pthread_create

作用是启动一个新的执行流(线程)。

cpp 复制代码
int pthread_create(
    pthread_t *thread,        // 输出:新线程的ID(类似进程PID)
    const pthread_attr_t *attr, // 线程属性:NULL表示用默认属性
    void *(*start_routine)(void *), // 线程要执行的函数(函数指针)
    void *arg                 // 传递给start_routine的参数
);

thread:传入一个pthread_t类型的指针,函数会把新线程的 ID 写入这个指针指向的变量。

attr:线程属性(如栈大小、调度策略),一般填NULL用默认属性。

start_routine:线程要执行的函数,必须是void* (*)(void*)类型(入参是void*,返回值是void*)。

arg:传递给start_routine的参数,若要传多个参数,需封装成结构体指针。

返回值:成功返回0;失败返回错误码(不是-1,需用strerror()查看错误信息)。

2. pthread_join

cpp 复制代码
int pthread_join(
    pthread_t thread,  // 要等待的子线程ID(由pthread_create生成)
    void **retval      // 输出:存储子线程的返回值;不需要则填NULL
);

thread:目标子线程的 ID(pthread_t类型,由pthread_create写入)。

retval:二级指针,用于接收子线程return的void*返回值;若无需返回值,填NULL。

返回值:成功返回0;失败返回错误码(非-1,可用strerror()查看错误信息)。

阻塞等待:调用pthread_join的线程(通常是主线程)会阻塞,直到目标子线程执行完毕。

回收资源:子线程结束后,若不调用pthread_join,其资源(如线程控制块 TCB、栈)不会自动释放,会成为 "僵尸线程",导致资源泄露。

获取返回值:可以拿到子线程的执行结果(子线程通过return返回的void*数据)。

3.pthread_self()

获取当前线程 ID

cpp 复制代码
pthread_t pthread_self(void);

返回值

当前线程的 pthread_t 类型 ID。

4.pthread_exit()

cpp 复制代码
void pthread_exit(void *retval);

前面我们学过的exit和_exit是进程的主动退出方式

pthread_exit()是线程的主动退出方式

立即终止调用线程,将 retval 作为线程的退出状态(可被 pthread_join 获取)。

retval:线程退出的返回值指针(不能指向线程栈上的局部变量,因为线程退出后栈会被销毁)。

返回值

无(线程已终止,不会返回)。

线程函数中可显式调用 pthread_exit 退出,也可通过 return 返回(效果等价,return 的返回值会被当作 pthread_exit 的 retval)。

主线程特殊行为:主线程调用 pthread_exit 后,主线程终止,但进程不会退出,其他子线程会继续运行(区别于 exit(),exit() 会终止整个进程)。

!!!注意!!!

主线程从main函数return时,其他线程会被终止,但这一现象的本质并非 "主线程 return 直接终止子线程",而是主线程 return 触发了进程退出,进程退出会导致所有线程被内核终止。

普通子线程的return 该线程结束,返回退出状态 继续运行 不受影响,正常运行

无论是子线程还是主线程调用exit的结果都是一样的 进程退出

5.pthread_detach()

cpp 复制代码
int pthread_detach(pthread_t thread);

设置线程为分离状态

将指定线程设置为分离状态(detached),线程退出后,其占用的系统资源会被内核自动回收,无需调用 pthread_join 等待。

thread:需要设置为分离状态的线程 ID。

返回值

成功:返回 0;

失败:返回非 0 的错误码(如 EINVAL:线程无效或已分离;ESRCH:线程不存在)。

主线程中调用:pthread_detach(tid)(tid 是创建的线程 ID);

线程内部调用:pthread_detach(pthread_self())(推荐,线程自身控制分离状态);

线程一旦被分离,无法再被 pthread_join 等待,调用 pthread_join 会返回 EINVAL 错误。

不能对已被 pthread_join 的线程调用 pthread_detach(会失败)。

若线程未被分离且未被 pthread_join,退出后会变成僵尸线程,占用系统资源直到进程退出。

5.pthread_cancel()

cpp 复制代码
int pthread_cancel(pthread_t thread);

发送线程取消请求

向指定线程发送取消请求,请求线程终止运行。线程是否响应、何时响应取决于其取消状态和取消类型。

参数

thread:需要取消的线程 ID。

返回值

成功:返回 0;

失败:返回非 0 的错误码(如 ESRCH:线程不存在)。

pthread_cancel() 仅发送请求,并非立即终止线程。

线程响应取消后,会返回 PTHREAD_CANCELED(宏定义,值为 (void*)-1),可被 pthread_join 获取。

++我们可以写三个代码用来测试这些函数++

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

void *thread_work(void *arg) {
    printf("【子线程】启动\n");
    
    unsigned long tid = (unsigned long)pthread_self();
    printf("【pthread_self()】执行成功,子线程自身ID:%lu\n", tid);
    
    printf("【pthread_exit()】即将执行,执行后子线程终止,后续代码不会运行\n");
    pthread_exit(NULL);
    printf("【pthread_exit()】未生效(此行不该打印)\n");
}

int main() {
    pthread_t tid;
    
    printf("【主线程】pthread_create()即将创建子线程\n");
    pthread_create(&tid, NULL, thread_work, NULL);
    printf("【pthread_create()】执行成功,创建的子线程ID:%lu\n", (unsigned long)tid);
    
    printf("【主线程】pthread_join()即将等待子线程结束\n");
    pthread_join(tid, NULL);
    printf("【pthread_join()】执行成功,子线程已结束\n");
    
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 线程工作函数(极简版)
void* thread_func(void* arg) {
    char* name = (char*)arg;
    for (int i = 1; i <= 3; i++) { // 循环3次,简化次数
        printf("线程[%s]:第%d次执行\n", name, i);
        sleep(1); // 取消点:响应cancel
    }
    printf("线程[%s]:执行完毕\n", name); // 被取消则不执行
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 创建线程t1(不取消)、t2(将取消)
    pthread_create(&t1, NULL, thread_func, "t1(不取消)");
    pthread_create(&t2, NULL, thread_func, "t2(将取消)");

    sleep(2); // 让t2执行2次后取消
    pthread_cancel(t2); // 发送取消请求

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

// 线程工作函数(极简版)
void* thread_func(void* arg) {
    printf("线程[%s]执行\n", (char*)arg);
    sleep(1); // 模拟工作
    printf("线程[%s]结束\n", (char*)arg);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    int ret;

    // 创建非分离线程t1、分离线程t2
    pthread_create(&t1, NULL, thread_func, "t1(未分离)");
    pthread_create(&t2, NULL, thread_func, "t2(已分离)");
    pthread_detach(t2); // 设置分离

    // 尝试join非分离线程t1(成功)
    ret = pthread_join(t1, NULL);
    printf("join t1:%s\n", ret ? strerror(ret) : "成功");

    // 尝试join分离线程t2(失败)
    ret = pthread_join(t2, NULL);
    printf("join t2:%s(预期失败)\n", ret ? strerror(ret) : "成功");

    sleep(1);
    return 0;
}

5.修饰符__thread

其核心作用是让被修饰的变量成为每个线程的私有副本,而非进程级的共享变量。

___thread只能对内置类型使用 不能对自定义类型使用

1.__thread定义在线程函数内时,主线程是否有副本,完全取决于主线程是否执行了这个定义了__thread变量的函数------ 只有执行了函数,才会触发副本的分配;未执行则无副本。

2.__thread定义在全局时 主线程必定拥有副本,这是进程启动的默认行为,与是否使用变量无关;

所有子线程创建时会自动获得独立副本,初始化值为编译期常量;

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

// 普通全局变量(共享)
int s = 0;
// __thread变量(线程私有)
__thread int t = 0;

// 线程函数:仅做一次自增+打印(精简核心逻辑)
void* f(void* arg) {
    int id = *(int*)arg;
    s++, t++; // 共享变量自增,私有变量自增
    printf("线程%d: s=%d, t=%d\n", id, s, t);
    return NULL;
}

int main() {
    pthread_t a, b;
    int id1=1, id2=2;
    pthread_create(&a, NULL, f, &id1);
    pthread_create(&b, NULL, f, &id2);
    pthread_join(a, NULL);
    pthread_join(b, NULL);
    printf("主线程: s=%d, t=%d\n", s, t); // 主线程的t是独立副本
    return 0;
}

6.线程管理

我们学语言的时候知道 临时变量是有生命周期的 是位于栈上的

对于线程同样适用

主线程和子线程一样有它们的栈区 但是子线程和主线程栈的位置在进程地址空间的不同地方

主线程的栈区位于进程地址空间中的用户地址空间中的栈区

而线程为了方便统一管理 被封装到一个库里面 运行的时候会被加载到内存的共享区

子进程的栈区位于进程地址空间中的用户地址空间中的mmap即共享区(暂时先这么理解)

这张图右侧的struct pthread对应的是用户态的线程控制块(TCB)主线程的tcb也在其中

但是主线程的栈是特殊的(位于进程默认栈区)------ 主线程的 TCB(struct pthread)虽然在 mmap 区,但它记录的主线程栈地址是「进程默认栈区」,

那么看到前面代码的运行结果

你不好奇为什么线程的tid这么大呢

线程在里面叫轻量化进程 进程有pid 线程同样也有tid

每一个线程的库级别的 tcb 的起始地址,叫做线程的 tid!!

线程在里面叫轻量化进程 进程有pcb 线程同样也有tcb

|----------------|--------------------------------------------|------------------------------------------------------------|
| 维度 | 主线程 ID | 子线程 ID |
| 用户态(pthread_t) | 类型为pthread_t,指向主线程的 TCB 地址 | 类型为pthread_t,指向子线程的 TCB 地址 |
| 内核态(LWP) | pid_t(整数类型,本质是内核轻量级进程编号)等于当前进程的 PID,内核全局唯一 | pid_t(整数类型,本质是内核轻量级进程编号)内核分配的独立pid_t类型整数(≠进程 PID),内核全局唯一 |

相关推荐
longxibo2 小时前
Ubuntu datasophon1.2.1 二开之二:解决三大监控组件安装后,启动失败:报缺失common.sh
大数据·linux·运维·ubuntu
小尧嵌入式2 小时前
Linux的shell命令
linux·运维·服务器·数据库·c++·windows·算法
OnlyEasyCode2 小时前
.net程序部署Linux运行
linux·运维·服务器
裤裤兔2 小时前
linux查看内存
linux
kobe_OKOK_2 小时前
在 Ubuntu Server 24.04 (Noble)** 上安装 **SQL Server 驱动程序
linux·运维·ubuntu
haiyanglideshi2 小时前
ubuntu上使用samba访问另一台ubuntu的数据
linux·运维·ubuntu
小嘟嘟26792 小时前
虚拟机网络问题故障定位
linux·服务器·网络
CQ_YM2 小时前
Linux进程终止
linux·服务器·前端·进程
_OP_CHEN3 小时前
【Git原理与使用】(六)Git 企业级开发模型实战:从分支规范到 DevOps 全流程落地
大数据·linux·git·gitee·项目管理·devops·企业级组件