在单片机上,常用的方式是:系统初始化-while (1) {}。(当然,微控制器也可以运行类似FreeRTOS,也可以有进程切换。)
在有操作系统的CPU上运行的逻辑是允许多个进程(实际上是程序)同时运行。例如,您可以在操作鼠标的同时播放音乐和编辑文本。宏观上看,好像是多个任务并行执行。事实的本质是CPU在不断的调度每个进程,让每个进程都能响应。同时,需要考虑不同场景下的响应效率(流程的执行时间)。
进程调度器的任务是将CPU时间合理分配给正在运行的进程,制造所有进程并行运行的假象。这对调度人员提出了要求:
1.调度器分配的CPU时间不能太长,否则会延迟其他程序的响应,很难保证公平性。
2.调度器分配的时间不能太短,每次调度都会导致上下文切换,代价很大。
调度器的任务是:1 .分配时间给过程2。上下文切换。
所以具体来说,调度器的任务是明确的:一句话,就是按照合理的调度算法选择一个进程,让进程运行到应该运行的时间,切换两个进程的上下文。
如果
如果绝大多数进程都在使用CPU做运算,那么这个进程就叫CPU消耗,比如打开Matlab做一个大型运算。I/O需求不多,从系统响应的角度来看,调度器应该不会让它们经常运行。对于消耗处理器的进程,调度策略往往是降低它们的执行频率,延长运行时间。
为了提高响应速度,Linux系统倾向于优先调度I/O消耗。
[文章福利]边肖收集了一些关于linux内核学习的书籍和视频,我个人觉得比较好,分享在小组文件里。如有需要,可以私信【内核】添加,免费获取!(包括视频教程、电子书、实践项目和代码)
一、普通进程
在Linux中,普通进程依靠一个叫做nice value的东西来描述进程的优先级。nice值的范围是[-20,19]。默认值为0;nice值越低,优先级越高;反之,nice值越高,优先级越低。
优先级越高,执行时间就越高(注意,这里的执行时间越高是指每个可执行进程在很短的观察时间内执行一次的情况,这里的描述可能会造成一些歧义,要冷静观察)。您可以通过ps-el查看系统中的进程列表。
二、实时进程
实时优先级是可配置的。默认情况下,范围是0~99。与nice值相反,实时优先级值越高,优先级越高。同时,任何实时进程的优先级都高于普通进程。
——小结
实时进程优先级:值越高,优先级越高。
普通进程优先级:nice值越高,普通进程的优先级越低。
任何实时进程的优先级普通进程
在
调度程序的优先级顺序是:
SchedulingClass的优先级顺序是Stop _ ask real _ timefairdidle _ Task,开发者可以根据自己的设计需求将自己的任务分配到不同的调度类中。其中,实时和公平是最常用的。这里有两个主要类别。
1.如何选择运行哪个进程?
——在CFS中,为每个进程安排了一个虚拟时钟vruntime(virtualruntime)。这个变量不直接等于它的绝对运行时间,而是根据运行时间按比例放大或缩小。CFS使用这个vruntime来表示一个进程的运行时间。如果执行一个进程,它的vruntime会不断增加,直到不执行为止。未执行的进程的运行时间保持不变。为了体现绝对完全公平的调度原则,调度器总是选择vruntime最小的进程来执行。他们被留在了一个叫vruntime的地方。
为顺序的红黑树rbtree中,每次去取最小的vruntime的进程来投入运行。实际运行时间到vruntime的计算公式为:[vruntime=实际运行时间*1024/进程权重]
这里的1024代表nice值为0的进程权重。所有的进程都以nice为0的权重1024作为基准,计算自己的vruntime。上面两个公式可得出,虽然进程的权重不同,但是它们的vruntime增长速度应该是一样的,与权重无关。既然所有进程的vruntime增长速度宏观上看应该是同时推进的,那么就可以用vruntime来选择运行的进程,vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间,这就是CFS的主要思想。
2.挑选的进程进行运行了,它运行多久?
进程运行的时间是根据进程的权重进行分配。
[分配给进程的运行时间=调度周期*(进程权重/所有进程权重之和)]
CFS调度器实体结构作为一个名为se的sched_entity结构,嵌入到进程描述符structtask_struct中
structsched_entity{structload_weightload;/*forload-balancing*/structrb_noderun_node;structlist_headgroup_node;unsignedinton_rq;u64exec_start;u64sum_exec_runtime;u64vruntime;u64prev_sum_exec_runtime;u64nr_migrations;#ifdefCONFIG_SCHEDSTATSstructsched_statisticsstatistics;#endif#ifdefCONFIG_FAIR_GROUP_SCHEDstructsched_entity*parent;/*rqonwhichthisentityis(tobe)queued:*/structcfs_rq*cfs_rq;/*rq"owned"bythisentity/group:*/structcfs_rq*my_q;#endif};对于实时调度策略分为两种:SCHED_FIFO和SCHED_RR:
这两种进程都比任何普通进程的优先级更高(SCHED_NORMAL),都会比他们更先得到调度。
SCHED_FIFO:一个这种类型的进程出于可执行的状态,就会一直执行,直到它自己被阻塞或者主动放弃CPU;它不基于时间片,可以一直执行下去,只有更高优先级的SCHED_FIFO或者SCHED_RR才能抢占它的任务,如果有两个同样优先级的SCHED_FIFO任务,它们会轮流执行,其他低优先级的只有等它们变为不可执行状态,才有机会执行。
SCHED_RR:与SCHED_FIFO大致相同,只是SCHED_RR级的进程在耗尽其时间后,不能再执行,需要接受CPU的调度。当SCHED_RR耗尽时间后,同一优先级的其他实时进程被轮流调度。
上述两种实时算法都是静态的优先级。内核不为实时优先级的进程计算动态优先级,保证给定的优先级的实时进程总能够抢占比他优先级低的进程。
一、进程切换
从进程的角度看,CPU是共享资源,由所有的进程按特定的策略轮番使用。一个进程离开CPU、另一个进程占据CPU的过程,称为进程切换(processswitch)。进程切换是在内核中通过调用schedule()完成的。
发生进程切换的场景有以下三种:
1、进程运行不下去了:
比如因为要等待IO完成,或者等待某个资源、某个事件,典型的内核代码如下:
//把进程放进等待队列,把进程状态置为TASK_UNINTERRUPTIBLEprepare_to_wait(waitq,wait,TASK_UNINTERRUPTIBLE);//切换进程schedule();2、进程还在运行,但内核不让它继续使用CPU了:
比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来;
3、进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:
用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到。
进程切换分为自愿切换(Voluntary)和强制切换(Involuntary),以上场景1属于自愿切换,场景2和3属于强制切换。
自愿切换发生的时候,进程不再处于运行状态,比如由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态;
强制切换发生的时候,进程仍然处于运行状态(TASK_RUNNING),通常是由于被优先级更高的进程抢占(preempt),或者进程的时间片用完了。
注意:进程可以通过调用sched_yield()主动交出CPU,这不是自愿切换,而是属于强制切换,因为进程仍然处于运行状态。有时候内核代码会在耗时较长的循环体内通过调用cond_resched()或yield(),主动让出CPU,以免CPU被内核代码占据太久,给其它进程运行机会。这也属于强制切换,因为进程仍然处于运行状态。
进程自愿切换(Voluntary)和强制切换(Involuntary)的次数被统计在/proc/
也可以用pidstat-w命令查看进程切换的每秒统计值:
pidstat-w1Linux3.10.0-229.14.1.el7.x86_64(bj71s060)02/01/2018_x86_64_(2CPU)12:05:20PMUIDPIDcswch/snvcswch/sCommand12:05:21PM012990.940.00httpd12:05:21PM0276870.940.00pidstat自愿切换和强制切换的统计值在实践中有什么意义呢?
大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高。如果一个进程的强制切换占多数,意味着对它来说CPU资源可能是个瓶颈,这里需要排除进程频繁调用sched_yield()导致强制切换的情况。
自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。
抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。
抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:
触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。抢占只在某些特定的时机发生,这是内核的代码决定的。
每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。
直接设置TIF_NEED_RESCHED标志的函数是set_tsk_need_resched();
触发抢占的函数是resched_task()。
TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:
周期性的时钟中断时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(schedulercore)的函数,它通过调度类(schedulingclass)的task_tick方法检查进程的时间片是否耗尽,如果耗尽则触发抢占:
/**Thisfunctiongetscalledbythetimercode,withHZfrequency.*Wecallitwithinterruptsdisabled.*/voidscheduler_tick(void){...curr->sched_class->task_tick(rq,curr,0);...}唤醒进程的时候当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
新进程创建的时候如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的task_fork方法触发抢占:
intsched_fork(unsignedlongclone_flags,structtask_struct*p){...if(p->sched_class->task_fork)p->sched_class->task_fork(p);...}进程修改nice值的时候如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见set_user_nice()。
进行负载均衡的时候在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占:
load_balance(){...move_tasks();...resched_cpu();...}RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。
触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。
抢占如果发生在进程处于用户态的时候,称为UserPreemption(用户态抢占);如果发生在进程处于内核态的时候,则称为KernelPreemption(内核态抢占)。
执行UserPreemption(用户态抢占)的时机
1.从系统调用(syscall)返回用户态时;
源文件:arch/x86/kernel/entry_64.Ssysret_careful:bt$TIF_NEED_RESCHED,%edxjncsysret_signalTRACE_IRQS_ONENABLE_INTERRUPTS(CLBR_NONE)pushq_cfi%rdicallschedulepopq_cfi%rdijmpsysret_check2.从中断返回用户态时:
retint_careful:CFI_RESTORE_STATEbt$TIF_NEED_RESCHED,%edxjncretint_signalTRACE_IRQS_ONENABLE_INTERRUPTS(CLBR_NONE)pushq_cfi%rdicallschedulepopq_cfi%rdiGET_THREAD_INFO(%rcx)DISABLE_INTERRUPTS(CLBR_NONE)TRACE_IRQS_OFFjmpretint_check执行KernelPreemption(内核态抢占)的时机
inux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:
CONFIG_PREEMPT_NONE=y在CONFIG_PREEMPT=y的前提下,内核态抢占的时机是:
1.中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
#ifdefCONFIG_PREEMPT/*Returningtokernelspace.Checkifweneedpreemption*//*rcx:threadinfo.interruptsoff.*/ENTRY(retint_kernel)cmpl$0,TI_preempt_count(%rcx)jnzretint_restore_argsbt$TIF_NEED_RESCHED,TI_flags(%rcx)jncretint_restore_argsbt$9,EFLAGS-ARGOFFSET(%rsp)/*interruptsoff?*/jncretint_restore_argscallpreempt_schedule_irqjmpexit_intr#endif2.当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
在preempt_enable()中,会最终调用preempt_schedule来执行抢占。preempt_schedule()是对schedule()的包装。
原文链接:https://blog.csdn.net/zhoutaopower/article/details/86290196