首页 > 系统相关 >第1讲:进程和线程

第1讲:进程和线程

时间:2024-06-10 10:36:22浏览次数:13  
标签:调度 线程 切换 进程 CPU 内核

扫盲课。对 Linux 系统下,进程和线程的基本概念和对比进行阐述。

一、进程

进程是处于执行期的程序及相关资源的总称。操作系统为进程提供两种虚拟机制:虚拟处理器 & 虚拟内存,目的是让进程有一种假象:“独享处理器和整个内存空间”。

关于进程描述符 struct task_struct 放在后续内容中解释。

(一)进程状态机

这里我们介绍两个版本的“进程状态机”。

(1)从逻辑上进程可分为五种状态:创建、运行、就绪、阻塞、结束。
进程的五种逻辑状态

(2)也可根据 task_struct 的五个标志位进行分类:

  • task_running:图中的“就绪队列”和“运行队列”。
  • task_interruptible:阻塞挂起,收到某些信号后反应。
  • task_uninterruptible:忽略信号,fork(clone调用)会用到。
  • __task_traced:被其他进程跟踪调试,比如 ptrace。
  • __task_stopped:停止。不仅是退出,调试期间也会进入该状态。
    进程状态转换

(二)常见的进程

(1)孤儿进程

  • 子进程退出时发现父进程已经退出了,会导致子进程的资源无法正确释放而泄露。
  • 避免孤儿进程:在进程结束时会寻找新的父进程或托管给0号进程,即 init 进程。

(2)僵尸进程

  • 子进程在运行结束退出时,内核会释放所有的资源;但是仍然会保留其进程描述符中的相关信息,直到父进程通过 wait 或 waitpid 来获取这些信息时才释放。如果不释放这些信息就会变成“僵尸进程”。
  • 注意:子进程销毁时会临时进入“僵死状态”,而不是“僵尸进程”,注意区分。
  • 避免僵尸进程:子进程在退出时向父进程发送信号,让父进程调用wait函数释放。

(3)守护进程

  • 运行在后台、独立于运行终端、属于1号进程管理。用于周期性的执行某种任务或等待处理某些发生的事件(比如 ssh 服务器、ftp 服务器等)。
  • 创建守护进程:fork调用后父进程退出,使子进程变成孤儿进程,归1号进程管理。

二、线程

线程是进程中的一条执行流。Linux没有实现专门的线程,Windows下专门实现了“轻量级进程”。

同一个进程内多个线程之间可以共享:代码段、数据段、堆区(打开的文件)等资源,但每个线程各自都有一套独立的PID、栈(指针)、PC计数器、程序运行所需的寄存器,从而确保线程的控制流是相对独立的。

主要是私有栈:用于保存函数调用、局部变量以及其他临时数据,不能让其他线程访问。

主线程的私有栈是占用进程虚拟内存空间的栈内存;其他线程的私有栈则是共享内存映射区的内存。

(一)进程崩溃

C/C++进程中的一个线程崩溃时,有可能会导致其所属进程的所有线程崩溃。

  • 理论上每个线程都是独立执行的,线程的崩溃通常只会影响到它本身,所以不会导致整个进程崩溃。
  • 线程之间共享地址空间和数据资源,导致这些共享资源处于不一致的状态,该不确定性会让OS直接结束当前进程。

因为各个线程的地址空间是共享的,所以某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程。既然这种操作是危险的,OS会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃

有一些特殊情况下,线程的崩溃可能会影响整个进程。

  • 主线程的崩溃:主线程通常负责启动和管理其他线程,所以它的崩溃会导致进程终止,进而导致子线程终止。
  • 关键资源的破坏:关键资源(如共享内存区域或文件句柄)受损或无法使用,其他线程可能会受到影响而导致进程的异常终止。
  • 无法处理的异常:行为引发无法恢复的异常,例如访问非法内存地址或遇到不可处理的硬件错误。

结束进程的方式:使用 kill 系统调用向进程发送信号。也支持自定义信号处理回调函数 sigHandler,在结束前“垂死挣扎”一番。

可以通过 kill -l 查看所有信号。比如常见的 kill -9 pid_num 就是一种信号,-9 代表 SIGKILL 信号,该信号会忽略任何信号处理函数,会马上干掉该进程。

Java 不会崩溃:JVM 有自定义的信号处理函数,会执行资源清理后再 exit 退出。

举例:StackOverflow 报错,如果发生无限递归会导致栈帧(默认为 8M 大小)被占满。

当发生崩溃后,JVM 自定义的信号处理函数会拦截 SIGSEGV 信号、恢复进程的执行、抛出 StackoverflowError 和 NPE 两个错误供我们捕获。

(二)分类:用户线程 & 内核线程

线程控制块TCB(Thread Control Block)

(1)用户线程(逻辑线程)

  • 在用户态下通过“线程管理库”创建、管理、运行,不受内核空间的调度。Linux下的线程管理库 pthread 用法之后的文章会具体分析。
  • 不能参与 CPU 的抢占,只能共享进程的时间片,实现“线程的并行”。
  • 操作系统只能看到进程的PCB,看不到线程的TCB。因此操作系统不直接参与用户线程的线程管理和调度,一切由用户级线程库函数管理,包括线程的创建、终止、同步和调度等

优点:(1)通过 TCB 可以在不支持线程技术的OS中使用线程;(2)切换时无需内核态和用户态的切换,所以切换速度很快;

缺点:同一进程中只能同时有一个线程在运行,所以(1)不能充分利用多核CPU;(2)如果一个线程发起系统调用而阻塞,会导致整个进程中所有线程的阻塞。

(2)内核线程(物理线程)

  • 又称为“守护进程”。运行在“内核态”,只能由内核进行管理调度,也是内核最小的调度单位。
  • 参与 CPU 的抢占,可以实现真正的多核并发。由于它的数据结构和堆栈很小,所以它的创建、调度的开销比进程小的多。

一个内核线程阻塞不影响同一进程下的其他线程,可以在多核处理器中执行。
举例:中断的下半部分为“软中断”,便是通过内核线程的方式进行实现的。

(3)对比

  • 内核支持:用户线程可在一个不支持线程的OS中实现;内核线程则需要得到OS内核的支持。因此上一条、两者建立联系的前提是OS必须支持内核线程。
  • CPU分配:内核只为用户线程所在的进程分配一个处理器,用户线程通过时间片调度的方式竞争该处理器;内核线程则可以在多个处理器上并行。
  • 调度:只有内核线程才是CPU调度的单位,用户线程则通过所在的进程去调度。

根据上述原因,用户线程必须要映射到内核线程上,才能让内核看到 & 调度用户线程执行,从而让用户态的多线程实现并发执行。

建立映射的方式有:一对一模型、混合线程模型(多对一、多对多)。也要考虑到OS对线程数量的限制、选择合适的模型。

创建和管理大量的内核线程需要消耗系统资源,包括内存和调度开销;并且线程之间的切换也需要时间和开销,频繁的上下文切换会导致开销增加。

(三)多线程/线程池的简单入门

根据CPU核数不同,多线程实际上分为“并行”和“并发”两种。我们最感兴趣的其实是“线程池”(Thread Pool)技术。它专门用于管理和复用多个线程,能极大减小线程的创建和销毁的开销。适用于需要处理大量短时间任务的场景,并能根据系统的承受能力调整可运行线程的数量。

一个简单的线程池由三部分组成:

  • 线程池管理器(Thread Pool Manager):负责线程池的创建、销毁和管理;并维护着线程队列和任务队列,并负责分配任务给空闲线程执行。
  • 线程池(Thread Pool):由一组线程组成,当线程池中的线程处于可用状态时,便可以执行提交给线程池的任务。
  • 任务队列(Task Queue):用于存储待执行的任务,当线程池中的线程空闲时,会从任务队列中获取任务进行执行。

线程池的工作步骤有以下几步:

  • 初始化线程池,创建一定数量的线程并将其置于可用状态(阻塞状态)。
  • 当有任务提交给线程池时,线程池从任务队列中获取任务。
  • 线程池分配空闲线程来执行任务。
  • 执行完任务后,线程继续保持可用状态,返回到线程池中,等待下一个任务。
  • 如果任务队列为空且没有待处理的任务,空闲线程等待新任务的到来。

三、进程和线程的区别

  • “进程”是资源分配的最小单位,“线程”是程序执行(CPU 调度)的最小单位。
  • 地址空间是否独立。每一个进程都有它自己的独立地址空间,一般不会发生读写同一块内存(共享内存除外);线程共享所在进程的地址空间,对于临界区数据需要进行同步或互斥处理。
  • 上下文切换的开销不同。(1)进程的上下文切换开销比较大:需要切换像虚拟内存等页表,对 Cache 缓存机制影响较大;(2)同一个进程的线程切换很快:只需要切换像“寄存器”和“栈”等私有数据即可;但如果是不同进程的线程进行切换,那么和进程切换一样

上下文切换会导致 CPU Cache 缓存全部作废,虚拟内存的切换会导致 Page Cache(TLB)全部失效,导致在一定时间范围内、内存访问的低效

  • 创建方式不同。两者都执行 fork 系统调用(底层为 clone 系统调用),线程会传入代表共享资源的参数标志,创建速度更快(不涉及像内存、文件等资源管理信息);此外进程还有 COW 策略,以及创建子进程后执行 exec() 优先运行子进程。
  • 通信方式。进程之间通信记为 IPC(比如共享内存);线程因为共享内存和文件资源,不需要经过内核,所以数据交换效率很高。

标签:调度,线程,切换,进程,CPU,内核
From: https://www.cnblogs.com/7ytr5/p/18236737

相关文章

  • 避免 OOMKilled:在 Kubernetes 环境中优化 Java 进程的内存配置
    避免OOMKilled:在Kubernetes环境中优化Java进程的内存配置DevOps云学堂译 奇妙的Linux世界 2024-06-1009:53 重庆 听全文公众号关注 「奇妙的Linux世界」设为「星标」,每天带你玩转Linux! 管理KubernetesPod中运行的Java进程的内存使用情况比人们想象......
  • 第3讲:进程调度
    分为两类:抢占式多任务非抢占式多任务进程可分为:IO消耗型、CPU消耗型。调度方式(1)优先级调度nice值(-20~19):值越大、优先级越低。nice值映射到时间片问题:(1)绝对时间片无法保证最优解;(2)nice值越靠近边界、波动越大;(3)定时器节拍问题实时优先级。(2)时间片CFS调度分配的是......
  • 第2讲:进程管理
    本文的主要内容:一个进程从生到死的过程。一、任务队列和task_struct任务描述符Linux的“任务队列”是一个双向链表,链表中每一项为进程描述符task_struct,它包含了一个正在执行的程序的完整信息:它打开的文件、进程的地址空间、挂起的信号、进程的状态等等。Linux通过slab分......
  • 线程池代码详解
    线程池概念线程池是一种基于池化技术的多线程管理机制。在这种模式下,一组线程被创建并且维护在一个"池"中,这些线程可以被循环利用来执行多个任务。当有新的任务到来时,线程池会尝试使用已经存在的空闲线程,而不是每次都创建新线程。这样做的目的是为了减少因频繁创建和销毁线程所带......
  • JUC及多线程,线程安全
    JUC及多线程返回到Java开发知识汇总目录@程序员猴哥1.什么是JUCjava.util.concurrent:核心并发工具类。java.util.concurrent包含了许多线程安全,测试良好,高性能的并发模块。创建java.util.concurrent的目的就是要实现Collection框架对数据结构所执行的并发操作。核心......
  • 【QT5】<总览五> QT多线程、TCP/UDP
    文章目录前言一、QThread多线程二、QT中的TCP编程1.TCP简介2.服务端程序编写3.客户端程序编写4.服务端与客户端测试三、QT中的UDP编程1.UDP简介2.UDP单播与广播程序前言承接【QT5】<总览四>QT常见绘图、图表及动画。若存在版权问题,请联系作者删除!一、QThre......
  • android主线程与子线程
    创建子线程创建子线程在android中穿件子线程的方案很简单创建子线程的几种方法///////第一种///////classThreadoneextendsThread{@Overridepublicvoidrun(){}//重写run方法,这个方法就是子线程一旦启用就会执行的方法}newThreadone().start()//启动子线程/......
  • C++ MPI多进程并发
    下载用法mpiexec-n8$PROCESS_COUNTx64\Debug\$TARGET.exe 多进程并发启动mpiexec-fhosts.txt-n3$PROCESS_COUNTx64\Debug\$TARGET.exe  联机并发进程,其它联机电脑需在相同路径下有所有程序//hosts.txt192.168.86.16192.168.86.123192.168.86.108De......
  • python 字典是不是线程安全的
    Python字典(dict)对象本身不是线程安全的。在多线程环境下,对同一个字典对象的读写操作需要额外的同步机制来确保线程安全性。如果需要在多线程环境下使用线程安全的字典,可以使用collections.Counter对象,它是线程安全的,或者使用threading.local,它提供了线程局部存储的功能。另外......
  • “另一个程序已锁定文件的一部分,进程无法访问 打不开磁盘“G:\Ubuntu20.04.3\Ubuntu
    文章目录前言:一、删除lck文件二、移除挂载硬盘总结:前言:在重新刷了系统进行对虚拟机移植的过程中我遇到了“另一个程序已锁定文件的一部分,进程无法访问打不开磁盘"G:\Ubuntu20.04.3\Ubuntu20.04.3.vmdk"或它所依赖的某个快照磁盘……”的问题,因为情况慌乱,所以我没......