您的位置:首页 > 运维架构 > Linux

Linux内核-进程调度

2017-02-21 15:40 260 查看
在分析调度策略之前,我们先来看下进程的三种类型:

交互式进程:这些进程经常与用户进行交互,因此需要花很多时间等待键盘鼠标等操作。当接受输入后,进程必须很快被唤醒。

批处理进程:这些进程不必与用户交互,因此经常在后台运行。因为这类进程不必很快地响应,因此常受到调度程序的慢待。典型的批处理进程是程序设计语言的编译程序、数据库搜索引擎等。

实时进程:这些进程有很强的调度需要。这样的进程不会被低优先级的进程阻塞,它们应该有一个短的响应时间。典型的实时程序有视频和音频应用程序等。

Linux的调度基于分时技术:CPU的时间被分成“片”,给每个可运行进程分配一片,如果当前运行的进程的时间片或时限到期时,该进程还没有运行完毕,进程切换就可以发生。分时依赖于定时中断,因此对进程是透明的,不需要在程序中插入额外的代码来保证CPU分时。

Linux总是按照下面的调度类型被调度:

SCHED_FIFO:先进先出的实时进程。当调度程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。如果没有其他可运行的更高优先级实时进程,进程就继续使用CPU,即使还有其他的具有相同优先级的实时进程处于可运行状态。

SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程公平地分配CPU时间。

SCHED_NORMAL:普通的分时进程。

普通进程的调度

每个普通进程都有它自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间调度的程度。内核用从100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。

基本时间片:

如果静态优先级 < 120,则基本时间片 = (140 - 静态优先级)* 20

如果静态优先级 >= 120,则基本时间片 = (140 - 静态优先级)*5

普通进程除了静态优先级,还有动态优先级,其值的范围是100(最高) ~ 139(最低)。动态优先级是调度程序在选择新进程来运行时使用的数,它与静态优先级的关系如下:

动态优先级 = max(100,min(静态优先级 - bonus + 5,139))

bonus是范围从0~10的值,值小于5表示降低动态优先级以示惩罚,值大于5表示增加动态优先级以示奖赏。bonus的值依赖于进程过去的情况,说得更准确一些,是与进程的平均睡眠时间相关,如下:

平均睡眠时间bonus
大于或等于0小于100ms0
大于或等于100ms小于200ms1
大于或等于200ms小于300ms3
大于或等于300ms小于400ms4
大于或等于400ms小于500ms5
大于或等于500ms小于600ms6
大于或等于600ms小于700ms7
大于或等于700ms小于800ms8
大于或等于800ms小于900ms9
大于或等于900ms小于1000ms10
1s10
平均睡眠时间也被调度程序用来确定一个给定进程是交互式进程还是批处理进程。更明确地说,如果一个进程满足下面的公式,就被看做是交互式进程:

动态优先级 <= 3*静态优先级/4 + 28

它相当于下面的公式:

bonus - 5 >= 静态优先级/4 -28

可以看出,高优先级进程比低优先级进程更容易成为交互式进程。例如,具有最高静态优先级(100)的进程,当它的bonus值超过2,即睡眠时间超过200ms时,就被看作是交互式进程;相反,最低优先级(139)的进程决不会被当作交互式进程,因为bonus值总是小于11,相应地需要(静态优先级/4 - 28)等于6。

活动和过期进程

即使具有较高静态优先级的普通进程获得了较大的CPU时间片,也不应该使静态优先级较低的进程无法运行。为了避免进程饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的低优先级进程取代。为了实现这种机制,调度程序维持两个不相交的可运行进程的集合。

活动进程:这些进程还没有用完它们的时间片,因此允许它们运行。

过期进程:这些可运行进程已经用完了它们的时间片,因此被禁止运行,知道所有活动进程都过期。

不过,总体方案要稍微复杂一些,因为调度程序试图提升交互式进程的性能。用完其时间片的活动批处理进程总是变成过期进程。用完其时间片的交互式进程通常仍然是活动进程:调度程序重填其时间片并把它留在活动进程集合中。但是,如果最老的过期进程已经等待了很长的时间,或者过期进程比交互式进程的静态优先级高,调度程序就把用完时间片的交互式进程移到过期进程集合中。结果,活动进程集合最终会变为空,过期进程将有机会运行。

实时进程的调度

每个实时进程都与一个实时优先级相关,实时优先级是一个范围从1~99(最低)的值。调度程序总是让优先级高的进程运行,实时进程总是被当作活动进程。只有在下述事件之一发生时,实时进程才会被另一个进程取代:

进程被另一个具有更高实时进程优先级的实时进程抢占

进程执行了阻塞操作并进入睡眠

进程停止或被杀死

进程通过系统调用sched_yield()自愿放弃CPU

进程是基于时间片轮转的实时进程,而且用完了它的时间片

数据结构与源码

数据结构runqueue是Linux2.6调度程序最重要的数据结构。系统中每个CPU都有它自己的运行队列,所有的runqueue结构都存放在runqueues每CPU变量中。宏this_rq()产生本地CPU运行队列的地址,而宏cpu_rq(n)产生索引为n的CPU的运行队列的地址,runqueue如下:

/**
* CPU任务运行队列
*/
struct runqueue {
/**
* 保护进程链表的自旋锁
*/
spinlock_t lock;

/**
* 进程链表中可运行进程的数量。
*/
unsigned long nr_running;
#ifdef CONFIG_SMP
/**
* 基于运行队列中进程的平均数量的CPU负载因子。
*/
unsigned long cpu_load;
#endif
/**
* CPU执行进程切换的次数。
*/
unsigned long long nr_switches;

/**
* 先前在运行队列链表中而现在 睡眠在TASK_UNINTERRUPTIBLE状态的进程的数量。
*/
unsigned long nr_uninterruptible;

/**
* 过期队列中最老的进程被插入队列的时间。
*/
unsigned long expired_timestamp;
/**
* 最近一次定时器中断的时间戳的值。
*/
unsigned long long timestamp_last_tick;
/**
* curr-当前正在运行进程的进程描述符指针(对于本地CPU来说,它与current相同)。
* idle-当前CPU的swapper进程的进程描述符指针。
*/
task_t *curr, *idle;
/**
* 在进程切换期间用来存放被替换进程的内存描述符的地址。
*/
struct mm_struct *prev_mm;
/**
* active-指向活动进程的链表数组
* expired-指向过期进程的链表数组
* arrays-活动进程和过期进程的两个集合
*/
prio_array_t *active, *expired, arrays[2];
/**
* 过期进程中静态优先级最高的进程(权值最小)
*/
int best_expired_prio;
/**
* 先前在运行队列的链表中,而现在正等待IO操作结束的进程的数量。
*/
atomic_t nr_iowait;

#ifdef CONFIG_SMP
/**
* 当前CPU的基本调度域
*/
struct sched_domain *sd;

/**
* 如果要把一些进程从本地运行队列迁移到另外的运行队列,就设置这个标志。
*/
int active_balance;
/**
* 未使用.
* xie.baoyou注:从代码上看,应该是指需要将本运行队列中,向哪个CPU迁移任务。
*/
int push_cpu;

/**
* 迁移内核线程的进程描述符指针
*/
task_t *migration_thread;
/**
* 从运行队列中被删除的进程的链表。
*/
struct list_head migration_queue;
#endif

#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;

/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;

/* schedule() stats */
unsigned long sched_noswitch;
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;

/* pull_task() stats */
unsigned long pt_gained[MAX_IDLE_TYPES];
unsigned long pt_lost[MAX_IDLE_TYPES];

/* active_load_balance() stats */
unsigned long alb_cnt;
unsigned long alb_lost;
unsigned long alb_gained;
unsigned long alb_failed;

/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_attempts;
unsigned long ttwu_moved;

/* wake_up_new_task() stats */
unsigned long wunt_cnt;
unsigned long wunt_moved;

/* sched_migrate_task() stats */
unsigned long smt_cnt;

/* sched_balance_exec() stats */
unsigned long sbe_cnt;
#endif
};


调度程序依靠几个函数来完成调度工作,其中最重要的函数是:

scheduler_tick():维持当前最新的time_slice计数器

try_to_wake_up():唤醒睡眠进程

recale_task_prio():更新进程的动态优先级

schedule():选择要被执行的新进程

load_balance():维持多处理器系统中运行队列中的平衡

下面分析一下try_to_wake_up和schedule两个函数源码,如下:

/**
* 通过把进程状态设置为TASK_RUNNING,并把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程
* p-被唤醒进程的描述符
* state-可以被唤醒的进程状态掩码。
* sync-一个标志,用来禁止被唤醒的进程抢占本地CPU上正在运行的进程。
*/
static int try_to_wake_up(task_t * p, unsigned int state, int sync)
{
int cpu, this_cpu, success = 0;
unsigned long flags;
long old_state;
runqueue_t *rq;
#ifdef CONFIG_SMP
unsigned long load, this_load;
struct sched_domain *sd;
int new_cpu;
#endif

/**
* 调用task_rq_lock来禁止中断,并获得进程所在CPU上的运行队列的锁(可能与当前CPU的运行队列不一样,并且被唤醒的进程可能并不在队列上)
*/
rq = task_rq_lock(p, &flags);
schedstat_inc(rq, ttwu_cnt);
old_state = p->state;
/**
* 只唤醒state对应状态的进程。如果被唤醒的进程状态不在state中,直接退出。本次唤醒无效。
* 例如:通过信号就不会唤醒TASK_UNINTERRUPTIBLE状态的进程。
*/
if (!(old_state & state))
goto out;

/**
* 如果进程已经属于某个运行队列,就跳转到out_running,将它的状态修改为TASK_RUNNING状态后退出。
*/
if (p->array)
goto out_running;

cpu = task_cpu(p);
this_cpu = smp_processor_id();

#ifdef CONFIG_SMP
/**
* 在SMP上,需要检查被唤醒的进程是否应该从最近运行的CPU的运行队列迁移到另外一个CPU的运行队列。
*/

/**
* 被唤醒任务正在CPU上运行,不必考虑迁移了。
*/
if (unlikely(task_running(rq, p)))
goto out_activate;

/**
* 优先将进程放到进程所在CPU上运行。
*/
new_cpu = cpu;

/**
* 如果进程所在CPU就是当前进程所在CPU,或者被唤醒进程不允许在当前进程所在CPU上运行,那么跳转到out_set_cpu
*/
if (cpu == this_cpu || unlikely(!cpu_isset(this_cpu, p->cpus_allowed)))
goto out_set_cpu;

load = source_load(cpu);
this_load = target_load(this_cpu);

/*
* If sync wakeup then subtract the (maximum possible) effect of
* the currently running task from the load of the current CPU:
*/
if (sync)
this_load -= SCHED_LOAD_SCALE;

/* Don't pull the task off an idle CPU to a busy one */
/**
* 如果被唤醒任务所在的CPU工作量小于当前CPU的工作量,也跳转到out_set_cpu
*/
if (load < SCHED_LOAD_SCALE/2 && this_load > SCHED_LOAD_SCALE/2)
goto out_set_cpu;

/**
* 试图将进程迁移到本地CPU。
*/
new_cpu = this_cpu; /* Wake to this CPU if we can */

/*
* Scan domains for affine wakeup and passive balancing
* possibilities.
*/
for_each_domain(this_cpu, sd) {
unsigned int imbalance;
/*
* Start passive balancing when half the imbalance_pct
* limit is reached.
*/
imbalance = sd->imbalance_pct + (sd->imbalance_pct - 100) / 2;

if ((sd->flags & SD_WAKE_AFFINE) &&
!task_hot(p, rq->timestamp_last_tick, sd)) {
/*
* This domain has SD_WAKE_AFFINE and p is cache cold
* in this domain.
*/
if (cpu_isset(cpu, sd->span)) {
schedstat_inc(sd, ttwu_wake_affine);
goto out_set_cpu;
}
} else if ((sd->flags & SD_WAKE_BALANCE) &&
imbalance*this_load <= 100*load) {
/*
* This domain has SD_WAKE_BALANCE and there is
* an imbalance.
*/
if (cpu_isset(cpu, sd->span)) {
schedstat_inc(sd, ttwu_wake_balance);
goto out_set_cpu;
}
}
}

new_cpu = cpu; /* Could not wake to this_cpu. Wake to cpu instead */
out_set_cpu:
schedstat_inc(rq, ttwu_attempts);
new_cpu = wake_idle(new_cpu, p);
if (new_cpu != cpu) {
schedstat_inc(rq, ttwu_moved);
set_task_cpu(p, new_cpu);
task_rq_unlock(rq, &flags);
/* might preempt at this point */
rq = task_rq_lock(p, &flags);
old_state = p->state;
if (!(old_state & state))
goto out;
if (p->array)
goto out_running;

this_cpu = smp_processor_id();
cpu = task_cpu(p);
}

out_activate:
#endif /* CONFIG_SMP */
/**
* 如果是TASK_UNINTERRUPTIBLE,就递减nr_uninterruptible
* 并将activated设为-1,表示进程是从TASK_UNINTERRUPTIBLE状态被唤醒这个事实。
*/
if (old_state == TASK_UNINTERRUPTIBLE) {
rq->nr_uninterruptible--;
/*
* Tasks on involuntary sleep don't earn
* sleep_avg beyond just interactive state.
*/
p->activated = -1;
}

/**
* activate_task函数依次执行以下步骤澹?
*     1:调用sched_clock获得当前时间戳,如果目标CPU不是本地CPU,那么还会补偿时钟中断的偏差。
*     2:调用recalc_task_prio,计算进程的动态优先级。
*     3:根据情况设置activated
*     4:设置进程的时间戳。
*     5:将进程插入进程集合。
*/
activate_task(p, rq, cpu == this_cpu);
/**
* 如果目标CPU不是本地CPU,或者没有SYNC标志,就检查新进程的动态优先级是否比运行队列中当前进程的优先级高。
*/
if (!sync || cpu != this_cpu) {
if (TASK_PREEMPTS_CURR(p, rq))/* 进程的优先级比所在队列的当前进程优先级高,需要抢占。 */
/**
* resched_task函数进行进程抢占。
* 在单处理器上,它仅仅设置TIF_NEED_RESCHED标志。
* 在多处理器上,它可能会发送IPI,强制让CPU产生调度。
*/
resched_task(rq->curr);
}
success = 1;

out_running:
/**
* 将进程状态设置为为TASK_RUNNING,注意两个流程会走到这里。
*/
p->state = TASK_RUNNING;
out:
/**
* 开中断并打开运行队列的锁。
*/
task_rq_unlock(rq, &flags);

/**
* 返回0:进程没有被唤醒。否则返回1,进程被唤醒。
*/
return success;
}


activate_task函数如下:

static void activate_task(task_t *p, runqueue_t *rq, int local)
{
unsigned long long now;

now = sched_clock();
#ifdef CONFIG_SMP
if (!local) {
/* Compensate for drifting sched_clock */
runqueue_t *this_rq = this_rq();
now = (now - this_rq->timestamp_last_tick)
+ rq->timestamp_last_tick;
}
#endif

recalc_task_prio(p, now);

/*
* This checks to make sure it's not an uninterruptible task
* that is now waking up.
*/
if (!p->activated) {
/*
* Tasks which were woken up by interrupts (ie. hw events)
* are most likely of interactive nature. So we give them
* the credit of extending their sleep time to the period
* of time they spend on the runqueue, waiting for execution
* on a CPU, first time around:
*/
if (in_interrupt())
p->activated = 2;
else {
/*
* Normal first-time wakeups get a credit too for
* on-runqueue time, but it will be weighted down:
*/
p->activated = 1;
}
}
p->timestamp = now;

__activate_task(p, rq);
}


recalc_task_prio函数如下:

/**
* 更新进程的动态优先级和平均睡眠时间。
* p-进程描述符
* now-由函数sched_clock计算出的当前时间戳。
*/
static void recalc_task_prio(task_t *p, unsigned long long now)
{
/**
* timestamp是进程进入睡眠状态的进程切换的时间戳。
* __sleep_time是进程从上次被切换出去后,经过的时间。
*/
unsigned long long __sleep_time = now - p->timestamp;
unsigned long sleep_time;

/**
* 如果进程睡眠时间超过1秒,就算成1秒。
*/
if (__sleep_time > NS_MAX_SLEEP_AVG)
sleep_time = NS_MAX_SLEEP_AVG;
else
sleep_time = (unsigned long)__sleep_time;

/**
* 只有当进程睡眠时间大于0才需要更新平均睡眠时间。
*/
if (likely(sleep_time > 0)) {
/**
* 如果进程不是内核线程(mm!=NULL),并且进程不是从TASK_UNINTERRUPTIBLE状态被唤醒,并且睡眠时间超过给定的睡眠时间极限
* 就把平均睡眠时间设置为相当于900个时钟节拍的值。这仅仅是经验值而已,没有什么科学道理。
* 睡眠时间极限依赖于进程的静态优先级,这个规则的目的是保证已经在不可中断模式(通常是等待磁盘IO)上,睡眠了很久的进程获得足够长的平均睡眠时间,以使这些进程尽快得到服务。
*/
if (p->mm && p->activated != -1 &&
sleep_time > INTERACTIVE_SLEEP(p)) {
p->sleep_avg = JIFFIES_TO_NS(MAX_SLEEP_AVG -
DEF_TIMESLICE);
} else {
/**
* CURRENT_BONUS宏计算进程原来的平均睡眠时间的bonus值。如果10-bonus>0,就用这个值与sleep_time相乘。
* 所以平均睡眠时间越短,它增加得越快。
*/
sleep_time *= (MAX_BONUS - CURRENT_BONUS(p)) ? : 1;

/**
* 如果不是内核线程,并且处于TASK_UNINTERRUPTIBLE状态。以下两种情况是为了不对睡眠时间很长的批处理进程给予太多的奖励。
*/
if (p->activated == -1 && p->mm) {
/**
* 如果平均睡眠时间已经超过极限了,就不用再调整平均睡眠时间了。
*/
if (p->sleep_avg >= INTERACTIVE_SLEEP(p))
sleep_time = 0;
else if (p->sleep_avg + sleep_time >=
INTERACTIVE_SLEEP(p)) {/* 如果加上本次sleep_time时间大于极限睡眠时间,就将睡眠时间设置为最大 */
p->sleep_avg = INTERACTIVE_SLEEP(p);
sleep_time = 0;
}
}

/**
* 将本次睡眠时间加到平均睡眠时间上。
*/
p->sleep_avg += sleep_time;

/**
* 如果平均睡眠时间超过1秒,那么就将它调整为1秒。
*/
if (p->sleep_avg > NS_MAX_SLEEP_AVG)
p->sleep_avg = NS_MAX_SLEEP_AVG;
}
}

/**
* 更新进程的动态优先级。
*/
p->prio = effective_prio(p);
}


下面看一下schedule()函数

asmlinkage void __sched schedule(void)
{
long *switch_count;
/**
* next指向被选中的进程,这个进程将取代当前进程在CPU上执行。
* 如果系统中没有优先级高于当前进程,那么next会和current相等。不发生任何切换。
*/
task_t *prev, *next;
runqueue_t *rq;
prio_array_t *array;
struct list_head *queue;
unsigned long long now;
unsigned long run_time;
int cpu, idx;

/*
* Test if we are atomic.  Since do_exit() needs to call into
* schedule() atomically, we ignore that path for now.
* Otherwise, whine if we are scheduling when we should not be.
*/
if (likely(!current->exit_state)) {
if (unlikely(in_atomic())) {
printk(KERN_ERR "scheduling while atomic: "
"%s/0x%08x/%d\n",
current->comm, preempt_count(), current->pid);
dump_stack();
}
}
profile_hit(SCHED_PROFILING, __builtin_return_address(0));

need_resched:
/**
* 先禁止抢占,再初始化一些变量。
* 此处需要禁止抢占,因为后面需要访问任务的运行队列。禁止抢占后可以防止进程飘移。
*/
preempt_disable();
prev = current;
/**
* 释放大内核锁。当内核抢占打开时,并且当前中断正在抢占当前进程,那么会将lock_depth置为-1.
* 这样,不会释放内核锁。只有当进程获得了大内核锁并且是主动调度出来时,才会释放锁。
* 注意,释放锁并不会修改lock_depth。当进程恢复执行后,如果lock_depth>=0,就会再次获得大内核锁。
*/
release_kernel_lock(prev);
need_resched_nonpreemptible:
rq = this_rq();

/*
* The idle thread is not allowed to schedule!
* Remove this check after it has been exercised a bit.
*/
if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) {
printk(KERN_ERR "bad: scheduling from the idle thread!\n");
dump_stack();
}

schedstat_inc(rq, sched_cnt);
/**
* 计算当前进程的运行时间。不超过1秒。
*/
now = sched_clock();
if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG))
run_time = now - prev->timestamp;
else
run_time = NS_MAX_SLEEP_AVG;

/*
* Tasks charged proportionately less run_time at high sleep_avg to
* delay them losing their interactive status
*/
/**
* 对有较长睡眠时间的进程,进行一定奖励。
*/
run_time /= (CURRENT_BONUS(prev) ? : 1);

/**
* 在开始寻找可运行进程之前,需要关中断并获得保护运行队列的自旋锁。
*/
spin_lock_irq(&rq->lock);

/**
* 当前进程可能是一个正在准备被终止的进程。可能现在是通过do_exit进入schedule函数。
*/
if (unlikely(prev->flags & PF_DEAD))
prev->state = EXIT_DEAD;

switch_count = &prev->nivcsw;
/**
* 如果进程不是TASK_RUNNING状态,并且没有被内核抢占。就把该进程从运行队列中删除。
*/
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
switch_count = &prev->nvcsw;
/**
* 如果进程是被信号打断的,就将它设置成TASK_RUNNING
*/
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev))))
prev->state = TASK_RUNNING;
else {/* 将它从运行队列中删除 */
if (prev->state == TASK_UNINTERRUPTIBLE)
rq->nr_uninterruptible++;
deactivate_task(prev, rq);
}
}

cpu = smp_processor_id();
/**
* 检查是否有可运行的进程。
*/
if (unlikely(!rq->nr_running)) {/* 没有了 */
go_idle:
/**
* 运行队列中没有可运行的进程存在,调用idle_balance,从另外一个运行队列迁移一些可运行进程到本地运行队列中。
*/
idle_balance(cpu, rq);
if (!rq->nr_running) {/* 没有迁移新进程到本运行队列。 */
next = rq->idle;
rq->expired_timestamp = 0;
/**
* wake_sleeping_dependent重新调度空闲CPU中的可运行进程。主要是处于超线程的情况。
*/
wake_sleeping_dependent(cpu, rq);
/*
* wake_sleeping_dependent() might have released
* the runqueue, so break out if we got new
* tasks meanwhile:
*/
if (!rq->nr_running)/* 如果支持超线程,并且其他逻辑CPU也没有可运行进程,那么只好运行IDLE进程了。 */
goto switch_tasks;
}
} else {/* 有可能运行的进程 */
/**
* dependent_sleeper一般返回为0,但是如果内核支持超线程技术,函数检查要被选中执行的进程。
* 其优先级是否比当前已经在相同物理CPU的逻辑CPU上运行的兄弟进程的优先级,如果新进程优先级低,就拒绝选择低优先级进程,而去执行swapper进程。
*/
if (dependent_sleeper(cpu, rq)) {
next = rq->idle;
goto switch_tasks;
}
/*
* dependent_sleeper() releases and reacquires the runqueue
* lock, hence go into the idle loop if the rq went
* empty meanwhile:
*/
if (unlikely(!rq->nr_running))
goto go_idle;
}

/**
* 运行到此,说明运行队列中有线程可被运行。
*/
array = rq->active;
if (unlikely(!array->nr_active)) {
/**
* 活动队列中没有可运行进程了。交换活动集合和过期集合。
*/
/*
* Switch the active and expired arrays.
*/
schedstat_inc(rq, sched_switch);
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = MAX_PRIO;
} else
schedstat_inc(rq, sched_noswitch);

/**
* 现在开始在活动集合中搜索一个可运行的进程。
* 首先搜索第一个非0位,并找到对应的链表。
*/
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
/**
* 将下一个可运行进程描述符放到next中
*/
next = list_entry(queue->next, task_t, run_list);

/**
* 如果进程是一个普通进程,并且是从TASK_INTERRUPTIBLE或者TASK_STOPPED状态被唤醒。
* 就把自从进程插入运行队列开始所经过的纳秒数加到平均睡眠时间中。
*/
if (!rt_task(next) && next->activated > 0) {
unsigned long long delta = now - next->timestamp;

/**
* 如果是被系统调用服务例程或者内核线程所唤醒,就只增加部分睡眠时间(30%)
* 否则增加100%的睡眠时间。这样,交互式进程由于经常被中断打断,它的睡眠时间会增加得更快。
*/
if (next->activated == 1)
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;

array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta);
enqueue_task(next, array);
}
next->activated = 0;
switch_tasks:
/**
* 运行到这里,开始进行进程切换了。
*/
if (next == rq->idle)
schedstat_inc(rq, sched_goidle);
/**
* prefetch提示CPU控制单元把next的进程描述符的第一部分字段的内容装入硬件高速缓存。
* 这改善了schedule的性能。
*/
prefetch(next);
/**
* 清除TIF_NEED_RESCHED标志。
*/
clear_tsk_need_resched(prev);
/**
* 记录CPU正在经历静止状态。主要与RCU相关。
*/
rcu_qsctr_inc(task_cpu(prev));

/**
* 减少prev的平均睡眠时间
*/
prev->sleep_avg -= run_time;
if ((long)prev->sleep_avg <= 0)
prev->sleep_avg = 0;
/**
* 更新进程的时间戳
*/
prev->timestamp = prev->last_ran = now;

sched_info_switch(prev, next);
if (likely(prev != next)) {/* prev和next不同,需要切换 */
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
++*switch_count;

prepare_arch_switch(rq, next);
/**
* context_switch执行真正的进程切换
*/
prev = context_switch(rq, prev, next);

/**
* 当进程再次被切换进来后,以下代码被接着运行。
* 但是此时prev并不指向当前进程,而是指代码从哪一个进程切换到本进程。
* 由于此时已经进行了进程空间的切换,寄存器中缓存的变量等都不再有效,所以用barrier产生一个优化屏障。
*/
barrier();

/**
* 对前一个进程进行一些收尾工作,比如减少它的mm_struct,task_struct的引用计数等。
*/
finish_task_switch(prev);
} else/* 如果prev和next是同一个进程,就不做进程切换。当prev仍然是当前活动集合中的最高优先级进程时,这是有可能发生的。 */
spin_unlock_irq(&rq->lock);

/**
* 在前几句中(context_switch之后),prev代表的是从哪个进程切换到本进程。
* 在继续进行调度之前(因此在context_switch中开了中断,可能刚切回本进程就来了中断,并需要重新调度),将prev设置成当前进程。
*/
prev = current;
/**
* 重新获得大内核锁。
*/
if (unlikely(reacquire_kernel_lock(prev) < 0))
goto need_resched_nonpreemptible;
/**
* 打开抢占,并检查是否需要重新调度。
*/
preempt_enable_no_resched();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}


/**
* 建立next的地址空间
*/
static inline
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;

/**
* 如果是切换到一个内核线程,新进程就使用pre的地址空间,避免了TLB的切换
*/
if (unlikely(!mm)) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
/**
* 作为更进一步的优化措施,如果新进程是内核线程,就将进程设置为懒惰TLB模式
* xie.baoyou注:请想一下,如果内核线程切换出去后,可能又会回到上一个进程,此时就根本不需要切换地址空间。
* 皆大欢喜,大家都省事了,这叫“懒人有懒福”
*/
enter_lazy_tlb(oldmm, next);
} else/* 否则就需要切换内地址空间。 */
switch_mm(oldmm, mm, next);

/**
* 如果上一个线程是内核线程,就把prev内存描述符的指针保存到运行队列的prev_mm中。
* 并清空rq->prev_mm
*/
if (unlikely(!prev->mm)) {
prev->active_mm = NULL;
WARN_ON(rq->prev_mm);
rq->prev_mm = oldmm;
}

/* Here we just switch the register state and the stack. */
/**
* 终于可以真正的切换了。
*/
switch_to(prev, next, prev);

return prev;
}


/**
* 进程切换时,切换内核态堆栈和硬件上下文。
* prev-被替换的进程
* next-新进程
* last-在任何进程切换中,到三个进程而不是两个。假设内核决定暂停A而激活B,那么在schedule函数中,prev指向A而next指向B。
*      当切换回A后,就必须暂停另外一个进程C。而LAST则指向C进程。
*/
#define switch_to(prev,next,last) do {                  \
unsigned long esi,edi;                      \
/**
* 在真正执行汇编代码前,已经将prev存入eax,next存入edx中了。
* 没有搞懂gcc汇编语法,反正结果就是这样。
* 应该是"2" (prev), "d" (next)这句的副作用。
*/

/**
* 保存eflags和ebp到内核栈中。必须保存是因为编译器认为在switch_to结束前,
* 它们的值应当保持不变。
*/
asm volatile("pushfl\n\t"                   \
"pushl %%ebp\n\t"                  \
/**
* 把esp的内容保存到prev->thread.esp中
* 这样该字段指向prev内核栈的栈顶。
*/
"movl %%esp,%0\n\t"    /* save ESP */      \
/**
* 将next->thread.esp装入到esp.
* 此时,内核开始在next的栈上进行操作。这条指令实际上完成了从prev到next的切换。
* 由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变当前进程。
*/
"movl %5,%%esp\n\t"    /* restore ESP */   \
/**
* 将标记为1f的地址存入prev->thread.eip.
* 当被替换的进程重新恢复执行时,进程执行被标记为1f的那条指令。
*/
"movl $1f,%1\n\t"     /* save EIP */      \
/**
* 将next->thread.eip的值保存到next的内核栈中。
* 这样,_switch_to调用ret返回时,就会跳转到next->thread.eip执行。
* 这个地址一般情况下就会是1f.
*/
"pushl %6\n\t"     /* restore EIP */   \
/**
* 注意,这里不是用call,是jmp,这样,上一条语句中压入的eip地址就可以执行了。
*/
"jmp __switch_to\n"                \
/**
* 到这里,进程A再次获得CPU。它从栈中弹出ebp和eflags。
*/
"1:\t"                     \
"popl %%ebp\n\t"                   \
"popfl"                        \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \
/* last被作为输出参数,它的值会由eax赋给它。 */
"=a" (last),"=S" (esi),"=D" (edi)         \
:"m" (next->thread.esp),"m" (next->thread.eip),    \
"2" (prev), "d" (next));              \
} while (0)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息