[linux] 进程调度 & 系统调用
文章来自公众号 : 技术乱舞
关于进程调度和系统调用也有很多知识,在这浅析一下这两方面的知识。
进程调度
上节谈了进程,进程算是程序运行状态的表现,接下来谈谈进程调度,直白点就是哪个进程要投入运行,占用cpu,什么时候运行,运行多长时间。
多任务操作系统
多任务操作系统,可以同时运行多个进程的操作系统,官方点就是同时并发的执行多个进程。在单核处理器上,其实单位时间内只能处理一个程序,多核处理器上,才算是真正的多任务,就比如四核处理器,其实他真正能同时并发运行的程序最多只有四个,但在使用操作系统的我们看来却不是,感觉能同时运行好多进程。
多任务操作系统主要分为两类:非抢占式多任务,抢占式多任务
- 抢占式(这节最主要谈的),就是利用调度程序来决定进程什么时候停止,什么时候从阻塞再运行,这个动作或者操作就叫抢占
- 非抢占式,说白了就是只有这个进程自己不运行完成,或者主动阻塞,它会一直运行下去。
对于linux来说肯定是抢占式的,并且随着时间的推移,调度算法也在慢慢改进,现在使用的是CFS(完全公平调度算法)
进程调度策略
策略决定调度程序在什么时候运行什么程序,在进程调度中如何更好的来让进程运行达到最优,一般考虑下面两种进程
- I/O消耗型
- 处理器消耗型
对于第一种来说,说白了就是大量的时间在等待I/O请求,比如键盘输入,网络,GUI图形界面,对于这种类型的进程最重要的就是响应时间最重要。
对于第二种cpu密集型的进程,大多数时间消耗在执行代码上,,这种进程调度策略应该是降低调度频率,延长运行时间。
对于系统调度策略来说,其实就是要平衡这两个矛盾,一个进程响应速度(极小化响应时间)另一个最大系统利用率(极大化系统吞吐率)
基本调度算法
- 优先级调度
- 时间片
优先级调度,一般有两种,一种是nice值,nice 值的范围是 [-20, 19]。默认的 default 值为 0;越低的 nice 值,代表着越高的优先级,反之,越高的 nice 值代表着越低的优先级。另一种是实时优先级,实时优先级是可配置的默认情况下的范围是 0~99,与 nice 值相反,越高的实时优先级数值代表着越高的优先级。任何实时进程的优先级都高于普通进程的优先级。
时间片,其实就是当前进程被抢占前能运行多久,每个时间片运行不同的进程。
关于基本调度算法,只是稍微提及了一下,像时间片有关的CFS,后面再介绍,如果有兴趣网上有很多解答。
Linux调度算法
Linux 总的调度结构,称之为 调度器类(scheduler class),允许不同的可动态添加的调度算法并存,总调度器根据调度器类的优先顺序,依次去进行调度器类的中的进程进行调度,挑选了调度器类,再在这个调度器内,使用这个调度器类的算法(调度策略)进行内部的调度。
Scheduling Class 的优先级顺序为 Stop_ask > Real_Time > Fair > Idle_Task,用的最多的是 Real_time 和 Fair ,下面主要介绍这两类。
CFS调度算法
在 CFS 中,给每一个进程安排了一个虚拟时钟 vruntime(virtual runtime),这个变量并非直接等于他的绝对运行时间,而是根据运行时间放大或者缩小一个比例,CFS 使用这个 vruntime 来代表一个进程的运行时间。如果一个进程得以执行,那么他的 vruntime 将不断增大,直到它没有执行。没有执行的进程的 vruntime 不变。调度器为了体现绝对的完全公平的调度原则,总是选择 vruntime 最小的进程,让其投入执行。他们被维护到一个以 vruntime 为顺序的红黑树 rbtree 中,每次去取最小的vruntime 的进程来投入运行。实际运行时间到 vruntime 的计算公式为:
[ vruntime = 实际运行时间 * 1024 / 进程权重 ]
这里的1024代表nice值为0的进程权重。所有的进程都以nice为0的权重1024作为基准,计算自己的vruntime。上面两个公式可得出,虽然进程的权重不同,但是它们的 vruntime增长速度应该是一样的 ,与权重无关。既然所有进程的vruntime增长速度宏观上看应该是同时推进的,那么就可以用vruntime来选择运行的进程,vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间,这就是CFS的主要思想。(上面两段话来源:https://blog.csdn.net/zhoutaopower/article/details/86290196)
实时调度策略
实时策略主要分为两种:
SCHED_FIFO :一个这种类型的进程出于可执行的状态,就会一直执行,直到它自己被阻塞或者主动放弃 CPU;它不基于时间片,可以一直执行下去,只有更高优先级的 SCHED_FIFO 或者 SCHED_RR 才能抢占它的任务,如果有两个同样优先级的 SCHED_FIFO 任务,它们会轮流执行,其他低优先级的只有等它们变为不可执行状态,才有机会执行。
SCHED_RR :与 SCHED_FIFO 大致相同,只是 SCHED_RR 级的进程在耗尽其时间后,不能再执行,需要接受 CPU 的调度。当 SCHED_RR 耗尽时间后,同一优先级的其他实时进程被轮流调度。
抢占和上下文切换
上下文切换,也就是从一个执行进程切换到另一个可执行进程。
在内核里,如上面图中所描绘的,当一个优先级高的进程进入可执行状态的时候,切换进程,其实图中说的听明白,设置标志位,调用schedule()切换新进程。
用户抢占
内核即将返回用户空间的时候,如果need_resched被设置,此时就会发生用户抢占。简单的说,用户抢占发生在下面两种情况下
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
内核抢占
简单的说说在什么时候发生内核抢占
- 中断处理程序正在执行,返回内核空间之前
- 内核代码再一次具有可抢占性的时候
- 内核中的任务显式地调用schedule()
- 内核中的任务阻塞
小结
调度是内核重要的组成部分,但是在这只是简单的介绍了介绍,很多细节都没有太多提及,大体上谈了谈调度的相关知识,但对于入门来说,够用了。
系统调用
什么是系统调用,说白了就是用户空间与内核空间交互的媒介。系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要有三个作用,首先为用户空间提供了一种硬件的抽象接口,第二,系统调用保证了系统的安全和稳定性,最后,进程运行在虚拟系统中,不能直接与硬件随意访问。系统调用是用户空间访问内核的唯一手段。
上图简单明了的解释了系统调用的过程,同时也可以系统调用一个处理程序调用执行一个系统调用,像内核需要给系统调用乘以4,如下图调用过程。
copy_to_user() copy_from_user()
从函数名字就可以看出函数大概的意思了,在写驱动的时候会经常用到这两个函数,之前学习驱动的时候专门了解了这两个函数。
在linux系统中,每个进程的运行空间分为内核空间和用户空间。之所以划分成这两个空间,是因为在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。这样将进程的运行空间分为内核空间和用户空间,会大大降低系统崩溃的可能性。
对于32位系统,每个进程拥有 4G 的地址空间,较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由进程使用,称为用户空间。而最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。由于两个空间是独立的,要实现内核空间与用户空间的数据传递就会用到copy_to_user()和copy_from_user()这两个函数。
unsigned long copy_to_user(void *to, const void *from, unsigned long n);
上面的函数是将内核空间的数据复制到用户空间,to:目标地址,from:源地址,n拷贝的大小。
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
上面的函数是将用户空间的数据复制到内核空间,to:目标地址,from:源地址,n拷贝的大小。
这两个函数可以实现内核空间和用户空间的数据交互。
小结
写这个的时候感觉有些知识点自己明白,可是表达出来却有难度,共同学习,共同进步,文章写的有些粗糙,宏观上谈论了进程调度,内核知识的学习我在路上,下一知识点重中之重interrupt。
欢迎关注 #公众号:技术乱舞 一起交流
灵魂碰撞