目录
三 内核的运行
1
2
3
4
三 内核的运行
1
内核启动后,控制权交给用户空间程序,操作系统进入怠速状态。这个时候就可以加载新的业务应用来执行。
当然内核也提供了多种途径,来保障新业务运行所需环境的构建。比如,如果有专有驱动,可以在业务应用构建过程中动态加载。
动态库的环境变量可以进行修改。也允许一些依赖库可以早于C库加载。
系统正常运行后,内存中存在用户进程,内核进程,空闲进程及中断这几种上下文。当前CPU执行的任何一行代码都可以归类到上述某种上下文中。
2
到目前这里,我们一直混用进程和线程的概念,特别是在讨论到有关内核部分时。关于这两个概念的差异,个人理解如下:
为什么讲内核线程、内核进程和应用进程?分清楚进程和线程的定义,这一点在内核也是不例外的,有助于进一步理清上述几个概念。首先,线程之间是共享全局信息的,进程之间通过虚拟地址空间反而进行了隔离。所以,我们在应用层讲应用进程,因为各个应用之间是虚拟隔离的,这一点很好理解。提到应用线程,肯定是某个应用进程内的线程,这一点也是没有异议的。此时的沟通语境是一致的。
其次,为啥又在内核时,多会讲内核线程,因为内核线程是没有像应用那样的地址映射的。内核线程之间共享内核地址空间,映射一般是单一的对应映射。不像应用进程,每个有其独立的进程页表,用于获取内存页映射信息,在不连续的物理页上构建连续的虚拟地址空间。可以将内核看作一个大的进程,这样内核里共享内核全局信息的各个线程自然就被称作内核线程了。这样来看,内核线程提法也算是比较准确的。
那为啥又会提到内核进程呢?个人觉得这主要是与应用进程对应。一般讲,内核实现进程调度,进程管理,这里应该是与应用进程对应的。应用进程通过do_fork系统调用进入内核空间,进行调度时,跟内核其他线程是类似的。这时候为了方便理解,我们也可以将内核线程叫做内核进程。
实际上,一个应用进程对应到内核的是一个或多个内核线程。如果是单线程的应用进程,则是一个,否则是多个。这里针对pthread库创建线程的情况,下同。进一步的,应用进程对应的各个线程可以对应到内核线程,但是如何又做到共享应用进程全局信息的呢,个人猜测这些线程通过pthread或者其他库创建时,
虽然都是一个一个的内核线程,但是同属于一个应用进程的各个线程应该是共享了应用的地址空间映射,从而感觉是单进程多线程模式。实际上,通过不同的系统调用及参数,内核在创建线程时,会进行特殊处理。比如,可以指定是否共享虚拟内存空间,是否共享文件、设备句柄等。通过这种方式,实现了线程的要求。
Linux线程在内核看跟普通进程没有区别。所有用户空间进程的创建都是通过fork调用完成。fork调用通过clone系统调用创建进程。clone最终调用内核的do_fork完成进程的创建。
在clone系统调用中,可以提供诸多参数,这些参数就有是否共享父进程的文件系统、虚拟空间、打开的文件等等。可见,线程的创建是通过控制这些参数完成的。
最终线程在内核跟其他进程一样,也是task的实例,只不过他们跟父进程共享虚拟空间,我们才可以在用户空间实现全局变量在线程间的共享。
3
上下文切换
前面提到,内存中的任何一条指令,或者说CPU执行的任何一行代码,都是可以归类到某个上下文中的。不管这个上下文是用户进程、内核线程还是中断环境。
所谓的上下文切换,就是CPU在上述几种环境中切换。其实对CPU来讲,比如将自己想想成CPU,它是不知道当前是什么上下文的,CPU看到的只是内存中的指令序列。
上下文是用户人为设定的,方便系统资源的使用和管理。
有不同的上下文,就涉及到上下文切换。比如,从用户进程切换到内核空间,此时的内核空间可以看作还是用户进程上下文。从内核空间切换到内核中断,从内核中断切换到软中断,从软中断切换到内核线程,等等。不同的切换情况,做不同的动作,这在后面会介绍。
有上下文切换,就涉及到切换时机。切换时机包括两类,一种是主动切换,一种是被动切换。主动切换是指通过执行某些代码,主动让出CPU。
比如,通过执行触发睡眠的函数,等待IO的函数等,资源不满足的情况下,内核都会将当前进程放入等待队列,然后调度新的进程,此时发生主动切换。
如果进程在执行过程中发生了中断或新的更高优先级的进程资源准备好了,内核会将当前运行进程挂起,然后调度执行新的进程或处理中断,此时发生被动切换。
不管是主动切换还是被动切换,不管是从用户进程切换到内核线程还是从内核中断切换到其他环境,发生切换时,需要做哪些动作,这是切换的实质。
要了解切换做哪些动作,就需要知道与上下文相关的东西。上下文相当于一个执行实体的环境,主要包括了三个大的部分:
首先是CPU寄存器。因为切换后还要切换回来,所以当前CPU执行的环境是需要保存的,否则切换回来不知道下一条指令在哪里。
其次是内存中任务结构体。这是进程在内核中的表示。通过结构体,可以保存进程打开的系统资源。比如文件、网络等。这些也是需要保存好的,每个进程都不一样。
最后是与内存相关的。一是进程的虚拟内存映射表,特别是目录表,需要保存和恢复;二是cache。进程切换,可能会引发大量cache miss,目前硬件提供专门的模块,支持减少cache miss的情况。
4
优先级与调度策略
nice
rt
120
20
-20 --- 19
bonus -5
存在疑问,调度器是存在多个?普通进程和实时进程分别调度是什么意思?nice对应到调度方式(创建线程时)跟调度器的关系?调度时机的问题。
内核通过调用schedule接口进行调度。
站在不同角度,如果你是操作系统调度器的设计者,会如何设计调度器,会考虑哪些因素,结合这些理解Linux调度器设计。
实际场景中,几乎都是需要并行执行多个任务。
实际硬件上,单核就不用说了,即使是多核CPU,比如SMP,核心数量跟需要并行运行的任务相比,也是不在一个数量级的。
操作系统中,任务都会抽象为进程或者线程,而且系统中的进程往往会有上百甚至几百个,但是CPU的资源是有限的,时间是单调流失的,如何让这些进程在宏观上看起来是并行的,这就是操作系统调度要做的工作
不单是要任务看起来并行运行,而且不同的任务对执行有着不同的要求。
交互性任务,系统在事件满足的时候,要能够立即得到CPU进行处理,否则用户就会有延迟滞后的感觉,比如鼠标操作。
计算性任务需要得到更多的CPU执行时间,以便能够完成计算任务。这类任务对响应的及时性要求没有交互性任务敏感。
当然,还有一些任务,可能是交互和计算并重的。比如视频编辑软件,操作的流畅性,既需要交互得到及时响应,也需要进行大量CPU计算来反馈结果。二者相互影响。
操作系统调度的理想效果就是让需要交互的任务能够在需要响应时尽快的运行,让需要计算的任务能够获得更多的运行时间,让二者并重的任务能够在响应的及时性和计算资源获取上得到很好的平衡。
对操作系统来讲,它看到的是成百的需要调度的任务,有限的可运行的CPU核心,以及时限的敏感要求。
操作系统调度器需要在有限的时间内将可运行的任务调度,并让每个任务有一定的运行时间。
这里,时间是一个硬性要求,如果一个任务每间隔好几秒才会调度运行一次,那么在用户层最可能的感受就是系统快卡死了。所以,一个任务的最大调度间隔是多少才不会影响用户使用,这没有一个标准答案,调度器面对的场景是复杂的。但是通常来讲,感觉上这个值不能够大于1秒。
Linux内核中,有一个最小运行时间参考,是0.75ms,时钟tick是100或者1000,对应10ms或者1ms,这些都是可参考的数值。
当然,这里的时间是受到任务数量的影响的,不能脱离实际。如果系统中有几千个需要调度运行的任务,即使按照0.75来计算,也难以在1秒内调度一遍。当然,这种情况有点极端。
前面提到,实际场景中,任务是有其特点的,结合任务的特点,调度器需要做到:
等待事件的任务,在事件到达后,能够尽快的调度到并运行。
等待运行的任务,能够合理的分配给相应的CPU,选择CPU的原则有CPU的繁忙程度和CACHE匹配情况
所有等待运行的任务,能够在特定的时间内调度运行一次。避免长时间处于饥饿状态
任务要有区别,这种区别会影响到调度器对任务的选择。简单来讲,就是任务要有优先级差异。高优先级先调度,多运行。
任务运行时间要有限制,无论是主动放弃还是被动抢占,所以任务要有运行时间记录
将自己想象成上帝,站在全局角度,俯视下界,可以看到一些CPU和一堆要运行的有各种各样特点的任务。此时,你的思考、选择及这其中对平衡的把握,就是调度器要实现、要做、要完成的事。
在Linux中,相关概念如下:
系统中所有的任务可以分为两大类,实时任务和普通任务
每个CPU有运行队列和等待队列
队列采用红黑树进行插入删除
调度的时机有主动调度和被动调度。主动调度是当前任务放弃执行或者通过中断和系统调用返回用户空间时,执行schedule进行主动调度
被动调度有时钟中断处理触发的调度,也有任务因为等待特定事件而放弃执行导致调度,还有任务等待的事件发生后触发的调度。(最后这一种待确定。根据资料,Linux在唤醒队列中任务时,只是设置了调度标识,而非立即介入调度)。
调度时,先检查当前是否存在实时任务,如果没有,则使用cfs调度器对普通任务进行O(1)复杂度的公平调度。否则,优先调度实时任务。实时任务分两种,FIFO和RR,FIFO按先到先调度,RR按轮流调度
按照内核的设计,似乎是定义好了抽象的调度接口,系统各个地方需要时会调用这些接口。但是接口的具体实现,可以不同。就是可以根据需要添加不同规则和策略的调度器。
关于优先级,实时进程优先级1-99,普通进程100-139,具体受nice影响。nice范围-20到19。内核中对不同nice的进程,分配不同的权重。
上述优先级为静态优先级,任务运行过程中,会对优先级进行计算,生成动态优先级,最终依据动态优先级进行调度。
进程的时间片转换为虚拟运行时间,该时间为物理时间与权重的比重乘积。虚拟时间越小,越先调度。