struct sched_ext_ops代表了一个调度器,结构体中定义了很多回调函数,本文分析init_task这个hook,sched ext的部分hook如下:

不管task有没有被sched ext接管,都会触发init_task,分两种场景:

1)注册sched ext调度器前,已存在的task

scx_tasks是个全局变量,不管有没没有启动sched ext调度器,这个全局变量都是存在的。在fork 时,新task被加入到scx_tasks表示的链表中。

注册sched ext调度器时,遍历scx_tasks链表,对链表中的task执行init_task回调。

2)注册sched ext调度器后,新创建的task

注册sched ext调度器时,scx_ops_init_task_enabled标记设置为true,fork task时,如果该标记为true,立即执行init_task回调。

一、已存在的task,触发init_task的调用栈

注册sched ext调度器前,已存在的task触发init_task稍微有点复杂,分2个步骤,以下讨论与sched ext调度器是否接管task无关,所以不区分全量接管、部分接管模式。

首先,fork task时,task加入到scx_tasks链表中,见fork调用链的蓝色部分。

接着,注册调度器时,遍历scx_tasks链表,对链表中的每个task执行init_task。

eBPF用户态程序通过执行SCX_OPS_ATTACH,注册sched ext调度器,把“已经通过 verifier 的 struct sched_ext_ops ”注册到内核中,struct sched_ext_ops(见最文章开头图片)代表sched ext调度器。

SCX_OPS_ATTACH(…,numa_aware_ops,  …) ← numa_aware_ops是sched ext调度器
   ├─  bpf_map__attach_struct_ops
    |      ├─ bpf_link_create
    |       |      └─ BPF_CALL_3(bpf_sys_bpf, int, cmd, union bpf_attr *, attr, u32, attr_size)
                                                              ↓  (进入内核态)
bpf_sys_bpf
 ├─  __sys_bpf(BPF_LINK_CREATE,…)
  |         ├─  link_create
  |          |      ├─  bpf_struct_ops_link_create
  |          |       |      └─ st_map->st_ops_desc->st_ops->reg(st_map->kvalue.data, &link->link)
                                               ↓  ( 即 bpf_scx_reg )
bpf_scx_reg
 ├─  scx_ops_enable
  |     ├─ scx_ops_bypass(true)
  |     ├─ scx_ops_init_task_enabled = true
  |      |
  |      |-------  scx_task_iter_start(&sti);
  |      |代     while ((p = scx_task_iter_next_locked(&sti))) {
  |      |码          …
  |      |段         scx_ops_init_task  ← 对scx_tasks中的每个task执行init_task
  |      | ------   }
  |      |
  |      | -------  scx_task_iter_start(&sti);
  |      |   代     while ((p = scx_task_iter_next_locked(&sti))) {
  |      |   码          …
  |      |   段          __setscheduler_class  ←  全量接管模式fair_sched_class的task,返回ext class
  |      |                check_class_changing  ← 对scx_tasks中的每个task执行switching_to_scx
  |      | ------   }    └─   p->sched_class->switching_to   ← 即 switching_to_scx
  |      |                         └─  scx_ops_enable_task
  |      |                                     └─ SCX_CALL_OP_TASK(SCX_KF_REST, enable, rq, p)
  |     ├─ scx_ops_bypass(false)

二、新task,触发init_task的调用栈

SYSCALL_DEFINE0(fork)
  ├─ kernel_clone
   |   ├─ copy_process()
   |    |    ├─ dup_task_struct
   |    |     |    ├─  arch_dup_task_struct
   |    |     |     |    └─ memcpy(dst, src, arch_task_struct_size) ← 继承父task名
   |    |     |     | 
   |    |    ├─  strscpy_pad(p->comm, args->name, sizeof(p->comm)) ← 内核线程设置task
   |    |    ├─  sched_fork()
   |    |     |     └─ scx_pre_fork()
   |    |    ├─  ...
   |    |     |
   |    |    ├─  sched_cgroup_fork()
   |    |     |    ├─  设置 p->sched_task_group   ← 设置cgroup
   |    |     |    ├─  __set_task_cpu()
   |    |     |    ├─  p->sched_class->task_fork() ← sched ext没有实现这个hook
   |    |     |    └─ scx_fork()
   |    |     |           └─ scx_ops_init_task() ← scx_ops_init_task_enabled为true时,有条件触发
   |    |     |                   └─ BPF_STRUCT_OPS(init_task)  
   
|    |    ├─  sched_post_fork
   |    |     |         └─  scx_post_fork
   |    |     |                └─  list_add_tail(&p->scx.tasks_node, &scx_tasks)
   |   ├─  wake_up_new_task  

注册sched ext后,新创建的task都会触发init_task的调用链(注意,即使调度策略不是sched ext也会触发),见fork栈红字部分。

执行到init_task这个钩子函数时,用户态task有可能还是父task名称(取决于有没有pctl或者execv设置了task名),内核态task是自己真实名称。

另外,需注意wake_up_new_task的时间点。

三、init_task中current task的名称是什么

bpf_get_current_task / bpf_get_current_comm获取到的进程名称分两种情况:

1)如果task是内核态的,获取到的是task自己真实的名称

2)如果task是用户态的,只有执行execve或者prctl设置名称后,task的名字才会更新成自己真实的名称。

用户态task被fork后,task继承父进程名称,见(一)调用栈。

exec修改用户态task名称调用栈如下:

SYSCALL_DEFINE3(execve,…
├─ do_execve
 |     └─  do_execveat_common
 |             ├─  alloc_bprm 
 |             ├─  bprm_execve
 |              |     ├─ exec_binprm
                      ├─ search_binary_handler
                       |     ├─ fmt->load_binary(bprm)  ← 即 load_elf_binary
                              |     ├─  begin_new_exec
                              |      |      └─ __set_task_comm 

四、init_task与其他hook的时序

1,sched ext调度器加载完成后,对于新创建的task,由于fork流程先执行init_task,然后才执行wake_up_new_task,所以一定是先init_task,再enqueue

2,sched ext调度器加载过程中,即正在执行scx_ops_enable函数,根据(二)中bpf_scx_reg调用栈,先执行scx_ops_bypass(true),接着遍历task并设置成sched_ext_class后,最后执行scx_ops_bypass(false)。在bypass为true期间,sched ext框架是不会执行struct sched_ext_ops -> enqueue。

enqueue_task_scx
├─  SCX_CALL_OP_TASK(SCX_KF_REST, runnable, rq, p, enq_flags)
└─  do_enqueue_task
 |      ├─  if (scx_rq_bypassing(rq))   ← scx_ops_enable没有完全加载调度器,不会执行enqueue
 |       |          goto global
 |       |
 |      └─  SCX_CALL_OP_TASK(SCX_KF_ENQUEUE, enqueue, rq, p, enq_flags)

总结,按时序,inti_task  ->  enqueue

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐