一.概述
通常情况下,电脑的CPU会有多个进程或线程同时竞争。当CPU被一个进程占用时,其他的进程不得不等待。当CPU中的进程完成或者中断等待IO时,我们必须从等待的进程中挑选合适的进程放进CPU中处理。挑选进程的方法便是调度算法。
一个好的调度算法需要考虑到三个方面。首先是公平,需要它给每个进程公平的CPU份额。其次是策略强制执行,保证规定的策略能被执行。最后是平衡,保持系统的所以部分都忙碌。协调这三点有助于提高操作系统工作的效率。本文选取了CFS(完全公平调度)算法来了解linux中是怎样完成调度的,原因主要是其他调度系统的源码资源比较匮乏。
二.进程
A)进程的组织
当一个程序加载到内存中时,它就变成了一个进程。系统会为它建立一个PCB块记录各种信息,PCB经常被系统访问,所以常驻于内存中。部分定义如
struct task_struct { volatile long state; //说明了该进程是否可以执行,还是可中断等信息 unsigned long flags; //Flage 是进程号,在调用fork()时给出 int sigpending; //进程上是否有待处理的信号 mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 //0-0xBFFFFFFF for user-thead //0-0xFFFFFFFF for kernel-thread //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度 volatile long need_resched; int lock_depth; //锁深度 long nice; //进程的基本时间片 //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER unsigned long policy; struct mm_struct *mm; //进程内存管理信息 int processor; //若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新 unsigned long cpus_runnable, cpus_allowed; struct list_head run_list; //指向运行队列的指针 ……………… pid_t pid; //进程标识符,用来代表一个进程 pid_t pgrp; //进程组标识,表示进程所属的进程组
B)进程状态的转换
Linux进程状态有
TASK_RUNNING : 就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU,对应进程状态的R
TASK_INTERRUPTIBLE:睡眠态,但是进程处于浅度睡眠,可以响应信号,一般是进程主动sleep进入的状态,对应进程状态S
TASK_UNINTERRUPTIBLE:睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞,对应进程状态D
TASK_ZOMBIE:僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态,对应进程状态Z
TASK_STOPED:停止,调试状态,对应进程状态T
它们之间的转换如下图:
三.CFS算法
A)CFS原理
CFS算法的核心是实现公平,确保每个进程尽可能得到相同的处理器资源。内核给每个进程维护了一个虚拟运行时间vruntime ,调度器基于vruntime(程序已运行的时间)来衡量哪个进程最值得调度,所以它的就绪队列是一棵以vruntime(实际上是min_vruntime)为键值的red-black tree (如下图)。占用资源时间越小的进程就越靠近树的左端。因此,调度器每次选择最左边的节点存储的进程,这个进程的已运行的时间最小。
B)CFS中重要的数据结构
cfs_rq是用来描述运行在同一个CPU上处于TASK_RUNNING状态的普通进程的各种运行信息。
struct cfs_rq { struct load_weight load; //运行队列总的进程权重 unsigned int nr_running, h_nr_running; //进程的个数 u64 exec_clock; //运行的时钟 u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值 struct rb_root tasks_timeline; //红黑树的根结点 struct rb_node *rb_leftmost; //指向vruntime值最小的结点 //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, struct sched_entity *curr, *next, *last, *skip; struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中 ... };
调度实体sched_entity 是用来记录一个进程的运行状态信息,将在下面介绍。
进程描述符task_struct ,调度实体sched_entity ,和运行队列cfs_rq的关系如下图:
C)vruntime
vruntime是由程序已运行的时间决定的,但又不是任何两个程序运行相同的时间后vruntime的增量都相同。它还由程序的优先级所决定,但是在CFS中,优先级的概念被弱化,而是强调进程的权重(weight),一个进程的权重越大,就说明它越需要运行,对应的vruntime也就越小。看一下
一个进程在一个调度周期里的运行时间是
分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和
一个进程的实际运行时间和vruntime的关系是
vruntime = 实际运行时间 * NICE_0_LOAD / 进程权重 = 实际运行时间 * 1024 / 进程权重 NICE_0_LOAD = 1024, 表示nice值为0的进程权重
可以看到,进程的权重越大,运行同样的时间,它的vruntime增长的越慢,需要运行的优先级就越高。
而一个进程在一个调度周期内的vruntime大小为:
vruntime = 进程在一个调度周期内的实际运行时间 * 1024 / 进程权重 = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重 = 调度周期 * 1024 / 所有进程总权重
可以看出一个进程在一个调度周期内的vruntime的值不与自己的的权重有关,而是所有进程都是一样的。从上面可以看出,在一个调度周期内,每个进程将系统分配给自己的运行时间使用完后,它们的vruntime的值是一样大的,因此一个进程的vruntime值越大,说明它得到的运行时间就越多
有关优先级和权重的关系Linux 2.6.23在内核中通过prio_to_weigth 数组进行优先级nice值和权重的转换。
static const int prio_to_weight[40] = { /* -20 */ 88761, 71755, 56483, 46273, 36291, /* -15 */ 29154, 23254, 18705, 14949, 11916, /* -10 */ 9548, 7620, 6100, 4904, 3906, /* -5 */ 3121, 2501, 1991, 1586, 1277, /* 0 */ 1024, 820, 655, 526, 423, /* 5 */ 335, 272, 215, 172, 137, /* 10 */ 110, 87, 70, 56, 45, /* 15 */ 36, 29, 23, 18, 15, };
从这个数组中可以获得nice值从-20至19所对应的权值。从数组中可以看到,nice值越小的进程它的执行优先级会更高(权重更大)。
为了更深入的了解vruntime,我们先来看它的定义。
struct sched_entity { struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */ struct rb_node run_node; unsigned int on_rq; /* 是否在就绪队列上 */ u64 exec_start; /* 上次启动的时间*/ u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; ... };
在这里sum_exec_runtime是用于记录进程消耗的CPU时间,并在进程撤销时保存到prev_sum_exec_runtime中。
D)vruntime值的设置
无论是新进程vruntime的设置,还是进程中断以后的更新。vruntime的值都是直接由update_curr()函数进行操作,代码如下:
static void update_curr(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; u64 now = rq_of(cfs_rq)->clock; unsigned long delta_exec; delta_exec = (unsigned long)(now - curr->exec_start); //得到本次tick实际运行的时间值 __update_curr(cfs_rq, curr, delta_exec); //将本次tick实际运行的时间值更新到vruntime和实际运行时间 curr->exec_start = now; //设置下次tick的开始时间 if (entity_is_task(curr)) { struct task_struct *curtask = task_of(curr); cpuacct_charge(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } } static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec) { unsigned long delta_exec_weighted; //前面说的sum_exec_runtime就是在这里计算的,它等于进程从创建开始占用CPU的总时间 curr->sum_exec_runtime += delta_exec; //下面变量的weighted表示这个值是从运行时间考虑权重因素换算来的vruntime,再写一遍这个公式 //vruntime(delta_exec_weighted) = 实际运行时间(delta_exe) * 1024 / 进程权重 delta_exec_weighted = calc_delta_fair(delta_exec, curr); //将进程刚刚运行的时间换算成vruntime后立刻加到进程的vruntime上 curr->vruntime += delta_exec_weighted; //因为有进程的vruntime变了,因此cfs_rq的min_vruntime可能也要变化,更新它。 //就是先取tmp = min(curr->vruntime,leftmost->vruntime) //然后cfs_rq->min_vruntime = max(tmp, cfs_rq->min_vruntime) update_min_vruntime(cfs_rq); }
四.浅谈操作系统进程模型
操作系统进程模型在我认为主要是用来存储和管理进程状态。如上面说的,当一个程序加载到内存时,它就变成了进程。而在我们使用计算机时会同时运行多个程序,每个程序又可能由多个进程组成,所以就要对进程进行管理,判断哪个程序中的哪个进程更需要CPU资源并分配给它。哪些优先级低,如等待IO操作等,可以让它将资源让出。使CPU资源得到充分的使用,并确保任意一个程序能尽可能的得到运行。
五.参考资料