linux中core调度器

背景

开始把core调度器当成了linux的主调度器,导致查找网上资料时总觉得对不上,最后从linux的rust文档中明白了,core调度器是为了解决超线程场景下缓存漏洞(如mds、L1HF)而存在的。简单来说就是一个cpu上同时运行两个线程时,线程1预加载到缓存的数据可以被线程2看到。

网上只找到了公开熔断(Meltdown)和幽灵(Spectre)漏洞的代码(可以参考这个:侧信道攻击实践 - 知乎),没找到mds和L1HF的POC代码,不太理解超线程的漏洞具体怎么实现,只能找到非超线程的漏洞原理。

这两种非超线程的漏洞原理是这样:都是刷掉所有缓存,再通过分支预测和缺页异常访问预先加载出一个字节非法地址的数据,将这个数据乘一个cacheline大小,作为地址访问,从而触发缓存把,以这字节数据为index位置的缓存行load上来,由于其它cache都刷掉了,访问速度肯定没这个快。所以只要访问合法的(0~256)*cache_line地址数据,统计触发每个cacheline访问时间中最快的那个index,就能知道最初的数据(cacheline的index)是多少,从而窃取一个字节的数据。

linux解决超线程缓存漏洞的方式是让同一个cpu的多个超线程,在同一时间段内只能跑一个trust组中的多个task。

实现

在支持超线程时,每个超线程存了一个smt_mask (cpu_sibling_map),标记与此超线程相同cpu的其它超线程编号,每个超线程有个runqueues,其中rq->core绑定了leader(同一cpu的第一个编号位置的)超线程的runqueue。

用户可以为几个线程指定相同的cookie值(sched_core_share_pid),从而将它们归为一个trust组,同一时间同一个cpu的超线程上只能跑一个trust组的线程。

core调度器的runqueue是以cookie值为key的红黑树,用于找相同cookie的task。

当同一trust组中task数比一个cpu的超线程数少时,会有一部分超线程强制置为idle状态,运行超线程数与idle超线程数分别用core_forceidle_occupation和core_forceidle_count表示,force idle的超线程可以偷取其它同cpu的force idle超线程的同一trust组的task(sched_core_balance)。

在一个调度周期内,每个超线程都可能触发调度pick 下一个task,但如果没有发生过新的enqueue或dequeue,则不需要重新pick,只需要第一个触发的人pick一次,然后每个超线程用它pick的结果就可以了。这需要维护三个sequence来实现:core->core_task_seq(开始选task时的seq),core->core_pick_seq(成功选出的task时的seq),core_sched_seq(当前超线程的seq)。当一次pick完成时,所有其它超线程在pick时,只需要core_sched_seq赶上leader的core_pick_seq即可,不用重选(有点像简化版本的Paxos协议,但不是多者赢而是先者赢)。

启动入口是 migration_init,要看完整细节可以从这个地方开始看,但与调度相关的函数最重要的是 pick_next_task,另外有sched_core_balance让force idle超线程做任务偷取,sched_core_tick做统计。

pick_next_task

代码为逻辑代码,非真实代码。linux版本:v6.6

pick_next_task():
  // 其它超线程已经pick过任务,在这个超线程上还没调度过
  if (rq->core->core_pick_seq == rq->core->core_task_seq &&
	    rq->core->core_pick_seq != rq->core_sched_seq &&
	    rq->core_pick) {
    put_prev_task(rq, prev);
    set_next_task(rq, next);
    goto out
  }
    
  // 从上一个task的级别调度类到最低级别调度类分别调一次balance的hook
  put_prev_task_balance();

  // rq->core指向leader的queue, 开始选 task
  rq->core->core_task_seq++;

  // 从同cpu所有超线程选出最高优先级的task
  for_each_cpu_wrap(smt_mask) {
    rq_i->core_pick = pick_task(rq_i);
    if (!max || prio_less(max, p, fi_before))
      max = p;
  }

  // 以这个最高优先级task为准,找出其它同cpu超线程上可以跑的同一trust组的task。
  for_each_cpu(i, smt_mask) {
    p = sched_core_find(rq_i, cookie);
    if (p) {
      rq->core->core_forceidle_occupation++;
    } else {
      p = idle_sched_class.pick_task(rq_i);
      rq->core->core_forceidle_count++;
    }
  }

  // 选task成功
  rq->core->core_pick_seq = rq->core->core_task_seq;
  // 标记当前超线程在本轮已经选过了task
  rq->core_sched_seq = rq->core->core_pick_seq;

  // 为有cfs类任务的超线程更新vruntime与forceidle_seq,
  // 并对需要切任务的超线程触发调度
  for_each_cpu(smt_mask) {
    task_vruntime_update(rq_i, rq_i->core_pick, !!rq->core->core_forceidle_count);
    resched_curr(rq_i);
  }

  // 如果有超线程被forceidle了,则在调度时尝试偷一个同cpu其它超线程的同trust组任务执行
  // 下一个任务与上一个一样,则在__schedule->__balance_callbacks处触发
  // 下一个任务与上一个不一样,则在切之后finish_task_switch时触发
 queue_core_balance();

你可能感兴趣的:(linux,linux)