文章目录
一、如何使CPU工作起来
计算机是取值执行的,把程序放在内存中,设置PC等于程序的入口地址,例如PC=50,CPU就会把50放在地址总线上,把50这条指令从总线传回CPU,让其解释执行。执行完这条指令后,CPU会自动的取下一条地址执行。
一个一个程序按指令执行,存在的问题是利用率太低,为了提高CPU的利用率,CPU交替执行多道程序
一个CPU上交替执行多个程序,这个操作称为并发,如何切换,操作系统修改PC即可。为了能继续执行切换之前的程序,需要记录切换时刻该程序的样子,每个程序有一个存放信息的结构:PCB。执行中的程序和静态的程序不同,引入进程来描绘执行中的程序,启动了的程序就是进程。
二、多进程图像
上层用户怎么使用计算机了,启动了进程,比如制作PPT,听歌,写Word等等,下层即操作系统管理三个进程,为每个进程创建一个PCB,记录好每个进程的执行位置和状态等,让这些进程按照合理的次序推进,使进程向前同时给用户感觉计算机被使用起来了。
用户无需关心磁盘有多大,显存是怎样,声卡怎么放歌的,关心的是我启动的应用使用流不流畅。多进程存在于操作系统从开机启动到关机结束整个过程当中。
操作系统启动过程中最后执行的是main.c,main中的fork()创建了第一个进程:
if(!fork()){init();}
init执行了shell(win桌面),shell再启动其他进程,下面是shell的核心代码,根据用户输入的命令创建新的进程。
int main(int argc, chat * argv[])
{
while(1){
scanf("%s",cmd);
if(!fork()){
exec(cmd);
}
wait();
}
}
我们启动一个任务,操作系统就会启动一个进程,我们任务执行的好与坏和进程的推进情况有关系。
在本节中简单介绍了操作系统如何组织多进程,多进程又是如何交替执行的,多进程之间如何影响以及如何合作。
1、如何组织多进程
答:PCB+状态+队列
操作系统通过管理PCB来组织多进程,PCB(Process Control Block)是一种数据结构。当一个进程被创建时,操作系统会为其分配一个PCB,并将其加入到相应的进程队列中。在进程执行过程中,操作系统可以通过修改PCB中的信息来控制进程的状态转换、资源分配和调度。
根据进程运行的情况把进程分成下图五个状态,同一时间不能保证每个进程都处在运行态,当进程处于非运行态时,会被放入相应的队列中,有一些进程再等待执行,它们的PCB就放入就绪队列中,有一些进程在等待某事件,它们的PCB放入磁盘等待队列中,操作系统知道对各个进程的PCB在什么位置。
PCB中保存了进程的状态信息、上下文数据、资源分配情况以及其他与进程相关的信息。PCB通常包含以下重要的信息:
1.进程标识符(Process ID):用于唯一标识一个进程。
2.进程状态(Process State):表示进程当前所处的状态,如就绪、运行、阻塞等。
3.程序计数器(Program Counter):记录了进程下一条要执行的指令的地址。
4.寄存器状态(Register State):保存了进程在执行过程中的寄存器内容,包括5.通用寄存器、程序状态字等。
6.内存管理信息:包括进程的内存分配情况、页面表等。
7.资源分配信息:记录了进程所分配的资源,如打开的文件、网络连接、IO设备等。
8.进程优先级(Priority):用于调度器确定进程的执行顺序。
9.进程父子关系:记录了进程的父进程和子进程的关系。
2、多进程如何交替
答:队列操作+调度+切换
举个例子:
进程1 启动了磁盘读写,此时必须等待,将自己的状态变为阻塞态,将进程1放入阻塞磁盘等待队列中,接下来调用schedule()函数进行进程的切换,通过switch_to()将进程1的PCB切换到进程2的PCB,恢复进程2的执行现场。
schedule()
{
pNew = getNext(ReadyQueue);// 调度
switch_to(pCur,pNew); // 切换
}
下图展示了进程1和进程2交替运行,物理CPU上存放PCB的寄存器上内容的变化
3、多进程如何影响
当多个进程同时存在于内存中时,可能会修改同一块内存空间,导致其中一个进程的修改失效,使用映射表对进程进行地址空间的分离,映射表是内存管理的主要内容。
4、多进程如何进行合作
经典合作,生产者消费者问题。核心在于进程同步(合理的推进顺序)。
三、用户级线程的切换
进程 = 资源 + 指令执行序列,在进程的切换过程中,资源的切换消耗大部分时间,在一个进程中如果将资源和指令执行序列分开,指令执行序列共用一个资源,在这个进程中,进行指令序列的切换,使得指令序列交替执行,这样会提高计算机的运行效率以及计算机与用户的交互性。我们称运行的指令序列为线程,线程:保留了并发的优点,避免了进程切换的代价
线程(Thread)和进程(Process)是操作系统中两个重要的概念,它们具有以下区别:
1.资源和独立性:进程是操作系统中资源分配的基本单位,每个进程拥有独立的内存空间、文件描述符、堆栈等资源。而线程是进程内的执行单元,多个线程共享同一个进程的资源,包括内存空间和文件等。因此,线程之间的切换比进程之间的切换更轻量级。
2.执行和调度:每个进程都有独立的执行流,从程序的启动到结束。而线程是在进程内部调度执行的,多个线程可以并发执行,共享相同的进程上下文。线程的调度和切换更加高效,因为不需要切换整个进程的上下文。
3.同步和通信:线程之间共享进程的内存空间,因此通信和同步相对容易。线程之间可以通过共享变量进行数据交换和通信。而进程之间的通信则需要使用特定的机制,如管道、消息队列、共享内存等。
4.容错性:由于每个进程都有独立的内存空间,一个进程的崩溃通常不会影响其他进程。而线程共享相同的内存空间,一个线程的错误或崩溃可能会导致整个进程的崩溃。
5.创建和销毁开销:创建和销毁进程的开销通常比创建和销毁线程的开销大。这是因为创建进程需要分配独立的资源和初始化环境,而创建线程则只需要分配一些轻量级的数据结构。
浏览器加载一个网页时,图片,文本,logo等依次出现。从输入网址到完全显示网页这个过程发生了什么,操作系统做了什么事情?
打开浏览器相当于启动了一个进程,这个进程启动了多个线程
- 一个线程用来从浏览器接收数据
- 一个线程用来显示文本
- 一个线程用来处理图片(如压缩)
- 一个线程用来显示图片
如果线程按顺序执行,等文本全部下载好了之后,再切换另一个线程将内容显示出来,用户的体验不好,输入网址后,一片空白,过一段时间后刷的一下才出现内容。要提升用户的体验,得提高与用户的交互性,下载一部分内容,立马显示出来,下载内容的程序和显示内容的线程交替执行。下载的内容放在一个缓冲区中,显示内容的线程从缓冲区中取内容,这几个线程共享资源。
2、Create()函数和Yield()函数
保留线程切换时刻的现场,每个线程都有一个TCB,TCB中记录了线程切换时刻的样子,TCB的数据结构是栈,线程的切换,也就是从一个TCB切换到另一个TCB
PCB和TCB之间存在一定的关系和区别。它们都是用于管理和控制执行单元(进程或任务)的数据结构,都保存了执行单元的状态和相关信息。它们都可以用于实现调度、同步、通信和资源管理等功能。然而,PCB主要用于管理进程,而TCB主要用于管理任务。在某些操作系统中,进程可以包含多个任务,每个任务都有自己的TCB,但它们共享相同的PCB。
- 执行地址100处的A函数,执行到B()位置时,将地址104压栈,104是B函数的返回地址。
- 跳转到地址200处的B函数,执行到Yield函数时,进行栈的切换,从而完成线程的切换,跳转到地址300处执行C函数,执行到D函数处时,将304压栈,304作为D函数的返回地址。
- 跳转到地址400处的D函数,执行到Yield函数处时,进行栈的切换,切换到线程1,从栈中弹出线程1中Yield函数的返回地址204,继续往下执行。
切换线程时, 调用Yield(),上图红色Yield()切换代码:首先复原TCB2的esp,找到线程1的TCB1,取出esp,将1000赋值给CPU中的寄存器esp,完成栈的切换,从而完成了线程的切换 ,回到了线程1切换的时刻,Yield函数执行完毕,从栈中弹出此时的地址,继续往下执行。
void Yield(){
TCB2.esp = esp;
esp = TCB1.esp;
}
ThreadCreate()函数的核心就是用程序创建TCB,栈,切换的PC在栈中,并且使TCB和栈关联。
void ThreadCreate(A)
{
TCB *tcb = malloc(); // 申请一段内存作为TCB
*stack = malloc(); // 申请一段内存作为栈
*stack = A; // 线程的起始地址,func等地址入栈
tab.esp = stack; // 关联TCB和栈
}
用户级线程切换是在用户态完成的,没有进入内核,操作系统感知不到用户级线程。然而用户级有时候线程需要内核的帮忙,这种情况可能会出现问题,比如打开浏览器启动进程1,进程1getData时,需要等待网卡IO,如果进程1进入阻塞状态,则会切换到进程2执行,而不是切换到线程show,因为进程1看不到用户态的线程show,这样会造成浏览器页面处于卡顿状态。
四、内核级线程的切换
中断进入内核,找到TCB,切换TCB,切换内核栈,根据中断返回切换用户栈
为什么进程在内核中?因为进程需要分配资源,要访问内存,就必须通过内核的操作系统完成资源的分配。为什么有核心级线程,优点之一是在多核处理器计算机中,可以实现并行操作(将多个线程分别分配在不同的核上),提高运行效率。
内核级线程的切换是从一套栈切换到另一套栈,每一套栈包含用户栈和内核栈,并且用户栈和内核栈相关联。
1、内核线程switch_to的五段论
- 中断入口:进入切换,将用户栈与内核栈的链拉好
- 中断处理:引发切换,启动磁盘读或者时钟中断
- schedule:找到下一个线程的TCB
- switch_to :内核栈切换(第一级切换),执行ret切到某个内核程序(一段能返回第二级的代码,一段包含iret的代码(中断返回))
- 中断出口:iret(第二级切换),内核栈切到用户栈
2、ThreadCreate
void ThreadCreate(...)
{
TCB tcb = get_free_page(); //申请一段内存作为tcb
//申请一段内存作为内核栈并进行初始化,申请一段用户态的内存作为用户栈,把指针置好
*krlstack = ...;
*userstack传入;
填写两个stack
tcb.esp = krlstack;
tcb.状态 = 就绪;
tcb 入队;
}