本文采用Linux 内核 v3.10 版本 x86-64 架构
一、概述
在 Linux 内核中,使用位图来保存 CPU
的状态,每个 CPU
对应着位图中的一个比特位。内核为 CPU
定义了 4 种可能的状态,对应着 4 个位图:
cpu_possible_mask
cpu_online_mask
cpu_present_mask
cpu_active_mask
cpu_possible_mask
指示系统实际支持的 CPU
,主要用于为 per-cpu 变量分配启动时内存。该值在系统启动阶段确定,限制了系统允许使用的最大 CPU
数量,一旦确定,在整个系统运行期间都是静态的、不可修改的。在支持热插拔的系统中,CPU
可在系统运行时添加或移除,但系统可用的最大 CPU 数量不能超过 cpu_possible_mask
指示的值。该值受内核配置选项 CONFIG_NR_CPUS
、命令行参数 possible_cpus=n
、nr_cpus=n
、maxcpu=n
以及实际插入的 CPU
数量共同影响,计算过程详见 “3.4 参数的使用” 小节。
cpu_present_mask
指示系统当前存在的 CPU
,这些 CPU
并不一定全部处于 online
状态。
cpu_online_mask
指示系统当前在线的 CPU
,即当前可调度的 CPU
,或者说内核使用中的 CPU
。cpu_online
是 cpu_present
的一个动态子集。在CPU
能被内核调度并能够接收设备中断之后,可在 __cpu_up()
函数中将其置位;当使用 __cpu_disable()
关闭某个 CPU
时,会清除对应比特位,在此之前,包括中断在内的所有操作系统服务都会迁移到另一个目标 CPU
。
cpu_active_mask
是指允许任务迁入的 CPU
。
在支持 CPU
热插拔(CPU-hotplu
g)的系统中,cpu_present_mask
是动态的,因为可能会有 CPU
被添加或移除。
如果不支持热插拔(!CONFIG_HOTPLUG_CPU
),那么 present == possible
并且 active == online
。
下文均假设系统支持热插拔,即内核配置选项 CONFIG_HOTPLUG_CPU
为真。
二、数据结构
2.1 结构体 cpumask / cpumask_t
内核使用结构体 cpumask
来表示 CPU
的状态位图,cpumask_t
是 cpumask
的别名。cpumask
定义如下:
// file: include/linux/cpumask.h
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
cpumask
是对位图的包装,其内部引用了宏 DECLARE_BITMAP
。
2.1.1 宏 DECLARE_BITMAP
宏 DECLARE_BITMAP
用来创建一个位图,其定义如下:
// file: include/linux/types.h
#define DECLARE_BITMAP(name,bits) \
unsigned long name[BITS_TO_LONGS(bits)]
该宏接收 2 个参数:
name
-- 位图名称bits
-- 位图的容量
由于我们使用 unsigned long
数组来表示位图,所以需要将比特位数量转换成数组的成员数量。宏 BITS_TO_LONGS
用来实现对应的转换,其定义如下:
// file: include/linux/bitops.h
#define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))
BITS_TO_LONGS
中又引用了宏 DIV_ROUND_UP
和 BITS_PER_BYTE
。
宏 BITS_PER_BYTE
扩展为 8,表示每个字节包含 8 个比特位:
// file: include/linux/bitops.h
#define BITS_PER_BYTE 8
BITS_PER_BYTE * sizeof(long)
计算 long
类型数据占用的比特位数量。
宏 DIV_ROUND_UP
用来把整数 n
向上圆整到 d
的倍数,其实现方式就是把 n
加上 d-1
后再除以 d
,该宏定义如下:
// file: include/linux/kernel.h
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))
在我们的案例中,就是将比特位数量向上圆整到unsigned long
类型的整数倍,保证数组有足够的容量来保存比特位。
由于执行了向上圆整操作,实际的位图中有些比特位是无效的,如下图所示:
结构体 cpumask
扩展如下:
typedef struct cpumask { unsigned long bits[BITS_TO_LONGS(NR_CPUS)]; } cpumask_t;
可以看到,结构体 cpumask
的成员是一个名称为 bits
拥有 NR_CPUS
个有效比特位的 unsigned long
数组。
2.1.2 宏 NR_CPUS
宏 NR_CPUS
扩展为 CONFIG_NR_CPUS
,其定义如下:
// file: include/linux/threads.h
/* Places which use this should consider cpumask_var_t. */
#define NR_CPUS CONFIG_NR_CPUS
宏 CONFIG_NR_CPUS
是内核编译时指定的配置选项,其定义如下:
// file: include/generated/autoconf.h
#define CONFIG_NR_CPUS 4096
该宏扩展为 4096
:
// file: arch/x86/Kconfig
config NR_CPUS
int "Maximum number of CPUs" if SMP && !MAXSMP
range 2 8 if SMP && X86_32 && !X86_BIGSMP
range 2 512 if SMP && !MAXSMP // --- 没有设置了 MAXSMP 选项时,SMP 系统下,NR_CPUS 最大为 512 ---
default "1" if !SMP
default "4096" if MAXSMP // --- 当设置了 MAXSMP 选项时,NR_CPUS 默认为 4096 ---
default "32" if SMP && (X86_NUMAQ || X86_SUMMIT || X86_BIGSMP || X86_ES7000)
default "8" if SMP
---help---
This allows you to specify the maximum number of CPUs which this
kernel will support. The maximum supported value is 512 and the
minimum value which makes sense is 2.
This is purely to save memory - each supported CPU adds
approximately eight kilobytes to the kernel image.
4096
是在设置了 MAXSMP(Enable Maximum number of SMP Processors and NUMA Nodes)
选项时的默认值,不同的内核版本该值可能不同。
MAXSMP
选项会将处理器的数量设为最大值:
// file: arch/x86/Kconfig
config MAXSMP
bool "Enable Maximum number of SMP Processors and NUMA Nodes"
depends on X86_64 && SMP && DEBUG_KERNEL
select CPUMASK_OFFSTACK
---help---
Enable maximum number of CPUS and NUMA Nodes for this architecture.
If unsure, say N.
如果没有配置 MAXSMP
选项,在 x86-64
架构 SMP
系统下, NR_CPUS
最大只能设置为 512
(见 config NR_CPUS
中的说明)。
使用 make menuconfig
命令, 配置 MAXSMP
及 NR_CPUS
的界面如下图所示:
可以看到,在选中 MAXSMP
的情况下,并不会出现设置最大 CPU
数量的选项,直接默认使用最大值。
当 MAXSMP
未选中时,显示设置 CPU
数量的选项,默认值为 8。
CONFIG_NR_CPUS
指示系统配置的最大 CPU
数量。我编译时使用的是默认配置,即选中了 MAXSMP
,所以CONFIG_NR_CPUS
的值比较大。在下文将会看到,内核中使用的多个 CPU
相关的位图,其默认容量都是 CONFIG_NR_CPUS
,如果此值设置的太大超出实际 CPU
数量很多的话,会浪费内存空间。所以,还是要根据实际情况设置 NR_CPUS
的值,避免浪费内存。
2.2 cpu_possible_mask
cpu_possible_mask
是指系统实际支持的 CPU
位图,其定义如下:
// file: kernel/cpu.c
const struct cpumask *const cpu_possible_mask = to_cpumask(cpu_possible_bits);
变量 cpu_possible_mask
的实现中,又引用了 cpu_possible_bits
变量以及 to_cpumask()
函数。
2.2.1 位图 -- cpu_possible_bits
cpu_possible_bits
是使用宏 DECLARE_BITMAP
创建的位图,其定义如下:
// file: kernel/cpu.c
#ifdef CONFIG_INIT_ALL_POSSIBLE
static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly
= CPU_BITS_ALL;
#else
static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly;
#endif
可以看到,该位图的容量为 CONFIG_NR_CPUS
,即配置的 CPU
数量。
cpu_possible_bits
的实现依赖于内核配置选项 CONFIG_INIT_ALL_POSSIBLE
。当该选项为假时,只是创建位图,并未初始化;当选项为真时,在创建位图后,还将其初始化为 CPU_BITS_ALL
,即将 cpu_possible_bits
中的所有的有效位(不包括因对齐到 long
类型边界而填充的位)初始化为 1。
宏 CPU_BITS_ALL
定义如下:
// file: include/linux/cpumask.h
#define CPU_BITS_ALL \
{ \
[0 ... BITS_TO_LONGS(NR_CPUS)-2] = ~0UL, \
[BITS_TO_LONGS(NR_CPUS)-1] = CPU_MASK_LAST_WORD \
}
由于在将比特位数量转成数组成员数量时,数组最后一个成员中可能会填充无效比特位,所以需要对其进行特殊处理。在 CPU_BITS_ALL
的实现中,最后一个数组成员调用宏 CPU_MASK_LAST_WORD
进行处理,其它成员的比特位全部初始化为 1。
宏 CPU_MASK_LAST_WORD
定义如下,该宏内部又引用了 BITMAP_LAST_WORD_MASK
:
// file: include/linux/cpumask.h
#define CPU_MASK_LAST_WORD BITMAP_LAST_WORD_MASK(NR_CPUS)
宏 BITMAP_LAST_WORD_MASK
会根据比特位数量 NR_CPUS
,计算最后一个数组成员中的有效比特位,并将这些位设置为 1,其它位为 0,该宏定义如下:
// file: include/linux/bitmap.h
#define BITMAP_LAST_WORD_MASK(nbits) \
( \
((nbits) % BITS_PER_LONG) ? \
(1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL \
)
当比特位数量不是 long
类型容量的整数倍时,宏 BITMAP_LAST_WORD_MASK
效果如下:
宏 to_cpumask
用来将位图转换成 struct cpumask
类型,详见 to_cpumask
小节。
2.3 cpu_online_mask、cpu_present_mask、cpu_active_mask
这 3 个变量的实现与 cpu_possible_mask
类似,其定义如下:
// file: kernel/cpu.c
static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_online_mask = to_cpumask(cpu_online_bits);
static DECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_present_mask = to_cpumask(cpu_present_bits);
static DECLARE_BITMAP(cpu_active_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_active_mask = to_cpumask(cpu_active_bits);
可以看到,这 3 个变量的位图容量均为 CONFIG_NR_CPUS
,因为是静态变量,所有比特位的初始值都为 0。
2.4 cpu_all_bits
cpu_all_bits
用以表示在 menuconfig
中配置的 CPU
的数量。该位图的容量为 NR_CPUS
,并通过宏 CPU_BITS_ALL
将所有的有效位(不包括因对齐到 long
类型边界而填充的位)设置为 1。
const DECLARE_BITMAP(cpu_all_bits, NR_CPUS) = CPU_BITS_ALL;
2.5 nr_cpumask_bits
宏 nr_cpumask_bits
定义如下:
// file: include/linux/cpumask.h
#ifdef CONFIG_CPUMASK_OFFSTACK
/* Assuming NR_CPUS is huge, a runtime limit is more efficient. Also,
* not all bits may be allocated. */
#define nr_cpumask_bits nr_cpu_ids
#else
#define nr_cpumask_bits NR_CPUS
#endif
如果配置了内核选项 CONFIG_CPUMASK_OFFSTACK
,该宏扩展为 nr_cpu_ids
,否则,扩展为 NR_CPUS
。NR_CPUS
是系统配置的最大 CPU
数量,在我们的案例中扩展为 4096;nr_cpu_ids
是系统实际支持的 CPU 数量,即 nr_cpu_ids == possible
,具体计算见 “prefill_possible_map()
函数”。
CPUMASK_OFFSTACK
选项说明如下:
// file: lib/Kconfig
config CPUMASK_OFFSTACK
bool "Force CPU masks off stack" if DEBUG_PER_CPU_MAPS
help
Use dynamic allocation for cpumask_var_t, instead of putting
them on the stack. This is a bit more expensive, but avoids
stack overflow.
可以看到,该选项是为了防止当 CPU
数量过大时,使用栈保存变量可能导致栈溢出的情况,从而不使用栈而使用动态分配变量内存的方案。在我们选中 MAXSMP
选项时,该选项默认也会选中。
2.6 cpu_bit_bitmap
cpu_bit_bitmap
声明如下:
// file: include/linux/cpumask.h
/*
* Special-case data structure for "single bit set only" constant CPU masks.
*
* We pre-generate all the 64 (or 32) possible bit positions, with enough
* padding to the left and the right, and return the constant pointer
* appropriately offset.
*/
extern const unsigned long
cpu_bit_bitmap[BITS_PER_LONG+1][BITS_TO_LONGS(NR_CPUS)];
其中,宏 BITS_PER_LONG
扩展为 64,NR_CPUS
是系统配置的最大 CPU
数量,BITS_TO_LONGS
用于将比特位数量转换成 long
类型数组成员数量。
cpu_bit_bitmap
定义如下:
// file: kernel/cpu.c
/*
* cpu_bit_bitmap[] is a special, "compressed" data structure that
* represents all NR_CPUS bits binary values of 1<<nr.
*
* It is used by cpumask_of() to get a constant address to a CPU
* mask value that has a single bit set only.
*/
const unsigned long cpu_bit_bitmap[BITS_PER_LONG+1][BITS_TO_LONGS(NR_CPUS)] = {
MASK_DECLARE_8(0), MASK_DECLARE_8(8),
MASK_DECLARE_8(16), MASK_DECLARE_8(24),
#if BITS_PER_LONG > 32
MASK_DECLARE_8(32), MASK_DECLARE_8(40),
MASK_DECLARE_8(48), MASK_DECLARE_8(56),
#endif
};
/* cpu_bit_bitmap[0] is empty - so we can back into it */
#define MASK_DECLARE_1(x) [x+1][0] = (1UL << (x))
#define MASK_DECLARE_2(x) MASK_DECLARE_1(x), MASK_DECLARE_1(x+1)
#define MASK_DECLARE_4(x) MASK_DECLARE_2(x), MASK_DECLARE_2(x+2)
#define MASK_DECLARE_8(x) MASK_DECLARE_4(x), MASK_DECLARE_4(x+4)
可以看到,cpu_bit_bitmap
是一个包含 65 个成员的数组,每个成员都是一个容量为 NR_CPUS
的位图。其中,第 0 个成员未做任何设置,其余的成员都只置位了一个比特位。
cpu_bit_bitmap
中,每个成员中置位的比特位是有规律的,如下所示:
cpu_bit_bitmap[][] = {
[1][0] = 1 << 0,
[2][0] = 1 << 1,
[3][0] = 1 << 2,
[4][0] = 1 << 3,
...
[64][0] = 1 << 63
}
三、命令行参数
3.1 maxcpus=n
maxcpus
参数限制启动时激活的 CPU
数量。举例来说,假如有 4 个 CPU
,使用 maxcpus=2
只会启动 2 个,剩余的 2 个 CPU
可以在以后上线。
maxcpus
参数会修改变量 setup_max_cpus
的值。默认情况下,该值为 NR_CPUS
,即配置选项 CONFIG_NR_CPUS
的值。
// file: kernel/smp.c
/* Setup configured maximum number of CPUs to activate */
unsigned int setup_max_cpus = NR_CPUS;
指定了命令行参数 maxcpus
后,通过参数处理函数 maxcpus()
,将 setup_max_cpus
设置为指定的值。
// file: kernel/smp.c
static int __init maxcpus(char *str)
{
get_option(&str, &setup_max_cpus);
if (setup_max_cpus == 0)
arch_disable_smp_support();
return 0;
}
early_param("maxcpus", maxcpus);
maxcpus()
函数内部,调用 get_option()
函数,解析参数并赋值给 setup_max_cpus
。
如果 setup_max_cpus
的值为 0,说明是单处理器系统,调用 arch_disable_smp_support()
函数禁用 SMP
系统支持。
3.2 nr_cpus=n
nr_cpus
参数限制内核支持的最大 CPU
数量。如果该参数的值小于物理 CPU
的数量,那么多出的 CPU
将不能上线使用。
nr_cpus
参数将修改变量 nr_cpu_ids
的值。默认情况下,该值为 NR_CPUS
,即配置选项 CONFIG_NR_CPUS
的值。
// file: kernel/smp.c
/* Setup number of possible processor ids */
int nr_cpu_ids __read_mostly = NR_CPUS;
指定了命令行参数 nr_cpus
后,通过参数处理函数 nrcpus()
,将 nr_cpus
设置为参数指定的值。
// file: kernel/smp.c
/* this is hard limit */
static int __init nrcpus(char *str)
{
int nr_cpus;
get_option(&str, &nr_cpus);
if (nr_cpus > 0 && nr_cpus < nr_cpu_ids)
nr_cpu_ids = nr_cpus;
return 0;
}
early_param("nr_cpus", nrcpus);
nrcpus()
函数内部,调用 get_option()
函数,解析参数并赋值给 nr_cpus
。
如果指定的值小于 nr_cpu_ids
的初始值(即 NR_CPUS
),那么将nr_cpu_ids
调整为参数 nr_cpus
指定的数值。也就是说,系统实际支持的最大 CPU
数量,是配置选项 CONFIG_NR_CPUS
和命令行参数 nr_cpus
之中较小的那个值。
3.3 possible_cpus=n
possible_cpus
参数跟其它参数一起,共同决定系统支持的最大 CPU
数量。计算过程详见 “3.4 参数的使用” 小节。
指定了该参数后,通过参数处理函数 _setup_possible_cpus()
,将变量 setup_possible_cpus
设置为指定的值。 setup_possible_cpus
的默认值为 -1。
// file: arch/x86/kernel/smpboot.c
static int __initdata setup_possible_cpus = -1;
static int __init _setup_possible_cpus(char *str)
{
get_option(&str, &setup_possible_cpus);
return 0;
}
early_param("possible_cpus", _setup_possible_cpus);
3.4 参数的使用
在 prefill_possible_map()
函数中,会对 cpu_possible_mask
进行填充。填充过程中,使用到了我们上文介绍过的参数。
该函数的调用链: start_kernel() -> setup_arc() -> prefill_possible_map()
。
另外,在 prefill_possible_map()
函数中,还用到了 2 个全局变量:num_processors
和 disabled_cpus
。
// file: arch/x86/kernel/apic/apic.c
unsigned int num_processors;
unsigned disabled_cpus __cpuinitdata;
num_processors
是插入且可用的 CPU
数量,该值不能大于 nr_cpu_ids
;disabled_cpus
是指已插入但因超出 nr_cpu_ids
数量而被禁用的 CPU
数量。两者相加,即为实际插入的 CPU
总数量。
3.4.1 generic_processor_info() 函数
变量 num_processors
和 disabled_cpus
的值在 generic_processor_info()
函数中被更新:
// file: arch/x86/kernel/apic/apic.c
void __cpuinit generic_processor_info(int apicid, int version)
{
int cpu, max = nr_cpu_ids;
bool boot_cpu_detected = physid_isset(boot_cpu_physical_apicid,
phys_cpu_present_map);
/*
* If boot cpu has not been detected yet, then only allow upto
* nr_cpu_ids - 1 processors and keep one slot free for boot cpu
*/
if (!boot_cpu_detected && num_processors >= nr_cpu_ids - 1 &&
apicid != boot_cpu_physical_apicid) {
int thiscpu = max + disabled_cpus - 1;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i almost"
" reached. Keeping one slot for boot cpu."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
if (num_processors >= nr_cpu_ids) {
int thiscpu = max + disabled_cpus;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i reached."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
num_processors++;
if (apicid == boot_cpu_physical_apicid) {
/*
* x86_bios_cpu_apicid is required to have processors listed
* in same order as logical cpu numbers. Hence the first
* entry is BSP, and so on.
* boot_cpu_init() already hold bit 0 in cpu_present_mask
* for BSP.
*/
cpu = 0;
} else
cpu = cpumask_next_zero(-1, cpu_present_mask);
......
set_cpu_possible(cpu, true);
set_cpu_present(cpu, true);
}
generic_processor_info()
函数执行过程如下:
- 检查当前
CPU
是否是系统启动的CPU
,即 BSP(Bootstrap Processor)。 - 如果不是 BSP,且已检测到的
CPU
数量num_processors
大于等于nr_cpu_ids - 1
,说明实际插入的CPU
数量超出允许的最大数量了,那么该CPU
会被禁用,此时禁用数量加 1,即disabled_cpus++
。然后直接返回。之所以将上限值设置为nr_cpu_ids - 1
,是因为检测到的CPU
不是 BSP,为了保证 BSP 是可用的,必须为 BSP 保留一个位置。 - 如果是 BSP,且已检测到的
CPU
数量num_processors
大于等于nr_cpu_ids
,说明实际插入的CPU
数量已经超出允许的最大数量了,那么该CPU
会被禁用,此时禁用数量加 1,即disabled_cpus++
。然后直接返回。 - 如果没有超出
nr_cpu_ids
的限制,那么可用的CPU
数量加 1,即num_processors++
。 - 确定
CPU
编号。如果apicid == boot_cpu_physical_apicid
,说明当前CPU
是 BSP,BSP 的CPU
编号为 0。否则,说明不是 BSP,那么调用cpumask_next_zero()
函数,从位图cpu_present_mask
中找出第一个为 0 的位,将该位的索引设置为CPU
的编号。 - 调用
set_cpu_possible()
函数,将cpu
状态设置为possible
。 - 调用
set_cpu_present()
函数,将cpu
状态设置为present
。
3.4.2 prefill_possible_map() 函数
prefill_possible_map()
函数会计算 cpu_possible
的值,计算过程中会用到上文介绍的参数,该函数定义如下:
// file: arch/x86/kernel/smpboot.c
/*
* cpu_possible_mask should be static, it cannot change as cpu's
* are onlined, or offlined. The reason is per-cpu data-structures
* are allocated by some modules at init time, and dont expect to
* do this dynamically on cpu arrival/departure.
* cpu_present_mask on the other hand can change dynamically.
* In case when cpu_hotplug is not compiled, then we resort to current
* behaviour, which is cpu_possible == cpu_present.
* - Ashok Raj
*
* Three ways to find out the number of additional hotplug CPUs:
* - If the BIOS specified disabled CPUs in ACPI/mptables use that.
* - The user can overwrite it with possible_cpus=NUM
* - Otherwise don't reserve additional CPUs.
* We do this because additional CPUs waste a lot of memory.
* -AK
*/
__init void prefill_possible_map(void)
{
int i, possible;
/* no processor from mptable or madt */
if (!num_processors)
num_processors = 1;
i = setup_max_cpus ?: 1;
if (setup_possible_cpus == -1) {
possible = num_processors;
#ifdef CONFIG_HOTPLUG_CPU
if (setup_max_cpus)
possible += disabled_cpus;
#else
if (possible > i)
possible = i;
#endif
} else
possible = setup_possible_cpus;
total_cpus = max_t(int, possible, num_processors + disabled_cpus);
/* nr_cpu_ids could be reduced via nr_cpus= */
if (possible > nr_cpu_ids) {
pr_warn("%d Processors exceeds NR_CPUS limit of %d\n",
possible, nr_cpu_ids);
possible = nr_cpu_ids;
}
#ifdef CONFIG_HOTPLUG_CPU
if (!setup_max_cpus)
#endif
if (possible > i) {
pr_warn("%d Processors exceeds max_cpus limit of %u\n",
possible, setup_max_cpus);
possible = i;
}
pr_info("Allowing %d CPUs, %d hotplug CPUs\n",
possible, max_t(int, possible - num_processors, 0));
for (i = 0; i < possible; i++)
set_cpu_possible(i, true);
for (; i < NR_CPUS; i++)
set_cpu_possible(i, false);
nr_cpu_ids = possible;
}
prefill_possible_map()
函数执行流程如下:
i = setup_max_cpus ?: 1;
setup_max_cpus
是使用命令行参数 maxcpus=n
设置的系统启动时激活的 CPU
数量,默认为 NR_CPUS
。当该值为 0 时,按照单处理器系统启动;否则,按照 SMP 系统启动。使用命令行选项 maxcpus=0
或 nosmp
,都会导致 setup_max_cpus
的值为 0。maxcpus=n
参数我们在上文已经介绍过了,nosmp
参数处理如下:
// file: kernel/smp.c
/*
* Setup routine for controlling SMP activation
*
* Command-line option of "nosmp" or "maxcpus=0" will disable SMP
* activation entirely (the MPS table probe still happens, though).
*
* Command-line option of "maxcpus=<NUM>", where <NUM> is an integer
* greater than 0, limits the maximum number of CPUs activated in
* SMP mode to <NUM>.
*/
static int __init nosmp(char *str)
{
setup_max_cpus = 0;
arch_disable_smp_support();
return 0;
}
early_param("nosmp", nosmp);
setup_max_cpus ?: 1
等同于 setup_max_cpus ? setup_max_cpus : 1
。当 setup_max_cpus
为真时,说明是 SMP 系统,将 i
设置为 setup_max_cpus
;否则,说明是单处理器系统,将 i
设置为 1。我们仅考虑 SMP 系统,此时 setup_max_cpus
大于 0 ,i
等于 setup_max_cpus
。
if (setup_possible_cpus == -1) {
possible = num_processors;
#ifdef CONFIG_HOTPLUG_CPU
if (setup_max_cpus)
possible += disabled_cpus;
#else
if (possible > i)
possible = i;
#endif
} else
possible = setup_possible_cpus;
setup_possible_cpus
的默认值为 -1
,使用命令行参数 possible_cpus=n
可以修改此值。
如果 setup_possible_cpus == -1
,说明没有使用命令行参数,possible
的默认值为 num_processors
,即系统实际可用的 CPU
数量。
- 如果支持热插拔(
CONFIG_HOTPLUG_CPU
)且setup_max_cpus
不为 0(SMP 系统),possible
的值还要再加上被禁用的CPU
数量,即possible += disabled_cpus
。此时,possible
的值为实际插入的CPU
数量。 - 如果不支持热插拔,当
possible
的值大于i
时,即比启动时激活的CPU
数量要大,将possible
修正为i
的值,即setup_max_cpus
。
如果 setup_possible_cpus != -1
,说明使用命令行参数对可能的 CPU
数量做了设置,那么 possible
的值即为设置值 setup_possible_cpus
。
/* nr_cpu_ids could be reduced via nr_cpus= */
if (possible > nr_cpu_ids) {
pr_warn("%d Processors exceeds NR_CPUS limit of %d\n",
possible, nr_cpu_ids);
possible = nr_cpu_ids;
}
如果此时 possible
的值大于系统支持的最大值 nr_cpu_ids
,那么将 possible
的值修正为 nr_cpu_ids
。
#ifdef CONFIG_HOTPLUG_CPU
if (!setup_max_cpus)
#endif
if (possible > i) {
pr_warn("%d Processors exceeds max_cpus limit of %u\n",
possible, setup_max_cpus);
possible = i;
}
如果系统支持热插拔且 setup_max_cpus
为 0(单处理器系统), 当possible
的值大于 i
时,将 possible
的值修正为 i
。在单处理器系统下,i
的值为 1,所以此时 possible
值也为 1。
如果系统支持热插拔且 setup_max_cpus
不为 0(SMP 系统), 这段代码什么也不做。
如果系统不支持热插拔,当possible
的值大于 i
时,将 possible
的值修正为 i
,即系统启动时激活的处理器数量。对于单处理器系统,该值为 1;对于 SMP 系统,该值为 setup_max_cpus
。
for (i = 0; i < possible; i++)
set_cpu_possible(i, true);
for (; i < NR_CPUS; i++)
set_cpu_possible(i, false);
nr_cpu_ids = possible;
将 cpu_possible_mask
中所有小于 possible
的位设置为 1,其它位设置为 0。
最后,将 possible
赋值给 nr_cpu_ids
,即系统允许的最大 CPU
数量与 possible
一致。
3.4.3 总结
在单处理器系统(setup_max_cpus == 0
)下,possible
值为 1;
在 SMP 系统下(setup_max_cpus > 0
):
- 如果
setup_possible_cpus == -1
(未使用命令行参数possible_cpus=n
)- 支持热插拔,则
possible = min(num_processors + disabled_cpus, nr_cpu_ids)
- 不支持热插拔,则
possible = min(num_processors, setup_max_cpus, nr_cpu_ids)
- 支持热插拔,则
- 如果
setup_possible_cpus != -1
(使用了命令行参数possible_cpus=n
)- 支持热插拔的情况下,则
possible = min(setup_possible_cpus, nr_cpu_ids)
- 不支持热插拔,则
possible = min(setup_possible_cpus, nr_cpu_ids, setup_max_cpus)
- 支持热插拔的情况下,则
possible
计算示意图:
3.5 查看 CPU 状态
可以在 /sys/devices/system/cpu
目录下查看 CPU
相关信息:
$ pwd
/sys/devices/system/cpu
$ ls -l
total 0
drwxr-xr-x 6 root root 0 Nov 27 03:12 cpu0
drwxr-xr-x 6 root root 0 Nov 27 03:12 cpu1
...
-r--r--r-- 1 root root 4096 Nov 27 03:12 kernel_max
-r--r--r-- 1 root root 4096 Nov 27 03:12 offline
-r--r--r-- 1 root root 4096 Nov 27 03:12 online
-r--r--r-- 1 root root 4096 Nov 27 03:12 possible
-r--r--r-- 1 root root 4096 Nov 27 03:12 present
...
在该目录下,有几个文件,对应着系统中 CPU
的状态信息。各文件说明如下:
kernel_max
: 内核配置的最大CPU
的索引值,即NR_CPUS-1
;offline
:不在线的或超出内核配置的CPU
,即~cpu_online_mask + cpus >= NR_CPUS
;online
:在线的、正在被调度的CPU
,即cpu_online_mask
;possible
:已经分配资源,如果存在则可以上线的CPU
,即cpu_possible_mask
;present
:系统中存在且可用的CPU
,即cpu_present_mask
。
示例 1
如果系统中有 64 个 CPU
,内核配置参数 NR_CPUS
为 32,且 cpu2
和 cpu4 - 31
不在线,那么各文件内容如下:
kernel_max: 31
offline: 2,4-31,32-63
online: 0-1,3
possible: 0-31
present: 0-31
示例 2
如果内核配置参数 NR_CPUS
的值为 128,但是命令行参数 possible_cpus=144
。系统中有 4 个 CPU
,其中 cpu2
被手动下线,那么各文件内容如下:
kernel_max: 127
offline: 2,4-127,128-143
online: 0-1,3
possible: 0-127
present: 0-3
注:以上 2 个示例均来自内核文档 Documentation/cputopology.txt
。
示例 3
在我的 2 核虚拟机(内核版本 v4.15.0)上,各文件内容如下:
$ cat kernel_max
8191
$ cat online
0-1
$ cat possible
0-1
$ cat present
0-1
$ cat offline
3.6 CPU 逻辑下线
在 /sys/devices/system/cpu
目录下,还存在多个 cpuX
目录,其中 X
是 CPU
编号。cpuX
目录下有个 online
文件:
$ ls -l cpu1
total 0
...
-rw-r--r-- 1 root root 4096 Nov 30 16:11 online
...
可以使用如下命令,使 CPU
逻辑下线:
#echo 0 > /sys/devices/system/cpu/cpuX/online
四、APIs
提示:在 cpumask
相关的接口函数中,大量使用了位图(bitmap)相关的接口。位图(bitmap)相关内容请参考:Linux Kernel:内核数据结构之位图(Bitmap)。
4.1 宏 cpumask_bits
宏 cpumask_bits
用来将结构体 cpumask
解包装,返回内部的位图成员 bits
。
/**
* cpumask_bits - get the bits in a cpumask
* @maskp: the struct cpumask *
*
* You should only assume nr_cpu_ids bits of this mask are valid. This is
* a macro so it's const-correct.
*/
#define cpumask_bits(maskp) ((maskp)->bits)
4.2 宏 to_cpumask
宏 to_cpumask
与 cpumask_bits
的功能相反,用来将位图包装成 cpumask
结构体。
该宏定义如下:
// file: include/linux/cpumask.h
/**
* to_cpumask - convert an NR_CPUS bitmap to a struct cpumask *
* @bitmap: the bitmap
*
* There are a few places where cpumask_var_t isn't appropriate and
* static cpumasks must be used (eg. very early boot), yet we don't
* expose the definition of 'struct cpumask'.
*
* This does the conversion, and can be used as a constant initializer.
*/
#define to_cpumask(bitmap) \
((struct cpumask *)(1 ? (bitmap) \
: (void *)sizeof(__check_is_bitmap(bitmap))))
因为三元运算符的条件始终为真,所以该宏会将 bitmap
转换成 struct cpumask
类型并返回。
__check_is_bitmap
函数的作用是在编译时检查入参类型是否为 unsigned long
指针。如果入参类型错误,在编译时就会报错。
// file: include/linux/cpumask.h
static inline int __check_is_bitmap(const unsigned long *bitmap)
{
return 1;
}
4.3 宏 cpu_all_mask
宏 cpu_all_mask
用来将全 CPU
位图 cpu_all_bits
转换成 cpumask
结构体。
// file: include/linux/cpumask.h
#define cpu_all_mask to_cpumask(cpu_all_bits)
4.4 cpumask_size()
cpumask_size()
函数计算创建 cpumask
结构体时需要分配的字节数,其位图容量为 NR_CPUS
。
// file: include/linux/cpumask.h
/**
* cpumask_size - size to allocate for a 'struct cpumask' in bytes
*
* This will eventually be a runtime variable, depending on nr_cpu_ids.
*/
static inline size_t cpumask_size(void)
{
/* FIXME: Once all cpumask assignments are eliminated, this
* can be nr_cpumask_bits */
return BITS_TO_LONGS(NR_CPUS) * sizeof(long);
}
4.5 cpumask_check()
cpumask_check()
函数定义如下:
// file: include/linux/cpumask.h
/* verify cpu argument to cpumask_* operators */
static inline unsigned int cpumask_check(unsigned int cpu)
{
#ifdef CONFIG_DEBUG_PER_CPU_MAPS
WARN_ON_ONCE(cpu >= nr_cpumask_bits);
#endif /* CONFIG_DEBUG_PER_CPU_MAPS */
return cpu;
}
如果配置了选项 CONFIG_DEBUG_PER_CPU_MAPS
,会检查指定的 CPU
编号是否超过了最大值 nr_cpumask_bits
,超过的话会打印警告信息。
最后,会将 CPU
编号原样返回。
4.6 cpumask_set_cpu()
cpumask_set_cpu()
函数接收 2 个参数:
cpu
-- 要测试的CPU
编号cpumask
-- 掩码指针
该函数调用位图接口 set_bit
,将目的掩码dstp
中 cpu
对应的比特位置位。cpumask_check
函数和 cpumask_bits
函数我们在上文已经介绍过,此处不再赘述。
// file: include/linux/cpumask.h
/**
* cpumask_set_cpu - set a cpu in a cpumask
* @cpu: cpu number (< nr_cpu_ids)
* @dstp: the cpumask pointer
*/
static inline void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp)
{
set_bit(cpumask_check(cpu), cpumask_bits(dstp));
}
4.7 cpumask_setall()
cpumask_setall()
函数接收 1 个参数:
dstp
--cpumask
结构体指针
该函数调用位图接口 bitmap_fill()
,将目的掩码 dstp
中前 nr_cpumask_bits
个比特位置位(设置为 1)。
// file: include/linux/cpumask.h
/**
* cpumask_setall - set all cpus (< nr_cpu_ids) in a cpumask
* @dstp: the cpumask pointer
*/
static inline void cpumask_setall(struct cpumask *dstp)
{
bitmap_fill(cpumask_bits(dstp), nr_cpumask_bits);
}
4.8 cpumask_clear_cpu()
cpumask_clear_cpu()
函数实现的功能与 cpumask_set_cpu()
函数相反,其内部调用位图接口 clear_bit()
将目的掩码 dstp
中 cpu
对应的比特位清除。
// file: include/linux/cpumask.h
/**
* cpumask_clear_cpu - clear a cpu in a cpumask
* @cpu: cpu number (< nr_cpu_ids)
* @dstp: the cpumask pointer
*/
static inline void cpumask_clear_cpu(int cpu, struct cpumask *dstp)
{
clear_bit(cpumask_check(cpu), cpumask_bits(dstp));
}
4.9 cpumask_clear()
cpumask_clear()
函数接收 1 个参数:
dstp
--cpumask
结构体指针
该函数调用位图接口 bitmap_zero()
,将目的掩码 dstp
中前 nr_cpumask_bits
个比特位清除(设置为 0)。
/**
* cpumask_clear - clear all cpus (< nr_cpu_ids) in a cpumask
* @dstp: the cpumask pointer
*/
static inline void cpumask_clear(struct cpumask *dstp)
{
bitmap_zero(cpumask_bits(dstp), nr_cpumask_bits);
}
4.10 宏 cpumask_test_cpu()
宏 cpumask_test_cpu()
的功能是测试位图掩码 cpumask
中 cpu
对应的比特位是否被置位。如果置位,返回 1;否则,返回 0。
该宏其定义如下:
// file: include/linux/cpumask.h
/**
* cpumask_test_cpu - test for a cpu in a cpumask
* @cpu: cpu number (< nr_cpu_ids)
* @cpumask: the cpumask pointer
*
* Returns 1 if @cpu is set in @cpumask, else returns 0
*
* No static inline type checking - see Subtlety (1) above.
*/
#define cpumask_test_cpu(cpu, cpumask) \
test_bit(cpumask_check(cpu), cpumask_bits((cpumask)))
该函数内部调用位图接口 test_bit()
完成实际的测试工作。
4.11 cpumask_test_and_set_cpu()
该函数将掩码 cpumask
中的指定 cpu
处的比特位设置为 1,并返回旧值。
/**
* cpumask_test_and_set_cpu - atomically test and set a cpu in a cpumask
* @cpu: cpu number (< nr_cpu_ids)
* @cpumask: the cpumask pointer
*
* Returns 1 if @cpu is set in old bitmap of @cpumask, else returns 0
*
* test_and_set_bit wrapper for cpumasks.
*/
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()
的简单包装。
4.12 cpumask_test_and_clear_cpu()
将掩码 cpumask
中的指定 cpu
处的比特位设置为 0,并返回旧值。该函数是对位图接口 test_and_clear_bit()
的简单包装。
/**
* cpumask_test_and_clear_cpu - atomically test and clear a cpu in a cpumask
* @cpu: cpu number (< nr_cpu_ids)
* @cpumask: the cpumask pointer
*
* Returns 1 if @cpu is set in old bitmap of @cpumask, else returns 0
*
* test_and_clear_bit wrapper for cpumasks.
*/
static inline int cpumask_test_and_clear_cpu(int cpu, struct cpumask *cpumask)
{
return test_and_clear_bit(cpumask_check(cpu), cpumask_bits(cpumask));
}
4.13 cpumask_first()
cpumask_first()
函数查找掩码 srcp
中第一个为 1 的比特位,其定义如下:
// file: include/linux/cpumask.h
/**
* cpumask_first - get the first cpu in a cpumask
* @srcp: the cpumask pointer
*
* Returns >= nr_cpu_ids if no cpus set.
*/
static inline unsigned int cpumask_first(const struct cpumask *srcp)
{
return find_first_bit(cpumask_bits(srcp), nr_cpumask_bits);
}
内部调用了位图接口 find_first_bit()
完成实际的查找功能。
4.14 cpumask_next()
cpumask_next()
函数查找位图掩码 srcp
中,n
位之后(不包括 n
)的第一个为 1 的比特位,该函数定义如下:
// file: include/linux/cpumask.h
/**
* cpumask_next - get the next cpu in a cpumask
* @n: the cpu prior to the place to search (ie. return will be > @n)
* @srcp: the cpumask pointer
*
* Returns >= nr_cpu_ids if no further cpus set.
*/
static inline unsigned int cpumask_next(int n, const struct cpumask *srcp)
{
/* -1 is a legal arg here. */
if (n != -1)
cpumask_check(n);
return find_next_bit(cpumask_bits(srcp), nr_cpumask_bits, n+1);
}
当比特位 n
不等于 -1
时,调用 cpumask_check()
函数检查 n
是否超出最大值。如果超出,会打印警告信息。
然后,调用位图接口 find_next_bit()
,从 n+1
位开始,搜索第一个为 1 的比特位,并返回其索引。
4.15 cpumask_next_zero()
cpumask_next_zero()
函数查找位图掩码 srcp
中,n
位之后(不包括 n
)的第一个为 0 的比特位,该函数定义如下:
// file: include/linux/cpumask.h
/**
* cpumask_next_zero - get the next unset cpu in a cpumask
* @n: the cpu prior to the place to search (ie. return will be > @n)
* @srcp: the cpumask pointer
*
* Returns >= nr_cpu_ids if no further cpus unset.
*/
static inline unsigned int cpumask_next_zero(int n, const struct cpumask *srcp)
{
/* -1 is a legal arg here. */
if (n != -1)
cpumask_check(n);
return find_next_zero_bit(cpumask_bits(srcp), nr_cpumask_bits, n+1);
}
当比特位 n
不等于 -1
时,调用 cpumask_check()
函数检查 n
是否超出最大值。如果超出,会打印警告信息。
然后,调用 find_next_zero_bit()
函数,从 n+1
位开始,搜索第一个为 0 的比特位,并返回其索引。
4.16 宏 for_each_cpu()
宏 for_each_cpu()
会遍历位图掩码中所有为 1 的位,其定义如下:
// file: include/linux/cpumask.h
/**
* for_each_cpu - iterate over every cpu in a mask
* @cpu: the (optionally unsigned) integer iterator
* @mask: the cpumask pointer
*
* After the loop, cpu is >= nr_cpu_ids.
*/
#define for_each_cpu(cpu, mask) \
for ((cpu) = -1; \
(cpu) = cpumask_next((cpu), (mask)), \
(cpu) < nr_cpu_ids;)
4.17 宏 for_each_cpu_not()
宏 for_each_cpu()
会遍历位图掩码中所有为 0 的位,其定义如下:
/**
* for_each_cpu_not - iterate over every cpu in a complemented mask
* @cpu: the (optionally unsigned) integer iterator
* @mask: the cpumask pointer
*
* After the loop, cpu is >= nr_cpu_ids.
*/
#define for_each_cpu_not(cpu, mask) \
for ((cpu) = -1; \
(cpu) = cpumask_next_zero((cpu), (mask)), \
(cpu) < nr_cpu_ids;)
4.18 统计不同状态的处理器数量
4.18.1 num_online_cpus()
4.18.2 num_possible_cpus()
4.18.3 num_present_cpus()
4.18.4 num_active_cpus()
内核中提供了一组宏,用来统计不同状态下的 CPU
数量:
num_online_cpus()
:统计在线CPU
数量num_possible_cpus()
:统计系统支持的最大CPU
数量num_present_cpus()
:统计已插入并可用的CPU
数量num_active_cpus()
:统计active
状态下的 CPU 数量
// file: include/linux/cpumask.h
#define num_online_cpus() cpumask_weight(cpu_online_mask)
#define num_possible_cpus() cpumask_weight(cpu_possible_mask)
#define num_present_cpus() cpumask_weight(cpu_present_mask)
#define num_active_cpus() cpumask_weight(cpu_active_mask)
这些宏内部,又调用了位图接口cpumask_weight()
计算出指定位图掩码中为 1 的比特位数量。
// file: include/linux/cpumask.h
/**
* cpumask_weight - Count of bits in *srcp
* @srcp: the cpumask to count bits (< nr_cpu_ids) in.
*/
static inline unsigned int cpumask_weight(const struct cpumask *srcp)
{
return bitmap_weight(cpumask_bits(srcp), nr_cpumask_bits);
}
bitmap_weight
函数用于计算位图中为 1 的比特位数量,其实现详见 :Linux Kernel:内核数据结构之位图(Bitmap)。
4.19 测试 CPU 状态
4.19.1 cpu_online()
4.19.2 cpu_possible()
4.19.3 cpu_present()
4.19.4 cpu_active()
内核提供了 4 个宏,用来测试 CPU
的不同状态:
cpu_online()
-- 测试CPU
是否处于在线状态cpu_possible()
-- 测试CPU
是否在系统支持的最大范围cpu_present()
-- 测试CPU
是否存在cpu_active()
-- 测试CPU
是否处于 active 状态
// file: include/linux/cpumask.h
#define cpu_online(cpu) cpumask_test_cpu((cpu), cpu_online_mask)
#define cpu_possible(cpu) cpumask_test_cpu((cpu), cpu_possible_mask)
#define cpu_present(cpu) cpumask_test_cpu((cpu), cpu_present_mask)
#define cpu_active(cpu) cpumask_test_cpu((cpu), cpu_active_mask)
这 4 个宏内部,调用了上文介绍的 cpumask_test_cpu()
函数来完成具体工作。
4.20 设置 CPU 状态
4.20.1 set_cpu_possible()
4.20.2 set_cpu_present()
4.20.3 set_cpu_online()
4.20.4 set_cpu_active()
内核提供了 4 个函数,用来在不同的位图掩码中设置 CPU
的状态。
// file: include/linux/cpumask.h
/* Wrappers for arch boot code to manipulate normally-constant masks */
void set_cpu_possible(unsigned int cpu, bool possible);
void set_cpu_present(unsigned int cpu, bool present);
void set_cpu_online(unsigned int cpu, bool online);
void set_cpu_active(unsigned int cpu, bool active);
这 4 个函数,定义在文件 kernel/cpu.c
中,其实现过程也比较类似:
// file: kernel/cpu.c
void set_cpu_possible(unsigned int cpu, bool possible)
{
if (possible)
cpumask_set_cpu(cpu, to_cpumask(cpu_possible_bits));
else
cpumask_clear_cpu(cpu, to_cpumask(cpu_possible_bits));
}
void set_cpu_present(unsigned int cpu, bool present)
{
if (present)
cpumask_set_cpu(cpu, to_cpumask(cpu_present_bits));
else
cpumask_clear_cpu(cpu, to_cpumask(cpu_present_bits));
}
void set_cpu_online(unsigned int cpu, bool online)
{
if (online)
cpumask_set_cpu(cpu, to_cpumask(cpu_online_bits));
else
cpumask_clear_cpu(cpu, to_cpumask(cpu_online_bits));
}
void set_cpu_active(unsigned int cpu, bool active)
{
if (active)
cpumask_set_cpu(cpu, to_cpumask(cpu_active_bits));
else
cpumask_clear_cpu(cpu, to_cpumask(cpu_active_bits));
}
我们以 set_cpu_possible()
函数来说明其实现细节,其它函数与之类似。
set_cpu_possible()
函数接收 2 个参数:
cpu
-- 要设置的CPU
编号possible
-- 要设置的值,布尔类型。
如果 possible
为真,说明是要将位图中 CPU
对应的比特位置位,则调用 cpumask_set_cpu()
函数进行处理;否则,说明是要将位图中 CPU
对应的比特位清除,则调用 cpumask_clear_cpu()
函数进行处理。
4.21 CPU 位图遍历
4.21.1 宏 for_each_possible_cpu()
4.21.2 宏 for_each_online_cpu()
4.21.3 宏 for_each_present_cpu()
内核定义了 3 个宏,用来遍历对应位图掩码中所有置位的比特位:
// file: include/linux/cpumask.h
#define for_each_possible_cpu(cpu) for_each_cpu((cpu), cpu_possible_mask)
#define for_each_online_cpu(cpu) for_each_cpu((cpu), cpu_online_mask)
#define for_each_present_cpu(cpu) for_each_cpu((cpu), cpu_present_mask)
这 3 个宏内部又调用了 for_each_cpu
宏完成遍历工作,该宏实现详见上文。
4.22 宏 cpu_none_mask
宏 cpu_none_mask
将位图 cpu_bit_bitmap[0]
转换成 cpumask
结构体。cpu_bit_bitmap[0]
中的所有比特位全部为 0,相当于一个空的位图。
// file: include/linux/cpumask.h
/* First bits of cpu_bit_bitmap are in fact unset. */
#define cpu_none_mask to_cpumask(cpu_bit_bitmap[0])
4.23 宏 cpumask_of() / get_cpu_mask() 函数
宏cpumask_of()
接收 1 个参数,即 CPU
编号;返回一个 CPU
位图掩码,该掩码里只有CPU
对应的比特位置位,其余比特位全部为 0。该宏定义如下:
// file: include/linux/cpumask.h
/**
* cpumask_of - the cpumask containing just a given cpu
* @cpu: the cpu (<= nr_cpu_ids)
*/
#define cpumask_of(cpu) (get_cpu_mask(cpu))
cpumask_of()
内部调用了 get_cpu_mask()
函数,其实现如下:
// file: include/linux/cpumask.h
static inline const struct cpumask *get_cpu_mask(unsigned int cpu)
{
const unsigned long *p = cpu_bit_bitmap[1 + cpu % BITS_PER_LONG];
p -= cpu / BITS_PER_LONG;
return to_cpumask(p);
}
可以看到,get_cpu_mask()
函数是从cpu_bit_bitmap
中截取一段作为掩码返回的。我们在上文已经介绍过,cpu_bit_bitmap
是一个拥有 65 个成员的数组,每个数组成员是容量为 NR_CPUS
的位图。cpu_bit_bitmap
的特殊之处在于,除索引为 0 的成员外,其它每个成员都只有一个位被置位,且有如下规律:
cpu_bit_bitmap[][] = {
[1][0] = 1 << 0,
[2][0] = 1 << 1,
[3][0] = 1 << 2,
[4][0] = 1 << 3,
...
[64][0] = 1 << 63
}
由于位图是使用 unsigned long
数组来表示的, cpu % BITS_PER_LONG
得到 cpu
在某个数组成员中的比特位索引。由于我们要返回 cpu
对应位为 1 的位图,在 cpu_bit_bitmap
的 65 个 成员中,只有 cpu_bit_bitmap[1 + cpu % BITS_PER_LONG]
中 cpu
对应的比特位是 1,所以从该地址向后推进 cpu / BITS_PER_LONG
个数组成员,就得到位图的起始地址,该位图的 cpu
位为 1。
比如,当 cpu
为 68 时,它肯定位于某个成员的第 4(68 % 64
) 位;cpu_bit_bitmap[5]
的第 4 位为 1;我们往后推进 68 / 64
个成员(即 1 个成员,64位),就得到位图起始地址。该位图的第 68 位为 1。
最后调用 to_cpumask()
函数将位图转换成 struct cpumask
类型并返回。
get_cpu_mask
示意图:
五、CPU 状态的变化
5.1 通知链(notifier chains)机制简介
通知链机制是内核中的异步事件通知机制,当订阅者收到事件通知时,会执行相应的回调函数。
通知链分为 4 种类型:Atomic notifier chains,Blocking notifier chains,Raw notifier chains,SRCU notifier chains。
内核注释中,对这 4 种通知链说明如下:
// file: include/linux/notifier.h
/*
* Notifier chains are of four types:
*
* Atomic notifier chains: Chain callbacks run in interrupt/atomic
* context. Callouts are not allowed to block.
* Blocking notifier chains: Chain callbacks run in process context.
* Callouts are allowed to block.
* Raw notifier chains: There are no restrictions on callbacks,
* registration, or unregistration. All locking and protection
* must be provided by the caller.
* SRCU notifier chains: A variant of blocking notifier chains, with
* the same restrictions.
*
* atomic_notifier_chain_register() may be called from an atomic context,
* but blocking_notifier_chain_register() and srcu_notifier_chain_register()
* must be called from a process context. Ditto for the corresponding
* _unregister() routines.
*
* atomic_notifier_chain_unregister(), blocking_notifier_chain_unregister(),
* and srcu_notifier_chain_unregister() _must not_ be called from within
* the call chain.
*
* SRCU notifier chains are an alternative form of blocking notifier chains.
* They use SRCU (Sleepable Read-Copy Update) instead of rw-semaphores for
* protection of the chain links. This means there is _very_ low overhead
* in srcu_notifier_call_chain(): no cache bounces and no memory barriers.
* As compensation, srcu_notifier_chain_unregister() is rather expensive.
* SRCU notifier chains should be used when the chain will be called very
* often but notifier_blocks will seldom be removed. Also, SRCU notifier
* chains are slightly more difficult to use because they require special
* runtime initialization.
*/
在本文中,只关注 Raw notifier chains。
5.1.1 数据结构
通知链是由不同链块组成的单向链表,链表头和链表块分别有不同的数据类型。
链表头为 raw_notifier_head
结构体类型,该结构体只有一个指向链块(notifier_block
)的指针 head
。
// file: include/linux/notifier.h
struct raw_notifier_head {
struct notifier_block __rcu *head;
};
链块为 notifier_block
结构体类型,其定义如下:
// file: include/linux/notifier.h
struct notifier_block {
notifier_fn_t notifier_call;
struct notifier_block __rcu *next;
int priority;
};
链块包含三个成员:
notifier_call
-- 回调函数的指针;next
-- 指向下一个链块的指针;priority
-- 链块的优先级,决定链块在链表中的位置,优先级越高,位置越靠前。
回调函数 notifier_call
为 notifier_fn_t
类型,该类型定义如下:
// file: include/linux/notifier.h
typedef int (*notifier_fn_t)(struct notifier_block *nb,
unsigned long action, void *data);
该类型函数接收 3 个参数:
nb
-- 链块指针;action
-- 事件类型。在通知链中,包含多种事件的回调函数。该参数用来区分不同的事件。data
-- 附加的数据。
5.1.2 链表头的初始化
宏 RAW_NOTIFIER_HEAD
用来对 raw
类型通知链的表头进行初始化,该宏定义如下:
// file: include/linux/notifier.h
#define RAW_NOTIFIER_HEAD(name) \
struct raw_notifier_head name = \
RAW_NOTIFIER_INIT(name)
该宏内部又引用了宏 RAW_NOTIFIER_INIT
,其定义如下:
// file: include/linux/notifier.h
#define RAW_NOTIFIER_INIT(name) { \
.head = NULL }
CPU
通知链的表头定义如下:
// kernel/cpu.c
static RAW_NOTIFIER_HEAD(cpu_chain);
使用宏 RAW_NOTIFIER_HEAD
定义了一个名称为 cpu_chain
的链表头。
5.1.3 APIs
注册函数 -- raw_notifier_chain_register()
raw_notifier_chain_register()
函数接收 2 个参数:
nh
-- 链表头指针n
-- 要注册的链表块指针
该函数会将 n
指向的链表块插入以 nh
为表头的链表中,该函数定义如下:
/*
* Raw notifier chain routines. There is no protection;
* the caller must provide it. Use at your own risk!
*/
/**
* raw_notifier_chain_register - Add notifier to a raw notifier chain
* @nh: Pointer to head of the raw notifier chain
* @n: New entry in notifier chain
*
* Adds a notifier to a raw notifier chain.
* All locking must be provided by the caller.
*
* Currently always returns zero.
*/
int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *n)
{
return notifier_chain_register(&nh->head, n);
}
该函数内部,会调用 notifier_chain_register()
函数完成实际的注册工作:
// file: kernel/notifier.c
/*
* Notifier chain core routines. The exported routines below
* are layered on top of these, with appropriate locking added.
*/
static int notifier_chain_register(struct notifier_block **nl,
struct notifier_block *n)
{
while ((*nl) != NULL) {
if (n->priority > (*nl)->priority)
break;
nl = &((*nl)->next);
}
n->next = *nl;
rcu_assign_pointer(*nl, n);
return 0;
}
notifier_chain_register()
函数会遍历链表,将链表块插入到链表中。连标块的优先级越大,在链表中的位置越靠前。也就是说,链表元素是按照优先级从大到小的顺序排列的。
执行回调函数 -- raw_notifier_call_chain()
当发出事件通知时,最终会调用 raw_notifier_call_chain()
函数来执行通知链中的回调函数。
raw_notifier_call_chain()
函数定义如下:
// file: kernel/notifier.c
int raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v)
{
return __raw_notifier_call_chain(nh, val, v, -1, NULL);
}
该函数接收 3 个参数:
nh
-- 通知链的表头val
-- 通知类型v
-- 回调函数所用的参数
该函数内部又调用了 __raw_notifier_call_chain()
函数来完成实际功能。
__raw_notifier_call_chain()
函数定义如下:
// file: kernel/notifier.c
/**
* __raw_notifier_call_chain - Call functions in a raw notifier chain
* @nh: Pointer to head of the raw notifier chain
* @val: Value passed unmodified to notifier function
* @v: Pointer passed unmodified to notifier function
* @nr_to_call: See comment for notifier_call_chain.
* @nr_calls: See comment for notifier_call_chain
*
* Calls each function in a notifier chain in turn. The functions
* run in an undefined context.
* All locking must be provided by the caller.
*
* If the return value of the notifier can be and'ed
* with %NOTIFY_STOP_MASK then raw_notifier_call_chain()
* will return immediately, with the return value of
* the notifier function which halted execution.
* Otherwise the return value is the return value
* of the last notifier function called.
*/
int __raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
return notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls);
}
__raw_notifier_call_chain()
函数接收 5 个参数:
nh
-- 链表头指针val
-- 通知类型v
-- 回调函数参数nr_to_call
-- 回调函数被调用的次数,为-1
时表示不做限制nr_calls
-- 用于记录通知发送的次数
__raw_notifier_call_chain()
函数内部调用 notifier_call_chain()
函数来执行回调。 notifier_call_chain()
函数定义如下:
// file: kernel/notifier.c
/**
* notifier_call_chain - Informs the registered notifiers about an event.
* @nl: Pointer to head of the blocking notifier chain
* @val: Value passed unmodified to notifier function
* @v: Pointer passed unmodified to notifier function
* @nr_to_call: Number of notifier functions to be called. Don't care
* value of this parameter is -1.
* @nr_calls: Records the number of notifications sent. Don't care
* value of this field is NULL.
* @returns: notifier_call_chain returns the value returned by the
* last notifier function called.
*/
static int __kprobes notifier_call_chain(struct notifier_block **nl,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
int ret = NOTIFY_DONE;
struct notifier_block *nb, *next_nb;
nb = rcu_dereference_raw(*nl);
while (nb && nr_to_call) {
next_nb = rcu_dereference_raw(nb->next);
...
ret = nb->notifier_call(nb, val, v);
if (nr_calls)
(*nr_calls)++;
if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)
break;
nb = next_nb;
nr_to_call--;
}
return ret;
}
notifier_call_chain()
函数执行时,会遍历链表,逐个执行链表块中注册的回调函数,直到达到 nr_to_call
限制的次数或遍历到链表末尾。在回调函数中,会判断消息类型,如果不是自己关注的消息,不做任何处理,直接返回。
5.2 BSP 掩码位的填充 -- boot_cpu_init()
BSP(Bootstrap Processor)启动时,在 boot_cpu_init()
函数中,将该 CPU
对应的各状态位置位。
// file: init/main.c
/*
* Activate the first processor.
*/
static void __init boot_cpu_init(void)
{
int cpu = smp_processor_id();
/* Mark the boot cpu "present", "online" etc for SMP and UP case */
set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);
}
5.3 AP 掩码位的填充 -- smp_init()
在系统初始化后期,BSP 会调用 smp_init()
函数启动其它的处理器(Application Processor,AP)。在 smp_init()
函数内部,会调用 for_each_present_cpu()
函数,遍历 cpu_present_mask
中被置位的位,如果比特位对应的 CPU
没有上线,即 !cpu_online(cpu)
为 true
,则调用 cpu_up()
函数启动该 CPU
。另外,注意到,当在线的 CPU
数量大于等于setup_max_cpus
时,直接跳出循环。还记得么,setup_max_cpus
限制了系统启动的 CPU
数量,其默认值是内核配置 CONFIG_NR_CPUS
,但可以通过命令行参数 maxcpus
来修改。
// file: kernel/smp.c
/* Called by boot processor to activate the rest. */
void __init smp_init(void)
{
...
/* FIXME: This should be done in userspace --RR */
for_each_present_cpu(cpu) {
if (num_online_cpus() >= setup_max_cpus)
break;
if (!cpu_online(cpu))
cpu_up(cpu);
}
...
}
系统启动时,除了 BSP 外,其它处理器都处于 halt
状态。BSP 调用 cpu_up()
函数,通知其它处理器(AP)启动。AP 启动后,会执行 start_secondary()
函数,该函数内部,会调用 smp_callin()
函数以及 set_cpu_online()
函数。在 smp_callin()
函数中,会执行回调函数 sched_cpu_active()
,将 CPU
设置为 active
状态;然后执行set_cpu_online()
函数将 CPU
设置为 online
状态。
// arch/x86/kernel/smpboot.c
/*
* Activate a secondary processor.
*/
notrace static void __cpuinit start_secondary(void *unused)
{
...
smp_callin();
...
set_cpu_online(smp_processor_id(), true);
...
}
smp_callin()
函数内部会调用notify_cpu_starting()
函数,来执行事件回调函数。
// file: arch/x86/kernel/smpboot.c
/*
* Report back to the Boot Processor during boot time or to the caller processor
* during CPU online.
*/
static void __cpuinit smp_callin(void)
{
...
notify_cpu_starting(cpuid);
...
}
notify_cpu_starting()
函数内部,会触发 CPU_STARTING
事件,并执行系统启动时注册的事件回调函数。
// file: kernel/cpu.c
/**
* notify_cpu_starting(cpu) - call the CPU_STARTING notifiers
* @cpu: cpu that just started
*
* This function calls the cpu_chain notifiers with CPU_STARTING.
* It must be called by the arch code on the new cpu, before the new cpu
* enables interrupts and before the "boot" cpu returns from __cpu_up().
*/
void __cpuinit notify_cpu_starting(unsigned int cpu)
{
unsigned long val = CPU_STARTING;
...
cpu_notify(val, (void *)(long)cpu);
}
cpu_notify()
函数内部,调用了 __cpu_notify()
函数:
// file: kernel/cpu.c
static int cpu_notify(unsigned long val, void *v)
{
return __cpu_notify(val, v, -1, NULL);
}
__cpu_notify()
函数,调用了 __raw_notifier_call_chain()
函数:
// file: kernel/cpu.c
static int __cpu_notify(unsigned long val, void *v, int nr_to_call,
int *nr_calls)
{
int ret;
ret = __raw_notifier_call_chain(&cpu_chain, val, v, nr_to_call,
nr_calls);
return notifier_to_errno(ret);
}
__raw_notifier_call_chain()
函数,又调用了 notifier_call_chain()
函数。在 notifier_call_chain()
函数中,会遍历链表中的每一个成员,执行成员中的回调函数。
__raw_notifier_call_chain()
函数及 __raw_notifier_call_chain()
函数的实现,见 "5.1 通知链机制简介"。
5.3.1 sched_cpu_active()
回调函数
CPU_STARTING
事件的回调函数为 sched_cpu_active
,该函数会调用 set_cpu_active()
函数,将 CPU
设置为 acitve
状态。
// file: kernel/sched/core.c
static int __cpuinit sched_cpu_active(struct notifier_block *nfb,
unsigned long action, void *hcpu)
{
switch (action & ~CPU_TASKS_FROZEN) {
case CPU_STARTING:
case CPU_DOWN_FAILED:
set_cpu_active((long)hcpu, true);
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
该回调函数是何时注册的呢?在内核启动时,通过 early_initcall()
注册了 migration_init()
函数;在 migration_init()
函数中,调用 cpu_notifier()
函数注册了 CPU_STARTING
事件的回调函数 sched_cpu_active()
;与其一起注册的,还有 CPU_DOWN_PREPARE
事件的回调函数 sched_cpu_inactive()
。
// file: kernel/sched/core.c
static int __init migration_init(void)
{
...
/* Register cpu active notifiers */
cpu_notifier(sched_cpu_active, CPU_PRI_SCHED_ACTIVE);
cpu_notifier(sched_cpu_inactive, CPU_PRI_SCHED_INACTIVE);
...
}
early_initcall(migration_init);
通过 early_initcall()
注册的函数,会在系统启动时在 do_pre_smp_initcalls()
函数执行时被调用,其调用链路为:
start_kernel()
-> rest_init()
-> kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND)
-> kernel_init()
-> kernel_init_freeable()
->do_pre_smp_initcalls()
。
// file: init/main.c
static void __init do_pre_smp_initcalls(void)
{
initcall_t *fn;
for (fn = __initcall_start; fn < __initcall0_start; fn++)
do_one_initcall(*fn);
}
在 do_pre_smp_initcalls()
函数中,通过 early_initcall()
注册的每一个函数都会被执行。
另外,由于 do_pre_smp_initcalls()
函数在 smp_init()
之前执行,所以在初始化 SMP 系统之前,sched_cpu_active()
和 sched_cpu_inactive()
函数已经被注册到通知链中了。
migration_init()
函数中,宏 CPU_PRI_SCHED_ACTIVE
和 CPU_PRI_SCHED_INACTIVE
是消息优先级,数值越高,优先级越大,在通知链中的位置越靠前,越先被执行。
cpu_notifier()
宏定义如下:
// file: include/linux/cpu.h
#define cpu_notifier(fn, pri) { \
static struct notifier_block fn##_nb __cpuinitdata = \
{ .notifier_call = fn, .priority = pri }; \
register_cpu_notifier(&fn##_nb); \
}
cpu_notifier()
宏将回调函数 fn
以及事件优先级 pri
组合成链块 notifier_block
,然后调用 register_cpu_notifier()
函数将回调函数注册到通知链中。
register_cpu_notifier()
函数内部调用 raw_notifier_chain_register()
将完成实际的注册工作:
// file: kernel/cpu.c
/* Need to know about CPUs going up/down? */
int __ref register_cpu_notifier(struct notifier_block *nb)
{
...
ret = raw_notifier_chain_register(&cpu_chain, nb);
...
}
raw_notifier_chain_register()
函数的具体实现请参考 “5.1 通知链机制简介”。
SMP 系统启动过程中,与 CPU
状态变化相关的函数:
注意,红色背景区的函数修改了 CPU
的状态。notify_cpu_starting()
函数,将 CPU
设置为 active
状态,set_cpu_online()
函数将 CPU
设置为 online
状态。
5.4 掩码位的清除 -- cpu_down()
当处理器需要下线时,会调用 cpu_down()
函数。
// file: kernel/cpu.c
int __ref cpu_down(unsigned int cpu)
{
...
err = _cpu_down(cpu, 0);
...
}
cpu_down()
函数内部,会调用 _cpu_down()
函数。
// file: kernel/cpu.c
/* Requires cpu_add_remove_lock to be held */
static int __ref _cpu_down(unsigned int cpu, int tasks_frozen)
{
...
err = __cpu_notify(CPU_DOWN_PREPARE | mod, hcpu, -1, &nr_calls);
if (err) {
...
__cpu_notify(CPU_DOWN_FAILED | mod, hcpu, nr_calls, NULL);
...
}
...
err = __stop_machine(take_cpu_down, &tcd_param, cpumask_of(cpu));
if (err) {
...
cpu_notify_nofail(CPU_DOWN_FAILED | mod, hcpu);
...
}
...
/* CPU is completely dead: tell everyone. Too late to complain. */
cpu_notify_nofail(CPU_DEAD | mod, hcpu);
out_release:
...
if (!err)
cpu_notify_nofail(CPU_POST_DEAD | mod, hcpu);
return err;
}
在 _cpu_down()
函数内部,首先调用 __cpu_notify()
函数,发出 CPU_DOWN_PREPARE
事件通知。我们在上文介绍过,CPU_DOWN_PREPARE
事件的回调函数是 sched_cpu_inactive()
函数,该函数定义如下:
// file: kernel/sched/core.c
static int __cpuinit sched_cpu_inactive(struct notifier_block *nfb,
unsigned long action, void *hcpu)
{
switch (action & ~CPU_TASKS_FROZEN) {
case CPU_DOWN_PREPARE:
set_cpu_active((long)hcpu, false);
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
sched_cpu_inactive()
函数内部,会调用 set_cpu_active()
函数,将 cpu_active_mask
中对应 CPU
的比特位清除。
接下来执行 __stop_machine()
函数,该函数声明如下:
// file: include/linux/stop_machine.h
/**
* __stop_machine: freeze the machine on all CPUs and run this function
* @fn: the function to run
* @data: the data ptr for the @fn
* @cpus: the cpus to run the @fn() on (NULL = any online cpu)
*
* Description: This is a special version of the above, which assumes cpus
* won't come or go while it's being called. Used by hotplug cpu.
*/
int __stop_machine(int (*fn)(void *), void *data, const struct cpumask *cpus);
__stop_machine()
接收 3 个参数:
fn
-- 需要运行的函数data
--fn
函数的参数cpus
-- 允许执行fn
函数的 cpu 集合
在我们的案例中,会执行take_cpu_down()
函数,其定义如下:
// file: kernel/cpu.c
/* Take this CPU down. */
static int __ref take_cpu_down(void *_param)
{
struct take_cpu_down_param *param = _param;
int err;
/* Ensure this CPU doesn't handle any more interrupts. */
err = __cpu_disable();
if (err < 0)
return err;
cpu_notify(CPU_DYING | param->mod, param->hcpu);
/* Park the stopper thread */
kthread_park(current);
return 0;
}
在 take_cpu_down()
函数内,会执行 __cpu_disable()
函数并调用 cpu_notify()
发出 CPU_DYING
事件通知。
在 __cpu_disable()
函数内,会执行 cpu_disable()
函数:
// file: arch/x86/include/asm/smp.h
static inline int __cpu_disable(void)
{
return smp_ops.cpu_disable();
}
smp_ops
结构体定义了 SMP 系统的不同操作,其中 cpu_disable
操作实际执行的是 native_cpu_disable()
函数。
// file: arch/x86/kernel/smp.c
struct smp_ops smp_ops = {
...
.cpu_disable = native_cpu_disable,
...
};
native_cpu_disable()
函数定义如下:
// file: arch/x86/kernel/smpboot.c
int native_cpu_disable(void)
{
clear_local_APIC();
cpu_disable_common();
return 0;
}
native_cpu_disable()
函数内会调用 cpu_disable_common()
函数,其定义如下:
// file:
void cpu_disable_common(void)
{
int cpu = smp_processor_id();
...
remove_cpu_from_maps(cpu);
...
}
cpu_disable_common()
函数内会调用 remove_cpu_from_maps()
函数:
static void __ref remove_cpu_from_maps(int cpu)
{
set_cpu_online(cpu, false);
...
}
在 remove_cpu_from_maps()
函数内,会调用 set_cpu_online()
函数,将指定 cpu
修改为下线状态。
cpu_down()
函数中,与 CPU
状态变化有关的函数如下图所示:
其中,红色背景的函数会修改 CPU
的状态。CPU_DOWN_PREPARE
事件会清除 CPU
的 active
状态;__cpu_disable()
函数最终会调用 set_cpu_online()
函数清除 CPU
的 online
状态。
5.5 cpu_online 与 cpu_active 的区别
在前面的小节中,我们看到,CPU 启动时,先是达到 active
状态,然后才达到 online
状态。在移除 CPU
时,先是清除 active
状态,然后才清除 online
状态。在启动时,先达到 active
状态,说明此时 CPU
活跃的,可以从其它处理器迁移一些任务过来,然后打开 online
状态,就可以调度任务了。在移除 CPU
时,先清除 active
状态,实际是告诉内核,我要下线了,不要再给我分配任务了;而且,下线之前还有些工作要做,比如说把自己任务队列的任务迁移给其它处理器,等这些准备工作都做完了,就可以清除 online
状态,变成下线状态了。
六、参考资料
4、Linux Kernel:内核数据结构之位图(Bitmap)
5、Cleaning Up Linux’s CPU Hotplug For Real Time and Energy Management