【基础学习】操作系统学习笔记 - 进程与线程:多道程序、并发、多进程、用户级线程、内核级线程

在中国大学MOOC上学习操作系统
希望看视频可以直接点击 哈工大-操作系统课程MOOC

CPU管理的直观想法

CPU通电后发生了什么?

以下面的指令为例
在这里插入图片描述

  1. CPU发送一个地址50,即PC=50,也就是将50放在地址总线上
  2. 内存从50中取出指令,传回CPU的指令寄存器中(IR)
  3. CPU执行指令:将100地址中的值赋给ax寄存器
  4. PC++
  5. CPU执行指令:将101地址中的值赋给bx寄存器
  6. PC++
  7. CPU执行指令:ax + bx(一般直接将ax+bx放到ax中再取走)
问题

如果只是按照上面的使用方法,只是将CPU设置一个初始PC,就会出现下面的问题:I/O与计算的消耗差别。
在这里插入图片描述
两者甚至差了将近百万倍,I/O太耗费时间了,这是因为I/O需要对磁盘进行操作,磁盘是一个机械设备(使用磁头),内存是一个电子设备,两者工作速度不在一个量级上。
这样就导致了CPU的利用率极低。

能否在I/O时也使用CPU?

跳过I/O然后直接计算直到计算完毕再使用I/O?
错误,比如后面的指令依赖于这次I/O,那就读不到正确的数据,后面的程序就错了。
举一个例子:
比如烧水的时候(CPU在进行一个I/O任务),我们肯定不是干等,而是先做别的事情(切换任务),等到水烧好了自然会有滚水的声音(一个信号),这时我们再放下手中的事情(中断),去处理烧好的水。
所以我们是在做多项任务,这就是多道程序设计。
在这里插入图片描述
多道程序、交替执行
(DEV代表device)
在这里插入图片描述
这样理解:当A任务需要I/O时,把CPU控制权交给B,如果B也需要I/O,就等到A的I/O任务执行完成,这样不断交换资源控制权,就能提高CPU和I/O设备利用率。

当一个CPU上交替执行多个程序:并发

所以管理CPU就可以从并发入手。

如何实现并发?

在这里插入图片描述
在这里插入图片描述

显然不能光修改寄存器PC,还需要保存一下原来的一些状态以便后面回恢复状态,这就需要PCB(Process Control Block)。

程序与进程

在这里插入图片描述

  1. 运行的程序和静态的程序不一样
    PCB中保存的信息就是进程比程序多的那部分东西。
  2. 进程是进行(执行)中的程序
    进程有始有终,有走有停,需要记录。
小结
  1. 操作系统需要使用CPU来执行任务
  2. 如果只执行一个任务,CPU的利用率太低,需要交替执行多个程序
  3. 多道程序使得我们必须使一种数据结构来(PCB)记录不同程序的信息,这就引出了进程的概念
  4. 进程和程序存在很大的不同

多进程图像(Multiple Processes)

多个进程使用CPU的图像

根据前面的学习,我们可以回答下面的问题:

  1. 如何使用CPU?
    让程序执行起来
  2. 如何充分利用CPU?
    启动多个程序,交替执行

启动了的程序就是进程,所以是多个进程推进:

  • 操作系统只需要把这些进程记录好、要按照合理的次序推进(分配资源、进行调度)
多进程图像从启动开始到关机结束

在这里插入图片描述
每要解决一个问题就要启动一个进程,可见多进程极其其重要。
在Windows中可以通过任务管理器查看一些相关的进程信息,这些信息就是存在PCB中的。

多进程如何组织

在这里插入图片描述

  1. 就绪队列(CPU调度)和磁盘等待队列(I/O)存储的都是PCB。
  2. 只有一个进程在被执行,因为只有一个CPU
  3. 多进程=PCB+状态+队列
    在这里插入图片描述
多进程如何交替(切换)?

在这里插入图片描述

  1. 启动磁盘读写
  2. 当前状态置为阻塞态
  3. 将该进程A放在一个阻塞队列(磁盘等待队列)上
  4. 切换
    1. 从就绪队列中取出队首的进程B(PCB),注意这个getNext和具体的调度策略有关
    2. 切换至此进程,同时要保存前边的进程A的状态以便后面恢复

交替执行任务的三个部分:队列操作 + 调度 + 切换
两种常见的调度方式(后面会讲更多):

  1. FIFO?
    1. 表面上非常公平
    2. 显然没有考虑不同种类进程的区别,类比急诊和门诊的关系
  2. Priority?
    优先级如何设定?是不是会导致某些进程饥饿?
swtich_to

在这里插入图片描述

  1. 前四行先保存当前CPU的状态
  2. 后四行将调度过来的进程状态赋给CPU
  3. 上面的代码实际应该写成汇编代码,因为涉及到对具体寄存器等硬件的操作。
多个进程同时存在于内存会出现下面的问题

在这里插入图片描述

  1. 当进程1和进程2同时操作100号地址时,比如上图进程1修改了100号地址,那么进程2再去使用的时候就会拿到错误的数据,极容易使进程2崩溃。
  2. 要解决上面的问题,就要限制进程1对100号地址的读写,这就引入了多进程的地址空间分离的思想,这也是内存管理的主要内容。
  3. 注意这里不能用前面DPL、CPL来限制访问,这是因为这两个数据结构是为了保护OS和用户之间的边界,不是用来做地址分离的。

在这里插入图片描述
在这里插入图片描述
使用映射表的方式,将内存地址分离,限制进程可以读写的地址空间,做到进程地址空间的隔离。
只有将内存管理好了,才能保证多个进程之间能够互不影响地执行,因此我们说多进程图像包含了进程管理和内存管理

多进程如何合作?

在这里插入图片描述

打印机工作过程:

  1. 应用程序提交打印任务
  2. 打印任务被放进打印队列
  3. 打印队列从队列中取出任务
  4. 打印进程控制打印机打印

出现了什么问题?
当进程1和进程2都想放入7号地址,因为进程交替执行,可能导致两个进程都放入了一部分自己的数据,打印时就会出现问题。
生产者-消费者实例:
在这里插入图片描述
实例解释:
BUFFER_SIZE标记Buffer的最大值
counter作为指示当前Buffer的已经使用的资源
当Buffer为空,消费者进程进入死循环,不可以消费;假设没有这个死循环会读出来空值/错误值。
当Buffer为满,生产者进程进入死循环,不可以生产;假设没有这个死循环,就会覆盖掉原来的正确数据。
当Buffer非空非满时,生产者和消费者根据自己需要进行生产和消费。
CPU会按照下面的代码进行运算:
按照上面的思路,我们对生产者消费者案例已经做好了,可惜还是事与愿违:
在这里插入图片描述
当出现了右上角的那个执行序列,出现了错误结果。
当生产者执行counter++操作中的一半,切到了消费者的counter - -,
最后counter变成了4。我们期望的是生产者先将counter置为6后消费者再执行counter–,正确结果应该是5。
如何解决?
给counter上锁:
即在某些特殊情况下,禁止进程的切换,变成“合理”的执行。
在这里插入图片描述

  1. 生产者先关锁,然后执行counter++,执行到一半切换。
  2. 消费者检查counter锁,发现被锁住了,交出程序控制权
  3. 生产者继续执行完毕counter++,结束后开counter锁
  4. 消费者先关锁,执行counter–,执行完毕后,开锁
小结:如何形成多进程图像?
  1. 读写PCB(这是OS中最重要的数据结构,贯穿始终)
  2. 要操作寄存器完成切换(进程、线程)
  3. 要写调度程序(CPU调度策略)
  4. 要有进程同步与合作(进程同步与信号量、临界区保护)
  5. 要有地址映射(内存管理)
  6. 解决死锁问题

用户级线程(User Thread)

多进程是操作系统的基本图像在这里插入图片描述

在切换任务时那个映射表(也就是内存空间)我们应该如何切换?

  1. 切换:进程级切换
  2. 不切换:线程级切换

在这里插入图片描述

  1. 进程 = 资源 + 指令执行序列,当我们只是将指令执行序列改变,而没有对资源切换时,就是线程的切换。我们可以将资源和指令执行区别开来,采用一个资源+多个指令执行序列的形式创造进程,这就是多线程技术。
  2. 线程优点:保留了并发的优势,并减少了进程切换的代价
  3. 线程切换的实质:映射表不变而PC指针变

我们可以知道进程 = 资源 + 指令执行序列,那么进程切换 = 资源切换 + 指令序列切换,因此我们可以先搞明白指令执行序列如何切换,然后再搞明白进程是如何切换的,分而治之。
(切换指令序列的过程实际上就是将PC设置的过程)

多个执行序列 + 一个地址空间是否实用?有价值吗?
网页浏览器
  1. 一个线程用来从服务器接收数据
  2. 一个线程用来显示文本
  3. 一个线程用来处理图片(如解压缩)
  4. 一个线程用来显示图片

(题外话:作为前端程序员,我觉得这里老师讲的不是很准确,浏览器的线程分为GUI渲染线程-显示图片和文本、JavaScript引擎线程-解压缩、事件触发线程、定时触发线程、异步http请求线程-服务器数据)

浏览器的线程需要功向资源吗?
  1. 接收的数据可能各个线程都要读写(CPU、内存)
  2. 所有的内容都要显示一个屏幕上(资源)

显然这些线程需要共享这些资源,应当采用多线程,多线程是很有价值的。

实现浏览器的思路

在这里插入图片描述
(注意这里的“多线程“只限于用户级线程,而不是内核级线程。)
显然这里和核心就是如何实现Yield

  1. 能切换了就应该知道切换时需要是个什么样子
  2. Create实际上就是要制造出第一次切换时应该的样子。

在这里插入图片描述
当两个线程共用一个栈时:
线程1:A、B
线程2:C、D
在这里插入图片描述
(Yield函数是右侧红色的代码,用来找到下一条指令)
(这里老师没有画错栈,是因为下面是高地址,就是栈顶)

  1. 执行A,要调用B,104代码压栈
  2. 执行B,要调用Yield,204代码压栈
  3. 执行Yield,找到下一个线程,即调用C
  4. 执行C,要调用D,304代码压栈
  5. 执行D,要调用Yield,404代码压栈
  6. 执行Yield,切回线程1:出栈404
  7. 没有我们想要的204出栈(左侧的栈),而是404出栈(共用的栈),程序错误了

两个线程共用一个栈,造成了程序错误。
因此我们必须使用两个线程+两个栈来处理:
Yield切换时也必须要切换栈。
如何保存线程的状态呢?TCB(Thread Control Block)
在这里插入图片描述
(红色长箭头表示CPU在执行哪一个线程)
线程1要使用esp=1000的栈,线程2要使用esp=2000的栈,因此我们需要在TCB中保存两个栈的esp
(对于栈的描述要使用栈帧,栈帧保存着esp和ebp,分别为栈顶指针和栈底指针,esp:extended stack pointer,ebp:extended base pointer)
使用两个栈时:

  1. 执行A,调用B,104压入esp=1000栈中
  2. 执行B,调用Yield,204压入esp=1000栈中,
  3. 执行Yield,并切换至esp=2000栈,切换至线程2,调用D
  4. 执行D,调用Yield,404压入esp=2000栈中
  5. 执行Yield,并切换至esp=1000栈,切换到线程1,esp=1000出栈204,程序正常。
  6. 执行204,结束后检测到函数返回,esp=1000出栈104
  7. 执行104,结束后检测到函数返回,并且esp=1000栈空,自动调用Yield,切换至线程2,esp=2000
  8. 后边就很简单了,不再分析。

为什么说jmp语句可以不要:
因为Yield调用结束,上个线程正好是我们为了保存状态压栈了跳回的PC指针位置,当Yield函数调用结束后204立刻会出栈,所以不需要再jmp,这是多余的。

两个线程的样子
  1. 两个TCB
  2. 两个栈
  3. 需要切换的PC在自己的栈里面了

在这里插入图片描述

对浏览器的简单实现

在这里插入图片描述
几乎所有的浏览器都会按照上面的架构来实现“用户级多线程”

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

在这里插入图片描述
按照架构图,可以发现无论我们定义的Yield如何使“线程”切换,都只是在用户态来做文章,不会进入到内核态,因此说是用户级线程。
用户级线程的问题:

  1. GetData
    1. 连接URL发起请求
    2. 等待网卡I/O,必须要进入内核的进程调度
    3. 网卡阻塞了进程1,造成CPU空等
    4. 还是阻塞了整个进程,导致Show没法继续进行,也就是说一旦内核阻塞了进程,这种线程的并发就没有了。
内核级线程

在这里插入图片描述
当我们使用了内核级线程以后,会使用系统调用级(不再是Yield,而是系统的Schedule)的线程I/O来帮助我们处理网络请求部分,CPU又利用起来了。
(题外话:其实从名字中也可以看出来,Yield代表的是“让步、放弃、暂停”,而Schedule才是真正意义上的“调度”)
可见:内核级线程的并发性一定优于用户级线程

内核级线程(Kernel Threads)

为什么没有听说过用户级进程?
因为进程需要分配资源,而对于用户来讲,资源是不可见的,简单来讲就是用户态无法访问资源,只能依靠操作系统进行调度,所以不可能有用户级进程

开始在这里插入图片描述

多CPU与多核CPU(多核用的比较多)
区别(看架构图):

  1. 多处理器有自己的缓存(Cache)和MMU(Memory Management Unit,用于管理内存)
  2. 多核CPU的Cache和MMU是共享的

多核只有在多线程的辅助下才能发挥能力。
多处理器/多核心的产生,有了一个新概念,并行:多个程序同时(注意不是交替)执行。

内核级线程有什么不同?

在这里插入图片描述

  1. ThreadCreate是系统调用,内核管理TCB,内核负责切换线程。
  2. 如何让切换成型? - 内核栈
    用户栈依然需要,因为执行的代码仍在用户态,还是需要进行函数调用
  3. 一个栈到一套栈,两个栈到两套栈
  4. TCB关联内核栈,用户如何找到这个栈呢?
    当内核栈切换的时候,用户栈也必须跟着切换。
用户栈和内核栈之间的关联

中断是进入内核态的唯一方式。
(CS:Code Segment Register)
在这里插入图片描述

  1. 一旦使用了INT,操作系统就会自动启用内核栈,并通过指针整合用户栈,两个栈成一套栈,并且包含了源PC、源CS、EFLAGS。
  2. 一旦使用了IRET,操作系统自动退回到用户态,栈自动转为原来的用户栈。
实例

在这里插入图片描述

  1. 执行A,调用B,压栈104
  2. 执行B,调用read(),压栈204
  3. 执行read,调用int 0x80。
  4. 执行int 0x80,立刻中断,进入内核态,ss指向栈底,sp指向栈顶,PC指向304,CS指向段基址(也就是线程初始的位置100,所以指向了A)
  5. 执行1000,调用sys_read()
    在这里插入图片描述
  6. 执行sys_read(),启动磁盘读,并阻塞自己,找出下一个线程并切换线程(内核级的切换),然后调用swtich_to
    switch_to:
    cur:当前的TCB
    next:下一个TCB
    函数将当前内核栈中的esp保存到cur,并将内核栈的esp指向next中的esp。
  7. 执行switch_to,通过TCB找到内核栈指针,然后通过ret切到某个内核程序,最后iret再根据CS、PC切回用户态。
上面的那些问号

在这里插入图片描述
在这里插入图片描述

  1. ??:PC和CS,指向了返回用户态时的PC和CS,也就是上面进入内核态的时候存的,也还是中断(就是用户态的时候)执行的位置。
  2. ???:sys_read的某个地方
  3. ???:一段能完成第二级返回的代码,一定要包含一段iret代码。
内核级线程swtich_to的五段论

在这里插入图片描述

  1. 中断入口-进入切换,启动中断(int 0x80),此时刚进入内核态,需要保存一些用户态信息,并建立内核栈和用户栈关联。
  2. 中断处理-引发切换,可能会引起阻塞(sys read)
  3. schedule,找到TCB,准备内核栈的切换
  4. swtich_to:内核栈切换,ret,第一级切换
  5. 中断出口:第二级切换,iret返回用户态,用户栈切换,自此一套栈切换成功,内核级线程切换完成。

如果是需要切换进程,就有有右上角的那段代码,即,将地址映射表切换。

如何实现TreadCreate

在这里插入图片描述
只要保证有用户栈、内核栈、TCB,再准备好一段包含iret的代码就算做好了切换的准备,ThreadCreate按照这些数据结构创建即可。

  1. 创建一个TCP
  2. 申请一段内存作为内核栈,把内核栈(krlstack,kernel stack)内容初始化
  3. 传入用户栈
  4. 填写两个栈
  5. 填写状态
  6. 将TCB装入线程等待队列
用户级线程、内核级线程的对比

在这里插入图片描述
多对一、一对一、多对多的三种模型。

  1. 利用多核:显然多对一模型只能用一个核,多核利用性最差
  2. 并发度:多对一模型在内核级依然有阻塞的现象,并发性不强
  3. 代价:多对一模型只需要一个内核即可完成,一对一模型有几个内核就要用几个内核。
  4. 内核改动:多对一模型压根不需要改内核
  5. 用户灵活性:一对一模型没有给用户操作的空间。多对一模型和多对多模型想起几个线程就起几个线程。

内核级线程的代码实现

课程链接

内核级线程的两套栈,核心就是内核栈

内核级线程切换实现的难度就在内核栈的切换
对于用户来讲,上面的switch_to五段论是无感知的。
在这里插入图片描述

从fork理解内核级线程切换

在这里插入图片描述

  1. 对于用户栈main来说,ret=exit就是让程序回到A结束的后面,也就是B
  2. fork会变成下面的那段代码
    1. 将系统调用(__NR_fork)传给%eax
    2. 中断,int 0x80,一旦执行INT指令以后,CPU会找到内核栈,连接上用户栈,并将PC和CS压进栈。
    3. 将PC+1放到res,切换回来时,ret会找到res作为PC指向。
      此时由用户态进入内核态
五段论中的中断入口和出口

在这里插入图片描述
中断入口:
在这里插入图片描述

  1. 执行_system_call,将用户态的一堆东西记录下来,为了以后回来还能接着执行。然后执行sys_fork,这就有可能会引起内核级线程的切换。
  2. 读取PCB(_current)的state,比较是否为0(就绪状态,非0就阻塞了,比如write、read就会阻塞),jne(如果不是)就调用调度算法。

schedule(五段论中间三段,用C语言函数实现,这节课不关心这个):
在这里插入图片描述
schedule完成内核栈的切换,并把指令序列也切换

中断出口:
在这里插入图片描述

  1. 判断时间片是否为0,je(如果是)继续调用调度算法
  2. ret_from_sys_call,从系统中断中返回。

reschedule(在最上边的那个大图):

  1. 先把ret_from_sys_call压进来
  2. 转去执行schedule,当schedule结束后,弹出ret_from_sys_call,并调用。
swtich_to(这里没有看懂)

假设调度算法已经执行完,看一下swtich_to
Linux 0.11中使用了TSS(Task State Segment,任务状态段)进行切换,效率不如krlstack那种切换,即内核栈的切换。
在这里插入图片描述
类似前面CS通过GDT寻找指令的逻辑,可以通过TR(类比CS,是一个段寄存器)来找到当前进程的TSS。
TSS包含了所有的寄存器内容,比如PC、eax。
使用TSS类似快照,就是先将当前现场记录一下,然后选择一个新的现场来继续执行。长跳转至TSS段选择符造成CPU执行任务切换操作。
三句最重要的代码:

  1. int 0x80,开中断,进入内核态
  2. switch_to 中的那句ljmp(长跳转),调度
  3. iret,返回用户态,关中断
ThreadCreate

在这里插入图片描述

  1. 调用_copy_process,这是fork的作用,将父进程的信息copy,一堆在内核栈的东西都被复制过来了,创建一个近乎一样的内核栈。
    (_copy_process函数取值,是从栈顶开始取,也就是??4部分赋给nr,依次赋给copy_process)
    eip是int 0x80后面的那条指令。
  2. 创建栈
    在这里插入图片描述
    (一定不要使用malloc,这是用户态的代码)
    创建栈
    1. 使用get_free_page获取一段内存(在初始化mem_map时,将内存按照4kB分页,这里就是申请了4kB的内存)作为PCB
    2. 创建栈(包括内核栈和用户栈)并填写
    3. 关联栈和PCB
    4. 实际上那一页内存就包含了PCB和内核栈
  3. 执行前准备
    在这里插入图片描述
小结
  1. fork执行,造成父进程阻塞
  2. 系统调用schedule
  3. schedule内会调用switch_to,切换到子进程
  4. 子进程将TSS内容交给CPU,eip指向INT 0x80指令的后一条指令。

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、实验目的 本实验要求学生模拟作业调度的实现,用高级语言编写和调试一个或多个作业调度的模拟程序,了解作业调度在操作系统中的作用,以加深对作业调度算法的理解。 二、实验内容和要求 1、编写并调度一个多道程序系统的作业调度模拟程序。   作业调度算法:采用基于先来先服务的调度算法。可以参考课本中的方法进行设计。 对于多道程序系统,要假定系统中具有的各种资源及数量、调度作业时必须考虑到每个作业的资源要求。 三、实验主要仪器设备和材料 硬件环境:IBM-PC或兼容机 软件环境:C语言编程环境 四、实验原理及设计方案 采用多道程序设计方法的操作系统,在系统中要经常保留多个运行的作业,以提高系统效率。作业调度从系统已接纳的暂存在输入井中的一批作业中挑选出若干个可运行的作业,并为这些被选中的作业分配所需的系统资源。对被选中运行的作业必须按照它们各自的作业说明书规定的步骤进行控制。 采用先来先服务算法算法模拟设计作业调度程序。 (1)、作业调度程序负责从输入井选择若干个作业进入主存,为它们分配必要的资源,当它们能够被进程调度选中时,就可占用处理器运行。作业调度选择一个作业的必要条件是系统中现有的尚未分配的资源可满足该作业的资源要求。但有时系统中现有的尚未分配的资源既可满足某个作业的要求也可满足其它一些作业的要求,那么,作业调度必须按一定的算法在这些作业中作出选择。先来先服务算法是按照作业进入输入井的先后次序来挑选作业,先进入输入井的作业优先被挑选,当系统中现有的尚未分配的资源不能满足先进入输入井的作业时,那么顺序挑选后面的作业。 (2) 假定某系统可供用户使用的主存空间共100k,并有5台磁带机。 3)流程图:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值