操作系统进程的实现---上---04

2022-07-12 15:55:57 浏览数 (1)

操作系统进程的实现---04

  • 温故知新
    • 计算机硬件、操作系统、启动、接口…
    • 知新
  • 进程概念
    • 管理CPU,先要使用CPU…
    • 管理CPU的最直观方法
    • 多道程序、交替执行,好东西啊!
    • 一个CPU面对多个程序?
    • 修改寄存器PC就行了吗?
    • 引入“进程”概念
  • 多进程图像
    • 多个进程使用CPU的图像
    • 多进程图像从启动开始到关机结束
    • 多进程图像:多进程如何组织?
    • 多进程的组织:PCB 状态 队列
    • 多进程图像:多进程如何交替?
    • 交替的三个部分:队列操作 调度 切换
    • 多进程图像:多进程如何影响?
      • 进程执行时的100…
    • 多进程图像:多进程如何合作?
    • 从纸上到实际:生产者-消费者实例
    • 两个合作的进程都要修改counter
    • 核心在于进程同步(合理的推进顺序)
  • 用户级线程
    • 多进程是操作系统的基本图像
    • 是否可以资源不动而切换指令序列?
    • 多个执行序列 一个地址空间是否实用?
    • 开始实现这个浏览器…、
    • Create? Yield?
    • 两个执行序列与一个栈…
    • 从一个栈到两个栈…
    • 两个线程的样子:两个TCB、两个栈、切换的PC在栈中
    • 将所有的东西组合在一起……
    • 为什么说是用户级线程——Yield是用户程序
    • 核心级线程
  • 内核级线程
    • 开始核心级线程
    • 和用户级相比,核心级线程有什么不同?
    • 用户栈和内核栈之间的关联
    • 仍然是那个A(),B(),C(),D()…
    • 开始内核中的切换:switch_to
    • 回答上面的问号??, ???, ????…
    • 内核线程switch_to的五段论
    • ThreadCreate! 做成那个样子…
    • 用户级线程、核心级线程的对比

温故知新

计算机硬件、操作系统、启动、接口…

操作系统需要管理硬件,那么它需要管理哪些硬件呢?

  • CPU和内存 —> 涉及进程
  • 磁盘和外设 —> 文件管理

操作系统启动就是将操作系统从磁盘读入内存,然后调用相关初始化方法,初始化形成相关数据结构,让操作系统知道硬件的模样,然后启动shell,等待用户使用。


知新

  • 方便用户使用硬件资源
  • 需要学什么

进程概念

管理CPU,先要使用CPU…

  • CPU本质是取指执行

管理CPU的最直观方法

  • 设置好PC的初值,然后CPU就不断取指执行即可,然后pc不断 1

看看这样做有没有问题?

上面给出的是一段程序,执行一百万次不带IO指令fprintf的耗时,和带IO指令,但只执行1000次的耗时

  • 可以看出来,带上IO指令后,IO指令耗时非常大

IO指令非常慢,执行一条IO指令的时间,可以执行100万条计算指令

  • 如果CPU执行过程中遇到IO指令,就进入等待状态,直到IO就绪,才会继续往下取指执行,那么CPU将会有大量时间处于空闲等待状态,CPU利用率随着IO指令增多,而越来越低

怎么解决?

  • 当CPU遇到IO指令时,可以先启动先关IO设备,然后切换到其他程序执行,等到IO就绪后,发送一个中断过来,提醒CPU IO已经就绪了
  • 然后CPU在切换回来处理

多道程序、交替执行,好东西啊!

  • 如果采用单道程序的话,就是CPU不断执行,遇到IO时,就需要进入等待状态,直到IO就绪,然后CPU继续往下面执行
  • 如果采用多道程序的话,可以实现多程序并发执行的形式,即一开始程序B启动打印设备DEV1,然后切换到A程序执行,DEV1处理完后,再通知CPU,CPU再切换执行B程序,A程序此时又可以去利用DEV1执行某些操作。

上面的多道程序,可以让漫长的IO时间不必占用CPU资源,而让CPU可以充分被利用起来


一个CPU面对多个程序?

  • CPU实现程序间的切换,难道仅仅只是通过修改PC值就行了吗?

修改寄存器PC就行了吗?

  • 当CPU需要切换执行另一个程序时,需要保存当前程序的状态,包括当前程序PC值,相关寄存器状态等
  • 当再次切换回来时,需要恢复之前的状态

引入“进程”概念

如果CPU只执行单个程序,那么利用率会很低,因此CPU需要去执行多个程序,这就引出了多个程序的切换执行,切换就牵扯到了程序状态的保存。

并且执行起来的程序和静态程序是不一样的,需要一个概念来描述执行起来的程序,这个词就叫做进程。

所以多个进程向前跑的样子,就是管理CPU的核心模样。


多进程图像

多个进程使用CPU的图像

每个启动的程序会创建相应的PCB记录当前程序执行的状态信息,然后CPU根据PCB表,进行程序之间的切换,完成多进程推进执行。


多进程图像从启动开始到关机结束

每当需要执行一个任务的时候,就需要开启一个进程进行处理

  • 用户使用计算机,就是启动一堆进程
  • 用户管理计算机,就是管理一堆进程
  • 进程就是正在运行中的程序

多进程图像:多进程如何组织?

操作系统感知和组织进程都需要PCB的支持。

  • 一个程序运行起来时,对应的会创建一个PCB来记录当前程序运行状态
  • 因为一个CPU同时只能执行一个进程,因此其余已经就绪的进程,就需要放入到就绪队列中去
  • 而对于那些需要等待某就绪事件的进程,则放入对应的等待队列中

多进程的组织:PCB 状态 队列


多进程图像:多进程如何交替?

上面主要讲述了如何组织和推进多个进程的执行,下面需要来思考一下多进程之间切换的过程

举例:

  • 当前线程需要启动磁盘阻塞,首先需要设置当前进程状态进入阻塞态,将当前进程放入阻塞队列中去
  • 进入进程切换函数schedule
  • getNext完成进程调度,从就绪队列中找到一个进程
  • switch_to完成进程的切换,保护现场,切换到新进程执行

交替的三个部分:队列操作 调度 切换

  • 首先是调度,进程的调度是一个非常复杂的问题,这里不过多深究,先考虑几个简单的选择,先进先出和优先级

  • 完成了调度,下一步就是切换
  • 如果当前正在执行的进程是PCB1,要切换到PCB2执行,那么首先需要保存现场,将当前相关寄存器值和PC值,保存到PCB1中
  • 然后将PCB2保存的寄存器状态值和PC值设置到当前CPU中,即恢复现场

因为涉及到对寄存器相关操作,因此进程切换的代码需要使用汇编编写


多进程图像:多进程如何影响?

因为进程其实就是运行的程序,那么程序本质还是一堆存放在内存中的数据,既然如此,如果进程1程序执行过程中,不小心修改了进程2程序的内存数据,那么进程2不就直接奔溃了吗?

然后实现进程之间的隔离,让不同的进程只能在分配给当前进程的内存中活动,是我们需要解决的事情。


进程执行时的100…

通过映射表完成进程的隔离,进程1访问地址100时,通过映射表会将地址100映射到780.

通过映射表处理后,及时两个进程中访问的都是同样的地址,但经过映射表处理后,都会映射到各自进程的内存空间中,从而实现进程间内存的隔离


多进程图像:多进程如何合作?

考虑一个打印场景,进程1要将打印pdf1的任务放入打印文件队列中去,因为此时7的位置是空闲的,因此进程1将pdf1放入位置7,但是放置了一半,此时CPU进行了进程切换,切换到了进程2执行,进程2也要进行打印任务,进程2把pdf2的任务放入打印文件队列中去,因为进程2也发现7是空闲的,因此也往位置7放,此时就乱套了。


从纸上到实际:生产者-消费者实例

有两个进程和一块共享的区域,消费者和生产者都可以操作其中的数据。

  • 大家思考,上面两个进程同时运行过程中,会不会产生问题

两个合作的进程都要修改counter

  • 可以看到,因为执行序列的原因,会导致得到的结果不是我们想要的,那么应该采取什么办法,才能够确保两个进程读取同时操作一块共享内存区域时,不会发生问题呢?

核心在于进程同步(合理的推进顺序)

要确保多进程操作同一共享内存区域时的进程同步,常规思路就是加锁,操作系统也是这样做的,下面来看看:


用户级线程

多进程是操作系统的基本图像

上面主要讲了进程之间的切换,需要通过PCB表和映射表完成切换过程,那么相当于使用映射表对多个进程进行了内存隔离。

进程之间的切换需要通过映射表完成,切换表需要耗费一定的资源,能否不切换表,而直接从A指令序列跳到B指令序列执行呢?


是否可以资源不动而切换指令序列?

如果将进程看做是资源 指令序列,那么如果我们把资源和指令执行分开的话,,一个进程内可以有多套指令执行序列,而资源还是只有一份,相当于多套指令执行过程中共享当前进程的内存资源。

那么这些正运行在进程中的指令序列,就是线程,一个进程内可以存在多个线程,多个线程运行过程中不断切换执行,并且切换只需要保存pc和相关寄存器状态,不需要切换表,这样可以提高效率。

线程本质是指令之间的切换,一个进程中有代码片段,而多个指令序列会存在在这个代码片段中,每个指令序列一旦运行起来了,就是一个线程,当存在多个线程时,对于线程的切换,也只需要切换指令序列即可,不需要设计到映射表和内存段的改变。


多个执行序列 一个地址空间是否实用?

线程有有用,通过上面浏览器的例子也可以看出来,线程具有下面两个特点:

  • 共享进程资源
  • 切换代价小

开始实现这个浏览器…、

在没讲线程之前,我们的认识中一个进程同时只能去执行一个指令序列,而了解到线程的存在后,我们知道如果进程中可以启动对多个指令序列的执行,那么不就相当于在一个进程中创建了多个线程执行吗?

还有一个问题,我们必须让多个线程执行过程中不断切换执行,否则还是相当于多个指令序列同步执行,那么线程也就没啥用了,只有像CPU对待进程那样,不断切换进程执行,才能真正实现多线程执行的效果。

所以上面给出的代码中通过create函数同时触发了对多个指令序列的执行,相当于创建了多个线程。

而Yield函数用来完成线程之间的切换。


Create? Yield?

上面给出了两段指令序列,相当于两个线程在运行,然后B()函数执行过程中,通过yield函数,将线程切换到300地址,即c函数处执行


两个执行序列与一个栈…

两个指向序列如果共享一个栈会怎样呢?

函数嵌套调用过程中,需要通过栈保存当前pc值和相关寄存器状态

  • A调用B函数,需要在栈中保存pc值104
  • B通过调用yield函数,切换指令序列执行,栈中保存pc值204
  • C调用D时,栈中保存304
  • D中调用yield函数,切换指令序列执行,栈中保存pc值404
  • 此时因为线程切换,来到了B函数的204位置,函数执行结束后,通过弹出栈顶元素,获得接下来要跳去的地址,发现是404
  • 显然,不符合要求,应该跳去104才对
  • 这就是多线程共享栈的问题

从一个栈到两个栈…

每个执行序列,即每个线程执行过程中都单独分配一个栈,那么就没有问题了吗?

  • 因为引入了多个栈,因此就存在栈顶指针切换的问题,例如:

下面中,400:处的函数D执行Yield函数切换到B函数中的204处执行前,需要做一步工作,就是切换栈。

因为栈顶指针寄存器esp只有一个,并且当前栈顶指针寄存器esp指向的是右边指令序列的栈顶,而因为当前指令序列需要发生切换,所以栈顶指针esp指向也需要切换,切换到左边指令序列位置。

因此,我们需要提前用一个数据结构保存每个指令序列当前栈顶指针的值,即TCB。

这里有两个指令序列,因此存在两个TCB,分别保存各自的栈顶指针的值,并且TCB是一个全局数据结构。

操作系统中一个线程对应着一个TCB(Thread Control Block),叫做线程控制模块,控制着线程的运行和调度。

所以在Yield函数切换过程中,实际也是改变当前栈顶寄存器esp的值,然后跳转到对应指令序列执行即可。

Yield函数最后一行jmp 204有必要吗?

显然是没必要的,并且加了还会有问题,可以分析一下,当函数D中执行Yield函数切换到函数B的204处执行时,当函数B执行完毕,遇到 } 右括号时,会弹出栈顶元素,即回到先前调用函数B的地方,继续往下执行,按理应该是跳到104执行。

这里涉及到函数执行过程中入栈和出栈的编译知识

但是,可以发现此时左边指令序列对应的栈顶指针指向的位置是204,显然不合理呀!

这是为什么呢?

  • 本质原因是因为jmp指令可以乱跳,Yiled函数没有执行到最后一个小括号结束,却可以通过jmp指令直接跳到yield函数执行结束后的地址处
  • 解决方法也很简单,就是去掉jmp指令即可

去掉jmp指令后,yield函数中只需要完成栈顶指针保存和改变即可,然后Yield函数执行完毕后,会弹出改变指向后的esp指针指向的栈顶元素。

一开始esp指向线程1的栈顶位置,然后经过yield函数后,esp执行线程2的栈顶位置,此时yield函数执行完毕后,需要弹出esp指向的栈顶元素,即弹出线程2的栈顶元素,不是线程1的哦!!! 虽然yield函数是在线程1中被调用的,但是弹栈靠的是esp栈顶指针寄存器指向的栈顶位置


两个线程的样子:两个TCB、两个栈、切换的PC在栈中

esp是一个寄存器,用于指向当前cpu执行到某个线程时,对应线程关联的用户栈或者内核栈的栈顶位置。当某个函数执行结束后,会去弹出esp执行的栈顶元素,然后程序跳转到该元素位置处继续执行。

线程初始化,需要为当前线程创建一个栈,并且将当前函数入栈,再创建一个TCB保存当前线程的栈顶指针位置

具体流程如下:

  • 创建线程A,再创建线程B
  • 执行线程A
  • 执行到B函数中的Yield函数并调用时

在进行线程切换时,首先需要将当前esp指向栈顶地址保存到当前线程关联的tcb中,这里假设esp指向的栈顶地址和该栈顶地址存放元素值相同。

然后,通过将要切换到的线程B的TCB中的值,赋值给esp,就完成了线程的切换

代码语言:javascript复制
void Yield(){
  TCB1.esp=esp;
  esp=TCB2.esp
}

第二步结束后,Yield函数就执行结束了,函数执行结束后,会将esp栈顶寄存器指针指向的栈顶元素弹出,因为此时已经完成了esp指针指向的切换,因此这里弹出的是线程B的函数栈

  • 当线程B中的D函数,执行并调用Yield函数时
  • 当函数B执行结束后,会跳到哪里呢?

将所有的东西组合在一起……

如果要写出一个上面讲到的浏览器模型,其实主要就是下面几点:

  • 线程的创建函数 createThread
  • GetData函数在遇到下载需求后,先启动下载,然后调用Yield切换线程执行,因为下载过程不需要CPU参与
  • Yield函数,除了对相关寄存器状态保存,esp栈顶指针寄存器操作外,还有一点,就是需要通过调度算法选出一个线程来进行切换执行

为什么说是用户级线程——Yield是用户程序

用户级线程只会在用户态来回切换,内核态是不知道用户线程存在的。

  • 用户级线程缺点如下:

那上面浏览器案例举例,如果浏览器中某个用户线程执行了下载请求,因为下载需要访问网卡IO,网卡需要硬件,而使用硬件必须经过内核来操作,因此已访问网卡IO,就需要进入内核。

而网卡IO一阻塞住,内核就会切换进程执行,即从当前进程1切换到进程2执行,虽然此时进程1中还有其他线程可以切换执行,例如显示文本的线程,但是由于是用户级线程,操作系统看不见,因此不会处理,直接切换进程。

如果是采用用户级线程实现的浏览器,那么一般一个标签页对应一个用户线程,如果其中一个用户线程阻塞,那么会导致进程切换,即当前浏览器进程失去了对CPU的使用权,所以一旦一个标签卡住了,其他标签也动不了

即使此时只存在浏览器一个进程,那么也会因为其中某个用户级线程阻塞,失去对CPU控制权,CPU处于空转状态,因为CPU看不到其他用户线程,也就不会进行切换

用户级线程切换是不需要进入内核态完成的,并且线程调度算法需要用户自己完成


核心级线程

核心级线程和用户级线程区别,哪个快?

核心级线程和用户线程最大的区别在于,操作系统即内核可以看到相关的核心级线程存在,这样即使某个进程中某个核心级线程IO阻塞住了,CPU也可以切换到当前进程的其他核心级线程继续执行。

而对于用户级线程而言,用于不受内核控制,因此用户需要自己写相关线程调度算法


内核级线程

对于用户线程来讲,其切换过程就是先将指向当前线程函数栈顶的esp指针位置保持到本线程对应的TCB中,然后通过调度算法选择切换到哪一个用户线程,然后将对应用户线程关联的TCB恢复到esp上,然后在弹出esp指向的栈顶元素位置开始执行。

因为栈顶指针寄存器只有一个,而线程有多个,因此在线程切换时,需要一个TCB保存切换时esp指针指向的栈顶位置,再线程切换回来的时候,好恢复现场

开始核心级线程

  • 对于多处理器来将,每个CPU有其对应的MMU (MMU暂时可以理解为映射表)
  • 而多核来说,多个CPU共用一个MMU

多个执行序列使用一套映射,这不就是线程吗?

因此可以简单把多处理器看做是支持多个进程执行,但是由于一个MMU只对应一个CPU,因此该进程内,同时只能处理一个指令序列,因此指令序列只能并发执行。

而对于多核处理器来说,因为多CPU共用一个MMU,因此可以很好的支持一个进程内的多个核心级线程并行执行,因此每个CPU可以同时执行一段指令序列,并且是并行执行。

如果是多进程的话,对于多核处理器来说,需要不断对一套MMU进行切换,计算机根本并行不起来。

并发是同时触发,交替执行

并行是同时触发,并行执行

对于多核来说,为什么一定要是核心级线程呢?

  • 操作系统看不见用户级线程,因此也不能为用户级线程分配各种硬件资源
  • 因为要访问硬件资源,就必须要进入内核,因此必须要使用内核级线程

和用户级相比,核心级线程有什么不同?

首先,我们需要明白一点,用户区和内核区是分开的,因此对应的函数栈也是不同的。

因此用户区中的函数,如果调用了内核相关的函数,进入内核区的话,需要切换到内核的函数栈。

上面说过,对于内核级线程来说,因为需要在内核中创建,因此必须要进入内核去中。

因此,如果要从用户态切换到内核区,需要准备两套栈

  • 在用户态进行线程切换的步骤上面讲过了,就不多说
  • 而如果要在内核栈进行内核线程切换时,此时TCB关联内核栈,就需要连同用户栈一起切换过来

用户栈和内核栈之间的关联

进入内核的唯一方法就是中断

每个内核级线程都对应两套栈,分别是用户栈和内核栈,那么是如何找到内核栈的呢?

  • 暂时可以理解为,只要产生了中断,就会找到当前线程对应的内核栈地址

当发生中断,产生用户态到内核态的切换时,会定位到当前线程关联内核栈地址,然后将用户栈的两个状态寄存器SS和SP保存到内核栈中。

还要保存到内核栈中的是pc和cs寄存器的值。

当内核函数执行完毕后,通过IRET指令返回,该指令会弹出上面压入的五个元素,通过pc和cs恢复到刚才用户态中执行指令的位置。

通过SS和SP的值,恢复用户区中函数栈的状态。


仍然是那个A(),B(),C(),D()…

当执行到int 0x80的时候,会产生中断,将用户栈和指令执行位置状态入栈。

大家可以思考一下,如果中断返回了,是不是就直接恢复到用户区304位置去执行了,然后用户栈的状态也恢复了,非常完美

进入内核区后,因为会先调用sys_read函数,因此会把1000压栈,表示sys_read函数执行结束后,会调回到1000地址处继续执行。


开始内核中的切换:switch_to

当内核函数sys_read被调用后,会启动磁盘读,然后当前内核线程进入阻塞状态。

下面就需要进行内核线程的切换。

线程和进程没有本质的区别,区别在于切换时,是否需要切换映射表,以及是否会共享内存资源

内核栈的切换和用户栈切换类似,首先需要找到下一个内核线程。

然后通过switch_to方法进行内核线程的切换,cur是当前线程TCB,next是下一个线程的TCB。

  • 内核线程S要切换到内核线程T
  • 首先将指向当前线程S内核栈的esp指针状态保存到TCB1
  • 将TCB2中保存值赋值给esp,相当于完成了esp指向的切换
  • switch_to函数执行完毕后,会弹出当前esp指向的栈顶元素
  • 然后线程T切换到该栈顶元素对应地址处执行

esp是一个寄存器,用于指向当前cpu执行到某个线程时,对应线程关联的用户栈或者内核栈的栈顶位置。当某个函数执行结束后,会去弹出esp执行的栈顶元素,然后程序跳转到该元素位置处继续执行。


回答上面的问号??, ???, ???..

因为要完成内核线程的切换,就必须进入内核态才行,因此当前线程s通过中断切换到内核态进入阻塞后,因为此时需要发生内核线程的切换,要切换到线程t

因为线程t被创建时,会在内核栈中保存当前线程t的用户栈状态和pc,cs状态,因此当第一次切换到线程t执行时,便会弹出这些状态,好恢复到线程t原先运行的样子。

如果线程t是因为阻塞或者时间片到期,被切换的话,那么切换时,也会把相关状态压入当前线程t对应的内核栈中

所以,最后四个问号,保存的是一段包含iret的指令。


内核线程switch_to的五段论

假设是内核线程S需要切换到内核线程T

  • 线程S通过中断进入内核区,并且会保存用户态状态到内核栈中
  • 找到线程S的TCB1,将当前esp保存到TCB1中
  • 再通过相关调度算法,找到切换到下一个线程T
  • 获取到线程T的TCB2,赋值给esp,此时switch_to函数执行完毕,需要弹出esp指向的栈顶元素
  • 线程T跳到栈顶元素位置处执行,然后执行过程中会执行iret指令,弹出线程T保存的用户态状态
  • 线程T回到用户区继续执行
  • 后续,如果线程T因为调用相关系统函数,进入阻塞状态 , 或者因为时间片到期,也会触发中断,再次进行内核线程切换,重复上面步骤
  • 中断(磁盘读或者时钟中断)
  • 调用schedule完成切换
  • 调度算法获取下一个线程的TCB,然后调用switch_to进行切换
  • 完成内核栈的切换,即tcb切换
  • 第二级切换,通过iret指令,弹出用户态状态
  • 如果线程S和线程T属于不同的进程,还需要进行映射表切换

ThreadCreate! 做成那个样子…

  • 申请当前线程关联的TCB内存
  • 在内核态中分配当前内核线程的内存,在用户态分配对应内核线程的内存
  • 内核栈中会保存对应用户态的状态,最后还会加上相关中断出口,可以恢复到用户态继续执行
  • 设置TCB相关状态

内核线程之所以可以让操作系统看见,是因为他在内核态中有一套内核栈,内核栈中保存了对应用户区状态,这是和用户线程的区别

因此对于内核线程的创建来说,既需要在用户态中分配相关内存存放用户栈和其他数据,也需要在内核态中分配内存存放内核栈数据,并且还需要在内核栈中记录用户态的状态,方便在内核态完成线程切换后,可以恢复到用户态继续执行。


用户级线程、核心级线程的对比

  • 用户级线程的切换只在用户态完成,并且线程调度算法由用户自己完成,因此即使一个进程中存在多个用户级线程,其实也可以看做只存在一个核心级线程
  • 核心级线程的切换在内核态完成,并且线程调度算法由操作系统完成,因此一个进程中存在多个内核级进程,每一个内核级进程都可以利用一个CPU,这样就可以完成多线程并行执行。 但是由于内核级线程切换需要进入内核态完成,因此切换代价大。

内核级线程和进程的区别其实已经很小了,区别就在于完成了线程的切换后,再切换映射表

0 人点赞