操作系统的进程
操作系统的核心就是管理计算机硬件,包括cpu、内存等等。本文主要记录操作系统如何管理CPU的。
哈工大OS课程链接:https://www.bilibili.com/video/BV1d4411v7u7
1.进程和线程
cpu是如何工作的?取址指令、指令译码、执行。
如何管理cpu?因为cpu工作都是不断的取值执行,我们只需要管理PC的初始地址即可。
这样做有什么问题?
IO指令效率太慢了,如果我们的计算指令中间参杂着IO指令,那么每次执行到IO指令的时候,CPU就会等待IO返回数据,降低了CPU的利用率。浪费了很多时间等IO指令。
所以,操作系统在管理CPU的时候引入了多进程来解决CPU利用率低的问题。
进程就是运行中的程序,操作系统把所有的进程信息都存放在 PCB(Process Control Block) 中,记录进程的运行中状态。所以,一个启动了的程序在操作系统中就是一个进程,然后为了充分的利用CPU,我们启动的多个程序将交替执行,操作系统需要把每个进程的状态记录好,合理的调度资源。操作系统从开机到关机始终记录着所有的进程。
(1)进程切换
上面我们说到,进程执行到一定程度后可能发生阻塞、或者时间片无了,会发起进程切换。操作系统就要切换到别的进程上去执行,但是如果就绪队列上有多个进程,操作系统应该选择谁呢?这就牵扯到了CPU的调度策略。
对于不同的任务,任务之间的关注点都不一样。
比如前台任务可能更关注响应时间短,后台任务关注整体时间端。考虑IO密集型和CPU密集型任务的特点不同,所以CPU调度进程就需要这种考虑各种情况来调度。若响应时间小,则切换次数就要频繁,系统内耗就会变大且吞吐量变小。
所以一个调度算法既要调度的快、又要考虑不同的场景的关注点。
- FIFO:先来先服务。
- SJF:短作业优先,降低平均周转时间到最小,但是响应时间就不能保证。
- RR:按照时间片轮转调度,时间片一到就切换进程,保证响应时间。
- Priorty:设置优先级,但是可能进程饥饿。前台任务使用RR,后台任务使用SJF,动态调整任务的优先级。但是后台任务若占用太多时间又会阻塞前台任务的响应时间,所以后台任务也需要时间片限制并兼容优先级。
操作系统中进程的状态如下:
多进程之间的交替,就由就绪、运行、阻塞三个状态中切换。
如何从就绪的进程中选择下一个进程,就称为进程调度,然后就把运行的状态切换为当前的进程,然后再PCB中保存保存上一个进程的状态。操作系统组织进程的形式就是把进程状态放在PCB队列形成进程的交替执行分为:队列操作+调度+切换。
PCB里面存储了进程Pid、进程状态、进程资源分配(LDT表)等信息
- 保存当前的进程状态(PC、寄存器)到PCB,更改进程状态,放入阻塞队列。
- 切换内核栈,调度下一个进程,更新正在运行的进程,更改进程状态。
- 更新内存管理的数据结构,PCB信息。
(2)进程通信
进程通信是指进程与进程之间信息交换,但是各个进程的拥有的内存地址是相互独立的,为了保证安全,一个进程是无法访问另一个进程的地址空间的,为了实现进程通信…
操作系统为了实现进程通信,提供了以下方法:共享内存、信号、信号量、管道、消息队列、socket套接字
。
-
共享内存:顾名思义,多个进程共享一块儿内存区域,这样就可以一起食用里面的数据了。
-
信号量:共享内存的时候,多个进程的访问会产生数据的安全问题。所以需要互斥,信号量的PV操作就是其中之一。
-
消息队列:就是信息传达,进程之间通过发送/接受消息来通信。
-
管道:
管道
–用于连接读写进程的一个共享文件(pipe文件)管道是内存开辟的一块儿缓冲区,只支持半双工。这个文件如果没有写满就不允许读,没读到空就不允许写。
对管道进行读操作的时候,读进程必须互斥。
用户级线程
进程是资源分配的最小单位,线程是资源调度的最小单位。一个进程往往包含N个线程,线程之间并发不需要切换系统资源。
#如果一个进程包含多个线程,那么当一个线程阻塞住,进程也会被阻塞
所以我们将线程分为用户级线程和核心级线程,将m个用户级线程和n个内核级线程相映射,解决进程和线程间1:n并发度不够高的问题,也解决了一个进程占用太多核心线程的问题。
用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
线程切换不需要用户态/核心态切换,创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
以前网速比较慢的时候,打开浏览器访问一个网页,首先弹出来的是网页的文字部分,然后是一些图片,最后才是一些小视频之类的。为什么呢?浏览器向服务器发起访问的程序是一个进程,它包含若干线程,比如:一个线程用来从服务器接收数据,一个线程用来显示文本,一个线程用来显示文本,一个线程用来显示图片等等。
所以我们有了多线程,上面这个例子就牵涉到线程(用户级线程)的切换,多个线程之间线程的切换只有切换指令。线程中切换指令,用一个TCB(Thread Control Block)要存储一个线程的地址,并且每个线程独有一个栈来存储自己的指令,切换的PC也存入栈中。
用户可以使用yield()
函数来再用户态中完成线程的不断切换,每个线程有自己的栈,比如上图A函数调用B函数时,将地址放入线程1的栈1中,同理线程2的地址放在栈2中。切换栈的时候,用TCB将栈1的地址保存起来,esp
来保存栈1的栈顶指针。
切换栈的时候只需要更改寄存器esp
的值就可以切换栈:
void Yield2() //线程2切换到线程1
{
TCB2.esp = esp; // 保存当前栈顶地址
esp = TCB1.esp; // 切换栈
}
核心级线程
内核线程:由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。
当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态。
当有多个处理机时,一个进程的多个内核级线程可以同时执行。
因为想要线程被cpu调度,必须得建立一个TCB,而TCB属于内核数据,想要访问内核数据,必须得要通过系统调用中断 进入内核,而进入内核态执行代码,必须得要一个栈用来保存用户态下的状态。
内核级线程的并发性会更好,内核的线程不再受用户控制,完全由操作系统调度。所以在多核cpu中,就是操作核心级线程,将每个线程分配到多核cpu核心上,利用多核CPU处理多线程并行,多核CPU公用一套MMU
。
本质区别:核心级线程是需要进入到系统内核中执行的程序,核心级线程需要在用户态和核心态里面跑,在用户态里跑需要一个用户栈,在核心态里面跑需要一个核心栈。用户栈和核心栈合起来称为一套栈。
用户栈和核心栈之间的关联:(这里不太懂,参考即可)
通过INT
指令当从用户栈切换到内核栈,把用户占的SS
,SP
,PC
,CS
都给保存起来,也就是用户态执行到的位置和指令。然后因为在内核栈中记录了用户栈的状态TCB,所以统称内核线程使用是用户栈和核心栈的一套栈。执行IRET
指令切换回去的时候,将这五个栈弹出,随着就退回到用户栈执行的地方。
内核栈切换仍然使用TCB找到内核栈指针,切换内核程序;在CS后面入栈的肯定就是一条指令包含IRET
指令,通过栈内的PC:CS切换到用户程序完成程序。
中断(内核线程和用户线程切换)----->可能因为阻塞线程切换----->找TCB完成内核栈切换------->内核栈切换完IRET----->根据栈里面的CS和PC回到用户栈。如果线程S和T不在同一个进程,还需要切换映射表,也就是管理内存。
塞线程切换----->找TCB完成内核栈切换------->内核栈切换完IRET----->根据栈里面的CS和PC回到用户栈。如果线程S和T不在同一个进程,还需要切换映射表,也就是管理内存。
[外链图片转存中…(img-b53bTHyJ-1650710644519)]