提示
本篇博客是参考调度程序schedule()注释,结合我个人查询到的其他知识以及个人理解完成的。由于本人知识储备有限,其中的注释及理解是否正确我自己也不知道,只能作为参考,有问题的地方欢迎大家指出。
一、schedule()内核源码及注释分析
// 通过schedule函数,完成对进程的调度
asmlinkage void schedule(void)
{
//声明调度数据结构体变量:sched_data
struct schedule_data * sched_data;
// 声明进程控制块PCB结构体变量:上一个PCB prev,下一个PCB next,指针p
struct task_struct *prev, *next, *p;
//声明表头结构体:tmp
struct list_head *tmp;
// 声明两个整型:当前CPU this_cpu,时间片 c
int this_cpu, c;
// =============== 处理使用完cpu的当前进程current ===============
// #### 锁运行队列,关中断 ####
// ① 运行队列:即就绪队列,Linux中称为run queue;
// ② 锁运行队列:将运行的队列上锁,此期间只能由某一个程序执行操作,
// 避免多个程序同时对队列进行操作,造成数据污染产生差错;
// ③ 关中断:关闭中断信号产生的程序,主要用于在此期间执行
// 一些不能被中断信号打断的程序(开中断,即打开产生中断信号的程序)。
spin_lock_prefetch(&runqueue_lock);
// #### 将当前运行的进程调度出去 ####
// 正在运行的而进程采用current宏表示
// ① 如果active_mm没有借用当前进程的mm,则执行报错函数bug()
// (内核线程没有mm空间,但其active_mm会借用当前进程的mm,保持与用户线程处理的统一性)
if (!current->active_mm) BUG();
// 将当前正在运行的进程调度出去,current变为prev,变为上一个刚刚用完CPU的PCB(即成为过去式)
prev = current;
//再获取上次运行进程所用的cpu
this_cpu = prev->processor;
// #### 如果内核处于执行中断处理程序中(可能性小),则报错 ####
// ① 中断中,即内核执行中断处理程序的期间,若允许进程调度,那么当中断处理程序运行时,进行了内核抢占,
// 那么处理器就会去执行抢占的进程,这个进程的具体信息处理器能够通过进程控制块去找到;
// ② unlikely()这种设计的原因:注意:unlikely用于gcc>=2.96之后的编译优化,表示if内代码运行的可能性比较低,
// 这样编译器就可以将else里面的代码提前,cpu在进行指令预取方面有性能提高,反之,likely则是if内代码运行可能性高。
if (unlikely(in_interrupt())) {
// 提示
printk("Scheduling in interrupt\n");
//报错,提示出问题
BUG();
}
// #### 释放上次运行的进程prev所占用的资源 ####
// ① 如果之前运行的进程prev占用了全局内核锁,释放;
// ② 如果当前cpu占用了全局中断锁,释放;开当前cpu中断线
release_kernel_lock(prev, this_cpu);
// #### 对sched_data进行事实保护,每个CPU只能运行一个进程 ####
sched_data = &aligned_data[this_cpu].schedule_data;
// #### 锁运行队列,关中断 ####
spin_lock_irq(&runqueue_lock);
// #### 将耗尽时间片的上次运行进程prev移动到运行队列最后 ####
//如果是实时进程(小概率事件)
if (unlikely(prev->policy == SCHED_RR))
//如果时间片已经用完
if (!prev->counter) {
//将nice转换为时间片
//【nice为UNIX时期沿用的负向优,向优先级,取值-20~19,值越大越谦让,值越小,优先级越高】
prev->counter = NICE_TO_TICKS(prev->nice);
//将其移动到运行队列尾部
move_last_runqueue(prev);
}
// #### 根据上次进程prev的状态,给予对应操作 ####
//prev->state:获取进程状态
switch (prev->state) {
// 若进程状态处于TASK_INTERRUPTIBLE浅度睡眠,则执行下方代码(浅度睡眠,即可中断的睡眠)
case TASK_INTERRUPTIBLE:
//如果该进程接收到唤醒信号
if (signal_pending(prev)) {
//让该进程状态置为TASK_RUNNING(就绪态)
prev->state = TASK_RUNNING;
break;
}
// 如果该进程处于其他状态,如:TASK_STOPED(暂停),TASK_ZOMBE(死亡但用户未注销),
// TASK_UNINNTERRUPTIBLE(深度睡眠,不可中断的睡眠)状态,就执行如下代码。
default:
del_from_runqueue(prev);//比如调用exit(),wait4()等
//如果进程处于就绪态,则不做任何操作,让其等待执行即可
case TASK_RUNNING:;
}
// #### 清空need_resched ####
// ① need_resched条件:如果为真, 内核会重新进程一次调度
prev->need_resched = 0;
// =============== 调度一个进程来使用CPU ===============
repeat_schedule:
// #### 选择即将使用CPU的进程next ####
//获得空闲进程
next = idle_task(this_cpu);
//找最大值的常用初始化
c = -1000;
//遍历运行队列
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
//如果程序可以在cpu上跑,并且允许在这颗cpu上跑
if (can_schedule(p, this_cpu)) {
//获取调度权重
int weight = goodness(p, this_cpu, prev->active_mm);
//更新最大权重与选中进程
if (weight > c)
c = weight, next = p;
}
}
// #### 如果c==0,说明所有进程时间片用完了,可能性很小 ####
// ① 个人理解:c==0时,!c为1即为真;unlikely是一种对发生可能性的描述,unlikely(!c),
// 表示!c为真即c==0的概率是unlikely的,就是很小概率,几乎不可能的,但是如果真出现这种可能了,则执行if里的语句。
if (unlikely(!c)) {
struct task_struct *p;
//开运行队列锁,开中断
spin_unlock_irq(&runqueue_lock);
//锁住进程双向链表
read_lock(&tasklist_lock);
//更新每个进程的时间片
for_each_task(p)
p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
//开进程双向链表
read_unlock(&tasklist_lock);
//锁运行队列,关中断
spin_lock_irq(&runqueue_lock);
//再次寻找最值得调度的进程
goto repeat_schedule;
}
// #### 将选择好的进程切换来使用CPU ####
//将当前运行的进程指向为我们选择好的next进程
sched_data->curr = next;
//将正在使用CPU的进程以及CPU内核更新为所选择的进程next和this_cpu
task_set_cpu(next, this_cpu);
//解锁运行队列,开中断
spin_unlock_irq(&runqueue_lock);
// #### 如果选择到的进程仍为之前的进程(认为可能性很小) ####
if (unlikely(prev == next)) {
prev->policy &= ~SCHED_YIELD;
// 执行“处理相同进程”的方法
goto same_process;
}
// =============== 维护每个流程的“last schedule”值 ===============
//(即使我们重新调度到同一进程,也必须重新计算)目前,这仅用于SMP,而且是近似值,因此我们不必在保持运行队列自旋锁的同时维护它。
#ifdef CONFIG_SMP
//更新调度进程时的时钟,用于smp中另外一个cpu调度参考
sched_data->last_schedule = get_cycles();
/*
* 我们提前删除了调度程序锁(它是一个全局自旋锁),因此我们必须在switch_to()期间锁定前一个进程,以避免重新调度。
*/
#endif /* CONFIG_SMP */
//记录调度次数
// 有3个进程受上下文切换的影响:prev == .... ==> (last => next)
// 下一个堆栈上的是“更多以前的”“prev”,但通过switch_to()将prev设置为(刚才运行的)“last”进程。这听起来可能有点令人困惑,但很有道理。
kstat.context_swtch++;
prepare_to_switch();
{
//新进程的运行空间
struct mm_struct *mm = next->mm;
//原进程的运行空间
struct mm_struct *oldmm = prev->active_mm;
//如果新进程没有运行空间,则是内核进程
if (!mm) {
//内核进程在调度出去的时候会释放其借用的运行空间,如果此处仍然存在,则有问题
if (next->active_mm) BUG();
//借用原进程的运行空间
next->active_mm = oldmm;
//原进程运行空间计数加1,用于内存交换信息
atomic_inc(&oldmm->mm_count);
//tlb采用lazy刷新方式
enter_lazy_tlb(oldmm, next, this_cpu);
}
//反之,如果是用户进程
else {
//用户进程的两个运行空间应该相同,如果不同则报错
if (next->active_mm != mm) BUG();
//切换用户空间
switch_mm(oldmm, mm, next, this_cpu);
}
//如果原进程是内核进程
if (!prev->mm) {
//释放其引用的运行空间
prev->active_mm = NULL;
//运行空间计数-1
mmdrop(oldmm);
}
}
//切换寄存器状态与堆栈
switch_to(prev, next, prev);
//原进程放入运行队列尾部
__schedule_tail(prev);
// =============== 处理调度后仍为相同进程 ===============
same_process:
//针对smp,要将当前进程的内核深度清0
reacquire_kernel_lock(current);
//再次调度
if (current->need_resched)
goto need_resched_back;
return;
}