Linux进程管理:(六)SMP负载均衡

2024-03-18 10:44

本文主要是介绍Linux进程管理:(六)SMP负载均衡,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章说明:

  • Linux内核版本:5.0

  • 架构:ARM64

  • 参考资料及图片来源:《奔跑吧Linux内核》

  • Linux 5.0内核源码注释仓库地址:

    zhangzihengya/LinuxSourceCode_v5.0_study (github.com)

1. 前置知识

1.1 CPU管理位图

内核对CPU的管理是通过位图(bitmap)变量来管理的,并且定义了possible、present、online和active这4种状态:

/* 位图类型变量 */
// 表示系统中有多少个可以运行(现在运行或者将来某个时间点运行)的 CPU 内核
#define cpu_possible_mask ((const struct cpumask *)&__cpu_possible_mask)
// 表示系统中有多少个正处于运行(online)状态的 CPU 内核
#define cpu_online_mask   ((const struct cpumask *)&__cpu_online_mask)
// 表示系统中有多少个可处于运行状态的CPU内核,它们不一定都处于运行状态,有的CPU内核可能被热插拔了
#define cpu_present_mask  ((const struct cpumask *)&__cpu_present_mask)
// 表示系统中有多少个活跃的 CPU 内核
#define cpu_active_mask   ((const struct cpumask *)&__cpu_active_mask)
  • #define DECLARE_BITMAP(name,bits) \unsigned long name[BITS_TO_LONGS(bits)]typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
    
    • name[]:位图类型变量使用一个长整数类型数组name[],每位代表一个CPU。对于64位处理器来说,一个长整数类型数组成员只能表示64个CPU内核。内核配置中有一个宏CONFIG_NR_CPUS为8,那么只需要一个长整数类型数组成员可。
    • struct cpumask:cpumask数据结构本质上是位图,内核通常使用cpumask的相关接口函数来管理CPU内核数量。

CPU位图的初始化流程如下图所示:

在这里插入图片描述

  • cpu_possible_mask通过查询系统DTS表或者ACPI表获取系统CPU内核数量
  • cpu_present_mask等同于cpu_possible_mask
  • cpu_active_mask是经过使能后的CPU内核数量

1.2 CPU调度域

根据系统的内存和高速缓存的布局,CPU域可分为如下几类:

在这里插入图片描述

Linux内核通过数据结构sched_domain_topology_level来描述CPU的层次关系,简称为SDTL:

// 用于描述 CPU 的层级关系
struct sched_domain_topology_level {sched_domain_mask_f mask;			// 函数指针,用于指定某个 SDTL 的 cpumask 位图// sd_flags 函数指针里指定了该调度层级的标志位sched_domain_flags_f sd_flags;		// 函数指针,用于指定某个 SDTL 的标志位int		    flags;int		    numa_level;struct sd_data      data;
#ifdef CONFIG_SCHED_DEBUGchar                *name;
#endif
};

另外,内核默认定义了一个数组default_topology[]来概括CPU物理域的层次结构

// CPU 拓扑关系,从下往上
static struct sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT		// 超线程// cpu_smt_mask() 函数描述了 SMT 层级的 CPU 位图组成方式{ cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
#endif
#ifdef CONFIG_SCHED_MC		// 多核// cpu_coregroup_mask() 函数描述了 MC 层级的 CPU 位图组成方式{ cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif						// 处理器// cpu_cpu_mask() 函数描述了 DIE 层级的 CPU 位图组成方式{ cpu_cpu_mask, SD_INIT_NAME(DIE) },{ NULL, },
};

default_topology[]数组可知,系统默认支持DIE层级、SMT层级以及MC层级。另外,在调度域里划分调度组,使用sched_group来描述调度组,调度组是负载均衡调度的最小单位,在最低层级的调度域中,通常一个调度组描述一个CPU。

在一个支持NUMA架构的处理器中,假设它支持SMT技术,那么整个系统的调度域和调度组的关系如下图所示(它在默认调度层级中新增一个NUMA层级的调度域):

在这里插入图片描述

  • DIE表示处理器封装,它是CPU拓扑结构中的一个层级,在多核处理器中,通常有多个物理处理器核心,组成一个封装

在Linux内核中使用sched_domain数据结构来描述调度层级。在超大系统中系统会频繁访问调度域数据结构,为了提升系统的性能和可扩展性,调度域sched_domain数据结构采用Per-CPU变量来构建:

// 描述调度层级
struct sched_domain {// 父调度域指针,顶层调度域必须为 null 终止struct sched_domain *parent;// 子调度域指针,底层调度域必须为 null 终止struct sched_domain *child;// groups 指向该调度域的平衡组链表struct sched_group *groups;// 最小平衡间隔(毫秒)unsigned long min_interval;// 最大平衡间隔(毫秒)unsigned long max_interval;...
};

下面举例来说明CPU调度域的拓扑关系,如下图所示,假设在一个4核处理器中,每个CPU拥有独立L1高速缓存且不支持超线程技术,4个物理CPU被分成两个簇Cluster0和Cluster1,每个簇包含两个物理CPU,簇中的CPU共享L2高速缓存

在这里插入图片描述

因为每个CPU内核只有一个运行线程,所以4核处理器没有SMT层级。簇由两个CPU组成,这两个CPU处于MC层级且是兄弟关系。整个处理器可以看作处于DIE层级,因此该处理器只有两个层级,即MC和DIE。根据上述原则,4核处理器的调度域和调度组的拓扑关系如下图所示:

在这里插入图片描述

在每个SDTL都为每个CPU分配了对应的调度域和调度组,以CPU0为例:

  • 对于DIE层级,CPU0对应的调度域是domain_die_0,该调度域管辖着4个CPU并包含两个调度组,分别为group_die_0和group_die_1
    • 调度组group_die_0管辖CPU0和CPU1
    • 调度组group_die_1管辖CPU2和CPU3
  • 对于MC层级,CPU0对应的调度域是domain_mc_0,该调度域中管辖着CPU0和CPU1并包含两个调度组,分别为group_mc_0和group_mc_1
    • 调度组group_mc_0管辖CPU0
    • 调度组group_mc_1管辖CPU1

除此以外,4核处理器的调度域和调度组还有两层关系,如下图所示:

在这里插入图片描述

综上所述,为了提升系统的性能和可扩展性,sched_domain数据结构采用Per-CPU变量来构建,这样可以减少CPU之间的访问竞争。而sched_group数据结构则是调度域内共享的。

2. LLC调度域

在负载均衡算法中,我们常常需要快速找到系统中最高层级的并且具有高速缓存共享属性的调度域。通常在—个系统,我们把最后一级高速缓存(Last Level Cache,LLC)称为LLC。在调度域标志位中,SD_SHARE_PKG_RESOURCE标志位用于描述高速缓存的共享属性。因此,在调度域层级中,包含LLC的最高一级调度域称为LLC调度域。在Linux内核中实现一组特殊用途的指针,用来指向LLC调度域,这些指针是Per-CPU变量。查找和设置LLC调度域是在update_top_cache_domain()函数中实现的。在select_idle_cpu()等函数中会使用到LLC 调度域。

// 指向 LLC 调度域
DEFINE_PER_CPU(struct sched_domain *, sd_llc);
// LLC 调度域包含多少个 CPU
DEFINE_PER_CPU(int, sd_llc_size);
// LLC 调度域第一个 CPU 的编号
DEFINE_PER_CPU(int, sd_llc_id);
DEFINE_PER_CPU(struct sched_domain_shared *, sd_llc_shared);
DEFINE_PER_CPU(struct sched_domain *, sd_numa);
DEFINE_PER_CPU(struct sched_domain *, sd_asym_packing);
// 指向第一个包含不同 CPU 架构的调度域,主要用于大/小核架构
DEFINE_PER_CPU(struct sched_domain *, sd_asym_cpucapacity);
DEFINE_STATIC_KEY_FALSE(sched_asym_cpucapacity);// 查找和设置 LLC 调度域
static void update_top_cache_domain(int cpu)
{...
}

3. 负载均衡

负载均衡机制从注册软中断开始,每次系统处理调度节拍时会检查当前是否需要处理负载均衡。负载均衡的流程如图所示:

在这里插入图片描述

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

load_balance()

// this_cpu 指当前正在运行的 CPU
// this_rq 表示当前就绪队列
// sd 表示当前正在做负载均衡的调度域
// idle 表示当前 CPU 是否处于 IDLE 状态
// continue_balancing 表示当前调度域是否还需要继续做负载均衡
static int load_balance(int this_cpu, struct rq *this_rq,struct sched_domain *sd, enum cpu_idle_type idle,int *continue_balancing)
{...struct lb_env env = {// 当前调度域.sd		= sd,// 当前的 CPU,后面可能要把一些繁忙的进程迁移到该 CPU 上.dst_cpu	= this_cpu,// 当前 CPU 对应的就绪队列.dst_rq		= this_rq,// 当前调度域里的第一个调度组的 CPU 位图.dst_grpmask    = sched_group_span(sd->groups),.idle		= idle,// 本次最多迁移 32 个进程(sched_nr_migrate_break全局变量的默认值为 32).loop_break	= sched_nr_migrate_break,// load_balance_mask 位图.cpus		= cpus,.fbq_type	= all,.tasks		= LIST_HEAD_INIT(env.tasks),};// 把 sd 调度域管辖的 CPU 位图复制到 load_balance_mask 位图里cpumask_and(cpus, sched_domain_span(sd), cpu_active_mask);schedstat_inc(sd->lb_count[idle]);redo:// should_we_balance() 函数判断当前 CPU 是否需要做负载均衡if (!should_we_balance(&env)) {*continue_balancing = 0;goto out_balanced;}// 查找该调度域中最繁忙的调度组group = find_busiest_group(&env);if (!group) {schedstat_inc(sd->lb_nobusyg[idle]);goto out_balanced;}// 在刚才找到的最繁忙的调度组中查找最繁忙的就绪队列busiest = find_busiest_queue(&env, group);if (!busiest) {schedstat_inc(sd->lb_nobusyq[idle]);goto out_balanced;}...// env.src_cpu 指向最繁忙的调度组中最繁忙的就绪队列中的 CPUenv.src_cpu = busiest->cpu;// env.src_rq 指向最繁忙的就绪队列env.src_rq = busiest;ld_moved = 0;if (busiest->nr_running > 1) {...// detach_tasks() 函数遍历最繁忙的就绪队列中所有的进程,找出适合被迁移的进程,然后让这些// 进程退出就绪队列。cur_ld_moved 变量表示已经迁出了多少个进程cur_ld_moved = detach_tasks(&env);...if (cur_ld_moved) {// attach_tasks() 函数把刚才从最繁忙就绪队列中迁出的进程都迁入当前 CPU 的就绪队列中attach_tasks(&env);ld_moved += cur_ld_moved;}...// 由于进程设置了 CPU 亲和性,不能迁移进程到 dst_cpu,因此还有负载没迁移完成,需要换一个新的// dst_cpu 继续做负载迁移if ((env.flags & LBF_DST_PINNED) && env.imbalance > 0) {...goto more_balance;}...}...
}

load_balance()->should_we_balance()

// 判断当前 CPU 是否需要做负载均衡
static int should_we_balance(struct lb_env *env)
{// sg 指向调度域中的第一个调度组struct sched_group *sg = env->sd->groups;...// 查找当前调度组是否有空闲 CPU,如果有空闲 CPU,那么使用变量 balance_cpu 记录该 CPUfor_each_cpu_and(cpu, group_balance_mask(sg), env->cpus) {if (!idle_cpu(cpu))continue;balance_cpu = cpu;break;}// 查找该调度组有哪些 CPU 适合做负载均衡// 	若调度组里有空闲 CPU,则优先选择空闲 CPU// 	调度组里没有空闲 CPU,则选择调度组的第一个 CPUif (balance_cpu == -1)balance_cpu = group_balance_cpu(sg);// 然后判断适合做负载均衡的 CPU 是否为当前 CPU,若为当前 CPU,则 should_we_balance()函数返回true,表示可以做负载均衡return balance_cpu == env->dst_cpu;
}

load_balance()->find_busiest_group()

// 查找该调度域中最繁忙的调度组
static struct sched_group *find_busiest_group(struct lb_env *env)
{...// 初始化 sd_lb_stats 结构体init_sd_lb_stats(&sds);// 更新该调度域中负载的相关统计信息update_sd_lb_stats(env, &sds);...// 如果没有找到最繁忙的调度组或者最繁忙的组中没有正在运行的进程,那么跳过该调度域if (!sds.busiest || busiest->sum_nr_running == 0)goto out_balanced;// 计算该调度域的平均负载sds.avg_load = (SCHED_CAPACITY_SCALE * sds.total_load)/ sds.total_capacity;// 如果最繁忙的调度组的组类型是 group_imbalanced,那么跳转到 force_balance 标签处if (busiest->group_type == group_imbalanced)goto force_balance;...// 若本地调度组的平均负载比刚才计算出来的最繁忙调度组的平均负载还要大,那么不需要做负载均衡if (local->avg_load >= busiest->avg_load)goto out_balanced;// 若本地调度组的平均负载比整个调度域的平均负载还要大,那么不需要做负载均衡if (local->avg_load >= sds.avg_load)goto out_balanced;// 如果当前 CPU 处于空闲状态,最繁忙的调度组里的空闲 CPU 数量大于本地调度组里的空闲 CPU 数量,说明不需要做负载均衡if (env->idle == CPU_IDLE) {if ((busiest->group_type != group_overloaded) &&(local->idle_cpus <= (busiest->idle_cpus + 1)))goto out_balanced;// 如果当前 CPU 不处于空闲状态,那么比较本地调度组的平均量化负载和最繁忙调度组的平均量化负载,这里使用了 imbalance_pct// 系数,它在 sd_init() 函数中初始化,默认值为125。若本地调度组的平均量化负载大于最繁忙组的平均量化负载,说明该调度组不// 忙,不需要做负载均衡。} else {if (100 * busiest->avg_load <=env->sd->imbalance_pct * local->avg_load)goto out_balanced;}force_balance:env->src_grp_type = busiest->group_type;// 根据最繁忙调度组的平均量化负载、调度域的平均量化负载和本地调度组的平均量化负载来计算该调度域需要迁移的负载不均衡值calculate_imbalance(env, &sds);// 返回最忙的调度组return env->imbalance ? sds.busiest : NULL;out_balanced:env->imbalance = 0;return NULL;
}

load_balance()->find_busiest_queue()

  1. for_each_cpu_and()遍历该调度组里所有的CPU
  2. 通过就绪队列的量化负载(runnable_load_avg)与CFS额定算力(cpu_capacity)的乘积来进行比较。使用weighted_cpuload()来获取cfs_rq->avg.runnable_load_avg的值,通过capacity_of()来获取cpu_capacity的值。这里要考虑不同处理器架构计算能力的不同对处理器负载的影响。

注意,数据结构rq中的cpu_capacity_orig成员指的是CPU的额定算力,而cpu_capacity成员指的是CFS就绪队列的额定算力,通常CFS额定算力最大能达到CPU额定算力的80%。

load_balance()->detach_tasks()

// detach_tasks() 函数遍历最繁忙的就绪队列中所有的进程,找出适合被迁移的进程,
// 然后让这些进程退出就绪队列。返回值表示已经迁出了多少个进程
static int detach_tasks(struct lb_env *env)
{...// env->imbalance 表示还有多少不均衡的负载需要迁移if (env->imbalance <= 0)return 0;// while 循环遍历最繁忙的就绪队列中所有的进程while (!list_empty(tasks)) {...// 在can_migrate_task()函数中判断哪些进程可以迁移,哪些进程不能迁移。// 不适合迁移的原因://	一是进程允许运行的CPU位图的限制(cpus_allowed)//	二是当前进程正在运行//	三是热高速缓存(cache-hot)的问题//	四如果进程负载的一半大于要迁移负载总量(env->imbalance),则该进程也不适合迁移if (!can_migrate_task(p, env))goto next;load = task_h_load(p);if (sched_feat(LB_MIN) && load < 16 && !env->sd->nr_balance_failed)goto next;if ((load / 2) > env->imbalance)goto next;// detach_task()函数让进程退出就绪队列,然后设置进程的运行CPU为迁移目的地CPU,并设置p->on_rq为// TASK_ON_RQ_MIGRARINGdetach_task(p, env);// 把要迁移的进程添加到env->tasks链表中list_add(&p->se.group_node, &env->tasks);detached++;// 递减不均衡负载总量 env->imbalanceenv->imbalance -= load;...}return detached;
}

load_balance()->attach_tasks()

// 把detach_tasks()分离出来的进程,重新添加到目标就绪队列中
static void attach_tasks(struct lb_env *env)
{...// 遍历env->tasks链表while (!list_empty(tasks)) {...// 把进程添加到目标CPU的就绪队列中attach_task(env->dst_rq, p);}...
}

这篇关于Linux进程管理:(六)SMP负载均衡的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/822094

相关文章

Linux磁盘分区、格式化和挂载方式

《Linux磁盘分区、格式化和挂载方式》本文详细介绍了Linux系统中磁盘分区、格式化和挂载的基本操作步骤和命令,包括MBR和GPT分区表的区别、fdisk和gdisk命令的使用、常见的文件系统格式以... 目录一、磁盘分区表分类二、fdisk命令创建分区1、交互式的命令2、分区主分区3、创建扩展分区,然后

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资

SpringBoot使用minio进行文件管理的流程步骤

《SpringBoot使用minio进行文件管理的流程步骤》MinIO是一个高性能的对象存储系统,兼容AmazonS3API,该软件设计用于处理非结构化数据,如图片、视频、日志文件以及备份数据等,本文... 目录一、拉取minio镜像二、创建配置文件和上传文件的目录三、启动容器四、浏览器登录 minio五、

Linux限制ip访问的解决方案

《Linux限制ip访问的解决方案》为了修复安全扫描中发现的漏洞,我们需要对某些服务设置访问限制,具体来说,就是要确保只有指定的内部IP地址能够访问这些服务,所以本文给大家介绍了Linux限制ip访问... 目录背景:解决方案:使用Firewalld防火墙规则验证方法深度了解防火墙逻辑应用场景与扩展背景:

IDEA中的Kafka管理神器详解

《IDEA中的Kafka管理神器详解》这款基于IDEA插件实现的Kafka管理工具,能够在本地IDE环境中直接运行,简化了设置流程,为开发者提供了更加紧密集成、高效且直观的Kafka操作体验... 目录免安装:IDEA中的Kafka管理神器!简介安装必要的插件创建 Kafka 连接第一步:创建连接第二步:选

Linux下MySQL8.0.26安装教程

《Linux下MySQL8.0.26安装教程》文章详细介绍了如何在Linux系统上安装和配置MySQL,包括下载、解压、安装依赖、启动服务、获取默认密码、设置密码、支持远程登录以及创建表,感兴趣的朋友... 目录1.找到官网下载位置1.访问mysql存档2.下载社区版3.百度网盘中2.linux安装配置1.

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa