03.进程和线程.md

3. 进程是什么

3.1 进程包含的东西

进程的直观定义很多

  1. 一个正在执行的程序
  2. 内分配给处理器并由处理执行的实体

从操作系统管理的角度来说,我们可以把进程视为一组元素组成的实体。
进程的两个基本元素是程序代码和与代码相关的数据集。在进程进行的任何时候,可以用下面的一些元素来描述一个进程。

  1. 标识符:进程相关的唯一标识符
  2. 状态:运行时,就绪,阻塞等等
  3. 优先级
  4. 程序计数器
  5. 内存指针:包括程序代码和进程相关的数据指针
  6. 上下文数据:进程执行的时候寄存器中的数据
  7. I/O状态信息:显式IO请求,分配给进程的IO设备和被进程使用的文件列表

3.2 进程状态

3.2.1 操作系统常见状态定义
  1. 执行态:进程正在处理器运行
  2. 就绪态:表示进程已在内存中,已经分配到除CPU以外的资源,等CPU调度它时就可以马上执行了
  3. 阻塞态: 进程已在内存中并等待一个事件
  4. 阻塞-挂起态:进程已经在外存当中,并且在等待一个事件
  5. 就绪-挂起态:进程已经在外存当中,但是只要载入内存中就可以执行
3.2.1 linux系统状态定义

这里主要看看linux进程的状态,这一段参考了这里
执行态,可中断阻塞态,不可中断阻塞态,停止,僵死态

  1. 执行态:R (TASK_RUNNING),可执行状态,包括正在cpu执行的和就绪状态的(就绪表示进程已在内存中,已经分配到除CPU以外的资源,等CPU调度它时就可以马上执行了)。
  2. 可中断阻塞态:S (TASK_INTERRUPTIBLE),处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
    通过ps -aux命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。
  3. 不可中断阻塞态: 与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态。
    而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。
    在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到
  4. 终止态:X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁
  5. 僵死态:Z (TASK_DEAD - EXIT_ZOMBIE),进程成为僵尸进程,进程在退出的过程中,处于TASK_DEAD状态。
    在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。比如在shell中,$?变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为if语句的判断条件。当然,内核也可以将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。但是使用task_struct结构更为方便,因为在内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。释放掉task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。
    父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。
    在这里插入图片描述

3.3 进程的控制

3.3.1. 执行模式

执行模式分为 用户模式系统模式(也称为内核模式,控制模式等)。
处理器如何才能知道他正在什么模式下运行,模式如何变化。
程序状态字中通常会有一个指示模式的位。该位会因事件的改变而改变,当发生系统调用或者中断的时候,执行模式会被设置为内核模式。在这些程序执行完成后,会自动将该状态字复位。

3.3.2. 进程的创建
  1. 为新进程分配一个唯一的进程标识符
  2. 为进程分配空间
  3. 初始化进程控制块
  4. 设置正确的链接
  5. 创建或扩充其他数据结构
3.3.3. 进程的切换时机
  1. 时钟中断
  2. I/O中断
  3. 内存失效
  4. 系统调用:信号量,I/O操作等
3.3.3.4 进程的模式切换

是指进程因系统调用等从用户模式进入内核模式,他没有进行进程的切换,只是需要保存处理器状态信息的控制部分,包括程序计数器,其他处理器寄存器和栈信息。

3.3.3.5 进程切换
  1. 保存处理器的上下文,包括程序计数器和其他寄存器。
  2. 更新当前处于运行态进程的进程控制块,包括把进程的状态改变为另一状态(就绪态、阻塞 态、就绪/挂起态或退出态)。还须更新其他相关的字段,包括退出运行态的原因和记账信息。
  3. 把该进程的进程控制块移到相应的队列(就绪、在事件‘处阻塞、就绪/挂起)。
  4. 选择另一个进程执行。
  5. 更新所选进程的进程控制块,包括把进程的状态改为运行态。
  6. 更新内存管理数据结构。是否需要更新取决于管理地址转换的方式。
  7. 载入程序计数器和其他寄存器之前的值,将处理器的上下文恢复为所选进程上次退出运行时的上下文

3.4 操作系统的执行

操作系统的执行方式也有多种,常见的有三种

3.4.1 无进程内核

在许多老的操作系统中采用了这种方式,若当前用户进程发生系统调用或者产生一个中断,则会保存该进程的模式上下文,控制权转交给内核。操作系统具有本身控制过程的调用和返回的系统区域与系统栈。操作系统可以完成任何预期的功能并恢复被中断的进程的上下文,恢复中断进程的执行或者执行下一个进程。
无论上面什么功能实现,关键是这一概念只使用与用户程序,而操作系统则是在特权模式下单独运行的实体。

3.4.2 在用户进程内运行

这个是目前常用的方式,在用户进程的上下文执行操作系统软件,在中断,系统调用发生的时候,处理器cpu处于内核模式,但是不会保存当前进程的上下文,在后面恢复的时候如果还是调度给当前进程的话,那么久不需要进行上下文切换,只需要修改运行模式即可。

3.4.3 基于进程的操作系统

操作系统作为一个独立的进程执行。

4. 线程

4.1 什么是线程

进程具有如下两个个特点

  1. 资源所有权
  2. 调度执行

  这两个特点是独立的,因此操作系统应该能够分别处理他们。现代操作系统为了区分这一点,将cpu调度的基本单位成为线程(或者叫轻量级进程),而将资源所有权的单位成为进程或者任务。
但线程和多线程进程模型

4.2 用户级和内核级线程

用户线程和内核线程的对应关系分成三种。

  1. 一个进程的所有用户级线程对应一个内核级线程,这种是纯用户级线程,面对io调用等就不行了,会阻塞整个进程。python就是这样的
  2. 一个进程的每个用户级线程都对应了一个内核线程,这种就是内核支持的线程或轻量级进程,这种不会有阻塞,但是线程切换的代价大一些,都是在内核态进行切换,java就是这样的,也是主流
  3. 一个进程中m个用户级线程对应了n个内核级线程,m>n,这种调度起来比较复杂

**

4.3 线程和进程的区别

  1. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
  2. 因为上面的原因,线程的切换代价要小的多。

5. 处理器调度

处理器的调度涉及到很多调度算法之类的知识,对于这些东西,我觉得对于工程上实际意义不是很大,在这里只是聊一聊工作中遇到的一些与调度有关联的优化问题。

5.1 调度分类

  1. 长程调度:长程调度决定哪个进程可以进入系统中处理(并不是指cpu下一步去运行他,而是加入可以被cpu进行调度管理的相关队列当中)
  2. 中程调度: 中程调度是进程在内存和磁盘之间的调度(这一块儿在使用虚拟内存以后实际上交给内存管理模块了)
  3. 短程调度: 被称为分派程序,在三种调度中是执行的最频繁的
    在这里插入图片描述

5.2 短程调度发生的原因

  • 时钟中断
  • I/O中断
  • 操作系统调用
  • 信号(如信号量)

5.3 单处理器调度和多处理器调度

cpu绑定:
在处理器进行调度的时候,实际上是有两种调度的,为了简化这种讨论,我们先假设

  1. 只有一个处理器工作。
  2. 进程的模型是多线程的,有3个进程A,B,C,每个进程有5个线程(比如A1,A2,A3,A4,A5)

在线程从A1–>A2的时候只发生了线程调度,没有进程调度,切换上下文的时间较短
如果线程从A1–>B2这个时候发生的则是进程的切换(包含了线程的切换),相对来说代价更大。
在多核处理器当中,调度的基本模型是线程,所以会比这个更复杂。其基本原则肯定是希望将cpu的利用率更高,同时用在切换线程上的代价尽可能的小。

  所以一个进程的多个线程有可能是在不同的处理器上同时运行的,这样提高了并行的效率。但是对于某些单线程的进程,如果处于阻塞的时机比较少(大多是非阻塞式操作),而且对性能要求比较高的,是可以采取绑定cpu的方式的。比如redis的常规优化中就有将redis进程绑定到某个cpu核上,当然,并不会完全抢占该cpu核(完全抢占是指该cpu只能执行该进程),但是操作系统的调用会倾向于这么做,也就是进可能的让该cpu核多的执行该进程,那么这样的话,redis的效率反而是得到了提升的。可以参看这里

补一张AMD推土机结构,这也是多核cpu的缓存常见处理方案
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值