云计算百科
云计算领域专业知识百科平台

Linux之线程(一)

适合零基础入门,也可用于期末、考研复盘,全文整合实操代码、命令实验与底层原理,知识点完整无遗漏。

前言

当初学操作系统线程和虚拟内存分页时,很容易混淆进程与线程、搞不懂 32 位地址分段、CR3 寄存器作用这些高频考点。本文结合代码实操、服务器命令验证,把两大块内容完整梳理,逻辑循序渐进,没有晦涩的跳跃知识点,同时预留截图插入位置,方便自己复习查阅。

第一部分 Linux 线程:轻量级进程 LWP

1 进程、线程基础概念

1.1 什么是进程

我们把正在运行的程序称作进程,它由内核维护的管理结构、程序代码、全局数据共同构成,操作系统是以进程为单位分配硬件资源。 当系统创建新进程时,会一次性分配全套独立资源:专属虚拟内存空间、独立页目录、私有的文件描述符表、单独的信号处理规则,还有专属工作目录与权限掩码。 进程切换的成本很高,切换时需要替换页目录,刷新 CPU 内 TLB 缓存,消耗不少算力。

进程是运行起来的程序,内核数据结构+代码和数据=进程—>承担分配相同资源的的基本实体

1.2 什么是线程

线程是进程内部独立的执行分支,一个进程最少有一条主线程,也能创建多条子线程。平时直接运行的程序,本质就是单线程进程。 这里是 Linux 和其他系统最大的区别:内核没有专门存放线程信息的 TCB 结构体,Linux 里线程就是轻量级进程 LWP。不管是进程还是线程,内核都用task_struct统一管理,CPU 调度程序不会区分二者,只依靠 LWP 识别不同执行流。

线程是进程内部的执行分支

必须学习具体的操作通信的线程实现

线程资源分为共享、私有两类
  • 同进程下所有线程共享资源 整个进程的虚拟地址空间(代码段、全局数据、堆、动态链接库)、页目录和页表、打开的文件描述符、信号处理函数、文件系统配置(工作目录、umask)全部共享。
  • 每条线程独有的资源 独立栈空间、运行时寄存器上下文、错误标记 errno、调度相关数据,以及独属于自己的内核标识 LWP。
  • 1.3 进程与线程简单对比

    对比维度普通进程Linux 线程(LWP)
    资源隔离 全部资源独立 绝大多数资源共享
    切换时修改 CR3 需要修改 不用修改
    唯一标识 PID LWP,同进程 PID 完全相同

    原理

    Linux的线程原理

    2 创建线程的 pthread_create 函数详解

    2.1 头文件与完整函数原型

    pthread_create

    参数

  • pthread_t *restrict thread 输出型参数,传入pthread_t变量地址,函数执行成功后,会把新建线程的 TID(线程 ID,TCB 标识)写入该变量。 restrict是 C 关键字,标记该指针是访问对应内存的唯一途径,编译器优化用,写代码时直接传普通指针即可。

  • const pthread_attr_t *restrict attr 线程属性结构体,用来设置栈大小、分离属性、调度优先级等; 传nullptr/NULL代表使用系统默认线程属性。

  • void *(*start_routine)(void *) — 线程入口函数(重点) 函数指针,规定线程主函数固定格式:

    • 参数:void* 通用指针
    • 返回值:void* 你之前代码报错根源就是线程函数返回值写成void,不匹配该签名。
  • void *restrict arg 传给线程入口函数的唯一参数,无参数时填nullptr; 可传递数字、结构体指针等任意数据,在线程函数内强转类型使用。

  • 返回值

    • 返回 0:线程创建成功
    • 返回非 0 数字:创建失败,对应错误码(如内存不足、属性非法)

    我们发现这两个死循环居然同时在运行,也就是说明这是两个执行流,我们不妨可以进一步验证

    我们让这两个循环都打印自己的pid,发现pid都是一致的,说明这两个执行流属于同一个进程

    查看线程(LWP 轻量级进程)

    ps -aL

    PID LWP TTY TIME CMD
    2664911 2664911 pts/0 00:00:00 testThread
    2664911 2664912 pts/0 00:00:00 testThread

    字段解释:

  • PID = 进程 ID:两行 PID 完全一致 2664911,代表两个线程隶属于同一个进程;
  • LWP = 轻量级进程 ID(内核线程 ID)
    • LWP=2664911:主线程 main(主线程 LWP 号和进程 PID 相等)
    • LWP=2664912:pthread_create 创建的子线程 Linux 内核不区分进程与用户态线程,全部统一用 task_struct 管理,用户线程在内核层面就是轻量级进程 LWP。
  • 区分执行流严格意义上来说,并不是依靠pid而是lwp

    那么如何杀掉这个呢—->直接选择杀掉进程kill -9  pid

    虚拟地址空间第四讲–页表,虚拟地址  物理地址

    1 物理内存基础管理规则

    1.1 物理内存最小单元:4KB 页框

    内核会把整块物理内存切割成大小统一的页框,单块页框固定 4KB,换算公式4KB=2^12。内存和磁盘交换数据时,读写的最小单位同样是 4KB。

    1.2 页框管理结构体 struct page

    机器上每一块物理页框,都会对应一个 struct page 结构体。结构体内部 flags 采用位图存储页面状态,一位标记一种状态,包含是否空闲、是否脏页、是否锁定、文件缓存、交换分区标记等。使用位图能极大节省内核内存开销。

    1.3 全局数组 mem [] 统一管理所有页框

    系统全部物理页框由全局数组 mem [] 统一索引。32 位 4GB 内存设备,总页框数量:4GB / 4KB = 1048576。

    物理地址与数组下标换算公式
  • 物理地址转数组下标:index = 物理地址 / 4096
  • 数组下标转回页框起始地址:页基址 = index * 4096 同一页框里任意物理地址,最终都会映射到同一个 struct page,页面状态统一。
  • 整体逻辑流程 
  • 物理内存被切分为固定大小页框(4KB 标准物理块),每个页框状态、用途不同,操作系统必须为每一块单独描述、管理。
  • 使用 struct page 结构体作为页框描述符,记录单个物理页框所有信息。
  • 所有物理页框的 struct page 统一存放在全局数组 mem[] 中集中管理。
  • 数组下标和物理页框物理地址可以快速互相换算,实现地址与描述符一一映射。
  • 2 32 位程序虚拟地址空间布局

    32 位系统完整虚拟地址空间共 4GB,分为用户空间、内核空间两大区域:

    • 用户空间(0 ~ 0xC0000000,合计 3GB) 地址由低到高依次存放:程序代码段、初始化全局数据、未初始化数据、堆(地址向上增长)、共享库映射区域、栈(地址向下增长)。
    • 内核空间(0xC0000000 ~ 4GB,合计 1GB) 所有进程共用同一份内核空间,存放内核代码、硬件驱动、全局页表、内核栈等核心资源。

    3 32 位二级分页:虚拟地址 10+10+12 分段规则

     32 位虚拟地址一共 32 个 bit,硬件 MMU 会自动拆分三段:

    1)蓝色段:高 10 位,页目录索引

    • 长度:10bit,取值范围 \\(0 \\sim 2^{10}-1=1023\\),共 1024 个下标;
    • 对应结构:页目录(每个进程独立一张,固定 4KB 物理内存);
    • 作用:以 CR3 存放的页目录物理地址为基址,用这 10 位找到对应页目录项;
    • 页目录项内容:保存一张页表的物理起始地址。

    2)绿色段:中间 10 位,页表索引

    • 长度:10bit,同样 0~1023,单张页表 1024 个页表项;
    • 对应结构:页表(按需分配,进程没用到的虚拟区间不会创建页表);
    • 作用:拿到页表物理地址后,用这 10 位找到页表项 PTE;
    • 页表项核心:存放物理页框的起始地址,同时附带权限位(读 / 写 / 执行)、存在位、脏位。

    3)红色段:低 12 位,页内偏移

    • 长度:12bit,\\(2^{12}=4096\\),正好对应 4KB 一页;
    • 关键特性:不参与页表查找;
    • 作用:找到物理页框首地址后,直接加上这 12 位偏移,定位页内具体的字节;
    • 举例:物理页框起始地址 0x1000,偏移 0x0FF → 最终物理地址 0x10FF。

    每张页表同样包含 1024 个页表项 (PTE),单张页表 4KB; 一个页目录项对应一张独立页表,图里画出 3 张页表代表不同虚拟区间。

    一条虚拟地址完整翻译流程

  • CPU 执行代码,输出 32 位虚拟地址,送入 MMU;
  • MMU 拆分虚拟地址:高 10 位、中间 10 位、低 12 位;
  • 从 CR3 寄存器读取当前进程页目录物理首地址;
  • 高 10 位索引页目录,取出对应页表的物理地址;
  • 中间 10 位索引页表项,拿到物理页框首地址;
  • 物理页框地址 + 低 12 位页内偏移 = 最终物理内存地址;
  • 硬件访问物理内存;
  • 若页表项 “存在位 = 0”:代表该虚拟地址对应的页面在磁盘交换区,触发缺页异常,内核加载页面到物理内存后重试。
  • 页表大小:4MB+4kb

    单个进程不可能拥有全部内存,当前进程页表总数远远小于4MB

    懒加载,写实拷贝,缺页中断—>页表少–>页表会更少

    写时拷贝会讲整个4kb进行拷贝—–>局部性原理(磁盘上的数据可以提前加载到内存)

    任何一个页框的地址,用多少位就够了

    页框的起始地址低12位都是0—->用20比特位表示页框的起始地址,对地址进行相应的右移12位,左移12位来完成—>20位表示页表的地址剩下来12位—>标志位,

    以上工作由硬件完成mmu,因为效率高

    4 CR3 寄存器核心知识点

    4.1 CR3 寄存器存储内容

    CR3 是 CPU 专属硬件寄存器,里面存放的是当前进程页目录的物理起始地址,绝对不能存放虚拟地址。 原因很容易理解:翻译虚拟地址需要依赖页目录,如果 CR3 存虚拟地址,想要读取页目录又要翻译地址,会形成无限循环。

    4.2 CR3 和进程、线程切换的关联

  • 硬件层面:整颗 CPU 只存在一个 CR3 寄存器;
  • 内核层面:每个进程的 task_struct 结构体,都会保存自身页目录的物理基地址;
  • 进程切换完整流程: ① 保存旧进程:把当前 CR3 的值存入旧进程的 task_struct; ② 加载新进程:读取新进程存储的页目录物理地址,写入 CR3; ③ CR3 更新完成后,MMU 地址翻译规则同步切换,实现进程地址空间隔离。
  • 线程切换特殊点:同一个进程所有线程共用一套页目录,切换线程不用修改 CR3,这也是线程切换开销远小于进程的关键原因。
  • 5 MMU 完成虚实地址转换完整流程

  • CPU 执行指令,输出 32 位虚拟地址,送入内存管理单元 MMU;
  • MMU 读取 CR3 寄存器,拿到当前进程页目录的物理基地址;
  • 截取虚拟地址高 10 位作为索引,查询页目录,得到对应页表物理地址;
  • 截取虚拟地址中间 10 位作为索引,读取页表内部 PTE 页表项;
  • 判断 PTE 有效标记位:
    • 标记有效:提取物理页框基地址,拼接低 12 位偏移,访问物理内存;
    • 标记无效:触发缺页异常,内核分配物理页、从磁盘加载对应数据、更新页表项,之后重新执行出错指令。

    拥有更多的虚拟地址—>拥有更多的内存,只要我们将虚拟地址进行划分,本质就是划分物理内存

    页表的本质:是进程看到内存资源的窗口,拥有更多的虚拟地址,拥有更多的内存,划分区域,划分虚拟地址,在编译阶段进行划分.

    linux在物理上,存在真正意义上的线程吗??—>不存在,没有TCB,只存在轻量级进程

    进程拥有独立性大部分资源独立

    线程拥有共享性,大部分资源共享

    总结

  • CR3 寄存器只存放页目录物理地址;切换进程需要修改 CR3,同进程切换线程无需修改;
  • PID 区分不同进程,LWP 区分同进程内多条线程;多线程程序所有线程 PID 完全相同;
  • clone 依靠 flags 标识控制资源共享,CLONE_VM 是进程和线程的分界线,fork 是 clone 的特殊实现;
  • 32 位二级分页虚拟地址分段固定 10+10+12,低 12 位对应 4KB 页框偏移;
  • struct page 管理单块物理页框,全局 mem [] 数组索引整机所有物理内存;
  • Linux 没有独立线程内核结构,线程就是轻量级进程 LWP,进程、线程统一由 task_struct 调度;
  • pthread 库只是用户层封装,底层创建线程依赖 clone 系统调用。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux之线程(一)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!