内核:进程与调度机制(笔记)

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: 内核:进程与调度机制(笔记)

一、调度器

Linux 内核 sched_class 调度器有五种类型:dl_sched_class/rt_sched_class/stop_sched_class/idle_sched_c lass/fair_sched_class,其中每种调度类都有自己的调度策略。主要是为方便添加新的调度策略 ,Linux内核抽象一个调度sched_class。

1、核心调度器

调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间。这也是为什么整个方法称之为优先调度的原因。

a.周期性调度器函数

周期性调度器在scheduler_tick中实现,如果系统正在活动中,内核会按照频率HZ自动调用该函数。该函数主要有两个任务如下:

(1) 更新相关统计量:管理内核中与整个系统和各个进程的调度相关的 统计量。其间执行的主要操作是对各种计数器加1。

(2) 激活负责当前进程的调度类的周期性调度方法。

void scheduler_tick(void)
{
    // 获取当前CPU上的全局就绪队列rq和当前运行的进程curr
    // 在SMP(多核)的情况下,获得当前CPU的ID、如果不是SMP,那么返回0
  int cpu = smp_processor_id();
    // 获取CPU的全局就绪队列rq,每个CPU都有一个就绪队列rq
  struct rq *rq = cpu_rq(cpu);
    // 获取就绪队列上面正在运行的进程curr
  struct task_struct *curr = rq->curr;
  struct rq_flags rf;
  sched_clock_tick();
  rq_lock(rq, &rf);
    // 更新rq当前时间戳,即相当于rq->clock变为当前时间戳
    // 处理就绪队列时钟的更新,本质上就是增加struct rq当前实例的时钟时间戳
  update_rq_clock(rq);
    // 由于调度器的模块化结构,主要工作可以完全由特定调度器类方法,task_tick实现模式取决底层的调度器类
    // 执行当前运行进程所在调度类的task_tick函数进行周期性调整
  curr->sched_class->task_tick(rq, curr, 0);
    // 将当前负荷加入数组的第一个位置
  cpu_load_update_active(rq);
    // 更新全局CPU就绪队列的calc_load_update
    // 更新CPU的活动计数,主要是更新全局CPU就绪队列calc_load_uodate
  calc_global_load_tick(rq);
    // 解锁
  rq_unlock(rq, &rf);
    // 与perf计数事件有关
  perf_event_task_tick();
#ifdef CONFIG_SMP
    // 判断当前CPU是否为空闲状态
  rq->idle_balance = idle_cpu(cpu);
    // 如果进程周期性负载平衡则触发CCHED_SOFTIRQ终端
  trigger_load_balance(rq);
#endif
  rq_last_tick_reset(rq);
}

更新统计量函数:update_rq_clock()/calc_global_load_tick()   <update_rq_clock>函数

void update_rq_clock(struct rq *rq)
{
  s64 delta;
  lockdep_assert_held(&rq->lock);
  if (rq->clock_update_flags & RQCF_ACT_SKIP)
    return;
#ifdef CONFIG_SCHED_DEBUG
  if (sched_feat(WARN_DOUBLE_CLOCK))
    SCHED_WARN_ON(rq->clock_update_flags & RQCF_UPDATED);
  rq->clock_update_flags |= RQCF_UPDATED;
#endif
  delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
  if (delta < 0)
    return;
  rq->clock += delta;
  update_rq_clock_task(rq, delta);
}
/*
 * Called from scheduler_tick() to periodically update this CPU's
 * active count.
 */
void calc_global_load_tick(struct rq *this_rq)
{
  long delta;
  if (time_before(jiffies, this_rq->calc_load_update))
    return;
  delta  = calc_load_fold_active(this_rq, 0);
  if (delta)
    atomic_long_add(delta, &calc_load_tasks);
  this_rq->calc_load_update += LOAD_FREQ;
}

b.主调度器函数

在内核中的许多地方,如果要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主调度器函数(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());
}
EXPORT_SYMBOL(schedule);

主调度器负责将CPU的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。cfs队列实际上是用红黑树组织的,rt 队列是用链表组织的。

2、调度类及运行队列

a.调度类

为方便添加新的调度策略,Linux内核抽象一个调度类sched_class,目前 为止实现5种调度类

1 引入停机调度类:支持限期调度类,迁移线程的优先级必须比期限进程的优先级高,能够抢占所有其它进程,才能够快速处理调度器发出的迁移请求,把进程从当前处理器迁移到其它处理器。

2 限期调度类:使用优先算法(使用红黑树)把进程按照绝对截至期限从小到大排序,每次调度时选择绝对截至期限最小的进程。

3 实时调度类:为每个调度优先级维护一个队列源码如下:

位图bitmap用来快速查找第一个非空队列,数据组queue的下标是实时进程的调度优先级,下标越小,优先级越高。

4 公平调度类:使用完全公平调度算法,引入虚拟运行时间:

               虚拟运行时间=实际运行时间*nice 0对应的权重/进程的权重

nice 0对应权重1024 , nice n-1的权重大概是nice n权重的1.2倍左右。

5 空闲调度类:每个处理器上面有一个空闲的线程,即0号线程。空闲调度类的优先级最低,仅当没有其它进程可以调度的时候,才会执行调度空闲线程。

b.运行队列

每个处理器有一个运行队列,结构体是rq,定义的全局变量如下:

DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

rq是描述就绪队列,其设计是为每一个CPU就绪队列,本地进程在本地队列上排序:

6 运行队列:struct rq中嵌入公平运行队列cfs,实时运行队列rt,限期运行队列dl、停机调度类和空闲调度类在每个处理器上只有一个内核线程,不需要运行队列,直接定义成员stop/idle分别指向迁移线程的空闲线程。

3、调度进程

主动调度进程的函数是schedule() ,它会把主要工作委托给__schedule() 去处理。

preempt:表示是否抢占调度,值为true表示抢占调度,强制剥夺当前进程对处理器的使用权

值为false表示主动调度,当前进程主动让出处理器

函数__shcedule的主要处理过程如下:

调用pick_next_task()以选择下一个进程。

调用context_switch()以切换进程。

此函数针对公平调度类优化,5种调度类:优先级从高到低:

停机、限期、实时、公平、空闲。

7 停机调度类选择下一个进程:pick_next_task_stop。期限调度类选择下一个进程:pick_next_task_dl。实时调度类选择下一个进程:pick_next_task_rt。公平调度类选择下一个进程:pick_next_task_fair。

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;
    // 执行进程切换的准备工作
  prepare_task_switch(rq, prev, next);
  mm = next->mm;
  oldmm = prev->active_mm;
  /*
   * For paravirt, this is coupled with an exit in switch_to to
   * combine the page table reload and the switch backend into
   * one hypercall.
   */
    // 开始上下文的切换,是每一种处理器架构必须定义的函数
  arch_start_context_switch(prev);
    // 如果下一个进程是内核线程(mm是空指针),内核线程没有用户虚拟地址空间,
  if (!mm) {
    next->active_mm = oldmm;
    mmgrab(oldmm);
        // 此函数通知处理器架构不需要切换用户虚拟地址空间,这种加速进程切换的技术TLB
    enter_lazy_tlb(oldmm, next);
  } else
        // 如果下一个进程是用户进程,那么就调用此函数切换进程的用户虚拟地址空间。
    switch_mm_irqs_off(oldmm, mm, next);
    // 如果上一个进程是内核线程,把成员active_mm为空指针,断开它与借用的用户虚拟地址空间的联系,把它借用的用户虚拟地址空间保存在运行队列的成员prev_mm中。
  if (!prev->mm) {
    prev->active_mm = NULL;
    rq->prev_mm = oldmm;
  }
  rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
  /*
   * Since the runqueue lock will be released by the next
   * task (which is an invalid locking op but in the case
   * of the scheduler it's an obvious special-case), so we
   * do an early lockdep release here:
   */
  rq_unpin_lock(rq, rf);
  spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
  /* Here we just switch the register state and the stack. */
  switch_to(prev, next, prev);
  barrier();
  return finish_task_switch(prev);
}

a、切换用户虚拟地址空间,ARM64架构使用默认的switch_mm_irqs_off, 其内核源码定义如下:

static inline void __switch_mm(struct mm_struct *next)
{
  unsigned int cpu = smp_processor_id();
  /*
   * init_mm.pgd does not contain any user mappings and it is always
   * active for kernel addresses in TTBR1. Just set the reserved TTBR0.
   */
  if (next == &init_mm) {
    cpu_set_reserved_ttbr0();
    return;
  }
  check_and_switch_context(next, cpu);
}

b、切换寄存器,宏switch_to把这项工作委托给函数__switch_to:

用户态和内核态的切换:

主动:发生系统调用的时候。

被动:发生异常的时候:缺页中断、外设产生中断的时候。

内核态:CPU可以访问内存的所有数据,包括外围设备(网卡,硬盘等),CPU也可以将自己从一个程序切换至另一个程序。

用户态:只能受限访问,并且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其它程序获取。

/*
 * Thread switching.
 */
__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
        struct task_struct *next)
{
  struct task_struct *last;
  fpsimd_thread_switch(next); // 切换浮点寄存器
  tls_thread_switch(next);    // 切换线程本地存储相关的寄存器
  hw_breakpoint_thread_switch(next); // 切换调试寄存器
  contextidr_thread_switch(next);    // 切换上下文标识符寄存器
  entry_task_switch(next);   // 使用当前处理器每处理器变量记录下一个进程的进程描述符地址
  uao_thread_switch(next);   //         
  /*
   * Complete any pending TLB or cache maintenance on this CPU in case
   * the thread migrates to a different CPU.
   */
  // 在这个处理器上执行完前面的所有页表缓存或缓存维护操作,防止线程迁移到其它处理器上
  dsb(ish);
  /* the actual thread switch */
    // 实际线程切换
  last = cpu_switch_to(prev, next);
  return last;
}

为什么要切换浮点寄存器,因为不同的处理器架构的浮点寄存器可能不同,而且有的处理器架构不支持浮点运算。

4、调度时机

调度进程的时机如下:

进程主动调用schedule()函数。

周期性地调度,抢占当前进程,强迫当前进程让出处理器。

唤醒进程的时候,被唤醒的进程可能抢占当前进程。

创建新进程的时候,新进程可能抢占当前进程。

如果我们编译内核时开启对内核抢占的支持,那么内核含增加一些指占点。

a、主动调度

进程在用户模式下运行的时候,无法直接调用schedule()函数,只能通过系统调用进入内核模式,如果系统调用需要等待某个资源,如互斥锁或信 号量,就会把进程的状态设置为睡眠状态,然后调用schedule()函数来调度进程。

进程也可以通过系统调用shced_yield()让出处理器,这种情况下进程不会睡眠。

在内核中有3种主动调度方式:

直接调用schedule()函数来调用进程。

调用有条件重调度函数cond_resched()。

如果需要等待某个资源。

b、周期调度

有些“地痞流氓”进程不主动让出处理器,内核只能依靠周期性的时钟中断夺回处理器的控制权,时钟中断是调度器的脉博。时钟中断处理程序检 查当前进程的执行时间有没有超过限额,如果超过限额,设置需要重新调度的标志。当时钟中断处理程序准备返点处理器还给被打断的进程时,如 果被打断的进程在用户模式下运行,就检查有没有设置需要重新调度的标 志,如果设置了,调用schedule函数以调度进程。

如果需要重新调度,就为当前进程的thread_info结构体的成员flags设置需要重新调度的标志。

周期调度的函数为 scheduler_tick(),调用当前进程所属调度类 task_tick()方法

二、SMP调度

在SMP系统中,进程调度器必须支持如下:

需要使用每个处理器的负载尽可能均衡。

可以设置进程的处理器亲和性,即允许进程在哪些处理器上执行。

可以把进程从一个处理器迁移到另一个处理器。

1、进程的处理器亲和性

设置进程的处理器亲和性,通俗就是把进程绑定到某些处理器,只允许进 程在某些处理器上执行,默认情况是进程可以在所有处理器上执行。应用 编程接口和使用cpuset配置具体详解分析。

9 应用编程接口:内核只有2个系统调用

       sched_setaffinity:设置进程的处理器亲和性掩码。

       sched_getaffinity:用来获取进程的处理器亲和性掩码。

内核线程可以使用2个函数来设置处理器亲和性掩码:

        kthread_bind 用来把一个刚刚创建的内核线程绑定到一个处理器。

        set_cpus_allowed_ptr 用来设置内核线程处理器亲和性掩码。

2、期限调度类的处理器负载均衡

限期调度类的处理器负载均衡简单,调度选择下一个限期进程的时候,如果当前正在执行的进程是限期进程,将会试图从限期进程超载的处理器把限期进程搞过来。

限期进程超载定义:

限期运行队列至少有两个限期进程。

至少有一个限期进程绑定到多个处理器。

3、实时调度类的处理器负载均衡

实时调度类的处理器负载均衡和限期调度类相似。调度器选择下一个实时进程时,如果当前处理器的实时运行队列中的进程的最高调度优先级比当前正在执行的进程的调度优先级低,将会试图从实时进程超载的处理器把可推送实时进程拉过来。

实时进程超载的定义:

实时运行队列至少有两个实时进程。

至少有一个可推送实时进程。

4、公平调度类的处理器负载均衡

目前多处理器系统有两种体系结构:NUMA和SMP。

处理器内部的拓扑如下:

a.核(core):一个处理器包含多个核,每个核独立的一级缓存,所有核共享二级缓存。

b.硬件线程:也称为逻辑处理器或者虚拟处理器,一个处理器或者核包含多个硬件线程,硬件线程共享一级缓存和二级缓存。MIPS处理器的叫法是同步多线程(Simultaneous Multi-Threading,SMT),英特尔对它的叫法是超线程享二级缓存。

RCU 机制与内存优化屏障

RCU(read-copy-update)为 Linux 当中的一种同步机制,则为读/拷贝更新。

写者修改对象流程:先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象,在写者执行复制更新的时候读者则可以读数据。

内存屏障

内存屏障可分为两种类型:编译器内存屏障和 CPU 内存屏障。

防止编译器错误地重排,添加编译器优化屏障如下:

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
14天前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
11天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
32 3
|
15天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
49 4
|
16天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
14天前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
25 0
|
5月前
|
监控 Linux 应用服务中间件
探索Linux中的`ps`命令:进程监控与分析的利器
探索Linux中的`ps`命令:进程监控与分析的利器
126 13
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
167 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。
|
4月前
|
存储 缓存 安全
【Linux】冯诺依曼体系结构与操作系统及其进程
【Linux】冯诺依曼体系结构与操作系统及其进程
171 1

热门文章

最新文章

相关实验场景

更多