进一步学习操作系统 - 哈工大李治军老师 - 学习笔记 L10L11L12

进程如何切换?

L10 用户级线程

在这里插入图片描述

进程切换时,当前进程的CPU环境要保存,新进程的CPU环境要设置,线程切换时只须保存和设置少量寄存器,并不涉及存储管理方面的操作,可见,进程切换的开销远大于线程切换的开销。

因为资源切换的系统开销很大,若将资源和指令执行分开,只进行指令的切换,开销会很小。
所以将一个进程分为一个资源和多个指令执行序列,实质是映射表不变而PC指针变。

在这里插入图片描述

函数结束ret指令干了两件事:先出栈;再将出栈的值放到CPU的PC寄存器中。因为PC寄存器中永远放的是下一次执行指令的地址,所以就顺理成章的在函数调用完之后依旧接着原来的代码继续执行。
参考https://blog.csdn.net/qq_41431406/article/details/96446312

需要知道函数调用CALL 指令会将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当函数结束时, RET 指令从堆栈把返回地址弹回到指令指针寄存器,于是程序接着原来的代码继续执行。
如果两个线程共用一个栈,就会出现返回地址错误的情况,所以有几个线程就要有几个栈。

在这里插入图片描述

yield的意思是“屈服、礼让”,在程序中表现为当前线程会尽量让出CPU资源来给其他线程执行。
参考https://blog.csdn.net/ztliduo/article/details/54565504

每个线程拥有一个栈时,线程切换时要先切换栈,通过Yield函数,根据图中yield函数体,可知是通过yield函数其实就是把当前CPU执行的线程的栈保存起来,并且取出需要执行的线程的栈指针赋给寄存器开始执行
PCB是进程控制块
TCB是线程控制块
yield{
当前线程的栈指针 = 寄存器里的栈指针
寄存器里的栈指针 = 将要运行线程的栈指针
}

在这里插入图片描述
线程创建函数thread_create的核心是用程序做出线程的PCB、栈、保存在栈中的切换的PC(线程的初始地址)
根据图中函数体:
1.申请内存给TCB
2.申请内存给栈
2.把线程的初始地址 放在栈中
3.栈和TCB关联(把指向 初始地址 的 栈顶指针 赋给 TCB)

在这里插入图片描述
为什么用户级线程是用户级线程?因为yield是用户程序,就是用户自己编写的程序,不涉及到内核,即不涉及操作系统。
不管该进程有多少线程,这些线程都是用户自己写的,
多进程并发时,操作系统只看得到进程,所以进程里某线程阻塞,在操作系统看来就是进程阻塞,
所以在进程的某个线程进入内核并阻塞的时候,该进程就会被阻塞。
(这里是多线程模型中的多对一模型,参考https://blog.csdn.net/zxc024000/article/details/78972283)

L11 内核级线程

在这里插入图片描述

多核CPU和多CPU的区别主要在于性能和成本。多核CPU性能最好,但成本最高;多CPU成本小,便宜,但性能相对较差。
参考https://zhuanlan.zhihu.com/p/85819786

我本来以为多处理器相当于多个电脑,性能肯定比多核要好。但是正好相反,多核CPU性能最好。我的理解是因为多处理器之间的合作会有开销。

只有内核级线程才能发挥多核性能,多个CPU共用一套MMU设备情况下,可以一个CPU执行一个内核级线程,在内核级线程切换的时候,不需要切换内存映射关系,代价小很多,因为本来一个进程中MMU映射关系就是一样的
进程 无法发挥多核性能,因为只有一套MMU,进程切换时MMU映射关系也得跟着切换,即切换内存映射表,切换内存映射表代价比较大
参考https://blog.csdn.net/nkltc/article/details/73658210

MMU-内存管理单元,是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换。
参考https://baike.baidu.com/item/MMU/4542218

我的理解:
一个CPU(核)可以执行一个进程(内核级线程),多个CPU可以执行多个进程(内核级线程)
但是在多个CPU共用一套MMU设备情况下,进程切换时MMU映射关系也得跟着切换,而内核级线程切换时不需要切换内存映射关系

在这里插入图片描述

一个内核级线程有一套栈: 用户栈 + 内核栈
用户栈里存放代码和数据,内核栈里存放TCB

在这里插入图片描述

对于栈顶的段地址,其是存放在段寄存器SS中的,而对于栈顶的偏移地址,其则是存放在SP寄存器中的 。在任何时刻,SS:SP 都是指向栈顶元素
参考https://blog.csdn.net/qq_35212671/article/details/52761585

在用户态执行的时候,通过中断,进入核心态
通过硬件,将用户栈的相关信息(该线程的用户栈的栈地址、当前指令的地址等五个寄存器的内容),压入内核栈
SS栈段暂存器 - SP栈指针
在这里插入图片描述
这一段一直没听太明白,我觉得刚开始的时候,老师讲的是 上面用户程序的过程,不涉及内核程序,即不涉及系统调用即系统调用sys_read的返回地址1000
忽略内核程序部分,执行完内核程序后,就要弹栈CS、PC也就回到了用户态继续执行。

运行到A()将104压栈;运行到B()将204压栈;运行到read()将304压栈;运行int中断指令,将五个参数压栈,进入内核程序;运行到系统调用 sys_read 将1000压栈;运行系统调用 sys_read()。

11.2 完整的系统调用中断过程:

  1. INT 中断自动压栈的有下一条指令,以及用户级线程SS:SP,共五个参数
  2. _system_call 把寄存器保护压栈是压到内核栈中,需要手动压栈
  3. 系统调用,(有可能是_sys_fork,其实就是根据标号找到的系统调用),结束之后继续执行,要执行reschedule,先push $ret_from_sys_call,让其在_schedule之后返回到ret_from_sys_call, _schedule为c函数,结束右括号会把ret_from_sys_call pop出来,返回到这里执行,即执行ret_from_sys_call;
    这里注意call 和 jmp的区别!!!
  4. 在ret_from_sys_call中pop出_system_call时保护的寄存器内容,然后中断返回!!!
  5. 中断返回是在最后,中断返回会把SS:SP 以及用户态的下一条指令 POP出来,即把5个寄存器pop出来!!!这样就会返回到用户栈,运行用户态的下一条指令!!!

原文链接:https://blog.csdn.net/nkltc/article/details/73658210

在这里插入图片描述
内核级线程的切换 是在内核栈中 切换的
系统调用 sys_read 会启动磁盘读,将自己变成阻塞,引起调度,找到下个线程的TCB,执行switch_to切换线程。
switch_to:https://blog.csdn.net/chengonghao/article/details/51334705
在这里插入图片描述

当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。
参考https://blog.csdn.net/zhuyi2654715/article/details/8180933

在这里插入图片描述
1.进入中断,五参数压栈;
2.进行中断处理,可能会阻塞引起调度;
3.若要调度切换,先找到下一线程的TCB;
4.根据TCB,switch_to完成内核栈的切换;
5.通过iret从内核栈返回用户栈。

参考https://zhuanlan.zhihu.com/p/362263170

这里要注意,中断出口这里已经经过了前面的switch_to,中断的iret已经不是原先的中断返回了,是切换后的新中断的执行返回!!!这样返回以后就来到了引发该新中断的用户态代码来执行

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

L12 内核级线程实现

参考https://blog.csdn.net/weixin_43135178/article/details/105896431
参考https://blog.csdn.net/nkltc/article/details/73658210

在这里插入图片描述
调用main,返回地址exit压栈;调用A,返回地址B的开始地址压栈;调用fork,执行INT中断(INT执行的时候没有进入内核,执行完了才进入内核)CPU找到当前内核栈,在内核栈压入五参数;进入中断处理函数system_call
在这里插入图片描述
在这里插入图片描述

把用户态现场在内核态保存下来,执行系统调用sys_fork(可能会引起切换,需要检查(中间的三段论))
检查,若当前进程阻塞,需要进行调度,即执行reschedule,执行完后,进行系统调用返回ret_from_sys_call(5段论的最后一段:里面是一堆pop,与push相对应,然后再iread)(从内核栈切换到用户栈)
schedule中找到next(下一个进程的PCB),接下来进行切换switch_to()

在这里插入图片描述

Linux 0.11中,进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。

每个任务(进程或线程)都对应一个独立的 TSS。TSS 是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像
有一个任务寄存器(Task Register,简称 TR)指向当前进程对应的 TSS 结构体

所谓的 TSS 切换就是将 CPU 中的寄存器值都复制到 TR 指向的内存 TSS 结构体中保存起来,同时找到要切换到的下一进程对应的目标TSS,将其中存放的寄存器映像“扣在”CPU 上,这就完成了执行现场的切换。

Intel 架构不仅提供了 TSS 来实现任务切换,而且只要一条指令就能完成这样的切换,即 ljmp 指令。

虽然用一条指令能完成任务切换,但这指令的执行时间很长,这条 ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。

而通过堆栈实现任务切换需要更少的时钟周期,而且采用堆栈的切换还可以使用指令流水等并行优化技术,同时又使 CPU 的设计变得简单。
所以无论是 Linux 还是 Windows,进程或线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,都是通过堆栈实现的。

这一部分我也没捋清楚,大概知道了过程,没有死扣代码细节,等到实验的时候在回看这一部分,看看是不是能更明白。
这里推荐两个讲的挺清楚的文章:
https://blog.csdn.net/williamgavin/article/details/83062645

https://blog.csdn.net/qq_42518941/article/details/119145575

猜你喜欢

转载自blog.csdn.net/tfnmdmx/article/details/119424519