大家一定对进程调度有所了解,但了解不多。
我对这个概念有所了解,因为它被提到太多次了不太了解,因为总觉得不直观,浮在概念层面
今天,我们从三个角度来看进程调度当我们开始时,请帮助我们
提示:本文是关于linux—0.11的进程调度机制学习它的主干和框架,不要钻牛角尖
1.滴答透视
滴答声
计算机中有一种装置叫定时器,确切地说是可编程定时器/计数器。
这个定时器会每隔一段时间向CPU发送一次中断信号。
在linux—0.11中,这个间隔设置为10 ms,即100 Hz。
shedule.c
#定义100
发起的中断称为时钟中断,其中断向量号设置为0x20。
时钟中断
一切的源头都来自这个每10ms一次的时钟中断。
当然,如果没有操作系统,这个10ms的时钟中断一次,就会调用水漂,CPU会收到这个时钟中断信号,但不会做出任何响应。
不幸的是,linux提前设置了中断向量表。
附表. c
set _ intr _ gate
这样,当时钟中断,也就是0x20中断来临时,CPU会在中断向量表中查找0x20处的函数地址,也就是中断处理函数,跳转执行。
这个中断处理函数是timer_interrupt,是用汇编语言写的。
系统调用
_timer_interrupt://增加系统滴答数incl_jiffies//调用函数do_timercall_do_timer。
这个函数做两件事,一个是将系统滴答计数变量jiffies加1,另一个是调用另一个函数do_timer。
sched.c
Voiddo_timer...//当前线程还有剩余时间片,所以直接返回ifgt,0)返回,//如果没有剩余时间片,调度schedule(),
do_timer最重要的部分就是上面的代码,非常简单。
首先,将当前进程切片为—1,然后判断:
如果时间片仍然大于零,什么都不做,直接返回。
如果时间片已经为零,则调用schedule用脚思考,知道这是进程调度的主心骨
进程调度
别看一大堆,我把它简化的松散一点,你就明白了。
简而言之,这个函数做三件事:
1.获取剩余时间片最大且处于可运行状态(state = 0)的下一个进程号。
2.如果所有可运行进程的时间片都为0,重新分配所有进程的计数器(counter = counter/2+priority),然后再次执行步骤1。
3.最后我接下来得到一个进程号,调用switch_to方法,切换到这个进程执行。
转换过程
看看switch_to方法,它是用内联汇编语句编写的。
sched.h
#defineswitch_tostructlonga,b,_ _ tmp__asm__("cmpl%%ecx,_ current n t " " je1f n t " " movw % % dx,%1nt"\"xchgl%%ecx,_ current n t " " ljmp % 0 n t " " cmpl % % ecx,_ last _ task _ used _ math n t " " jne 1f n t " " clts n " " 1: ":: " m "(* amp,__tmp.a), " m "(* amp,__tmp.b),\"d"(_TSS),"c "((长)任务)),
这段话是过程开关的最低代码不懂也没关系其实你主要做两件事
1.通过ljmp跳转指令跳转到新进程的偏移地址。
2.将当前寄存器的值保存在当前进程的TSS中,并将新进程的TSS信息加载到每个寄存器中。
简单来说,保存当前进程的上下文,恢复下一个进程的上下文,然后跳过去!什么是语境只是他喵喵的一堆寄存器的值
至此,我们已经梳理了一个流程切换的整个环节我们先来复习一下
1.罪魁祸首是每10毫秒滴答一次的计时器。
2.而这个滴答会为CPU产生一个时钟中断信号。
3.这个中断信号会让CPU查找中断向量表,找到一个操作系统写的时钟中断处理函数do_timer。
4.do_timer首先将当前进程的计数器变量设置为—1如果此时计数器仍大于0,则此处结束
5.但是如果计数器= 0,则开始调度该进程。
6.进程调度就是把所有处于可运行状态的进程都找出来,找到一个计数器值最大的进程,扔进switch_to函数的参数里。
7.终极函数switch _ to会保存当前进程上下文,还原要跳转到的进程的上下文,同时让CPU跳转到这个进程的偏移地址。
8.然后,这个过程舒服地运行,等待下一个滴答。
好吧,好吧,我给你画张图听着,你很懒
这是滴答透视。
2.数据结构视角
我们从一个滴答开始,掀起一波,完成一个滴答的全过程。
让我们从静态的角度来看数据结构。
所有与承载过程相关的数据的罪魁祸首都来自这个数据结构。
struct task _ struct * task =,
没错,一个容量只有64的数组,数组中的元素是task_struct结构。
structtask _ structlongstatelongcounterlongprioritystructtss _ structtss,
这里只取我们需要关心的关键字段。
是状态进程的状态,值在linux中有明确的定义。
# define task _ running 0 # define task _ interruptible 1 # define task _ un interruptible 2 # define task _ zombie 3 # define task _ stopped 4
例如,如果state的值为未运行,则流程不会对其进行计划这一点在上面的tick—tock视角中解释得很清楚
计数器和优先级记录进程时间片counter记录剩余的时间片,priority的意思是优先级,实际上是给进程的初始时间片赋一个值这部分也是从滴答的角度在上面的代码里,非常清晰
最后一个重要的结构是tss,它是记录流程上下文信息的结构。
结构_结构...longeiplongeflagslongeax,ecx,edx,ebxlongesplongebp...,
在谈到滴答视角时,我们也说过,我们总是谈论语境什么是context其实就是这个结构中的值,只是一堆寄存器值
从滴答角度的解释中也提到,进程切换的核心步骤是一条ljmp指令此指令的副作用是将当前寄存器值保存在当前进程的TSS中,并将新进程的TSS信息加载到每个寄存器中这就是语境切换的本质
因此,我们可以看到,在数据结构透视图中提到的所有数据都在ticking透视图中使用。
3.操作系统启动过程透视
当您按下开机键时,引导程序将内核从硬盘加载到内存中。经过一番折腾,它开始执行系统初始化程序init/main.c
如果你对这部分的细节很好奇,可以看看我自制操作系统系列的前几篇文章。
好的,让我们从这个主线开始我们的旅程当然,我们只关注与过程相关的部分
虚空域...//第一步:进程调度初始化sched _ init(),...//第二步:创建一个新进程,做一些if(!fork())init(),//第三步:无限循环,操作系统正式启动进行()pause(),
第一步是调度sched_init进程的初始化什么是初始化很简单我就重点说一下要点
void _ init//初始化第一个进程的tssset _ TSS _ desc(),//将进程数组清除为for(I = 1,ilt64,i++)task(I)= NULL,//设置中断(滴答)set_intr_gate(0x20,amptimer _ interrupt),
其实就是为进程管理需要的数据结构做一些初始化工作,设置时钟中断,让滴答透视的进程可以走。
第二步跟进程调度关系不大,跟操作系统原理关系很大主要是最后的执行,直到shell程序等待用户的输入,我暂且不说
第三步,对于pause(),体现了操作系统的本质,即操作系统是一个中断驱动的无限循环代码。
这段代码是一个无限循环,让操作系统在这里空转进程调度是通过各种中断来完成的,比如本讲提到的时钟中断,用户输入是通过键盘中断来完成的,shell进程可能会执行一个新的程序来解释命令
当没有进程运行时,也就是CPU空闲时,操作系统会调度这段代码运行携带此代码的进程通常称为进程0这部分的原理可以在一篇文章《CPU空闲的时候在做什么,讲得很清楚,也很形象
这是操作系统启动过程的透视图我们可以看到,它其实是在做各种准备工作,然后启动一个shell进程,进入一个无限循环等待状态在此期间,时钟中断不断触发进程调度机制
附言
以上从滴答,数据结构,操作系统启动过程的角度,来说明进程调度的细节。
所谓滴答视角,可以理解为进程调度视角所谓数据结构视角,可以理解为流程管理视角
但我更喜欢这两个名字,尤其是滴答透视。他们是如此可爱和愚蠢!
但本文以linux最早的版本linux—0.11为例在操作系统的后期演进中,不断增加进程调度的细节例如,选择下一个要调度的进程不再是简单地比较时间片的大小例如,实际进程切换的时间被改变到系统调用返回之前,例如,页表切换改变
但整个骨架和进程是一样的,也就是说,当你学习现代操作系统更复杂的进程调度原理时,只要从这三个角度去分析,总能抓住主干。
[责任编辑:如思]
郑重声明:此文内容为本网站转载企业宣传资讯,目的在于传播更多信息,与本站立场无关。仅供读者参考,并请自行核实相关内容。