操作系统需要管理硬件,那么它需要管理哪些硬件呢?
操作系统启动就是将操作系统从磁盘读入内存,然后调用相关初始化方法,初始化形成相关数据结构,让操作系统知道硬件的模样,然后启动shell,等待用户使用。
看看这样做有没有问题?
上面给出的是一段程序,执行一百万次不带IO指令fprintf的耗时,和带IO指令,但只执行1000次的耗时
IO指令非常慢,执行一条IO指令的时间,可以执行100万条计算指令
怎么解决?
上面的多道程序,可以让漫长的IO时间不必占用CPU资源,而让CPU可以充分被利用起来
如果CPU只执行单个程序,那么利用率会很低,因此CPU需要去执行多个程序,这就引出了多个程序的切换执行,切换就牵扯到了程序状态的保存。
并且执行起来的程序和静态程序是不一样的,需要一个概念来描述执行起来的程序,这个词就叫做进程。
所以多个进程向前跑的样子,就是管理CPU的核心模样。
每个启动的程序会创建相应的PCB记录当前程序执行的状态信息,然后CPU根据PCB表,进行程序之间的切换,完成多进程推进执行。
每当需要执行一个任务的时候,就需要开启一个进程进行处理
操作系统感知和组织进程都需要PCB的支持。
上面主要讲述了如何组织和推进多个进程的执行,下面需要来思考一下多进程之间切换的过程
举例:
因为涉及到对寄存器相关操作,因此进程切换的代码需要使用汇编编写
因为进程其实就是运行的程序,那么程序本质还是一堆存放在内存中的数据,既然如此,如果进程1程序执行过程中,不小心修改了进程2程序的内存数据,那么进程2不就直接奔溃了吗?
然后实现进程之间的隔离,让不同的进程只能在分配给当前进程的内存中活动,是我们需要解决的事情。
通过映射表完成进程的隔离,进程1访问地址100时,通过映射表会将地址100映射到780.
通过映射表处理后,及时两个进程中访问的都是同样的地址,但经过映射表处理后,都会映射到各自进程的内存空间中,从而实现进程间内存的隔离
考虑一个打印场景,进程1要将打印pdf1的任务放入打印文件队列中去,因为此时7的位置是空闲的,因此进程1将pdf1放入位置7,但是放置了一半,此时CPU进行了进程切换,切换到了进程2执行,进程2也要进行打印任务,进程2把pdf2的任务放入打印文件队列中去,因为进程2也发现7是空闲的,因此也往位置7放,此时就乱套了。
有两个进程和一块共享的区域,消费者和生产者都可以操作其中的数据。
要确保多进程操作同一共享内存区域时的进程同步,常规思路就是加锁,操作系统也是这样做的,下面来看看:
上面主要讲了进程之间的切换,需要通过PCB表和映射表完成切换过程,那么相当于使用映射表对多个进程进行了内存隔离。
进程之间的切换需要通过映射表完成,切换表需要耗费一定的资源,能否不切换表,而直接从A指令序列跳到B指令序列执行呢?
如果将进程看做是资源+指令序列,那么如果我们把资源和指令执行分开的话,,一个进程内可以有多套指令执行序列,而资源还是只有一份,相当于多套指令执行过程中共享当前进程的内存资源。
那么这些正运行在进程中的指令序列,就是线程,一个进程内可以存在多个线程,多个线程运行过程中不断切换执行,并且切换只需要保存pc和相关寄存器状态,不需要切换表,这样可以提高效率。
线程本质是指令之间的切换,一个进程中有代码片段,而多个指令序列会存在在这个代码片段中,每个指令序列一旦运行起来了,就是一个线程,当存在多个线程时,对于线程的切换,也只需要切换指令序列即可,不需要设计到映射表和内存段的改变。
线程有有用,通过上面浏览器的例子也可以看出来,线程具有下面两个特点:
在没讲线程之前,我们的认识中一个进程同时只能去执行一个指令序列,而了解到线程的存在后,我们知道如果进程中可以启动对多个指令序列的执行,那么不就相当于在一个进程中创建了多个线程执行吗?
还有一个问题,我们必须让多个线程执行过程中不断切换执行,否则还是相当于多个指令序列同步执行,那么线程也就没啥用了,只有像CPU对待进程那样,不断切换进程执行,才能真正实现多线程执行的效果。
所以上面给出的代码中通过create函数同时触发了对多个指令序列的执行,相当于创建了多个线程。
而Yield函数用来完成线程之间的切换。
上面给出了两段指令序列,相当于两个线程在运行,然后B()函数执行过程中,通过yield函数,将线程切换到300地址,即c函数处执行
两个指向序列如果共享一个栈会怎样呢?
函数嵌套调用过程中,需要通过栈保存当前pc值和相关寄存器状态
每个执行序列,即每个线程执行过程中都单独分配一个栈,那么就没有问题了吗?
下面中,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指令后,yield函数中只需要完成栈顶指针保存和改变即可,然后Yield函数执行完毕后,会弹出改变指向后的esp指针指向的栈顶元素。
一开始esp指向线程1的栈顶位置,然后经过yield函数后,esp执行线程2的栈顶位置,此时yield函数执行完毕后,需要弹出esp指向的栈顶元素,即弹出线程2的栈顶元素,不是线程1的哦!!! 虽然yield函数是在线程1中被调用的,但是弹栈靠的是esp栈顶指针寄存器指向的栈顶位置
esp是一个寄存器,用于指向当前cpu执行到某个线程时,对应线程关联的用户栈或者内核栈的栈顶位置。当某个函数执行结束后,会去弹出esp执行的栈顶元素,然后程序跳转到该元素位置处继续执行。
线程初始化,需要为当前线程创建一个栈,并且将当前函数入栈,再创建一个TCB保存当前线程的栈顶指针位置
具体流程如下:
在进行线程切换时,首先需要将当前esp指向栈顶地址保存到当前线程关联的tcb中,这里假设esp指向的栈顶地址和该栈顶地址存放元素值相同。
然后,通过将要切换到的线程B的TCB中的值,赋值给esp,就完成了线程的切换
void Yield(){
TCB1.esp=esp;
esp=TCB2.esp
}
第二步结束后,Yield函数就执行结束了,函数执行结束后,会将esp栈顶寄存器指针指向的栈顶元素弹出,因为此时已经完成了esp指针指向的切换,因此这里弹出的是线程B的函数栈
如果要写出一个上面讲到的浏览器模型,其实主要就是下面几点:
用户级线程只会在用户态来回切换,内核态是不知道用户线程存在的。
那上面浏览器案例举例,如果浏览器中某个用户线程执行了下载请求,因为下载需要访问网卡IO,网卡需要硬件,而使用硬件必须经过内核来操作,因此已访问网卡IO,就需要进入内核。
而网卡IO一阻塞住,内核就会切换进程执行,即从当前进程1切换到进程2执行,虽然此时进程1中还有其他线程可以切换执行,例如显示文本的线程,但是由于是用户级线程,操作系统看不见,因此不会处理,直接切换进程。
如果是采用用户级线程实现的浏览器,那么一般一个标签页对应一个用户线程,如果其中一个用户线程阻塞,那么会导致进程切换,即当前浏览器进程失去了对CPU的使用权,所以一旦一个标签卡住了,其他标签也动不了
即使此时只存在浏览器一个进程,那么也会因为其中某个用户级线程阻塞,失去对CPU控制权,CPU处于空转状态,因为CPU看不到其他用户线程,也就不会进行切换
用户级线程切换是不需要进入内核态完成的,并且线程调度算法需要用户自己完成
核心级线程和用户级线程区别,哪个快?
核心级线程和用户线程最大的区别在于,操作系统即内核可以看到相关的核心级线程存在,这样即使某个进程中某个核心级线程IO阻塞住了,CPU也可以切换到当前进程的其他核心级线程继续执行。
而对于用户级线程而言,用于不受内核控制,因此用户需要自己写相关线程调度算法
对于用户线程来讲,其切换过程就是先将指向当前线程函数栈顶的esp指针位置保持到本线程对应的TCB中,然后通过调度算法选择切换到哪一个用户线程,然后将对应用户线程关联的TCB恢复到esp上,然后在弹出esp指向的栈顶元素位置开始执行。
因为栈顶指针寄存器只有一个,而线程有多个,因此在线程切换时,需要一个TCB保存切换时esp指针指向的栈顶位置,再线程切换回来的时候,好恢复现场
多个执行序列使用一套映射,这不就是线程吗?
因此可以简单把多处理器看做是支持多个进程执行,但是由于一个MMU只对应一个CPU,因此该进程内,同时只能处理一个指令序列,因此指令序列只能并发执行。
而对于多核处理器来说,因为多CPU共用一个MMU,因此可以很好的支持一个进程内的多个核心级线程并行执行,因此每个CPU可以同时执行一段指令序列,并且是并行执行。
如果是多进程的话,对于多核处理器来说,需要不断对一套MMU进行切换,计算机根本并行不起来。
并发是同时触发,交替执行
并行是同时触发,并行执行
对于多核来说,为什么一定要是核心级线程呢?
首先,我们需要明白一点,用户区和内核区是分开的,因此对应的函数栈也是不同的。
因此用户区中的函数,如果调用了内核相关的函数,进入内核区的话,需要切换到内核的函数栈。
上面说过,对于内核级线程来说,因为需要在内核中创建,因此必须要进入内核去中。
因此,如果要从用户态切换到内核区,需要准备两套栈
进入内核的唯一方法就是中断
每个内核级线程都对应两套栈,分别是用户栈和内核栈,那么是如何找到内核栈的呢?
当发生中断,产生用户态到内核态的切换时,会定位到当前线程关联内核栈地址,然后将用户栈的两个状态寄存器SS和SP保存到内核栈中。
还要保存到内核栈中的是pc和cs寄存器的值。
当内核函数执行完毕后,通过IRET指令返回,该指令会弹出上面压入的五个元素,通过pc和cs恢复到刚才用户态中执行指令的位置。
通过SS和SP的值,恢复用户区中函数栈的状态。
当执行到int 0x80的时候,会产生中断,将用户栈和指令执行位置状态入栈。
大家可以思考一下,如果中断返回了,是不是就直接恢复到用户区304位置去执行了,然后用户栈的状态也恢复了,非常完美
进入内核区后,因为会先调用sys_read函数,因此会把1000压栈,表示sys_read函数执行结束后,会调回到1000地址处继续执行。
当内核函数sys_read被调用后,会启动磁盘读,然后当前内核线程进入阻塞状态。
下面就需要进行内核线程的切换。
线程和进程没有本质的区别,区别在于切换时,是否需要切换映射表,以及是否会共享内存资源
内核栈的切换和用户栈切换类似,首先需要找到下一个内核线程。
然后通过switch_to方法进行内核线程的切换,cur是当前线程TCB,next是下一个线程的TCB。
esp是一个寄存器,用于指向当前cpu执行到某个线程时,对应线程关联的用户栈或者内核栈的栈顶位置。当某个函数执行结束后,会去弹出esp执行的栈顶元素,然后程序跳转到该元素位置处继续执行。
因为要完成内核线程的切换,就必须进入内核态才行,因此当前线程s通过中断切换到内核态进入阻塞后,因为此时需要发生内核线程的切换,要切换到线程t
因为线程t被创建时,会在内核栈中保存当前线程t的用户栈状态和pc,cs状态,因此当第一次切换到线程t执行时,便会弹出这些状态,好恢复到线程t原先运行的样子。
如果线程t是因为阻塞或者时间片到期,被切换的话,那么切换时,也会把相关状态压入当前线程t对应的内核栈中
所以,最后四个问号,保存的是一段包含iret的指令。
假设是内核线程S需要切换到内核线程T
内核线程之所以可以让操作系统看见,是因为他在内核态中有一套内核栈,内核栈中保存了对应用户区状态,这是和用户线程的区别
因此对于内核线程的创建来说,既需要在用户态中分配相关内存存放用户栈和其他数据,也需要在内核态中分配内存存放内核栈数据,并且还需要在内核栈中记录用户态的状态,方便在内核态完成线程切换后,可以恢复到用户态继续执行。
内核级线程和进程的区别其实已经很小了,区别就在于完成了线程的切换后,再切换映射表