Linux 内核 | CPU 状态管理


纪念自己第一次成为一个大开源项目的 contributor,尽管只是一个很小的PR

什么是CPU mask

内核使用CPU mask 来记录 CPU 的状态,cpumasks 提供了代表系统中 CPU 集合的位图,一个 bit 的0/1代表了一个 CPU在每种cpu mask 时的状态。实现(Linux kernel 5.10.20)主要包含在以下文件中:

  • include/linux/cpumask.h
  • kernel/cpu.c

CPU 的状态基本可分为以下四种:

  • cpu_possible :是一个在系统启动时任意时刻都可插入的 cpu ID 集合。它将等于通过CONFIG_NR_CPUS内核配置选项静态设置的NR_CPUS的值;
  • cpu_present :表示当前哪个CPU是插入的;
  • cpu_online : 是 cpu_present 的一个子集,表示哪些CPU 是调度器可访问的;
  • cpu_active : 该掩码的某些 bit 告诉Linux内核,一个任务可能已移至某个处理器。

内核声明了结构体cpumask ,并实例化 4 个cpumask 分别用来记录 CPU 的各种状态。以__cpu_possible_mask为例,其定义如下代码所示(定义在文件 kernel/cpu.c 中)。其中__read_mostly原语将定义的变量存放在 .data.read_mostly 段中,其定义在/arch/arm64/include/asm/cache.h文件中,原型为#define __read_mostly __section(.data..read_mostly),将经常需要被读取的数据定义为__read_mostly类型, 这样Linux内核被加载时,该数据将自动被存放到Cache中,以提高整个系统的执行效率。

struct cpumask __cpu_possible_mask __read_mostly;
EXPORT_SYMBOL(__cpu_possible_mask);

cpumask的定义如下,DECLARE_BITMAP宏用来计算 cpumask所需的大小。NR_CPUS为内核支持的最大 CPU 数,在内核编译时 make menuconfig 指定。

typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;

DECLARE_BITMAP宏定义在include/linux/types.h文件中,其接收两个参数:name,位图名;bits,位图所使用的bit数量。其展开也就是定义了一个以 long为元素的数组。

#define DECLARE_BITMAP(name,bits) \
    unsigned long name[BITS_TO_LONGS(bits)]

最后看一下BITS_TO_LONGS是如何根据 bits 计算出占用多少个 long 的。此宏定义在include/linux/bitops.h文件中,如下:

#define BITS_TO_LONGS(nr)    DIV_ROUND_UP(nr, BITS_PER_TYPE(long))

DIV_ROUND_UP宏展开如下:

// include/linux/kernel.h
#define DIV_ROUND_UP __KERNEL_DIV_ROUND_UP

// include/uapi/linux/const.h
#define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))

假设NR_CPUS为8,long 占 8 个字节,cpumask结构体展开如下。也就是说,内核为每个 CPU 分配 long 变量中的一个 bit 来保存各个 CPU 的状态。


BITS_TO_LONGS(8) -> DIV_ROUND_UP(8, BITS_PER_TYPE(long)) -> DIV_ROUND_UP(8, 64) -> (((8) + (64) - 1) / (64)) -> 1
    
typedef struct cpumask { 
    unsigned long bits[1]
} cpumask_t;

[注意!] 参考文档1按以下计算方式存在问题。若此时 CPU 数量是 9 个,计算得出需要 2 个 long 变量来保存CPU状态,这样就会很容易理解为一个 CPU的状态需要 1 个 btye 而不是 1 个 bit。 我向源Github仓库提交了 PR,不知道是否能合并进主线已经合并进了主线——首次成为一个大项目的 Contributor。

(((8) + (8) - 1) / (8)) = 1 

cpumask API

对 CPU mask 的访问通过函数set_cpu_xxxx()完成,其接收两个参数(cpuid ,true or false)。当第二个参数为 true 时,set_cpu的功能将被调用;当为 false 时, clear_cpu 的功能将被调用。例如,在系统初始化时,boot CPU 的API 接口调用如下代码所示:

// in kernel/cpu.c
void __init boot_cpu_init(void)
{
    int cpu = smp_processor_id();
    set_cpu_online(cpu, true);
    set_cpu_active(cpu, true);
    set_cpu_present(cpu, true);
    set_cpu_possible(cpu, true);

#ifdef CONFIG_SMP
    __boot_cpu_id = cpu;
#endif
}

set_cpu_online()接收两个参数(cpuid ,true or false),其实现如下。若第二个参数为 true, cpumask_test_and_set_cpu将被调用。

void set_cpu_online(unsigned int cpu, bool online)
{
    if (online) {
        if (!cpumask_test_and_set_cpu(cpu, &__cpu_online_mask))
            atomic_inc(&__num_online_cpus);
    } else {
        if (cpumask_test_and_clear_cpu(cpu, &__cpu_online_mask))
            atomic_dec(&__num_online_cpus);
    }
}

cpumask_test_and_set_cpu作为内联函数定义在include/linux/cpumask.h文件中,接收CPU id cpu 以及相应的 CPU mask __cpu_online_mask

static inline int cpumask_test_and_set_cpu(int cpu, struct cpumask *cpumask)
{
    return test_and_set_bit(cpumask_check(cpu), cpumask_bits(cpumask));
}

在调用test_and_set_bit之前,有两个宏对 cpu 和 cpumask 进行了处理。

  • cpumask_check是一个内联函数,检查传入的 cpu 参数是否大于了NR_CPUS,不展开
  • cpumask_bits获取了 cpumask 的 bits,即上文叙述的unsigned long bits[1]的内容。

    #define  cpumask_bits(maskp) ((maskp)->bits)

    test_and_set_bit()函数定义在include/asm-generic/bitops/atomic.h文件中,实现如下。

  • BIT_MASK将 1 左移 nr 位获得 mask,再与 (maskp->bits)相与。若为 1 则返回直接返回1;
  • 不为 1 则需要修改 cpumask。因此调用atomic_long_fetch_or()函数将 cpumask 与 mask 相或置位,并返回原 cpumask 给 old 变量;
  • old 变量与 mask 相与为0,经过两次 ,最后依然返回0;
static inline int test_and_set_bit(unsigned int nr, volatile unsigned long *p)
{
    long old;
    unsigned long mask = BIT_MASK(nr);

    p += BIT_WORD(nr);
    if (READ_ONCE(*p) & mask)
        return 1;

    old = atomic_long_fetch_or(mask, (atomic_long_t *)p);
    return !!(old & mask);
}

#define BIT_MASK(nr)        (UL(1) << ((nr) % BITS_PER_LONG))
#define BIT_WORD(nr)        ((nr) / BITS_PER_LONG)

其他关于 cpu mask 的 API 不一一详细例举,包括但不限于:

  • for_each_cpu:遍历一个mask的所有 cpu;
  • for_each_cpu_not:遍历所有补集的 cpu;
  • cpumask_clear_cpu:清除一个 cpumask 的 cpu;
  • cpumask_test_cpu:测试一个 mask 中的 cpu;
  • cpumask_setall :设置 mask 的所有 cpu;
  • cpumask_size : 返回分配 struct cpumask 字节数大小;

参考文档

  1. # CPU masks

声明:一丁点儿|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Linux 内核 | CPU 状态管理


勿在浮沙筑高台,每天进步一丁点儿!