内核调度器源码解析-Linux-从调度入口到挑选下一个进程 (内核调度器源码是什么)
在 Linux 内核中,调度器扮演着至关重要的角色,决定了哪个进程将获得 CPU 的执行时间。本文将深入剖析内核中调度器的代码实现,从入口函数开始,一步步分析如何选择下一个要执行的进程。让我们一同揭开这个内核之谜。
调度器入口
Linux 调度器入口函数定义在
kernel/sched/core.c
中:
asmlinkage__visible void __schedschedule(void) { // 获取当前任务结构体的指针 struct task_struct tsk = current; // 将任务提交到调度工作队列中 sched_submit_work(tsk); // 进入调度循环,直到没有需要被调度的任务 do { // 禁用抢占 preempt_disable(); // 调用实际的调度函数 __schedule,并传入调度策略参数 SM_NONE __schedule(SM_NONE); // 启用抢占,但不进行重新调度 sched_preempt_enable_no_resched(); } while (need_resched()); // 循环直到没有需要重新调度的任务 // 更新工作队列中的任务状态 sched_update_worker(tsk); } EXPORT_SYMBOL(schedule);
调度器的入口函数是
schedule
,首先获取当前任务结构体的指针,然后将任务提交到调度工作队列中,接着进入一个循环,该循环会禁用抢占,调用实际的调度函数
__schedule
,并在循环结束后启用抢占。循环会一直执行,直到没有需要重新调度的任务为止。最后,函数会更新工作队列中任务的状态。函数最后 export 导出
schedule
函数以供其他部分使用。
实际调度函数
这里,
__schedule
函数负责实际的调度操作。它获取了当前任务结构体的指针(
prev
)、运行队列(
rq
)以及切换计数器(
switch_count
)。通过调用
pick_next_task
函数,它选择下一个要运行的进程(
next
)。最后,通过
context_switch
函数,它进行进程切换,将 CPU 控制权移交给下一个进程。
static void __sched__schedule(bool preempt) { struct task_struct prev, next; unsigned long switch_count; struct rq rq; prev = current; rq = this_rq(); switch_count = &prev->nivcsw; // 获取下一个要运行的进程 next = pick_next_task(rq); // 切换到下一个进程 context_switch(rq, prev, next, switch_count); // 如果需要抢占,启用抢占 if (preempt) need_resched(); }
具体如何挑选下一个需要运行的进程,就要扒开
pick_next_task
函数。
选择下一个进程
pick_next_task
函数负责选择下一个要运行的进程。它主要根据以下规则进行选择:
- 如果存在可运行的实时任务,则选择优先级最高的实时任务。
- 否则,如果存在可运行的一般进程,则选择优先级最高的可运行一般进程。
- 否则,如果存在可运行的空闲任务,则选择优先级最高的可运行空闲任务。
-
否则,返回
NULL
。
以下是
pick_next_task
函数的代码:
static inline struct task_struct __pick_next_task(struct rq rq, struct task_struct prev, struct rq_flags rf) { const struct sched_class class; struct task_struct p; // 优化:如果前一个任务是公平调度类中的任务,且运行队列中的任务数与 CFS 队列中的任务数相等, // 则可以直接选择下一个公平类任务,因为其他调度类的任务无法抢占 CPU。 if (likely(!sched_class_above(prev->sched_class, &fr_sched_class) && rq->nr_running == rq->cfs.nr_running)) { p = pick_next_task_fair(rq); if (p) return p; } // 遍历所有调度类,选择优先级最高的可运行进程 for_each_class(class) { if ((p = class->pick_next_task(rq, prev, rf))) return p; } return NULL; }
在实际操作中,Linux 内核使用了一个层次化的调度器模型,其中每个调度类负责管理其自己的任务队列。例如,公平调度器管理着可运行的一般进程,实时调度器管理着可运行的实时任务,空闲调度器管理着可运行的空闲任务。
以上就是 Linux 内核调度器的代码实现的简要概述。这是一个复杂且重要的子系统,在保证系统性能和公平性方面发挥着至关重要的作用。
深入解析Linux系统下的进程切换
Linux内核下进程切换Linux切换并没有使用X86CPU的切换方法,Linux切换的实质就是cr3切换(内存空间切换,在switch_mm函数中)+ 寄存器切换(包括EIP,ESP等,均在switch_to函数中)。 这里我们讲述下switch_to主流程:1、 在switch_mm函数中将new_task-pgd设置到cr3寄存器中,实现页表切换,由于每个进程3-4G的页表映射机制完全一样(从内核页表中直接复制过来的),故这里虽然切换了pgd,但是并无影响,只是在任务回到用户空 间中时,才会发生变化,因为每个任务在0-3G中的页表映射都是各自独立的;2、 压入esi edi ebp到cur_task堆栈中;3、 将esp寄存器中的值保存到cur__中,也就是将cur_task切换时的堆栈指针保存起来;4、 将new__中的值设置到esp寄存器中,这里的new__中的值就是new_task上一次被换出时的堆栈指针,现在被恢复了,2和3结合实现了从cur_task到new_task的堆栈切换;5、 将1f地址设置到cur__中,当下次cur_task恢复运行时,将会从1f处开始运行,下面阐述了这种原理;6、 将new__压入到new_task的堆栈中,这里new__的值就是1f,因为从4中可知,new_task上一次被换出时,其也是和现在的cur_task类似,1f地址被设置到new__中;7、 随后CPU跳转到__switch_to函数中开始执行,注意这里使用的是jmp,不是call,call会pusheip,而jmp不会,由于__switch_to是函数,当CPU执行完该函数后,最后一条指令必然为iret,该指令会popeip,从5中可以知道,此时new_task堆栈中的镜像为[......., esi,edi,ebp,eip(1f)],故popeip将值eip(1f)设置到eip寄存器中,这样当iret执行完毕后,CPU将从eip处继续执行,也就是从1f处继续执行;8、 此时已经在new_task的执行环境中了,pop ebp, pop edi, popesi,回到schedule函数中,当返回用户空间中时,由于new_task用户空间的eip,ss,esp等均被从new_task的堆栈中弹出到对应寄存器中,从而new_task得以顺利执行。 Linux 前后台进程切换当你用shell启动一个程序时,往往他是在前台工作的。 例如经常用PUTTY连接到远程服务器执行脚本的时候,如果本地网络中断后,这个时候前台进程就结束了,比较的懊恼,必须重新执行。 因此有必要进行前后台进程的切换。 例如直接在终端里输入firefox,那么会打开firefox,但当你关闭此终端或者ctrl+c强制终止时,firefox也随机关闭了。 你可以在执行时后面加一个,这样就在后台工作了。 Shell支持作用控制,有以下命令:(1). command 让进程在后台运行(2). jobs –l 查看后台运行的进程(3). fg %n 让后台运行的进程n到前台来(4). bg %n 让进程n到后台去;PS:n为jobs查看到的进程编号。 1、执行命令切换至后台在Linux终端运行命令的时候,在命令末尾加上符号,就可以让程序在后台运行代码如下:root@Ubuntu$ ./tcpserv012、切换正在运行的程序到后台如果程序正在前台运行,可以使用Ctrl+z 选项把程序暂停,然后用 bg %[number]命令把这个程序放到后台运行,这个步骤分为3步,如下:2.1暂停程序运行CTRL+Zctrl + z跟系统任务有关的,ctrl + z可以将一个正在前台执行的命令放到后台,并且暂停。 代码如下:[Oracle@linuxidc ~]$ sh [1]+Stopped 2.2查看暂停的程序察看jobs使用jobs或ps命令可以察看正在执行的jobs。 代码如下:[oracle@linuxidc ~]$ jobs -l[1]+ 4524Stopped 命令执行的结果,+表示是一个当前的作业,减号表是是当前作业之后的一个作业。 jobs -l选项可显示所有任务的PID,jobs的状态可以是running, stopped,Terminated2.3切换程序至后台bg将一个在后台暂停的命令,变成继续执行如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出.代码如下:[oracle@linuxidc ~]$ bg %1[oracle@linuxidc ~]$ jobs -l[1]+ 4524Running 2.4切换程序至前台也可以用 fg %[number]指令把一个程序掉到前台运行代码如下:[oracle@linuxidc ~]$ fg %1./tcpserv012.5终止后台程序也可以直接终止后台运行的程序,使用 kill 命令代码如下:[oracle@linuxidc ~]$ kill %1但是如果任务被终止了(kill),shell 从当前的shell环境已知的列表中删除任务的进程标识;也就是说,jobs命令显示的是当前shell环境中所起的后台正在运行或者被挂起的任务信息。
Linux系统的进程调度
Linux进程调度
1.调度方式
Linux系统的调度方式基本上采用“ 抢占式优先级 ”方式,当进程在用户模式下运行时,不管它是否自愿,核心在一定条件下(如该进程的时间片用完或等待I/O)可以暂时中止其运行,而调度其他进程运行。一旦进程切换到内核模式下运行时,就不受以上限制,而一直运行下去,仅在重新回到用户模式之前才会发生进程调度。
Linux系统中的调度基本上继承了UNIX系统的 以优先级为基础 的调度。也就是说,核心为系统中每个进程计算出一个优先级,该优先级反映了一个进程获得CPU使用权的资格,即高优先级的进程优先得到运行。核心从进程就绪队列中挑选一个优先级最高的进程,为其分配一个CPU时间片,令其投入运行。在运行过程中,当前进程的优先级随时间递减,这样就实现了“负反馈”作用,即经过一段时间之后,原来级别较低的进程就相对“提升”了级别,从而有机会得到运行。当所有进程的优先级都变为0(最低)时,就重新计算一次所有进程的优先级。
2.调度策略
Linux系统针对不同类别的进程提供了3种不同的调度策略,即SCHED_FIFO、SCHED_RR及SCHED_OTHER。其中,SCHED_FIFO适合于 短实时进程 ,它们对时间性要求比较强,而每次运行所需的时间比较短。一旦这种进程被调度且开始运行,就一直运行到自愿让出CPU或被优先级更高的进程抢占其执行权为止。
SCHED_RR对应“时间片轮转法”,适合于每次运行需要 较长时间的实时进程 。一个运行进程分配一个时间片(200 ms),当时间片用完后,CPU被另外进程抢占,而该进程被送回相同优先级队列的末尾,核心动态调整用户态进程的优先级。这样,一个进程从创建到完成任务后终止,需要经历多次反馈循环。当进程再次被调度运行时,它就从上次断点处开始继续执行。
SCHED_OTHER是传统的UNIX调度策略,适合于交互式的 分时进程 。这类进程的优先级取决于两个因素:一个是进程剩余时间配额,如果进程用完了配给的时间,则相应优先级降到0;另一个是进程的优先数nice,这是从UNIX系统沿袭下来的方法,优先数越小,其优先级越高。nice的取值范围是-20 19。用户可以利用nice命令设定进程的nice值。但一般用户只能设定正值,从而主动降低其优先级;只有特权用户才能把nice的值设置为负数。进程的优先级就是以上二者之和。
后台命令对应后台进程(又称后台作业)。后台进程的优先级低于任何交互(前台)进程的优先级。所以,只有当系统中当前不存在可运行的交互进程时,才调度后台进程运行。后台进程往往按批处理方式调度运行。
3.调度时机
核心进行进程调度的时机有以下5种情况:
(1)当前进程调用系统调用nanosleep( )或者pause( ),使自己进入睡眠状态,主动让出一段时间的CPU的使用权。
(2)进程终止,永久地放弃对CPU的使用。
(3)在时钟中断处理程序执行过程中,发现当前进程连续运行的时间过长。
(4)当唤醒一个睡眠进程时,发现被唤醒的进程比当前进程更有资格运行。
(5)一个进程通过执行系统调用来改变调度策略或者降低自身的优先级(如nice命令),从而引起立即调度。
4.调度算法
进程调度的算法应该比较简单,以便减少频繁调度时的系统开销。Linux执行进程调度时,首先查找所有在就绪队列中的进程,从中选出优先级最高且在内存的一个进程。如果队列中有实时进程,那么实时进程将优先运行。如果最需要运行的进程不是当前进程,那么当前进程就被挂起,并且保存它的现场—— 所涉及的一切机器状态,包括程序计数器和CPU寄存器等,然后为选中的进程恢复运行现场。
(二)Linux常用调度命令
· nohup命令
nohup命令的功能是以忽略挂起和退出的方式执行指定的命令。其命令格式是:
nohup command [arguments]
其中,command是所要执行的命令,arguments是指定命令的参数。
nohup命令告诉系统,command所代表的命令在执行过程中不受任何结束运行的信号(hangup和quit)的影响。例如,
$ nohup find / -name -print>f1 &
find命令在后台运行。在用户注销后,它会继续运行:从根目录开始,查找名字是的文件,结果被定向到文件f1中。
如果用户没有对输出进行重定向,则输出被附加到当前目录的文件中。如果用户在当前目录中不具备写权限,则输出被定向到$HOME/ 中。
· at命令
at命令允许指定命令执行的时间。at命令的常用形式是:
at time command
$ at 15:00 Oct 20
回车后进入接收方式,接着键入以下命令:
mail -s Happy Birthday! liuzheny
按下D键,屏幕显示:
job .a at Wed Oct 20 15:00:00 CST 1999
表明建立了一个作业,其作业ID号是.a,运行作业的时间是1999年10月20日下午3:00,给liuzheny发一条标题为“Happy Birthday!”(生日快乐)的空白邮件。
利用 at -l 可以列出当前at队列中所有的作业。
利用 at -r 可以删除指定的作业。这些作业以前由at或batch命令调度。例如,
将删除作业ID号是.a的作业。其一般使用形式是:
at -r job_id
注意,结尾是.a的作业ID号,表示这个作业是由at命令提交的;结尾是.b的作业ID号,表示这个作业是由batch命令提交的。
· batch命令
batch命令不带任何参数,它提交的作业的优先级比at命令提交的作业的优先级低。batch无法指定作业运行的时间。实际运行时间要看系统中已经提交的作业数量。如果系统中优先级较高的作业比较多,那么,batch提交的作业则需要等待;如果系统空闲,则运行batch提交的作业。例如,
回车后进入接收方式,接着键入命令:
find / -name -print
按下D。退出接收方式,屏幕显示:
job .b at Thu Nov 18 14:30:00 CST 1999
表示find命令被batch作为一个作业提交给系统,作业ID号是.b。如果系统当前空闲,这个作业被立即执行,其结果同样作为邮件发送给用户。
· jobs命令
jobs命令用来显示当前shell下正在运行哪些作业(即后台作业)。例如:
[2] + Running tar tv3 *&
[1] - Running find / -name README -print > logfile &
其中,第一列方括号中的数字表示作业序号,它是由当前运行的shell分配的,而不是由操作系统统一分配的。在当前shell环境下,第一个后台作业的作业号为1,第二个作业的作业号为2,等等。
第二列中的“ ”号表示相应作业的优先级比“-”号对应作业的优先级高。
第三列表明作业状态,是否为运行、中断、等待输入或停止等。
最后列出的是创建当前这个作业所对应的命令行。
利用 jobs -l 形式,可以在作业号后显示出相应进程的PID。如果想只显示相应进程的PID,不显示其它信息,则使用 jobs -p 形式。
· fg命令
fg命令把指定的后台作业移到前台。其使用格式是:
其中,参数job是一个或多个进程的PID,或者是命令名称或者作业号(前面要带有一个“%”号)。例如:
[2] + Running tar tv3 *&
[1] - Running find / -name README -print > logfile&
find / -name README -print > logfile
注意,显示的命令行末尾没有“&”符号。下面命令能产生同样的效果:
这样,find命令对应的进程就在前台执行。当后台只有一个作业时,键入不带参数的fg命令,就能使相应进程移到前台。当有两个或更多的后台作业时,键入不带参数的fg,就把最后进入后台的进程首先移到前台。
· bg命令
bg命令可以把前台进程换到后台执行。其使用格式是:
其中,job是一个或多个进程的PID、命令名称或者作业号,在参数前要带“%”号。例如,在cc(C编译命令)命令执行过程中,按下Z键,使这个作业挂起。然后键入以下命令:
该挂起的作业在后台重新开始执行。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。