Linux内核源码分析-进程调度(三)-从进程创建到唤醒的过程去了解CFS调度器

news/2024/12/3 0:41:38/

进程的创建是通过do_fork函数。新进程的诞生,我们调度核心层会通知调度类来初始化新进程。
我们一路顺着do_fork函数往下看:do_fork()->_do_fork()->copy_process()->sched_fork()。

long do_fork(unsigned long clone_flags, // 创建进程的标志位集合unsigned long stack_start,  // 用户态栈的起始地址unsigned long stack_size,  // 用户态栈的大小,一般情况下设置位0int __user *parent_tidptr,  //  指向用户空间中地址的指针, 指向父进程的PIDint __user *child_tidptr)  //  指向用户空间中地址的指针, 指向子进程的PID 
{struct kernel_clone_args args = {.flags		= (clone_flags & ~CSIGNAL),.pidfd		= parent_tidptr,.child_tid	= child_tidptr,.parent_tid	= parent_tidptr,.exit_signal	= (clone_flags & CSIGNAL),.stack		= stack_start,.stack_size	= stack_size,};if (!legacy_clone_args_valid(&args))return -EINVAL;return _do_fork(&args);
}
long _do_fork(struct kernel_clone_args *args)
{u64 clone_flags = args->flags;struct completion vfork;struct pid *pid;struct task_struct *p;int trace = 0;long nr;if (!(clone_flags & CLONE_UNTRACED)) {if (clone_flags & CLONE_VFORK)trace = PTRACE_EVENT_VFORK;else if (args->exit_signal != SIGCHLD)trace = PTRACE_EVENT_CLONE;elsetrace = PTRACE_EVENT_FORK;if (likely(!ptrace_event_enabled(current, trace)))trace = 0;}p = copy_process(NULL, trace, NUMA_NO_NODE, args);  // 创建子进程wake_up_new_task(p);  // 唤醒新创建的进程,把进程加入就绪队列里等待调度。
}

创建子进程

static __latent_entropy struct task_struct *copy_process(struct pid *pid,int trace,int node,struct kernel_clone_args *args)
{p = dup_task_struct(current, node);  // 复制当前进程(父进程)描述符task_struct、创建内核堆栈等u64 clone_flags = args->flags;/** 初始化新建进程p相关的调度参数、设置进程p状态为TASK_NEW、优先级、调度策略、权重、调度器,并分配CPU资源。*/int retval = sched_fork(clone_flags, p);
}

初始化新建进程p相关的调度参数

针对sched_fork()函数如下:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{unsigned long flags;__sched_fork(clone_flags, p);  // 初始化p中与调度相关的参数。/** 标记进程为Running状态,用于保证实际上并没有运行,信号或许其他外部事件都无法唤醒该进程,同时把它插入运行队列。*/p->state = TASK_NEW;/** Make sure we do not leak PI boosting priority to the child.*/p->prio = current->normal_prio;  // 因为父进程的动态优先级可能会被临时提升,因此新进程的优先级需要从父进程的normal_prio中继承,而不能从其动态优先级prio中继承。/** Revert to default priority/policy on fork if requested.*/if (unlikely(p->sched_reset_on_fork)) {if (task_has_dl_policy(p) || task_has_rt_policy(p)) {p->policy = SCHED_NORMAL;p->static_prio = NICE_TO_PRIO(0);p->rt_priority = 0;} else if (PRIO_TO_NICE(p->static_prio) < 0)p->static_prio = NICE_TO_PRIO(0);p->prio = p->normal_prio = __normal_prio(p);set_load_weight(p, false);/** We don't need the reset flag anymore after the fork. It has* fulfilled its duty:*/p->sched_reset_on_fork = 0;}if (dl_prio(p->prio))return -EAGAIN;else if (rt_prio(p->prio))p->sched_class = &rt_sched_class;  // 为进程p选择实时调度类elsep->sched_class = &fair_sched_class;  // 为进程p选择CFS调度类init_entity_runnable_average(&p->se);  // 设置该进程对应调度实体的初始负载__set_task_cpu(p, smp_processor_id());  // 为进程p分配相应CPU,这个值在该进程加入就绪队列前可能会根据实际需求更改,故此处只是简单地将其初始化为与父进程相同。if (p->sched_class->task_fork)p->sched_class->task_fork(p);  // 调用调度类中的task_fork函数。task_fork方法主要做fork相关操作。传递的参数p就是创建的task_struct。
}

cfs的task_fork操作

CFS调度类fair_sched_class方法如下:

const struct sched_class fair_sched_class = {.task_fork		= task_fork_fair,
}

task_fork_fair实现如下:

static void task_fork_fair(struct task_struct *p)
{struct cfs_rq *cfs_rq;struct sched_entity *se = &p->se, *curr;struct rq *rq = this_rq();struct rq_flags rf;rq_lock(rq, &rf);update_rq_clock(rq);cfs_rq = task_cfs_rq(current);  // 获取当前进程所在的per-cpu rq中的cfs_rq。curr = cfs_rq->curr;  // curr指向当前正在cpu上运行的task的调度实体。if (curr) {update_curr(cfs_rq);  // 更新当前正在运行的调度实体的运行时间信息(上次运行(启动)开始时间、总的实际运行时间、虚拟运行时间等等),并更新cfs_rq的最小虚拟运行时间。se->vruntime = curr->vruntime;  // 初始化当前创建的新进程p对应的se的虚拟运行时间。}/** place_entity()函数在进程创建以及唤醒的时候都会调用,创建进程的时候传递参数initial=1。* 此处的目的是更新进程p对应的调度实体se的虚拟时间(se->vruntime),使得se->vruntime和cfs_rq->min_vruntime的值保持差别不大,如果非常小的话,就会导致创建的新进程一直占用cpu。*/place_entity(cfs_rq, se, 1);if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {/** Upon rescheduling, sched_class::put_prev_task() will place* 'current' within the tree based on its new key value.*/swap(curr->vruntime, se->vruntime);resched_curr(rq);}/** 这里为什么要减去cfs_rq->min_vruntime?因为现在计算进程的vruntime是基于当前cpu上的cfs_rq,并且现在还没有加入当前cfs_rq的就绪队列上。* 等到当前进程创建完毕开始唤醒的时候,加入的就绪队列就不一定是现在计算基于的cpu。* 因此非就绪态或者非运行态时虚拟运行时间这个字段保存的其实是一个从生命周期开始的一个虚拟运行时间,是与cpu无关的。* 所以,在加入就绪队列的函数中会根据情况加上当前就绪队列cfs_rq->min_vruntime。为什么要“先加后减”?* 假设cpu0上的cfs就绪队列的最小虚拟运行时间min_vruntime的值是1000000,此时创建进程的时候赋予当前进程虚拟时间是1000500。* 但是唤醒此进程,并让进程加入的就绪队列是CFS就绪队列,cpu1上的cfs就绪队列的最小虚拟运行时间min_vruntime的值如果是9000000。* 如果不采用“先减后加”的方法,那么该进程就在cpu上长时间运行。现在的处理计算得到的调度实体的虚拟运行时间是1000500-1000000+9000000 = 9000500。*/se->vruntime -= cfs_rq->min_vruntime; rq_unlock(rq, &rf);
}

更新cfs_rq上正在运行的进程的运行时间信息

我们来对上面出现的update_curr函数来深挖一把。

/** 此函数的作用是更新cfs_rq中正在cpu上运行的调度实体的运行时间信息(上次运行(启动)开始时间、总的实际运行时间、虚拟运行时间等等)。* 所有与虚拟时钟有关的计算都在update_curr中执行,该函数在系统中各个不同地方调用,包括周期性调度器在内。* update_curr的流程如下:*   1、首先计算当前时间与当前正在运行进程的上次启动时间的差值*   2、通过负荷权重和当前系统时间计算出进程的虚拟运行时间*   3、重新设置cfs的min_vruntime保持其单调性*/
static void update_curr(struct cfs_rq *cfs_rq)
{struct sched_entity *curr = cfs_rq->curr;  // 获取就绪队列的当前执行进程/** rq_of:用于确定CFS就绪队列所属的struct rq实例。csf_rq就绪队列中存储了指向就绪队列的实参,而rq_of就返回了这个指向rq的指针。* rq_of => return cfs_rq->rq 返回cfs队列所属的全局就绪队列  */u64 now = rq_clock_task(rq_of(cfs_rq)); // 返回了rq的clock_tasku64 delta_exec;if (unlikely(!curr))  //  如果队列上没有进程在执行,即没事可干就返回呗。return;delta_exec = now - curr->exec_start;  //  计算出当前运行的进程距离上次运行并更新虚拟运行时间的差值,更本质的说法是:内核计算当前运行进程和此进程上一次更新负荷权重时两次的时间的差值。if (unlikely((s64)delta_exec <= 0))return;curr->exec_start = now;  // 重新更新启动时间为now,以备下次计算时使用。schedstat_set(curr->statistics.exec_max,max(delta_exec, curr->statistics.exec_max));curr->sum_exec_runtime += delta_exec;  // 更新当前进程总共执行的时间,即将计算出来的时间差加到先前的统计时间上。schedstat_add(cfs_rq->exec_clock, delta_exec);/** calc_delta_fair的运行依据公式为:delta = delta * (NICE_0_LOAD / curr->vruntime->load.weight)*/curr->vruntime += calc_delta_fair(delta_exec, curr);  // 更新当前调度实体的虚拟运行时间。/** 更新CFS就绪队列的最小虚拟运行时间min_vruntime。min_vruntime也是不断更新的,主要就是跟踪就绪队列中所有调度实体的最小虚拟运行时间。* 如果min_vruntime一直不更新的话,由于min_vruntime太小,导致后面创建的新进程根据这个值来初始化新进程的虚拟运行时间,这样新进程又会一个个的霸占着cpu。*/update_min_vruntime(cfs_rq);if (entity_is_task(curr)) {struct task_struct *curtask = task_of(curr);trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);cgroup_account_cputime(curtask, delta_exec);account_group_exec_runtime(curtask, delta_exec);}account_cfs_rq_runtime(cfs_rq, delta_exec);
}

更新cfs_rq的最小虚拟运行时间

calc_delta_fair函数我们在前面的文章中已经说过不再赘述,我们主要来看一下update_min_vruntime的实现:

/** 既然要更新就绪队列的最小虚拟运行时间min_vruntime,试想一下,拥有最小虚拟运行时间的地方有哪儿?*   1、就绪队列本身的cfs_rq->min_vruntime成员。*   2、当前正在运行的进程的虚拟运行时间,因为CFS调度器选择最适合运行的进程是选择维护的红黑树中虚拟运行时间最小的进程。*   3、如果在当前进程运行的过程中,有进程加入就绪队列,那么红黑树最左边的进程的虚拟运行时间同样也有可能是最小的虚拟运行时间。* 因此此函数从这三方面来入手,确保就绪队列的最小虚拟运行时间得到更新。*/
static void update_min_vruntime(struct cfs_rq *cfs_rq)  // 更新cfs就绪队列的最小虚拟运行时间min_vruntime
{struct sched_entity *curr = cfs_rq->curr;struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline);u64 vruntime = cfs_rq->min_vruntime;  // 首先用队列cfs_rq的虚拟运行时间初始化vruntime变量。// 其次当正在运行进程存在时用当前正在运行的进程的虚拟运行时间去更新vruntime变量。if (curr) {if (curr->on_rq)  // 如果当前调度实体在就绪队列中,则vruntime设置为当前调度实体的虚拟运行时间。vruntime = curr->vruntime;elsecurr = NULL;}if (leftmost) { /* non-empty tree */  // 如果在当前进程运行的过程中,有进程加入就绪队列,那么红黑树最左边的进程的虚拟运行时间同样也有可能是最小的虚拟运行时间。struct sched_entity *se;se = rb_entry(leftmost, struct sched_entity, run_node);  // 取红黑数最左边的进程(调度实体)。// 如果当前无运行中进程,则vruntime更新为红黑数最左边进程的虚拟运行时间(即整个红黑树中最小的时间),否则和前面得到的vruntime值相比取较小值。if (!curr)  vruntime = se->vruntime;elsevruntime = min_vruntime(vruntime, se->vruntime);}/* ensure we never gain time by being placed backwards. */cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);  // 就绪队列本身的最小虚拟运行时间与vruntime相比,取较小值。
#ifndef CONFIG_64BITsmp_wmb();cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime;
#endif
}

更新进程p对应的调度实体se的虚拟时间

update_curr函数我们基本已经了解清楚,接下来看看place_entity函数:

/** 在进程创建以及唤醒的时候都会调用,创建进程的时候传递参数initial=1,唤醒进程的时候传递参数initial=0* 目的是更新进程调度实体se的虚拟时间(se->vruntime),使得se->vruntime和cfs_rq->min_vruntime的值保持差别不大,如果非常小的话,就会导致创建的新进程一直占用cpu。*/
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{u64 vruntime = cfs_rq->min_vruntime;/** The 'current' period is already promised to the current tasks,* however the extra weight of the new task will slow them down a* little, place the new task so that it fits in the slot that* stays open at the end.*/	if (initial && sched_feat(START_DEBIT))vruntime += sched_vslice(cfs_rq, se);  // 针对刚创建的进程会进行一定的惩罚,将虚拟运行时间加上一个值就是惩罚,毕竟虚拟运行时间越小越容易得到调度执行。惩罚的时间由sched_vslice()计算。/* sleeps up to a single latency don't count. */if (!initial) {unsigned long thresh = sysctl_sched_latency;/** Halve their sleep time's effect, to allow* for a gentler effect of sleepers:*/if (sched_feat(GENTLE_FAIR_SLEEPERS))thresh >>= 1;vruntime -= thresh;  // 这里主要是针对唤醒的进程,针对睡眠很久的进程,我们总是期望它很快得到调度执行,所以会将虚拟运行时间减去一个值。}/* ensure we never gain time by being placed backwards. *//** 我们保证调度实体的虚拟运行时间不能倒退。为何呢?* 可以想一下,如果一个进程刚睡眠了1ms,然后醒来你却要奖励3ms(虚拟运行时间减去3ms),然后他竟然赚了2ms。* 作为一个公平的调度器,不做这种傻逼事情。你睡眠100ms,奖励你3ms没问题。*/se->vruntime = max_vruntime(se->vruntime, vruntime);
}

新创建的进程惩罚的时间是多少

在update_curr函数中,我们可以看到惩罚的时间计算函数是sched_vslice()函数,即将sched_slice(cfs_rq, se)算出来的实际运行时间转换为虚拟运行时间。

/** We calculate the vruntime slice of a to-be-inserted task.** vs = s/w*/
static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{return calc_delta_fair(sched_slice(cfs_rq, se), se);
}

calc_delta_fair函数我们之前介绍过,运行依据公式为:delta = delta * (NICE_0_LOAD / se->load.weight),此处就不过多赘述。

实际运行时间的计算

delta值由sched_slice函数计算得到:

/** 计算se的实际运行时间(wall-time)。*/
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);  // 根据就绪队列中调度实体个数计算调度周期。for_each_sched_entity(se) {  // 根据se->paren往上计算权重比例。struct load_weight *load;struct load_weight lw;cfs_rq = cfs_rq_of(se);load = &cfs_rq->load;  // 得到当前se所在的就绪队列的权重,也就是就绪队列上所有调度实体权重之和。if (unlikely(!se->on_rq)) {lw = cfs_rq->load;update_load_add(&lw, se->load.weight);load = &lw;}/* * 计算调度时间,slice = period * se->load.weight / cfs_rq->load.weight*/slice = __calc_delta(slice, se->load.weight, load);}return slice;
}

__calc_delta的详细介绍见前文。

唤醒新创建的子进程

经过copy_process创建了子进程之后,我们就可以唤醒新进程准备运行。也就是将新进程加入就绪队列准备调度。
wake_up_new_task函数负责唤醒新创建的进程,并将进程加入就绪队列里等待调度。

/** wake_up_new_task - wake up a newly created task for the first time.** This function will do some initial scheduler statistics housekeeping* that must be done for every newly created context, then puts the task* on the runqueue and wakes it.*/
void wake_up_new_task(struct task_struct *p)
{struct rq_flags rf;struct rq *rq;raw_spin_lock_irqsave(&p->pi_lock, rf.flags);p->state = TASK_RUNNING;
#ifdef CONFIG_SMPp->recent_used_cpu = task_cpu(p);__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));  // 选择调度类中最空闲的CPU
#endifrq = __task_rq_lock(p, &rf);update_rq_clock(rq);post_init_entity_util_avg(p);activate_task(rq, p, ENQUEUE_NOCLOCK);  // 把进程加入到相应的就绪队列trace_sched_wakeup_new(p);check_preempt_curr(rq, p, WF_FORK);  // 既然新进程已经准备就绪,那么此时需要检查新进程是否满足抢占当前正在运行进程的条件,如果满足则设置TIF_NEED_RESCHED标志位。
}

将新进程加入到对应的就绪队列

activate_task(rq, p, ENQUEUE_NOCLOCK)这一步将进程p加入到就绪队列rq中:

void activate_task(struct rq *rq, struct task_struct *p, int flags)
{enqueue_task(rq, p, flags);
}
static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{p->sched_class->enqueue_task(rq, p, flags);
}

CFS的入队函数如下:

const struct sched_class fair_sched_class = {.enqueue_task		= enqueue_task_fair,  // 当任务进入可运行状态时,用此函数将调度实体存放在红黑树中。(linux对于可运行状态定义比较模糊,可运行状态包括就绪态和运行态)
}

检测新进程是否可以抢占正在运行的进程

当唤醒一个新进程的时候,此时也是一个检测抢占的机会。因为唤醒的进程有可能具有更高的优先级或者更小的虚拟时间。

/** 用于在待运行任务插入rq后,检查是否应该抢占正在CPU上运行的当前任务。* wakeup preemption(唤醒抢占)的实现逻辑主要在这里。对CFS调度器而言,主要是在“是否能满足延迟调度时延”和“是否保证足够任务运行时间”之间来取舍。
*/
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{const struct sched_class *class;if (p->sched_class == rq->curr->sched_class) {  // 如果p的调度类和cpu上正在运行的进程属于同一个调度类rq->curr->sched_class->check_preempt_curr(rq, p, flags);  // 则调用该调度类的check_preempt_curr方法检查抢占条件,来决定是否抢占。} else {for_each_class(class) {  // 从高优先级调度类开始轮循if (class == rq->curr->sched_class)break;if (class == p->sched_class) {  // 因为是从高优先级调度类开始往低优先级调度类轮循,// 所以当进程p的优先级先满足的时候,即p的优先级高于当前正在cpu上运行的进程的优先级。即可发生抢占动作。resched_curr(rq);  // break;}}}
}

CFS调度类的抢占

我们现在来看一下同为CFS调度类的情况下,如何进行抢占条件检查。

const struct sched_class fair_sched_class = {.check_preempt_curr	= check_preempt_wakeup,  // 检测是否可以抢占当前正在cpu上运行的进程。
}

从上面代码可以看出,CFS调度类的抢占条件检测为check_preempt_wakeup函数。

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{struct task_struct *curr = rq->curr;struct sched_entity *se = &curr->se, *pse = &p->se;struct cfs_rq *cfs_rq = task_cfs_rq(curr);int scale = cfs_rq->nr_running >= sched_nr_latency;int next_buddy_marked = 0;if (unlikely(se == pse))  // 如果正在运行的调度实体和想要去抢占的进程p的调度实体是同一个,则是大水冲了龙王庙,直接返回。return;if (test_tsk_need_resched(curr))  // 当前正在运行的进程已经被设置了重新调度标志,则直接返回不进行抢占处理。return;/* Idle tasks are by definition preempted by non-idle tasks. */if (unlikely(task_has_idle_policy(curr)) &&  // 正在运行的进程是idle进程,而唤醒的进程不是idle进程,进行抢占。likely(!task_has_idle_policy(p)))goto preempt;find_matching_se(&se, &pse);update_curr(cfs_rq_of(se));  // 更新当前进程的时间统计BUG_ON(!pse);if (wakeup_preempt_entity(se, pse) == 1) {  // 检查唤醒的进程是否满足抢占当前进程的条件,如果满足就触发调度。/** Bias pick_next to pick the sched entity that is* triggering this preemption.*/if (!next_buddy_marked)set_next_buddy(pse);goto preempt;}return;
preempt:resched_curr(rq);  // 抢占处理
}

在抢占条件检测函数中,wakeup_preempt_entity负责真正地根据虚拟运行时间计算是否满足抢占条件。

static int
wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
{s64 gran, vdiff = curr->vruntime - se->vruntime;if (vdiff <= 0)  // 当前正在调度的实体虚拟运行时间比抢占实体的虚拟运行时间小,则不发生抢占。return -1;gran = wakeup_gran(se);  // 根据sysctl_sched_wakeup_granularity与se的权重计算出抢占成功需要的最小虚拟运行时间差值。目的是为了避免频繁抢占。if (vdiff > gran)return 1;return 0;
}

http://www.ppmy.cn/news/64209.html

相关文章

八岁都能懂:O(N)条件下在N个元素中找出第K小的元素

目录 1 进入情境1-1 金字塔道具1-2 感觉还不够1-3 万能筛子1-4 怎么用呢 2 代码实现2-1 伪代码描述2-2 完整实例c 3 引申3-1 完美的折半舍弃3-2 找出前K小的元素(topK方法&#xff09;3-3 O(n)效率下求中位数参考资料 1 进入情境 生日&#xff0c;朋友送了一堆弹珠&#xff0c;…

API Design principle 一些API设计原则

API&#xff08;应用程序编程接口&#xff09;是一种规范&#xff0c;定义了不同软件组件之间如何进行交互。API 描述了一组操作、输入和输出&#xff0c;这些操作独立于实现&#xff0c;使得开发人员可以访问其他程序、库或框架的功能&#xff0c;而无需了解其底层实现细节。A…

【论文笔记】Attention和Visual Transformer

Attention和Visual Transformer Attention和Transformer为什么需要AttentionAttention机制Multi-head AttentionSelf Multi-head Attention&#xff0c;SMA TransformerVisual Transformer&#xff0c;ViT Attention和Transformer Attention机制在相当早的时间就已经被提出了&…

VUE 学习笔记(二)VUE的深入理解

一、VUE 简介 1.什么是VUE ? VUE 是一套用于构建用户界面的渐进式JavaScript框架 &#xff0c;对于简单应用&#xff0c;只需要轻量小巧的核心库&#xff0c;对于复杂的应用&#xff0c;可以引入各种VUE 插件。 模板引擎是 Vue 里最主要、最核心的一个能力&#xff0c;在模板引…

基于趋动云部署B站大V秋葉aaaki的Stable Diffusion整合包v4--linux版

B站大V秋葉aaaki的Stable Diffusion整合V4版发布了&#xff0c;集成度比较高&#xff0c;在windows下解压缩直接就可以使用&#xff0c;整合的非常好。但是笔人没有RTX4090这样级别的显卡&#xff0c;又希望有个高速运行的效果。 所以索性到云GPU主机上来用秋叶aaaki的Stable …

医学影像系统源码,三维后处理和重建 PACS源码

医学影像系统源码&#xff0c;三维后处理和重建 PACS源码 医学影像系统由PACS系统、RIS系统组成&#xff0c;提供与HIS的接口&#xff08;HL7或其他类型&#xff09;。 主要功能介绍 信息预约登记 支持对患者、检查项目、申请医生、申请单据、设备等信息进行管理。且支持检查…

这次彻底不需要账号了,无需魔法永久白嫖GPT

免费GPT 自GPT风靡以来&#xff0c;大家用的是不亦乐乎&#xff0c;你用他去解决过实际问题&#xff0c;你用他去写过代码&#xff0c;你用他去修改过bug&#xff0c;你用他去写过sql&#xff0c;你用他去画过图&#xff0c;你问过他你能想到的任何“刁钻”问题。 你&#xff…

【最小步数模型】魔板【map<string,int>记录字符串的 变化 路径】

【最小步数模型】魔板 1. 题意解析问题一&#xff1a;ABC操作 具体怎么实现&#xff1f; 2. 普通算法3. 优化算法 原题链接 1. 题意解析 如果魔板的 原序列为 12345678 那么对应到魔板上就是 1 2 3 4 8 7 6 5 然后给出3种变化 ABC 求 所给一个序列 怎么可以变到 基准序列 问…