Linux进程调度(二)——主动调度
创始人
2024-03-18 02:02:27

目录

分析__schedule()

第一步:

第二步:

pick_next_task的实现如下:

第三步:

进程上下文切换

内存空间的切换:

 寄存器和栈的切换switch_to

指令指针的保存与恢复

总结


进程的调度分为两种方式,本篇文章先来看第一种主动调度。

有以下几个片段,第一个是Btrfs,等待一个写入,这个片段可以看作写入块设备的一个典型场景。写入需要一段时间,这段时间用不上CPU,还不如主动让给其他进程。

static void btrfs_wait_for_no_snapshoting_writes(struct btrfs_root *root)
{
......do {prepare_to_wait(&root->subv_writers->wait, &wait,TASK_UNINTERRUPTIBLE);writers = percpu_counter_sum(&root->subv_writers->counter);if (writers)schedule();finish_wait(&root->subv_writers->wait, &wait);} while (writers);
}

另外一个例子是,从Tap网络设备等待一个读取。Tap网络设备是虚拟机使用的网络设备。当没有数据到来的时候,它也需要等待,所以也会选择把CPU让给其他进程:

static ssize_t tap_do_read(struct tap_queue *q,struct iov_iter *to,int noblock, struct sk_buff *skb)
{
......while (1) {if (!noblock)prepare_to_wait(sk_sleep(&q->sk), &wait,TASK_INTERRUPTIBLE);
....../* Nothing to read, let's sleep */schedule();}
......
}

在操作外部设备的时候,往往需要让出CPU,就像上面两段代码一样,选择调用schedule()函数

schedule函数的调用过程如下:

asmlinkage __visible void __sched schedule(void)
{struct task_struct *tsk = current;sched_submit_work(tsk);do {preempt_disable();__schedule(false);sched_preempt_enable_no_resched();} while (need_resched());
}

这段代码的主要逻辑是在__schedule函数中实现的。下面分几个部分来讲。

分析__schedule()

static void __sched notrace __schedule(bool preempt)
{struct task_struct *prev, *next;unsigned long *switch_count;struct rq_flags rf;struct rq *rq;int cpu;cpu = smp_processor_id();rq = cpu_rq(cpu);//取出当前CPU的任务队列prev = rq->curr;//指向的是正在运行的进程curr
......

第一步:

首先,在当前的CPU上,我们取出任务队列rqtask_struct *prev指向这个CPU的任务队列上面正在运行的那个进程curr。为啥是prev?因为一 旦将来它被切换下来,那它就成了前任了。

接下来的代码如下:

next = pick_next_task(rq, prev, &rf);//指向下一个任务
clear_tsk_need_resched(prev);
clear_preempt_need_resched();

第二步:

获取下一个任务,task_struct *next指向下一个任务,这就是继任

pick_next_task的实现如下:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{const struct sched_class *class;struct task_struct *p;/** Optimization: we know that if all tasks are in the fair class we can call that         function direc*/if (likely((prev->sched_class == &idle_sched_class ||prev->sched_class == &fair_sched_class) &&rq->nr_running == rq->cfs.h_nr_running)) {p = fair_sched_class.pick_next_task(rq, prev, rf);if (unlikely(p == RETRY_TASK))goto again;/* Assumes fair_sched_class->next == idle_sched_class */if (unlikely(!p))p = idle_sched_class.pick_next_task(rq, prev, rf);return p;}
again:for_each_class(class) {p = class->pick_next_task(rq, prev, rf);if (p) {if (unlikely(p == RETRY_TASK))goto again;return p;}}
}

again这里,就是依次调用调度类。但是这里有了一个优化,因为大部分进程是普通进程,所以大部分情况下会调用上面的逻辑,调用的就是 fair_sched_class.pick_next_task。根据上一节对于fair_sched_class的定义,它调用的是pick_next_task_fair,代码如下:

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{struct cfs_rq *cfs_rq = &rq->cfs;struct sched_entity *se;struct task_struct *p;int new_tasks;

对于CFS调度类,取出相应的队列cfs_rq,这就是充当CFS队列的那棵红黑树。

取出当前正在运行的任务curr,如果依然是可运行的状态,也即处于进程就绪状态,则调用 update_curr更新vruntime。update_curr就是根据实际运行时间算出 vruntime来。接着,pick_next_entity从红黑树里面,取最左边的一个节点。代码如下:

struct sched_entity *curr = cfs_rq->curr;
if (curr) {if (curr->on_rq)update_curr(cfs_rq);elsecurr = NULL;
......}se = pick_next_entity(cfs_rq, curr);

对获取到的节点se,利用task_of得到下一个调度实体对应的task_struct,如果发现继任和前任不一样,这就说明有一个更需要运行的进程了,就需要更新红黑树了。前面前任的vruntime更新过了,put_prev_entity 放回红黑树,会找到相应的位置,然后set_next_entity将继任者设为当前任务。

第三步:

当选出的继任者和前任不同,就要进行上下文切换,继任者进程正式进入运行。

if (likely(prev != next)) {rq->nr_switches++;rq->curr = next;++*switch_count;
......rq = context_switch(rq, prev, next, &rf);

进程上下文切换

上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和CPU上下 文

内存空间的切换:

context_switch:

/** context_switch - switch to the new MM and the new thread's register state.*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,struct task_struct *next, struct rq_flags *rf)
{struct mm_struct *mm, *oldmm;
......mm = next->mm;oldmm = prev->active_mm;
......switch_mm_irqs_off(oldmm, mm, next);
....../* Here we just switch the register state and the stack. */switch_to(prev, next, prev);barrier();return finish_task_switch(prev);
}

 寄存器和栈的切换switch_to

它调用到了__switch_to_asm,这是一段汇编代码,主要用于栈的切换。对于32位操作系统来讲,切换的是栈顶指针esp。对于64位操作系统来讲,切换的是栈顶指针rsp。最终,都返回了__switch_to这个函数

在64位操作系统中__switch_to这个函数做的事情分析如下:

__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{struct thread_struct *prev = &prev_p->thread;struct thread_struct *next = &next_p->thread;
......int cpu = smp_processor_id();struct tss_struct *tss = &per_cpu(cpu_tss, cpu);
......load_TLS(next, cpu);
......this_cpu_write(current_task, next_p);/* Reload esp0 and ss1. This changes current_thread_info(). */load_sp0(tss, next);
......return prev_p;
}

这里面有一个Per CPU的结构体tss。对于每个进程,x86希望在内存里面维护一个TSS(Task State Segment,任务状态段)结构。这里面有所有的寄存器。另外,还有一个特殊的寄存器TR(Task Register,任务寄存器),指向某个进程的TSS。更改TR的值,将会触发硬件保存CPU所有寄存器的值到当前进程的TSS中,然后从新进程的TSS中读出所有寄存器值,加载到CPU对应的寄存器中。

但是这样有个缺点。我们做进程切换的时候,没必要每个寄存器都切换,这样每个进程一个 TSS,就需要全量保存,全量切换,动作太大了。

于是,Linux操作系统想了一个办法。在系统初始化的时候会调用cpu_init这里面会给每一个CPU关联一个TSS(X86中是每个进程维护一个TSS),然后将TR指向这个TSS,然后在操作系统的运行过程中,TR就不切换了,永远指向这个TSS。

void cpu_init(void)
{int cpu = smp_processor_id();struct task_struct *curr = current;struct tss_struct *t = &per_cpu(cpu_tss, cpu);......load_sp0(t, thread);set_tss_desc(cpu, t);load_TR_desc();......
}
struct tss_struct {/** The hardware state:*/struct x86_hw_tss x86_tss;unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
} 

在Linux中,真的参与进程切换的寄存器很少,主要的就是栈顶寄存器。在task_struct里面,还有一个我们原来没有注意的成员变量thread。这里面保留了要切换进程的时候需要修改的寄存器

/* CPU-specific state of this task: */
struct thread_struct thread;

所谓的进程切换,就是将某个进程的thread_struct里面的寄存器的值,写入到CPU的TR指向的 tss_struct,对于CPU来讲,这就算是完成了切换。

指令指针的保存与恢复

 从进程A切换到进程B,用户栈早就已经切换了,就在切换内存空间的时候。每个进程的用户栈都是独立的,都在内存空间里面。

内核栈已经在__switch_to里面切换了,也就是将current_task指向当前的task_struct。 里面的void *stack指针,指向的就是当前的内核栈。

内核栈的栈顶指在__switch_to_asm里面已经切换了栈顶指针,并且将栈顶指针在 __switch_to加载到了TSS里面。

用户栈的栈顶指针如果当前在内核里面的话,它当然是在内核栈顶部的pt_regs结构里面呀。当从内核返回用户态运行的时候,pt_regs里面有所有当时在用户态的时候运行的上下文信息,就可以开始运行了。

唯一让人不容易理解的是指令指针寄存器,它应该指向下一条指令的,那它是如何切换的呢?

进程的调度都最终会调用到__schedule函数,称之为“进程调度第一定律” 。

我们用最前面的例子仔细分析这个过程。本来一个进程A在用户态是要写一个文件的,写文件的操作用户态没办法完成,就要通过系统调用到达内核态。在这个切换的过程中,用户态的指令指针寄存器是保存在pt_regs里面的,到了内核态,就开始沿着写文件的逻辑一步一步执行,结果发现需要等待,于是就调用__schedule函数。

这个时候,进程A在内核态的指令指针是指向__schedule了。这里请记住,A进程的内核栈会保存这个__schedule的调用,而且知道这是从btrfs_wait_for_no_snapshoting_writes这个函数里面进去的。

__schedule里面经过上面的层层调用,到达了context_switch的最后三行指令

 当进程A在内核里面执行switch_to的时候,内核态的指令指针也是指向这一行的。但是在 switch_to里面,将寄存器和栈都切换到成了进程B的唯一没有变的就是指令指针寄存器。当 switch_to返回的时候,指令指针寄存器指向了下一条语句finish_task_switch

但这个时候的finish_task_switch已经不是进程A的finish_task_switch了,而是进程B的 finish_task_switch了。这样合理吗?你怎么知道进程B当时被切换下去的时候,执行到哪里了?恢复B进程执行的时候 一定在这里呢?这时候就要用到咱的“进程调度第一定律”了。当年B进程被别人切换走的时候,也是调用__schedule也是调用到switch_to,被切换成为C进程的,所以,B进程当年的下一个指令也是finish_task_switch,这就说明指令指针指到这里是没有错的。

接下来,我们要从finish_task_switch完毕后,返回__schedule的调用了。 按照函数返回的原理,当然是从内核栈里面去找应该从B进程的内核栈里面找。假设,B就是最前面例子里面调用tap_do_read读网卡的进程。它当年调用__schedule的时候,是从tap_do_read这个函数调用进去的。于是,从__schedule返回之后,当然是接着tap_do_read运行,然后在内核运行完毕后,返回用户态。这个时候,B进程内核栈的pt_regs也保存了用户态的指令指针寄存器,就接着在用户态的下一条指令开始运行就可以了。

假设,我们只有一个CPU,从B切换到C,从C又切换到A。在C切换到A的时候,还是按照“进程调度第一定律”,C进程还是会调用__schedule到达switch_to,在里面切换成为A的内核栈,然后运行finish_task_switch。

这个时候运行的finish_task_switch,才是A进程的finish_task_switch。运行完毕从__schedule 返回的时候,从内核栈上才知道,当年是从btrfs_wait_for_no_snapshoting_writes调用进去的,因而应该返回btrfs_wait_for_no_snapshoting_writes继续执行,最后内核执行完毕返回用户态,同样恢复pt_regs,恢复用户态的指令指针寄存器,从用户态接着运行。

总结

 主动调度的过程,也即一个运行中的进程主动调用__schedule让出CPU。在 __schedule里面会做两件事情,第一是选取下一个进程,第二是进行上下文切换。而上下文切换又分用户态进程空间的切换和内核态的切换。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...