进程调度,那么先从进程描述符的数据结构开始
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
...
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
...
}
在进程描述符中注意到sched_entity类型的se成员变量,这个是进程调度器的实体结构,同时我们也看到prio相关的参数(进程优先级)。我们来看下sched_entity的数据结构
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 last_wakeup;
u64 avg_overlap;
u64 nr_migrations;
u64 start_runtime;
u64 avg_wakeup;
...
};
我们关注下其中一些重要的参数,后面会重点分析。 vruntime变量字面意思虚拟运行时间,可以理解成进程实际调度运行时间(以ms为单位)的标准化处理的结果,作为抽象出来的指标。下面来看其相关的操作。
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;
if (unlikely(!curr))
return;
/*
* Get the amount of time the current task was running
* since the last time we changed load (this cannot
* overflow on 32 bits):
*/
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return;
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
}
首先通过cfs_rq获得当前的进程的调度实体cur,再记录下当前系统时间为now,通过进程实体的统计量exec_start即进程刚被调用时刻,计算出当前进程实际运行时间delta_exec。通过_update_curr()子函数计算出当前delta_exec归一化的结果,并更新对应的vruntime指标。
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = calc_delta_fair(delta_exec, curr);
curr->vruntime += delta_exec_weighted;
update_min_vruntime(cfs_rq);
}
# define schedstat_add(rq, field, amt) do { (rq)->field += (amt); } while (0)
这里统计了sum-exec_runtime、rq的field字段,我们可以看到vruntime的具体在calc_delta_fair()函数中求取。
static inline unsigned long
calc_delta_fair(unsigned long delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);
return delta;
}
static unsigned long
calc_delta_mine(unsigned long delta_exec, unsigned long weight,
struct load_weight *lw)
{
u64 tmp;
if (!lw->inv_weight) {
if (BITS_PER_LONG > 32 && unlikely(lw->weight >= WMULT_CONST))
lw->inv_weight = 1;
else
lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2)
/ (lw->weight+1);
}
tmp = (u64)delta_exec * weight;
/*
* Check whether we'd overflow the 64-bit multiplication:
*/
if (unlikely(tmp > WMULT_CONST))
tmp = SRR(SRR(tmp, WMULT_SHIFT/2) * lw->inv_weight,
WMULT_SHIFT/2);
else
tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);
return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);
}
我们看到如果load.weight == NICE_0_LOAD,那么实际运行实际与归一化的结果相同,直接返回delta。否则将实际运行时间归一化处理为delta_exec_weighted直接加到原先统计的vruntime上。
在这里需要提的是nice、weight、prio之间的关系,有助于我们理解,我们知道进程调度通过优先级来区分。其中NICE_0_LOAD即nice值为0时对应的weight的值为1024。
#define NICE_0_LOAD SCHED_LOAD_SCALE
#define SCHED_LOAD_SHIFT 10
#define SCHED_LOAD_SCALE (1L << SCHED_LOAD_SHIFT)
weight即sched_entity的数据结构中的load_weight数据结构。
struct load_weight {
unsigned long weight, inv_weight;
};
我们通过代码来具体看他们之间的关系
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,
};
static const u32 prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
这段数组体现了prio到weight和inv_的映射关系,下面这些代码是我总结出来的他们之间的关系
lw->inv_weight = 2^32 / weight
p->static_prio = NICE_TO_PRIO(nice);
/*
* Convert user-nice values [ -20 ... 0 ... 19 ]
* to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
* and back.
*/
#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
#define TASK_NICE(p) PRIO_TO_NICE((p)->static_prio)
p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO];
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];
其中MAX_RT_PRIO为100,MAX_PRIO为140,通过这些关系,把nice域线性映射到了prio域。
load_weight中的weight跟inv_weight关系:weight * inv_weight = 2 ^32,不禁要问为什么通过两个数组打表的方式构造这样的数据结构??我们继续看下去会发现,这样设计是为了通过维护一个inv_wight反向变量方式,通过乘法跟位运算代替了归一化计算需要用的除法操作。
回到核心的calc_delta_mine()方法中。因为维护了inv_wight变量即作为2 ^32/weight,那么inv_weight为0的话,即为weight变量的值大于2^32的话,inv_weight需要维持一个最小变量1,参与后面的乘法操作。如果weight为大于2^32,那么通过
lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2) / (lw->weight+1);
方式维护inv_weight的值,其中WMULT_CONST即为2^32,其实我们化简公式可以看到
lw->inv_weight ~= 1 + (WMULT_CONST / (lw->weight+1)) - 0.5(略小于0.5) ; 近似地维护了weight * inv_weight = 2 ^32关系
其中有一个很有意思的宏SSR
/*
* Shift right and round:
*/
#define SRR(x, y) (((x) + (1UL << ((y) - 1))) >> (y))
化成我们看得懂的式子 (x+0.5*2^y)/2^y ,目的应该明确了即(x/2^y)的结果四舍五入处理。
看了这么多总结一下
delat_vruntime = (delta_exec * nice_0_weight * lw->inv_weight) / (2^32)
到这里算是vruntime的计算过程清晰了。
其中update_curr()会被系统定时周期性且在多个地方调用,无论是进程处于可运行态,还是被堵塞处于不可运行态。后面调度系统会根据计算出的每个进程控制器的vruntime为依据进行优先队列的选择,然后“公平调度”。这一块内容被称为时间记账。本质上不过是维护了归一化处理的调度时长,以此为依据供进程调度选择。
关于红黑树,只是插入跟删除操作,取出最左边的进程节点、然后执行、在这之后会更新它的vruntime,等重新调度的时候,插入红黑树进行排序调整,需要理解的是红黑树中维护的都是就绪态的进程。我们只需维护最左边节点的指针,这样取操作时间复杂度O(1)、插入节点操作O(lgn)
个人感觉进程调度这块跟任务调度类似、只不过根据不同的需求考虑的点不一样罢了
提供数据结构:红黑树、堆、数组(桶排序、时间轮,这个个人认为是最有意思的点、用的好时间复杂度O1)