Skip to main content
 首页 » 操作系统

Linux 调度器之杂项汇总

2022年07月19日28grandyang

一、获取绑核信息

1. 通过 /proc/<pid>/status 获取

# cat /proc/<pid>/status | grep Cpus_allowed 
Cpus_allowed:    ff 
Cpus_allowed_list:    0-7

调用路径和函数:

struct pid_entry tgid_base_stuff[] //fs/proc/base.c 
    ONE("status", S_IRUGO, proc_pid_status), 
struct pid_entry tid_base_stuff[] //fs/proc/base.c 
    ONE("status", S_IRUGO, proc_pid_status), 
        proc_pid_status //fs/proc/array.c 
            task_cpus_allowed //fs/proc/array.c 
 
static void task_cpus_allowed(struct seq_file *m, struct task_struct *task) 
{ 
    //取的是 task->cpus_ptr 而不是 task->cpus_mask 
    seq_printf(m, "Cpus_allowed:\t%*pb\n", cpumask_pr_args(task->cpus_ptr)); 
    seq_printf(m, "Cpus_allowed_list:\t%*pbl\n", cpumask_pr_args(task->cpus_ptr)); 
}

注:

(1) 这个cat出来的cpu会受到cgroup cpuset的限制,比如echo pid到只有cpu0-3的background分组的cpuset中时,Cpus_allowed_list就是0-3。

(2) cgroup 中 cpuset 的绑核优先级高于per-任务的绑核。比如将任务p taskset -p 7f pid 绑定到CPU0-6,但是一旦将任务echo到 top-app 分组中,任务的cpu亲和性又是0xff了。但是将任务先放到background分组中,此时cpu亲和性是0x0f,此时将任务 taskset -p 7f pid 绑定到CPU0-6,任务的cpu亲和还是0x0f。

2. 通过 sched_getaffinity() 系统调用获取

//系统调用 
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); 
 
//内核函数,其中 mask 为返回给用户空间的参数 
long sched_getaffinity(pid_t pid, struct cpumask *mask) 
{ 
    struct task_struct *p = find_process_by_pid(pid); 
 
    //这里使用的是 p->cpus_mask,而且是与 cpu_active_mask 相与后的结果 
    cpumask_and(mask, &p->cpus_mask, cpu_active_mask); 
 
}

3. 两种获取方式的差别

sched_getaffinity() 系统调用返回的是 p->cpus_mask & cpu_active_mask 的结果,注意与上后,此结果就受到offline和isolate cpu核的影响了。sched_setaffinity() 系统调用最终设置的是 p->cpus_mask。而 cat /proc/<pid>/status 获取的直接是 task->cpus_ptr 的值,不会受到影响。

4. p->cpus_ptr 指针和 p->cpus_mask 变量的区别

sched_setaffinity() 系统调用最终调用到调度类的 .set_cpus_allowed 回调,5大调度类的此回调都指向 set_cpus_allowed_common(),它里面设置的是 p->cpus_mask。在 kernel/sched 下检索,发现 cpus_ptr 在 fair.c、deadline.c、rt.c、core.c 中使用,而 cpus_mask 只在 core.c 中使用。在 core.c 中可以看到 p->cpus_mask 只是在 sched_setaffinity()/sched_getaffinity() 系统调用执行路径中使用到。

其实二者是同一个东西,如下:

//fork --> copy_process --> dup_task_struct 
static struct task_struct *dup_task_struct(struct task_struct *orig, int node) 
{ 
    ... 
    if (orig->cpus_ptr == &orig->cpus_mask) //恒成立 
        tsk->cpus_ptr = &tsk->cpus_mask; 
    ... 
} 
 
//追溯到最初首个任务: 
 
//init/init_task.c 
struct task_struct init_task = { 
    ... 
    .cpus_ptr    = &init_task.cpus_mask, 
    .cpus_mask    = CPU_MASK_ALL, 
    .nr_cpus_allowed= NR_CPUS, //这个最初可能是32 
    ... 
}

二、调度中的锁

1. __task_rq_lock 注释翻译

(1) 串行化规则:

锁定顺序:

    p->pi_lock 
        rq->lock 
            hrtimer_cpu_base->lock (hrtimer_start() 带宽控制使用) 
 
    rq1->lock 
        rq2->lock  条件: rq1 < rq2

(2) 常规状态:

正常情况下调度状态由 rq->lock 序列化。 __schedule() 获取本地 CPU 的 rq->lock,它可以选择从运行队列中删除任务,并始终查看本地 rq 数据结构以找到最符合要求的任务来运行。

任务入队也在 rq->lock 的保护下,任务可能取自另一个 CPU。 来自另一个 LLC 域的唤醒可能会使用 IPI 将入队传输到本地 CPU,以避免在周围反弹(bouncing)运行队列的状态 [参见 ttwu_queue_wakelist()]

任务唤醒,特别是涉及迁移的唤醒,为避免不得不使用两个 rq->locks,非常复杂。

(3) 特殊状态:

系统调用和任何外部操作都将使用 task_rq_lock() 获取 p->pi_lock 和 rq->lock。 因此,它们更改的状态在持有任一锁时都是稳定的:

- sched_setaffinity()/set_cpus_allowed_ptr():    p->cpus_ptr, p->nr_cpus_allowed 
- set_user_nice():    p->se.load, p->*prio 
- __sched_setscheduler(): 
    p->sched_class, p->policy, p->*prio, 
    p->se.load, p->rt_priority, 
    p->dl.dl_{runtime, deadline, period, flags, bw, density} 
- sched_setnuma():    p->numa_preferred_nid 
- sched_move_task()/cpu_cgroup_fork():    p->sched_task_group 
- uclamp_update_active():    p->uclamp*

(4) p->state <- TASK_*:

使用 set_current_state(),__set_current_state() 或 et_special_state() 无锁地更改,查看它们各自的注释,或通过 try_to_wake_up()。 后者使用 p->pi_lock 针对并发自身进行序列化。

(5) p->on_rq <- { 0, 1 = TASK_ON_RQ_QUEUED, 2 = TASK_ON_RQ_MIGRATING }:

由 activate_task() 设置并由 deactivate_task() 在 rq->lock 下清除。 非零表示任务是可运行的,特殊的 ON_RQ_MIGRATING 状态用于迁移而不持有两个 rq->locks。它表示 task_cpu() 不稳定,参见 task_rq_lock()。

(6) p->on_cpu <- { 0, 1 }:

由 prepare_task() 设置并由 finish_task() 清除,这样它将在 p 被调度之前设置并在 p 被切换走之后清除,两者都在 rq->lock 的保护下。 非零表示任务正在其 CPU 上运行。[ 精明的读者会观察到,一个 CPU 上的两个任务可能同时具有 ->on_cpu = 1。]

(7) task_cpu(p):由 set_task_cpu() 改变,规则为:

- 不要在阻塞的任务上调用 set_task_cpu(): 
  我们不关心我们没有在上面运行的CPU,这简化了热插拔,阻塞任务的 CPU 分配不需要是有效的。 
 
- 用于 try_to_wake_up(),在 p->pi_lock 下调用: 
  这允许 try_to_wake_up() 只使用一个 rq->lock,查看它的注释。 
 
- 用于在 rq->lock 下调用的迁移: 
  [参见 task_rq_lock() 中的 task_on_rq_migrating()] 
    move_queued_task() 
    detach_task() 
 
- 用于在 double_rq_lock() 下调用的迁移: 
  __migrate_swap_task() 
  push_rt_task() / pull_rt_task() 
  push_dl_task() / pull_dl_task() 
  dl_task_offline_migration()

三、调度类在5.10上放弃使用next指针

改为使用链接器将调度类链接在一起了,低优先级的调度类在前,高优先的调度类在后。

//fair.c 里面已经没有 next 指针了 
const struct sched_class fair_sched_class __section("__fair_sched_class") = { 
    ... 
} 
 
#define __section(section)    __attribute__((__section__(section))) 
 
//include/asm-generic/vmlinux.lds.h 
#define SCHED_DATA                \ 
    STRUCT_ALIGN();                \ 
    __begin_sched_classes = .;        \ 
    *(__idle_sched_class)            \ 
    *(__fair_sched_class)            \ 
    *(__rt_sched_class)            \ 
    *(__dl_sched_class)            \ 
    *(__stop_sched_class)            \ 
    __end_sched_classes = .; 
 
 
//使用下面宏访问: 
 
//kernel/sched/sched.h 
 
/* Defined in include/asm-generic/vmlinux.lds.h */ 
extern struct sched_class __begin_sched_classes[]; 
extern struct sched_class __end_sched_classes[]; 
 
#define sched_class_highest (__end_sched_classes - 1) 
#define sched_class_lowest  (__begin_sched_classes - 1) 
 
#define for_class_range(class, _from, _to) \ 
    for (class = (_from); class != (_to); class--) //[ ) 
 
#define for_each_class(class) \ 
    for_class_range(class, sched_class_highest, sched_class_lowest)

四、cpumask_var_t 和 struct cpumask

typedef struct cpumask { unsigned long bits[1]; } cpumask_t; 
typedef struct cpumask cpumask_var_t[1];

cpumask_var_t 类型为 "struct cpumask [1]",是一个数组长度为1的数组名,也可以认为是指向struct cpumask类型的常指针,除了做左值外等效于"struct cpumask*"。

举例:

#include <stdio.h> 
 
typedef struct cpumask { unsigned long bits[1]; } cpumask_t; 
typedef struct cpumask cpumask_var_t[1]; 
 
cpumask_var_t mt = {0xef}; //定义数组并赋值 
 
void print_mask_bit(struct cpumask *mask) 
{ 
    printf("mask: 0x%lx\n", mask->bits[0]); 
     
} 
 
void main() 
{ 
    print_mask_bit(mt);    //注意传参 
} 
 
/* 
$ gcc main.c -o pp 
$ ./pp 
mask: 0xef 
*/

本文参考链接:https://www.cnblogs.com/hellokitty2/p/15834273.html