【linux线程(一)】线程概念、线程控制详细剖析

🎬 个人主页:HABuo

📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、页表

📚二、线程

[📖2.1 线程概念](#📖2.1 线程概念)

[📖2.2 线程和进程的关联](#📖2.2 线程和进程的关联)

[📖2.3 线程控制](#📖2.3 线程控制)

📚三、pthread线程库讲解

📚四、总结


前言:

本篇博客我们将进入线程篇章的学习,线程与进程类似,是我们要学习的一个大章节,在过去的学习中,我常听说线程进程的,却也从未认真的了解过他们,今天就让我们走进它,相信通过我的讲解你一定会受益匪浅!

本章重点:

页表结构、线程概念、线程与进程的关系、线程控制、Linux下线程库


📚一、页表

关于页表的认识,前期我们仅知道它就是从虚拟地址空间映射到物理内存的中间体,事实上页表的结构还是比较复杂的,下面我们来认识一下:

  • 32位虚拟地址的分割:32位虚拟地址被划分为三个部分:高10位(页目录索引)、中间10位(页表索引)、低12位(页内偏移)。

    • 10位可表示0~1023,共1024个索引。

    • 12位可表示0~4095,对应4KB页内偏移,因此页框大小为4KB。

  • 页目录与页表结构

    • CR3寄存器保存页目录的物理起始地址。页目录包含1024个页目录项(每个4字节),每个目录项指向一个二级页表的物理地址。

    • 二级页表同样包含1024个页表项(每个4字节),每个页表项指向一个物理页框的起始地址。

    • 页目录必须存在,但二级页表可以按需分配(部分存在或不存在),以节省内存。

  • 地址转换过程

    1. 从CR3取得页目录基址,用虚拟地址的高10位作为索引,找到对应的页目录项,获取二级页表的物理地址。

    2. 用中间10位作为索引,在二级页表中找到对应的页表项,获取物理页框的起始地址。

    3. 将低12位偏移量加上页框起始地址,得到最终的物理地址。

  • 数据类型与访问范围

    • 地址转换只定位到起始物理地址,要访问多少个字节由指令中的数据类型(如int、char)决定。

    • 类型信息在编译时确定,并体现在机器指令中(如操作数大小),CPU执行指令时据此访问相应字节数。因此变量地址通常只给出起始地址,结合类型即可确定完整访问范围。

  • 缺页中断处理

    • 当页表项的存在位为0(未建立映射)时,访问该虚拟地址会触发缺页异常。

    • CPU将引起异常的虚拟地址保存在CR2寄存器中,然后操作系统缺页处理程序从磁盘或交换区加载数据到物理内存,并更新页表项(填写物理地址和权限位)。

    • 缺页也可能因二级页表本身不存在而触发,此时需先分配二级页表。

  • 页表内存占用估算

    • 每个页表项4字节,一个二级页表大小为4KB(1024项)。

    • 若所有二级页表全存在(共1024个),则页表总大小约为4MB(页目录4KB + 1024×4KB = 4MB + 4KB ≈ 4MB)。

    • 实际系统中,二级页表通常不会全部创建,从而大幅减少内存占用,但进程创建仍需一定开销。

总结成一段话就是:

分页机制是将虚拟地址划分为10位页目录索引、10位页表索引和12位页内偏移,通过CR3定位页目录,逐级查找页表项获得物理页框基址,加上偏移得到物理地址,并由指令类型决定访问字节数,缺页时硬件保存地址至CR2由操作系统处理,且二级页表可部分存在以节省内存。

📚二、线程

📖2.1 线程概念

我们常听说线程线程的,还有什么多线程,但我们好像真心没了解过,它和进程又有啥区别呢?其实线程概念很简单,如下:但是理解需要下点功夫,让我们慢慢来!

线程概念:程序中的一个执行流就叫做线程

  • 一个进程至少要有一个执行线程,单个进程本身就是一个执行流,所以单个进程某种意义上也是一个线程(是主线程)。线程在进程内部运行,本质是在进程地址空间内运行。在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

所谓更轻量化,就是说,我们在进程那里,创建子进程,本质是将PCB、进程地址空间、页表等等内容全都拷贝一份,这也就是为什么进程具有独立性,但是线程不一样,线程仅仅是个执行流,是在进程内部进行执行的,因此它和进程共享地址空间和页表。

聪明的你一定想到了我没有强调PCB,没错,之前我们在讲解进程在CPU中运行时,本质就是将PCB放入CPU的运行队列里,那么线程运行呢,也是这样,不过他不叫做PCB,而是TCB,因此今天我们再说进程,就不再仅仅是只有一个PCB,而是和多个TCB在一起的整体,进程就是承担分配系统资源的一个基本实体。

但是,我又要告诉你,Linux下没有TCB,因为单纯从线程调度角度,线程和进程有很多的地方是重叠的!所以,Linux工程师,不想给Linux"线程"专门设计对应的数据结构!而是直接复用PCB!用PCB用来表示linux内部的"线程"

Windows下是有独立的TCB!

总结以下几点:

  • Linux内核中有没有真正意义的线程呢?没有的。Linux是用进程PCB来模拟线程的,是一种完全属于自己的一套线程方案
  • 站在CPU的视角,每一个PCB,都可以叫做轻量级进程
  • Linux线程是CPU调度的基本单位,而进程是承担分配系统资源的基本单位
  • 进程用来整体申请资源,线程用来伸手向进程要资源。
  • Linux中没有真正意义的线程
  • 好处是什么?简单,维护成本大大降低-可靠高效!

综上所述,线程就是一个没有独立的地址空间的PCB结构,线程的资源是从最开始的主线程,也就是进程来的.而站在CPU的视角,CPU调度的是PCB结构,CPU只认PCB,它并不关心此PCB是进程的还是线程的,所以线程被称为系统调度的基本单位.而在Linux操作系统下,线程就是轻量化的进程

📖2.2 线程和进程的关联

通过上面对线程的描述,我们可以窥探到:

  1. 线程是担任系统调度的基本实体
  2. 进程是担任系统资源分配的基本实体

虽然线程共享进程数据,但也拥有自己的数据:

  • 线程ID
  • 栈区资源
  • 信号屏蔽字
  • 调度优先级

使用指令: ps -al查看线程ID

主线程的PID和LWP相同,CPU调度时是在看LWP,而不是PID,线程的PID和主线程相同,自己独有LWP

几个小问题:

线程一旦被创建,几乎所有的资源都是被所有线程共享的,线程也一定要有自己私有的资源,什么资源应该是线程私有的呢?

    1. PCB属性私有
  • 2.要有一定私有上下文结构
  • 3.每一个线程都要有自己独立的栈结构

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 1.进程:切换页表&&虚拟地址空间&&切换PCB&&上下文切换
  • 2.线程:切换PCB&&上下文切换
  • 3.线程切换cache不用太更新,但是进程切换,全部更新

📖2.3 线程控制

  • 创建线程

我们已经说了,Linux下没有线程这个概念,而只是轻量化进程,但是它又是线程那一套,只不过换个名字而已,因此,Linux下就封装了第三方库来供我们使用,库中提供了操作线程的一些接口!

由于pthred是第三方库,所以编译时要加上-lpthread的字段

第一个参数需要传入一个pthread_t类型的变量,这个变量我们需要先定义.所谓的pthread_t,实际上本质就是unsigned long int类型,第二个参数我们一般设置为空。第三个参数要传入线程启动后要执行的函数的地址,这个函数的返回值和参数都是void*,最后一个参数是传入给函数形参的,要强转为void*

示例如下:

cpp 复制代码
void* thread_routine(void* args)
{
    const char* name = (const char*)args;
    while (true)
    {
        cout << "我是新线程, 我正在运行! name: " << name << endl;
    }
}
int main()
{
    // typedef unsigned long int pthread_t;
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void*)"thread one");
    assert(0 == n);
    (void)n;
    // 主线程
    while (true)
    {
        char tidbuffer[64];
        snprintf(tidbuffer, sizeof(tidbuffer), "0x%lx", tid);
        cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << endl;
        sleep(1);
    }
    return 0;
}
  • 如何终止线程
cpp 复制代码
pthread_exit((void*)7)//方法2:参数是设置的类似退出码的标识
pthread_cancel(tid)//方法3:参数是线程的tid

一般使用return或者pthread_exit来终止线程

  • 如何进行线程等待

如果不等待新线程,那么就会造成内存泄漏,那么等待新线程的目的就是如下几点:

  • 回收新线程PCB等内存资源,防止内存泄漏
  • 如果需要的话,获取新线程执行任务的返回值,确认新线程是否执行完任务
cpp 复制代码
void* threadRoutine(void* args)
{
    pthread_exit((void*)7);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
    void* retval;
    pthread_join(tid, &retval);//设置retval就是获取新线程的退出信息,不获取就设置nullptr仅回收线程的内存资源即可
    cout << "main thread quit..., ret: " << (long long int)retval << endl;
    return 0;
}
  • 获取线程的tid
cpp 复制代码
char hex[64];
snprintf(hex, sizeof(hex), "%p", pthread_self());
  • 线程分离
cpp 复制代码
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离

cpp 复制代码
pthread_detach(pthread_self()); 

一般编码格式:

cpp 复制代码
void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n", (char*)arg);
	return NULL;
}
int main(void)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {
		printf("create thread error\n");
		return 1;
	}
	int ret = 0;
	sleep(1);//很重要,要让线程先分离,再等待 因为不确定是新线程先分离,还是主线程先等待
             //如果主线程先等待,那么新线程分离后,主线程一直阻塞在那里
             //所以一般分离时是在主线程处进行分离
	if (pthread_join(tid, NULL) == 0) {
		printf("pthread wait success\n");
		ret = 0;
	}
	else {
		printf("pthread wait failed\n");
		ret = 1;
	}
	return ret;
}

📚三、pthread线程库讲解

首先,pthread_create的返回值是线程ID。那么线程ID的本质是什么呢?

线程ID的本质是一个地址,pthread库是一个动态库,是第三方库,这个库会被映射到进程的地址空间的共享区中,而线程ID所指的地址则是线程在线程库级别的tcb的起始地址,线程的函数想要对线程进行操作,那么只需要拿着线程的tid找到线程控制块tcb就可以进行访问并且操作

刚才我们说线程有自己的栈区,也就是说线程要维护自己的栈区,那么可以联想一下,谁帮线程维护这个栈区呢?答案是pthread库维护的栈区,也就是说其实线程的栈区也是被映射到共享区的,由pthread第三方库维护

补充知识:局部存储

在多线程编程中,我们常遇到两类变量:

  • 全局变量 :所有线程共享,一个线程修改,其他线程立即可见。这容易引发竞态条件,通常需要用锁来保护。

  • 局部变量:位于函数栈上,仅在该函数执行期间有效,无法跨函数调用共享。

线程局部存储正好填补了这两者之间的空白:它既有全局的"生命周期" (跟随线程的创建和销毁),又有线程私有的"可见性"(仅在本线程内可见)。

cpp 复制代码
使用方式:_thread int g_val = 1000

📚四、总结

今天这篇博客我们介绍了页表的具体结构、线程概念、线程控制、线程库等内容,小结一下:

页表:分页机制是将虚拟地址划分为10位页目录索引、10位页表索引和12位页内偏移,通过CR3定位页目录,逐级查找页表项获得物理页框基址,加上偏移得到物理地址,并由指令类型决定访问字节数

线程概念: Linux下线程就是轻量级进程,他没有按照windows下TCB的具体概念,而是复用了进程的PCB,也就是说他们共用了一套进程地址空间,之前我们所说的进程就是单线成流。所以现在进程的概念就是担任系统资源分配的基本实体。而线程就是CPU进行调度的基本实体。每一个线程有一个PCB。CPU调度的时候就是拿着PCB进行调度。

线程控制:pthread_creat、pthread_exit、pthread_join、pthread_self、pthread_detach

线程库:pthread库是一个动态库,是第三方库,这个库会被映射到进程的地址空间的共享区中,而线程ID所指的地址则是线程在线程库级别的tcb的起始地址,线程的函数想要对线程进行操作,那么只需要拿着线程的tid找到线程控制块tcb就可以进行访问并且操作,线程的栈帧结构以及局部存储的的变量都在库中定义

相关推荐
OKkankan2 小时前
深入理解linux进程
java·linux·c++
王老师青少年编程2 小时前
2026年3月GESP真题及题解(C++七级):物流网络
c++·题解·真题·gesp·csp·七级·物流网络
xushichao19892 小时前
C++中的职责链模式实战
开发语言·c++·算法
fqbqrr2 小时前
2603C++,C++强项
c++
2301_818419012 小时前
C++中的协程编程
开发语言·c++·算法
add45a2 小时前
C++中的工厂方法模式
开发语言·c++·算法
路溪非溪2 小时前
BLE的广播、扫描和连接等工作机制总结
linux·arm开发·驱动开发
xushichao19892 小时前
C++中的工厂模式高级应用
开发语言·c++·算法
2501_924952692 小时前
C++模块化编程指南
开发语言·c++·算法