linux进程调度线程调度
执行一个 ,但是只要任何修改,都造成分裂如,修改了chroot,写memory,mmap,sigaction 等。
p1 是一个 task_struct, p2 也是一个 task_struct. linux内核的调度器只认得task_struck (不管你是进程还是线程), 对其进行调度。
p2 的task_struck 被创建出来后,也有一份自己的资源。但是这些资源会短暂的与p1 相同。
进程是区分资源的单位,你的资源是我的资源,那从概念上将就不叫进程。
其他资源都好分配,唯一比较难的是内存资源的重新分配。
非常简单的程序,但是可以充分说明 COW。
结果:10 -> 20 -> 10
COW 是严重依赖于CPU中的MMU。CPU如果没有 MMU,fork 是不能工作的。
在没有mmu的CPU中,不可能执行COW 的,所以只有vfork
vfork与fork相比的不同
P2没有自己的 task_struct, 也就是说P1 的内存资源 就是 P2的内存资源。
结果 10,20,20
vfork:
vfork 执行上述流程,P2也只是指向了P1的mm,那么将这个vfork 放大,其余的也全部clone,共同指向P1,那么就是线程的属性了。
phtread_create -> Clone()
P1 P2 在内核中都是 task_struct. 都可以被调度。共享资源可调度,即线程。 这就是线程为什么也叫做轻量级进程
不需要太纠结线程和进程的区别。
4651 : TGID
4652, 4653 tid 内核中 task_struct 真正的pid
linux 总是白发人 送 黑发人。如果父进程在子进程推出前挂掉了。那么子进程应该怎么办?
p3 -> init, p5 -> subreaper
每一个孤儿都会找最近的火葬场
可以设置进程的属性,将其变为subreaper,会像1号进程那样收养孤儿进程。
linux的进程睡眠依靠等待队列,这样的机制类似与涉及模式中的订阅与发布。
睡眠,分两种
每一个进程都是创建出来的,那么第一个进程是谁创建的呢?
init 进程是被linux的 0 进程 创建出来的。开机创建。
父进程就是 0 号进程,但在pstree,是看不到0进程的。因为0进程创建子进程后,就退化成了idle进程。
idle进程是 linux内核里,特殊调度类。 所有进程都睡眠停止 ,则调度idle进程,进入到 wait for interrupte 等中断。此时 cpu及其省电,除非来一个中断,才能再次被唤醒。
唤醒后的任何进程,从调度的角度上说,都比idle进程地位高。idle是调度级别最最低的进程。
0 进程 一跑,则进入等中断。一旦其他进程被唤醒,就轮不到 0进程了。
所有进程都睡了,0就上来,则cpu需要进入省电模式
Ⅱ linux进程、线程及调度算法(三)
调度策略值得是大家都在ready时,并且CPU已经被调度时,决定谁来运行,谁来被调度。
两者之间有一定矛盾。
响应的优化,意味着高优先级会抢占优先级,会花时间在上下文切换,会影响吞吐。
上下文切换的时间是很短的,几微妙就能搞定。上下文切换本身对吞吐并多大影响, 重要的是,切换后引起的cpu 的 cache miss.
每次切换APP, 数据都要重新load一次。
Linux 会尽可能的在响应与吞吐之间寻找平衡。比如在编译linux的时候,会让你选择 kernal features -> Preemption model.
抢占模型会影响linux的调度算法。
所以 ARM 的架构都是big+LITTLE, 一个很猛CPU+ 多个 性能较差的 CPU, 那么可以把I/O型任务的调度 放在 LITTLE CPU上。需要计算的放在big上。
早期2.6 内核将优先级划分了 0-139 bit的优先级。数值越低,优先级越高。0-99优先级 都是 RT(即时响应)的 ,100-139都是非RT的,即normal。
调度的时候 看哪个bitmap 中的 优先级上有任务ready。可能多个任务哦。
在普通优先级线程调度中,高优先级并不代表对低优先级的绝对优势。会在不同优先级进行轮转。
100 就是比101高,101也会比102高,但100 不会堵着101。
众屌丝进程在轮转时,优先级高的:
初始设置nice值为0,linux 会探测 你是喜欢睡眠,还是干活。越喜欢睡,linux 越奖励你,优先级上升(nice值减少)。越喜欢干活,优先级下降(nice值增加)。所以一个进程在linux中,干着干着 优先级越低,睡着睡着 优先级越高。
后期linux补丁中
红黑树,数据结构, 左边节点小于右边节点
同时兼顾了 CPU/IO 和 nice。
数值代表着 进程运行到目前为止的virtual runtime 时间。
(pyhsical runtime) / weight * 1024(系数)。
优先调度 节点值(vruntime)最小的线程。权重weight 其实有nice 来控制。
一个线程一旦被调度到,则物理运行时间增加,vruntime增加,往左边走。
weight的增加,也导致vruntime减小,往右边走。
总之 CFS让线程 从左滚到右,从右滚到左。即照顾了I/O(喜欢睡,分子小) 也 照顾了 nice值低(分母高).所以 由喜欢睡,nice值又低的线程,最容易被调度到。
自动调整,无需向nice一样做出奖励惩罚动作,个人理解权重其实相当于nice
但是 此时 来一个 0-99的线程,进行RT调度,都可以瞬间秒杀你!因为人家不是普通的,是RT的!
一个多线程的进程中,每个线程的调度的策略 如 fifo rr normal, 都可以不同。每一个的优先级都可以不一样。
实验举例, 创建2个线程,同时开2个:
运行2次,创建两个进程
sudo renice -n -5(nice -5级别) -g(global), 会明显看到 一个进程的CPU占用率是另一个的 3倍。
为什么cpu都已经达到200%,为什么系统不觉得卡呢?因为,我们的线程在未设置优先级时,是normal调度模式,且是 CPU消耗型 调度级别其实不高。
利用chrt工具,可以将进程 调整为 50 从normal的调度策略 升为RT (fifo)级别的调度策略,会出现:
chrt , nice renice 的调度策略 都是以线程为单位的,以上 设置的将进程下的所有线程进行设置nice值
线程是调度单位,进程不是,进程是资源封装单位!
两个同样死循环的normal优先级线程,其中一个nice值降低,该线程的CPU 利用率就会比另一个CPU的利用率高。
Ⅲ linux调度是基于进程还是线程
在LINUX系统之中,被调度的应该是进程。因为只有进程才拥有一个独立的上下文环境,是分配系统资源的最小单位……而线程在SMP体系中加速了执行的效率……
在LINUX之中,线程也可称作轻量级进程,它能享有自己的堆栈,线程ID等独立资源,但大多还是要依赖其创建进程,比如地址空间,信号,文件句柄……
Ⅳ Linux 进程管理之进程调度与切换
我们知道,进程运行需要各种各样的系统资源,如内存、文件、打印机和最
宝贵的 CPU 等,所以说,调度的实质就是资源的分配。系统通过不同的调度算法(Scheling Algorithm)来实现这种资源的分配。通常来说,选择什么样的调度算法取决于资源分配的策略(Scheling Policy)。
有关调度相关的结构保存在 task_struct 中,如下:
active_mm 是为内核线程而引入的,因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文切换方式,当内核线程进行上下文切换时,让切换进来的线程的 active_mm 指向刚被调度出去的进程的 active_mm(如果进程的mm 域不为空,则其 active_mm 域与 mm 域相同)。
在 linux 2.6 中 sched_class 表示该进程所属的调度器类有3种:
进程的调度策略有5种,用户可以调用调度器里不同的调度策略:
在每个 CPU 中都有一个自身的运行队列 rq,每个活动进程只出现在一个运行队列中,在多个 CPU 上同时运行一个进程是不可能的。
运行队列是使用如下结构实现的:
tast 作为调度实体加入到 CPU 中的调度队列中。
系统中所有的运行队列都在 runqueues 数组中,该数组的每个元素分别对应于系统中的一个 CPU。在单处理器系统中,由于只需要一个就绪队列,因此数组只有一个元素。
内核也定义了一下便利的宏,其含义很明显。
Linux、c/c++服务器开发篇-------我们来聊聊进程的那些事
Linux内核 进程间通信组件的实现
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享
在分析调度流程之前,我们先来看在什么情况下要执行调度程序,我们把这种情况叫做调度时机。
Linux 调度时机主要有。
时机1,进程要调用 sleep() 或 exit() 等函数进行状态转换,这些函数会主动调用调度程序进行进程调度。
时机2,由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4 是一样的。
时机3,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查 need_resched 的值,如果必要,则调用调度程序 schele() 主动放弃 CPU。
时机4 , 如前所述, 不管是从中断、异常还是系统调用返回, 最终都调用 ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。
Linux 的调度程序是一个叫 Schele() 的函数,这个函数来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等。
从代码分析来看,Schele 主要完成了2个功能:
进程上下文切换包括进程的地址空间的切换和执行环境的切换。
对于 switch_mm 处理,关键的一步就是它将新进程页面目录的起始物理地址装入到寄存器 CR3 中。CR3 寄存器总是指向当前进程的页面目录。
switch_to 把寄存器中的值比如esp等存放到进程thread结构中,保存现场一边后续恢复,同时调用 __switch_to 完成了堆栈的切换。
在进程的 task_struct 结构中有个重要的成分 thread,它本身是一个数据结构 thread_struct, 里面记录着进程在切换时的(系统空间)堆栈指针,取指令地址(也就是“返回地址”)等关键性的信息。
关于__switch_to 的工作就是处理 TSS (任务状态段)。
TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。
linux 为每一个 CPU 提供一个 TSS 段,并且在 TR 寄存器中保存该段。
linux 中之所以为每一个 CPU 提供一个 TSS 段,而不是为每个进程提供一个TSS 段,主要原因是 TR 寄存器永远指向它,在任务切换的适合不必切换 TR 寄存器,从而减小开销。
在从用户态切换到内核态时,可以通过获取 TSS 段中的 esp0 来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的 cs,esp,eip 等上下文。
TSS 在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。
在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到 TR(任务寄存器)所指定的任务的 TSS 中;然后,下一任务的 TSS 被装入 TR;最后,从 TR 所指定的 TSS 中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在 TSS 中保存任务现场各寄存器状态的完整映象,实现任务的切换。
因此,__switch_to 核心内容就是将 TSS 中的内核空间(0级)堆栈指针换成 next->esp0。这是因为 CPU 在穿越中断门或者陷阱门时要根据新的运行级别从TSS中取得进程在系统空间的堆栈指针。
thread_struct.esp0 指向进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入 TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置。换句话说,进程的 thread_struct 结构中的 esp0 保存着其系统空间堆栈指针。当进程穿过中断门、陷阱门或者调用门进入系统空间时,处理器会从这里恢复期系统空间栈。
由于栈中变量的访问依赖的是段、页、和 esp、ebp 等这些寄存器,所以当段、页、寄存器切换完以后,栈中的变量就可以被访问了。
因此 switch_to 完成了进程堆栈的切换,由于被切进的进程各个寄存器的信息已完成切换,因此 next 进程得以执行指令运行。
由于 A 进程在调用 switch_to 完成了与 B 进程堆栈的切换,也即是寄存器中的值都是 B 的,所以 A 进程在 switch_to 执行完后,A停止运行,B开始运行,当过一段时间又把 A 进程切进去后,A 开始从switch_to 后面的代码开始执行。
schele 的调用流程如下:
Ⅳ Linux进程的调度
上回书说到 Linux进程的由来 和 Linux进程的创建 ,其实在同一时刻只能支持有限个进程或线程同时运行(这取决于CPU核数量,基本上一个进程对应一个CPU),在一个运行的操作系统上可能运行着很多进程,如果运行的进程占据CPU的时间很长,就有可能导致其他进程饿死。为了解决这种问题,操作系统引入了 进程调度器 来进行进程的切换,轮流让各个进程使用CPU资源。
1)rq: 进程的运行队列( runqueue), 每个CPU对应一个 ,包含自旋锁(spinlock)、进程数量、用于公平调度的CFS信息结构、当前运行的进程描述符等。实际的进程队列用红黑树来维护(通过CFS信息结构来访问)。
2)cfs_rq: cfs调度的进程运行队列信息 ,包含红黑树的根结点、正在运行的进程指针、用于负载均衡的叶子队列等。
3)sched_entity: 把需要调度的东西抽象成调度实体 ,调度实体可以是进程、进程组、用户等。这里包含负载权重值、对应红黑树结点、 虚拟运行时vruntime 等。
4)sched_class:把 调度策略(算法)抽象成调度类 ,包含一组通用的调度操作接口。接口和实现是分离,可以根据调度接口去实现不同的调度算法,使一个Linux调度程序可以有多个不同的调度策略。
1) 关闭内核抢占 ,初始化部分变量。获取当前CPU的ID号,并赋值给局部变量CPU, 使rq指向CPU对应的运行队列 。 标识当前CPU发生任务切换 ,通知RCU更新状态,如果当前CPU处于rcu_read_lock状态,当前进程将会放入rnp-> blkd_tasks阻塞队列,并呈现在rnp-> gp_tasks链表中。 关闭本地中断 ,获取所要保护的运行队列的自旋锁, 为查找可运行进程做准备 。
2) 检查prev的状态,更新运行队列 。如果不是可运行状态,而且在内核态没被抢占,应该从运行队列中 删除prev进程 。如果是非阻塞挂起信号,而且状态为TASK_INTER-RUPTIBLE,就把该进程的状态设置为TASK_RUNNING,并将它 插入到运行队列 。
3)task_on_rq_queued(prev) 将pre进程插入到运行队列的队尾。
4)pick_next_task 选取将要执行的next进程。
5)context_switch(rq, prev, next)进行 进程上下文切换 。
1) 该进程分配的CPU时间片用完。
2) 该进程主动放弃CPU(例如IO操作)。
3) 某一进程抢占CPU获得执行机会。
Linux并没有使用x86 CPU自带的任务切换机制,需要通过手工的方式实现了切换。
进程创建后在内核的数据结构为task_struct , 该结构中有掩码属性cpus_allowed,4个核的CPU可以有4位掩码,如果CPU开启超线程,有一个8位掩码,进程可以运行在掩码位设置为1的CPU上。
Linux内核API提供了两个系统调用 ,让用户可以修改和查看当前的掩码:
1) sched_setaffinity():用来修改位掩码。
2) sched_getaffinity():用来查看当前的位掩码。
在下次task被唤醒时,select_task_rq_fair根据cpu_allowed里的掩码来确定将其置于哪个CPU的运行队列,一个进程在某一时刻只能存在于一个CPU的运行队列里。
在Nginx中,使用了CPU亲和度来完成某些场景的工作:
worker_processes 4;
worker_cpu_affinity 0001001001001000;
上面这个配置说明了4个工作进程中的每一个和一个CPU核挂钩。如果这个内容写入Nginx的配置文件中,然后Nginx启动或者重新加载配置的时候,若worker_process是4,就会启用4个worker,然后把worker_cpu_affinity后面的4个值当作4个cpu affinity mask,分别调用ngx_setaffinity,然后就把4个worker进程分别绑定到CPU0~3上。
worker_processes 2;
worker_cpu_affinity 01011010;
上面这个配置则说明了两个工作进程中的每一个和2个核挂钩。
Ⅵ Linux - 进程调度
进程调度算法也称 CPU 调度算法,毕竟进程是由 CPU 调度的。
当 CPU 空闲时,操作系统就选择内存中的某个“就绪状态”的进程,并给其分配 CPU。
什么时候会发生 CPU 调度呢?通常有以下情况:
其中发生在 1 和 4 两种情况下的调度称为“非抢占式调度”,2 和 3 两种情况下发生的调度称为“抢占式调度”。
非抢占式的意思就是,当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞时,才会把 CPU 让给其他进程。
而抢占式调度,顾名思义就是进程正在运行的时候,可以被打断,使其把 CPU 让给其他进程。那抢占的原则一般有三种,分别是时间片原则、优先权原则、短作业优先原则。
你可能会好奇为什么第 3 种情况也会发生 CPU 调度呢?假设有一个进程是处于等待状态的,但是它的优先级比较高,如果该进程等待的事件发生了,它就会转到就绪状态,一旦它转到就绪状态,如果我们的调度算法是以优先级来进行调度的,那么它就会立马抢占正在运行的进程,所以这个时候就会发生 CPU 调度。
那第 2 种状态通常是时间片到的情况,因为时间片到了就会发生中断,于是就会抢占正在运行的进程,从而占用 CPU。
调度算法影响的是等待时间(进程在就绪队列中等待调度的时间总和),而不能影响进程真在使用 CPU 的时间和 I/O 时间。
最简单的一个调度算法,就是非抢占式的先来先服务(First Come First Severd, FCFS)算法了。
顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。
FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
这显然对长作业不利,很容易造成一种极端现象。
比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。
前面的“先来先服务调度算法”和“最短作业优先调度算法”都没有很好的权衡短作业和长作业。
那么,高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。
每次进行进程调度时,先计算“响应比优先级”,然后把“响应比优先级”最高的进程投入运行,“响应比优先级”的计算公式:
从上面的公式,可以发现:
最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法。
每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。
另外,时间片的长度就是一个很关键的点:
通常时间片设为 20ms~50ms 通常是一个比较合理的折中值。
前面的“时间片轮转算法”做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。
但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。
进程的优先级可以分为,静态优先级或动态优先级:
该算法也有两种处理优先级高的方法,非抢占式和抢占式:
但是依然有缺点,可能会导致低优先级的进程永远不会运行。
多级反馈队列(Multilevel Feedback Queue)调度算法是“时间片轮转算法”和“最高优先级算法”的综合和发展。
顾名思义:
工作原理:
设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
整体架构如下,即调度策略是模块化设计的,调度器根据不同的进程依次遍历不同的调度策略,找到进程对应的调度策略,调度的结果即为选出一个可运行的进程指针,并将其加入到进程可运行队列中。
以一棵红黑树管理所有需要调度的进程,
红黑树,左边节点小于右边节点的值,运行到目前为止vruntime最小的进程,同时考虑了CPU/IO和nice,总是找vruntime最小的线程调度。
vruntime = pruntime/weight × 1024;
vruntime是虚拟运行时间,pruntime是物理运行时间,weight权重由nice值决定(nice越低权重越高),则运行时间少、nice值低的的线程vruntime小,将得到优先调度。这是一个随运行而动态变化的过程。
Ⅶ 一文读懂Linux任务间调度原理和整个执行过程
在前文中,我们分析了内核中进程和线程的统一结构体task_struct,并分析进程、线程的创建和派生的过程。在本文中,我们会对任务间调度进行详细剖析,了解其原理和整个执行过程。由此,进程、线程部分的大体框架就算是介绍完了。本节主要分为三个部分:Linux内核中常见的调度策略,调度的基本结构体以及调度发生的整个流程。下面将详细展开说明。
Linux 作为一个多任务操作系统,将每个 CPU 的时间划分为很短的时间片,再通过调度器轮流分配给各个任务使用,因此造成多任务同时运行的错觉。为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。节拍率 HZ 是内核的可配选项,可以设置为 100、250、1000 等。不同的系统可能设置不同的数值,可以通过查询 /boot/config 内核选项来查看它的配置值。
Linux的调度策略主要分为实时任务和普通任务。实时任务需求尽快返回结果,而普通任务则没有较高的要求。在前文中我们提到了task_struct中调度策略相应的变量为policy,调度优先级有prio, static_prio, normal_prio, rt_priority几个。优先级其实就是一个数值,对于实时进程来说,优先级的范围是 0 99;对于普通进程,优先级的范围是 100 139。数值越小,优先级越高。
实时调度策略主要包括以下几种
普通调度策略主要包括以下几种:
首先,我们需要一个结构体去执行调度策略,即sched_class。该类有几种实现方式
普通任务调度实体源码如下,这里面包含了 vruntime 和权重 load_weight,以及对于运行时间的统计。
在调度时,多个任务调度实体会首先区分是实时任务还是普通任务,然后通过以时间为顺序的红黑树结构组合起来,vruntime 最小的在树的左侧,vruntime最多的在树的右侧。以CFS策略为例,则会选择红黑树最左边的叶子节点作为下一个将获得 CPU 的任务。而这颗红黑树,我们称之为运行时队列(run queue),即struct rq。
其中包含结构体cfs_rq,其定义如下,主要是CFS调度相关的结构体,主要有权值相关变量、vruntime相关变量以及红黑树指针,其中结构体rb_root_cached即为红黑树的节点
对结构体dl_rq有类似的定义,运行队列由红黑树结构体构成,并按照deadline策略进行管理
对于实施队列相应的rt_rq则有所不同,并没有用红黑树实现。
下面再看看调度类sched_class,该类以函数指针的形式定义了诸多队列操作,如
调度类分为下面几种:
队列操作中函数指针指向不同策略队列的实际执行函数函数,在linux/kernel/sched/目录下,fair.c、idle.c、rt.c等文件对不同类型的策略实现了不同的函数,如fair.c中定义了
以选择下一个任务为例,CFS对应的是pick_next_task_fair,而rt_rq对应的则是pick_next_task_rt,等等。
由此,我们来总结一下:
有了上述的基本策略和基本调度结构体,我们可以形成大致的骨架,下面就是需要核心的调度流程将其拼凑成一个整体,实现调度系统。调度分为两种,主动调度和抢占式调度。
说到调用,逃不过核心函数schele()。其中sched_submit_work()函数完成当前任务的收尾工作,以避免出现如死锁或者IO中断等情况。之后首先禁止抢占式调度的发生,然后调用__schele()函数完成调度,之后重新打开抢占式调度,如果需要重新调度则会一直重复该过程,否则结束函数。
而__schele()函数则是实际的核心调度函数,该函数主要操作包括选取下一进程和进行上下文切换,而上下文切换又包括用户态空间切换和内核态的切换。具体的解释可以参照英文源码注释以及中文对各个步骤的注释。
其中核心函数是获取下一个任务的pick_next_task()以及上下文切换的context_switch(),下面详细展开剖析。首先看看pick_next_task(),该函数会根据调度策略分类,调用该类对应的调度函数选择下一个任务实体。根据前文分析我们知道,最终是在不同的红黑树上选择最左节点作为下一个任务实体并返回。
下面来看看上下文切换。上下文切换主要干两件事情,一是切换任务空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。关于任务空间的切换放在内存部分的文章中详细介绍,这里先按下不表,通过任务空间切换实际完成了用户态的上下文切换工作。下面我们重点看一下内核态切换,即寄存器和CPU上下文的切换。
switch_to()就是寄存器和栈的切换,它调用到了 __switch_to_asm。这是一段汇编代码,主要用于栈的切换, 其中32位使用esp作为栈顶指针,64位使用rsp,其他部分代码一致。通过该段汇编代码我们完成了栈顶指针的切换,并调用__switch_to完成最终TSS的切换。注意switch_to中其实是有三个变量,分别是prev, next, last,而实际在使用时,我们会对last也赋值为prev。这里的设计意图需要结合一个例子来说明。假设有ABC三个任务,从A调度到B,B到C,最后C回到A,我们假设仅保存prev和next,则流程如下
最终调用__switch_to()函数。该函数中涉及到一个结构体TSS(Task State Segment),该结构体存放了所有的寄存器。另外还有一个特殊的寄存器TR(Task Register)会指向TSS,我们通过更改TR的值,会触发硬件保存CPU所有寄存器在当前TSS,并从新的TSS读取寄存器的值加载入CPU,从而完成一次硬中断带来的上下文切换工作。系统初始化的时候,会调用 cpu_init()给每一个 CPU 关联一个 TSS,然后将 TR 指向这个 TSS,然后在操作系统的运行过程中,TR 就不切换了,永远指向这个 TSS。当修改TR的值得时候,则为任务调度。
更多Linux内核视频教程文本资料免费领取后台私信【 内核大礼包 】自行获取。
在完成了switch_to()的内核态切换后,还有一个重要的函数finish_task_switch()负责善后清理工作。在前面介绍switch_to三个参数的时候我们已经说明了使用last的重要性。而这里为何让prev和last均赋值为prev,是因为prev在后面没有需要用到,所以节省了一个指针空间来存储last。
至此,我们完成了内核态的切换工作,也完成了整个主动调度的过程。
抢占式调度通常发生在两种情况下。一种是某任务执行时间过长,另一种是当某任务被唤醒的时候。首先看看任务执行时间过长的情况。
该情况需要衡量一个任务的执行时间长短,执行时间过长则发起抢占。在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统时间又过去一个时钟周期,通过这种方式可以查看是否是需要抢占的时间点。
时钟中断处理函数会调用scheler_tick()。该函数首先取出当前CPU,并由此获取对应的运行队列rq和当前任务curr。接着调用该任务的调度类sched_class对应的task_tick()函数进行时间事件处理。
以普通任务队列为例,对应的调度类为fair_sched_class,对应的时钟处理函数为task_tick_fair(),该函数会获取当前的调度实体和运行队列,并调用entity_tick()函数更新时间。
在entity_tick()中,首先会调用update_curr()更新当前任务的vruntime,然后调用check_preempt_tick()检测现在是否可以发起抢占。
check_preempt_tick() 先是调用 sched_slice() 函数计算出一个调度周期中该任务运行的实际时间 ideal_runtime。sum_exec_runtime 指任务总共执行的实际时间,prev_sum_exec_runtime 指上次该进程被调度时已经占用的实际时间,所以 sum_exec_runtime - prev_sum_exec_runtime 就是这次调度占用实际时间。如果这个时间大于 ideal_runtime,则应该被抢占了。除了这个条件之外,还会通过 __pick_first_entity 取出红黑树中最小的进程。如果当前进程的 vruntime 大于红黑树中最小的进程的 vruntime,且差值大于 ideal_runtime,也应该被抢占了。
如果确认需要被抢占,则会调用resched_curr()函数,该函数会调用set_tsk_need_resched()标记该任务为_TIF_NEED_RESCHED,即该任务应该被抢占。
某些任务会因为中断而唤醒,如当 I/O 到来的时候,I/O进程往往会被唤醒。在这种时候,如果被唤醒的任务优先级高于 CPU 上的当前任务,就会触发抢占。try_to_wake_up() 调用 ttwu_queue() 将这个唤醒的任务添加到队列当中。ttwu_queue() 再调用 ttwu_do_activate() 激活这个任务。ttwu_do_activate() 调用 ttwu_do_wakeup()。这里面调用了 check_preempt_curr() 检查是否应该发生抢占。如果应该发生抢占,也不是直接踢走当前进程,而是将当前进程标记为应该被抢占。
由前面的分析,我们知道了不论是是当前任务执行时间过长还是新任务唤醒,我们均会对现在的任务标记位_TIF_NEED_RESCUED,下面分析实际抢占的发生。真正的抢占还需要一个特定的时机让正在运行中的进程有机会调用一下 __schele()函数,发起真正的调度。
实际上会调用__schele()函数共有以下几个时机
从系统调用返回用户态:以64位为例,系统调用的链路为do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,会检测是否为_TIF_NEED_RESCHED,如果是则调用__schele()
内核态启动:内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。preempt_enable() 会调用 preempt_count_dec_and_test(),判断 preempt_count 和 TIF_NEED_RESCHED 是否可以被抢占。如果可以,就调用 preempt_schele->preempt_schele_common->__schele 进行调度。
本文分析了任务调度的策略、结构体以及整个调度流程,其中关于内存上下文切换的部分尚未详细叙述,留待内存部分展开剖析。
1、调度相关结构体及函数实现
2、schele核心函数
Ⅷ 浅析Linux下进程的调度策略与优先级
在 Linux 中,线程是由进程来实现的,可以认为线程就是一个轻量级的进程,因此,线程调度是按照进程调度的方式来进行的。这样设计,线程调度流程可以直接复用进程调度流程,没必要再设计一个进程内的线程调度器了。
在 Linux 中,进程调度器是基于进程的调度策略与调度优先级来决定调度哪个进程运行。
调度策略主要包括:
调度优先级的范围是 0~99,数值越大,表示优先级越高。
其中,SCHED_OTHER、SCHED_IDLE、SCHED_BACH 为非实时调度策略,其调度优先级为 0。而 SCHED_FIFO、SCHED_RR 是实时调度策略,其调度优先级范围为 1~99。
实时调度策略的进程总是比非实时调度策略的进程优先级高。
在 Linux 内部实现中,调度器会为每个可能的调度优先级维护一个可运行的进程列表,以最高优先级列表头部的进程作为下一次调度的进程,所有的调度都是抢占式的,如果一个具有更高调度优先级的进程转换为可运行状态,那么当前运行的进程将被强制进入其等待的队列中。
SCHED_OTHER
该调度策略是默认的 Linux 分时调度策略,该调度策略为非实时的,其调度优先级总是为 0。
对于该调度策略类型的进程,调度器是基于动态优先级来调度的。动态优先级跟属性 nice 有关,nice 的值会随着进程的运行时间而动态改变,以确保所有具有 SCHED_OTHER 策略的进程公平地得到调度。
在 Linux 中,nice 的值范围为-20 ~ +19,默认值为 0。nice 值越大,则优先级越低,因此相对较低 nice 值的进程可以获得更多的处理器时间。
通过命令 ps -el 查看系统中的进程列表,其中 NI 列就是进程对应的 nice 值。
使用 top 命令,看到的 NI 列也是进程的 nice 值。
调整 nice 值,可以通过 shell 命令 nice ,该命令可以按照指定的 nice 值运行 cmd ,命令的帮助信息为:
重新调整已运行进程的 nice 值,可通过 renice 命令实现,命令的帮助信息为:
另外,可以执行 top 命令,输入 r ,根据提示输入进程的 pid ,再输入 nice 数值,也可以调整进程的 nice 值。
SCHED_FIFO
该调度策略为先入先出调度策略,简单概括,就是一旦进程占用了 CPU,则一直运行,直到有更高优先级的任务抢占,或者进程自己放弃占用 CPU。
SCHED_RR
该调度策略为时间片轮转调度策略,该调度策略是基于 SCHED_FIFO 策略的演进,其在每个进程上增加一个时间片限制,当时间片使用完成后,调度器将该进程置于队列的尾端,放在尾端保证了所有具有相同调度优先级的进程的调度公平。
使用 top 命令,如果 PR 列的值为 RT ,则说明该进程采用的是实时调度策略,其调度策略为 SCHED_FIFO 或者 SCHED_RR,而对于非实时调度策略的进程,该列的值为 NI + 20 。
可以通过命令 ps -eo state,uid,pid,ppid,rtprio,time,comm 来查看进程对应的实时优先级,实时优先级位于 RTPRIO 列下,如果进程对应的列显示为 - ,说明该进程不是实时进程。
chrt 命令可以用来很简单地更改进程的调度策略与调度优先级。在 Linux 下查看 chrt 命令的帮助信息:
比如,获取某个进程的调度策略,使用如下命令:
在比如,设置某个进程的调度策略为 SCHED_FIFO,调度优先级为 70,使用如下命令:
Ⅸ Linux中进程和线程的对比与区别
线程和进程是另一对有意义的概念,主要区别和联系如下:
进程是操作系统进行资源分配的基本单位,拥有完整的进程空间。进行系统资源分配的时候,除了CPU资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。
线程是进程的一部分,如果没有进行显示的线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可认为系统是多线程的。
多线程和多进程是两种不同的概念。多线程与多进程有不同的资源共享方式。
进程有进程控制块PCB,系统通过PCB对进程进行调度。进程有线程控制块TCP,但TCB所表示的状态比PCB要少的多。