欢迎光临
我们一直在努力

[linux仓库]线程与进程的较量:资源划分与内核实现的全景解析[线程·贰]

🌟 各位看官好,我是egoist2023!

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的指令知识,并学会灵活使用这些指令。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

Linux线程控制

多线程角度理解资源"划分"

可执行程序角度理解资源"划分"

进程 vs 线程

线程背景

Linux多线程的实现 — 内核角度

pthread库

进程vs线程, 线程其他理论话题

面试题

线程优点

线程缺点

线程异常

线程用途

哪些资源共享,哪些独占

进程和线程

进程的多个线程共享

总结


Linux线程控制

为了方便理解资源划分的本质,这里直接通过编写代码从实践再到理论.

多线程角度理解资源"划分"

int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg); 功能:创建一个新的的线程(注意:该函数是第三方库,不属于C/C++)

参数:

  • thread:返回线程ID(输出型参数)
  • attr:设置线程的属性(优先级,栈大小之类),attr为NULL表⽰使⽤默认属性
  • start_routine:回调函数(函数指针类型),线程启动后要执⾏的函数
  • arg:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

void *thread_routine(void *arg)
{
std::string name = (const char *)(arg);
// 第二个死循环
while (true)
{
std::cout << "我是新线程…,名字: " << name << ", pid: " << getpid() << std::endl;
sleep(1);
}
}

int main()
{
fun();
pthread_t tid;
pthread_create(&tid, NULL, thread_routine, (void *)"thread-1");
while (true)
{
std::cout << "我是主线程…, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}

我们之前讲过如果是使用第三方库必须要指令库名称,而库路径是可以被放到特定路径下的.

g++ *.cc -pthread(指定库名称)

编译执行该可执行程序后:

疑惑一:一个单进程代码,可能让两个死循环跑起来??不可能!!这说明一个进程内部,现在已经有两个不同的执行流!!!

疑惑二:主线程和新线程的pid是一样的啊!和之前创建进程是不一样的,使用ps ajx查看也确实存在一个进程.

使用ldd指令查看该可执行程序所依赖的动态库:

疑惑三:我们不是说依赖第三方库吗?可是我怎么见到所谓的thread库呢?这是由于我是ubuntu24.04的OS,thread被集成进libc里了,而在ubuntu22.04确实能找到该库.

可执行程序角度理解资源"划分"

main函数和函数都有地址,一个函数所对应的地址难道就只有一个吗?

thread –>一个可执行程序 –>一个进程呀于 –>有几套页表?–>1套页表

反汇编观察该可执行程序:objdump -S thread > test.s

  • 进程页表的本质是什么?如果没有页表,虚拟地址空间就没有意义,也不能找到对应的物理内存,是进程看到资源的”窗口!
  • 所以,谁拥有更多的虚拟地址,谁就拥有更多的物理内存资源!
  • 对函数进行编址,让不同的执行流,执行不同的函数函数是虚拟地址的集合!
  • 让不同的线程,执行不同的函数本质是让不同的线程,通过拥有不同区域的虚拟地址拥有不同的资源!
  • 通过函数编译的方式,进行了进程内部的"资源划分"
  • 进程 vs 线程

    • 进程: 一个运行起来的执行流,一个加载到内存中的程序(教材),进程 = 内核数据结构 +自己的代码和数据
    • 线程:进程内部的一个执行流,轻量化

    如何理解进程内部的一个执行流?

    Linux中,一个线程,在进程内部运行?线程在进程的虚拟地址空间中运行!!!

    如何理解轻量化?

    让不同的"线程”访问虚拟地址空间钟的一部分资源!

    如何做到?让不同的线程未来执行不同的入口函数!

    • 观点:进程是系统分配资源的基本单位(内核角度,给进程下的定义),线程是CPU调度的基本单位

    我认为单从上面的概念来看,我们是会一头雾水的,只有等讲解到底层实现时才能反过来更好理解以上的含义.

    线程背景

    线程的提出本质上是为了解决进程在并发场景下的效率问题和资源浪费问题,是操作系统在 “并发能力” 和 “资源开销” 之间寻找平衡的产物。

    既然已经提出了"线程"这个场景的需求,自然也需要有各OS去实现对应的线程.而我们说一个进程中可能存在多个线程,线程也需要被调度,被切换等,因此也需要对线程进行管理,如何管理?

    先描述,在组织 –> struct TCB –> 线程控制块

    实现容器化管理线程TCB,也要有运行队列、等待队列、挂起,调度算法.

    那么,我们真的有必要单独创建 “线程” 吗?它的行为模式看起来与进程颇为相似,这难道不是在做无用功吗?

    实际上,不同操作系统对线程的实现思路存在差异:在 Windows 系统中,确实明确实现了 “线程” 这一独立概念;而在 Linux 系统中,线程的实现方式更为巧妙 —— 它通过创建多个 PCB(进程控制块),让这些 PCB 共同指向同一个进程的虚拟地址空间。每个 PCB 会负责执行该进程代码区中的特定部分,且仅访问分配给自身的资源区域,这便是 Linux 系统中 “线程” 的本质实现形式。

    Linux多线程的实现 — 内核角度

    先描述,在组织-> struct TCB -> 线程控制块实现容器化管理线程TCB, 调度算法。

    Linux系统,内核中,线程的实现,是用进程模拟的,复用了进程代码和结构!!!

    我们之前提到,每个进程都需要分配一个 PCB;但从现在的视角来看,每个进程实际上至少需要一个 PCB。这些 PCB 会被放入 CPU 的运行队列,等待调度执行。

    CPU看到的都叫做执行流: task struct 粒度<= 传统的进程(谈及执行流,CPU角度:不区分进程、线程,统一叫做,轻量级进程!!!)

    此时我们再来理解进程和线程:

    什么叫做进程?承担分配资源的基本实体 –> 如何理解我们以前讲的进程? –> 进程内部,只有一个执行流(线程)的进程

    什么叫做线程?OS调度的基本单位 –> 一个 task_struct 叫做线程

    今天的进程 –> 进程内部,可以有1个或者多个(执行流)的进程(只有一个执行流的进程是一种特殊情况)

    但我认为仅靠上面这些并不能够支撑我们去更好地理解进程和线程,因此这里讲一个小故事:

    在现实世界中,分配社会资源的基本单位是什么?家庭.

    一个家庭内部,是有多个家庭成员的.母亲负责每天早起做早餐,给家里人提供餐饭;父亲每天搭乘着地铁赶路,从早到晚都在上班,给家里人提供经济来源;孩子每天都需要去上学,每天好好学习,不让父母来操心.

    每一个家庭成员都各自在做着不同的事情.可是,有没有发现大家都有一个相同的目的:过好日子.

    而这里的家庭就是所谓的进程,家庭成员就是一个个的线程,线程被创建出来做不同的事情,而他们被创建出来的目标是为了达成一个目标 — 过好日子.

    一个家庭内部,有多个成员 —进程内具有多个执行流(线程);

    一个家庭内部,只有一个成员!!

    pthread库

    有没有对应的指令可以查看一个进程内部有多少个轻量级进程呢?

    ps -aL | head -1 && ps -aL | grep thread

    进程vs线程, 线程其他理论话题

      面试题

      与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多!少体现在哪些地方呢?

      • 线程切换不需要切换CR3寄存器,因为CR3保存页表基地址,只要CR3不切就是线程间切换,切了就叫进程间切换;
      • TLB->缓存虚拟到物理地址 —线程间切换,TLB不需要更新(进程间切换,TLB要重新缓存)
      • 进程内的多线程切换,cache缓存,不用更新 ; 但是进程间切换,就要重新把cache缓冲区“热起来
      • 进程间切换,虚拟地址空间就切了,所付出的成本更大;
      • CPU也是要统计每个线程的时间片,怎么知道?线程的pid是相等的,进程的所有线程的时间片被花完了,那么当前进程pid找到所有线程创建新线程,时间片会被均分.(OS怕被偷时间片)
      • 线程切换仍需保存和恢复硬件上下文,但进程间切换不也需要么;

      cat /proc/cpuinfo

      /proc/cpuinfo由内核动态生成,存储了当前系统中 CPU 的详细信息。其中就记录了cache缓存.

      线程优点

      • 创建⼀个新线程的代价要⽐创建⼀个新进程小得多
      • 与进程之间的切换相比,线程之间的切换需要操作系统做的⼯作要少很多
    • 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
    • 另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。
      • 线程占⽤的资源要⽐进程少很
      • 能充分利用多处理器的可并⾏数量
      • 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
      • 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现(不要太多)
      • I/O密集型应用,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作(可以多一些)。
      • 建议是cpu个数*核数,大部分操作都是内存级操作,如果创建太多了,额外的增加OS调度线程时切换线程的成本

      线程缺点

      • 性能损失:⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指的是增加了额外的同步和调度开销,⽽可⽤的资源不变。
      • 健壮性降低:编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护的。
      • 缺乏访问控制:进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。
      • 编程难度提⾼:编写与调试⼀个多线程程序⽐单线程程序困难得多

      线程异常

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

      线程用途

      • 合理的使⽤多线程,能提⾼CPU密集型程序的执行效率
      • 合理的使⽤多线程,能提⾼IO密集型程序的用户体验(如⽣活中我们⼀边写代码⼀边下载开发⼯具,就是多线程运行的⼀种表现)

      如今我们终于可以理解,一个线程出现异常,整个进程为何会崩溃?

    • 线程是代表一个进程;
    • 线程出现异常,OS就会给进程发送信号,就会被杀掉,而进程的资源就会被释放,而线程用的资源是进程提供的,没有生存空间.
    • 哪些资源共享,哪些独占

      • 进程间具有独立性(进程更强调独占,偶尔要有共享)
      • 线程共享地址空间,也就共享进程资源(线程更强调共享,偶尔要有独占)

      进程和线程

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

      线程是调度的基本单位

      线程共享进程数据,但也拥有⾃⼰的⼀部分数据,对于前3点是必答的,只有调度,,才需要切换上下文;除了被调用,还会产生各种临时数据,也要有函数调用,栈帧结构: 

      •  线程ID
      • ⼀组寄存器
      • errno
      • 信号屏蔽字
      • 调度优先级

      进程的多个线程共享

      同⼀地址空间,因此Text Segment、Data Segment都是共享的,如果定义⼀个函数,在各线程中都可以调用,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

      • ⽂件描述符表
      • 每种信号的处理⽅式(SIG_ IGN、SIG_ DFL或者⾃定义的信号处理函数)
      • 当前⼯作⽬录
      • 用户id和组id

      进程和线程的关系如下图:

      总结

      本文介绍了Linux系统中线程控制的核心概念与实现原理。主要内容包括:

    • 通过pthread_create函数创建线程的实践方法;
    • Linux内核使用进程模拟线程的独特实现方式(轻量级进程);
    • 进程与线程的区别与联系,强调进程是资源分配单位而线程是调度单位;
    • 线程切换相比进程切换的性能优势(如无需切换页表、TLB等);
    • 线程的应用场景与优缺点分析。文章通过代码示例和家庭比喻生动阐释了多线程工作机制,并指出线程异常会导致整个进程终止的原因。最后总结了线程共享的进程资源和独有的线程特性,帮助读者深入理解Linux多线程编程的本质。
    • 赞(0)
      未经允许不得转载:171主机测评 » [linux仓库]线程与进程的较量:资源划分与内核实现的全景解析[线程·贰]
      分享到: 更多 (0)

      评论 抢沙发

      • 昵称 (必填)
      • 邮箱 (必填)
      • 网址