Linux进程调度(二)——主动调度

news2024/9/21 14:25:19

目录

分析__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);
    else
        curr = 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里面会做两件事情,第一是选取下一个进程,第二是进行上下文切换。而上下文切换又分用户态进程空间的切换和内核态的切换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/61619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MOOC——多项式加法(5分)好难!

1.多项式加法(5分) 题目内容: 一个多项式可以表达为x的各次幂与系数乘积的和,比如: 2x63x512x36x20 现在,你的程序要读入两个多项式,然后输出这两个多项式的和,也就是把对应的幂…

java GUI 实现的socket在线聊天系统项目源码运行教程

大家好,今天给大家演示一下一个简单的图形界面的聊天程序,上次我们演示的是Java实现的群聊程序,还可以发送文件,今天这个小程序只能聊天,不能发文件,功能非常简单,可在局域网内实现互相聊天&…

html当当书网站 html网上在线书城 html在线小说书籍网页 当当书城网页设计

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

社区系统项目复盘-7

文章目录Spring Security权限控制置顶、加精、删除Redis高级数据类型网站数据统计使用Spring Security进行权限控制,对登录检查功能进行了重写。对不同的登录账号授予不同的权限,实现了置顶、加精、删除功能。使用Redis高级数据类型HyperLogLog和Bitmap实…

2-分类问题 SVM 核函数

目录 一,核函数的引入 二,核函数的定义 三,核函数介绍: 四,核函数总结: 一,核函数的引入 世界上本来没有两个完全一样的物体,对于所有的两个物体,我们可以通过增加维…

ES6 Reflect

前言 此文总结了Reflect对象的部分语法,对比了与Object方法的差异性,希望对你有用。 语法 Reflect与Math类似,都是JavaScript内置对象,提供了工具方法。 typeof Reflect // objectget Reflect.get(target, property, receiver) …

冷热电气多能互补的微能源网鲁棒优化调度(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

Linux——VIM编辑器(详细)

目录 一、基本简介 1.1 基本简介 1.2 我们怎么使用终端进入到桌面? 1.3 模式间转换 二、一般模式 2.1 操作指令 2.2 什么情况下是一般模式呢? 2.3 怎么进行光标的快速移动? 2.4 复制粘贴操作 2.5 删除操作 三、编辑模式 四、命令行…

Java项目:SSM教师师资管理系统

作者主页:源码空间站2022 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目包含管理员与教师两种角色; 管理员角色包含以下功能: 管理员角色登录,教师管理,教师授课管理,审批教师的项目开…

【前沿技术RPA】 一文了解UiPath 文件与文件夹自动化功能

🐋作者简介:博主是一位.Net开发者,同时也是RPA和低代码平台的践行者。 🐬个人主页:会敲键盘的肘子 🐰系列专栏:UiPath 🦀专栏简介:UiPath在传统的RPA(Robotic…

[YOLOv7/YOLOv5系列改进NO.40]融入适配GPU的轻量级 G-GhostNet

文章目录前言一、解决问题二、基本原理三、​添加方法四、总结前言 作为当前先进的深度学习目标检测算法YOLOv7,已经集合了大量的trick,但是还是有提高和改进的空间,针对具体应用场景下的检测难点,可以不同的改进方法。此后的系列…

头歌计算机组成原理汉字字库存储芯片扩展实验

全部答案点击底部 <?xml version"1.0" encoding"UTF-8" standalone"no"?> <project source"2.15.0.2.exe" version"1.0"> This file is intended to be loaded by Logisim http://logisim.altervista.org &…

如何在 Spring 或 Spring Boot 中使用键集分页

介绍 在本文中&#xff0c;我将向您展示如何在 Spring 或 Spring Boot 中使用键集分页技术。 虽然 Spring DataPagingAndSortingRepository提供的基于偏移量的默认分页在许多情况下很有用&#xff0c;但如果您必须迭代大型结果集&#xff0c;那么键集分页或查找方法技术可以提…

使用awk聚合和排序

用awk聚合和排序 文章目录用awk聚合和排序一、需求1.1 源文件格式1.2 需求二、用awk实现2.1 写法2.2 效果一、需求 1.1 源文件格式 一份csv文件&#xff08;默认逗号分隔&#xff09;一共五列&#xff0c;其中一列是用户名文件名&#xff1a;日志文件.csv type日记idusernam…

Android使用ListView,DrawerLayout实现简单注册功能界面

1.效果展示 2.实现 1.主页面activity_main.xml 主页面就是简单的几个TextView和EditText以及单选框组成的一个注册表单。 <?xml version"1.0" encoding"utf-8"?> <LinearLayoutxmlns:android"http://schemas.android.com/apk/res/andro…

[附源码]JAVA毕业设计口腔医院网站(系统+LW)

[附源码]JAVA毕业设计口腔医院网站&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

java通过lock实现同步锁

这里我们是一个卖票的演示代码 其实 同步锁 远不止一个synchronized 它本身有一个 加上锁 和释放锁的过程 为了 让我们更好的理解这个过程 JDK5之后 为我们提供了一个单独的锁工具 lock lock是一个接口 他提供了 synchronized 方法 和 更广泛的语句操作 lock方法 获得锁 unl…

【C语言】函数传参与指针理解

文章目录指针与变量注意指针的本质指针和变量的用法函数与传参传变量与传指针的区别传变量与传指针的时机指针与变量 大三&#xff0c;但是C语言。目标&#xff1a;高屋建瓴&#xff0c;深入浅出。 注意 所有人在最开始学C语言的时候&#xff0c;老师都会和你说指针指向一个…

[附源码]JAVA毕业设计课程答疑系统(系统+LW)

[附源码]JAVA毕业设计课程答疑系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

LLM.int8()——自适应混合精度量化方法

Paper地址&#xff1a;https://arxiv.org/abs/2208.07339 GitHub链接&#xff1a;GitHub - TimDettmers/bitsandbytes: 8-bit CUDA functions for PyTorch 随着模型参数规模的增加&#xff0c;大模型&#xff08;如GPT-3&#xff0c;OPT-175B等NLP稠密大模型&#xff09;的实际…