【Linux】线程基础

发布于:2025-03-25 ⋅ 阅读:(58) ⋅ 点赞:(0)

Alt

🔥个人主页Quitecoder

🔥专栏linux笔记仓

Alt

01.背景知识

在这里插入图片描述
OS进行内存管理,不是以字节为单位的,而是以内存块为单位的,默认大小为4kb,我们也学过,系统和磁盘文件进行IO的基本单位是4kb–8个扇区

可执行文件要加载到内存中,我们前面知道,文件系统中文件是以块为单位进行存储的,加载到内存中,也以块的方式加载访问,所以才有了IO的基本单位是4kb

内存块是操作系统进行内存管理的基本单位,称为页(page),操作系统为文件分配内存空间,以页为单位

操作系统现在对内存的管理及转为对一个个页的管理,先描述,再组织

struct page
{
	int flag;//是否被占用,是否是脏页,是否被锁定
	int mode;
	....
}
struct page memory[1048576];

每一个页帧用数组管理

页表我们学过,是虚拟地址到物理地址的映射,详细讲解
在这里插入图片描述

虚拟地址就是二进制构建的32个比特位的数据

虚拟地址被拆分为3部分,10位10位12位,页表也不是只有一张,页表开始以前十个bit位,表示的范围是2的十次方

页目录索引(Page Directory Index):高 10 位,用于定位 页目录表(Page Directory) 中的条目。
页表索引(Page Table Index):中间 10 位,用于定位 页表(Page Table) 中的条目。
页内偏移(Page Offset):低 12 位,用于定位页内的具体字节。

页表中放的是指向页框的起始地址

虚拟地址最低十二位,范围【0,4095】,页内偏移,刚好就是一个页的大小,用于定位页内具体字节的部分,例如一个整数,我就连续读取四个字节

还有一部分page不通过页表映射,通过struct file,缓冲区

在这里插入图片描述
每行代码都有地址,函数是连续的代码地址构成代码块,一个函数对应一批连续的虚拟地址

虚拟地址到物理地址的转换过程
以下是通过二级页表将虚拟地址转换为物理地址的过程:

虚拟地址分解
假设虚拟地址为 0x00401234,其二进制表示为:

0000 0000 0100 0000 0001 0010 0011 0100

分解为:
• 页目录索引:0000 0000 01(高 10 位,值为 1
• 页表索引:00 0000 0001(中间 10 位,值为 1
• 页内偏移:0010 0011 0100(低 12 位,值为 0x234

  1. CR3 寄存器获取页目录表的基地址
  2. 使用页目录索引(1)找到页目录表中的条目,获取页表的基地址。
  3. 使用页表索引(1)找到页表中的条目,获取物理页帧的基地址。
  4. 将物理页帧基地址与页内偏移(0x234)相加,得到物理地址。

页表项(PTE)的结构
每个页表项(PTE)的大小为 4 字节(32 位),包含以下字段:
物理页帧地址(Physical Page Frame Address):20 位(实际使用 20 位,支持 4GB 物理内存)。
标志位(Flags):12 位,包括:
有效位(Present Bit):指示页是否在内存中。
可写位(Writeable Bit):指示页是否可写。
用户位(User Bit):指示用户程序是否可以访问该页。
脏位(Dirty Bit):指示页是否被修改过。
访问位(Accessed Bit):指示页是否被访问过。


页表的大小
页目录表:1024 个条目,每个条目 4 字节,总大小为 4KB。
页表:1024 个条目,每个条目 4 字节,总大小为 4KB。
总大小:页目录表和所有页表的总大小取决于进程的虚拟地址空间使用情况。在最坏情况下,需要 1024 个页表,总大小为:
4 KB + 1024 × 4 KB = 4 MB + 4 KB ≈ 4 MB 4\text{KB} + 1024 \times 4\text{KB} = 4\text{MB} + 4\text{KB} \approx 4\text{MB} 4KB+1024×4KB=4MB+4KB4MB

02.线程概念

线程:在进程内部运行,是CPU调度的基本单位
以前我们知道,每次创建一个进程,都要创建一个地址空间和页表

现在不想给“进程”重新创建地址空间加载数据,直接让你新的pcb和父进程指向同一个地址空间,正文部分拆成多份由不同task_struct执行,这一部分task_struct就叫做Linux中的线程
在这里插入图片描述
上面一个整体为一个进程,进程=内核数据结构+进程代码和数据

线程实现基于 ​轻量级进程(LWP)​

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

cpu对task_struct是进程还是线程不做区分,cpu看到的执行流为进程,Linux中的执行流:轻量级进程

Linux是用进程模拟的线程

OS要单独设计线程,先描述再组织,现在给线程专门设计结构TCB,可以复用PCB,用PCB统一表示执行流,这样的话,我们就不需要为线程单独设计数据结构和调度算法了

简单使用线程

int pthread_create(pthread_t *restrict thread,
        const pthread_attr_t *restrict attr,
        void *(*start_routine)(void *),
        void *restrict arg);

参数分别为:

  • 线程标识符tid
  • 用于设置线程的属性,传入NULL使用默认属性
  • 指定线程启动后执行的函数
  • 传递给 start_routine 函数的参数
#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;
//新线程
void *threadStart(void *args)
{
    while(true)
    {
        sleep(1);
        cout<<"new thread run..."<<endl;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,threadStart,(void*)"thread-new");

   //主线程
    while(true)
    {
        sleep(1);
        cout<<"main thread run.."<<endl;
    }
    return 0;
}

在这里插入图片描述
注意这里的makefile里编译那一步必须链接thread库

testthread:testthread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f testthread

在这里插入图片描述
这里系统只有一个进程,我们还可以让线程输出它的pid

在这里插入图片描述
在这里插入图片描述
LWP:light weight process 轻量级进程,lwp就是轻量级进程的id

我们发现,有一个lwp与pid相同,为主线程。OS调度的时候,用的是LWP

进程创建成本非常高,创建线程,只需要创建pcb,然后把进程的资源全部给线程即可

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多,页表,地址空间都不需要切换,线程的调度成本低

删除一个线程成本也低

但线程也有劣势,一个线程出错可能会影响整个进程。这是因为线程是进程内的执行单元,它们共享进程的资源(如内存地址空间、文件描述符等)。如果某个线程出现错误(如非法内存访问、未捕获的异常等),可能会导致整个进程崩溃或进入不可预期的状态

线程调度成本更低

进程上下文切换:

  • 进程拥有独立的虚拟地址空间,切换时需要切换页表,切换页表需要刷新 CPU 的 ​TLB(Translation Lookaside Buffer)​,这是一个耗时的操作
  • 进程上下文需要保存和恢复CPU寄存器状态
  • 还需要处理其他资源(文件fd,信号处理函数等)

线程共享进程的虚拟地址空间和资源,切换时不需要切换页表,也不需要分配和释放fd,内存等的资源,硬件只需要关注线程的私有数据,线程上下文切换也需要保存和恢复CPU寄存器状态

CPU 缓存的影响:线程共享进程的内存地址空间,因此线程切换时 ​CPU 缓存(Cache)的命中率较高。缓存中的数据可以继续被新线程使用,减少了内存访问的延迟
进程拥有独立的内存地址空间,因此进程切换时 ​CPU 缓存的命中率较低

线程私有的部分:一组寄存器:硬件上下文数据–线程可以动态运行
栈:线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区

某一个线程将来也会被页表映射到物理内存,以4kb为单位的代码块


网站公告

今日签到

点亮在社区的每一天
去签到