三、进阶 | Linux 内核概念(3):CPU masks

CPU masks

介绍

Cpumasks 是Linux内核提供的保存系统CPU信息的特殊方法。包含 Cpumasks 操作 API 相关的源码和头文件:

正如 include/linux/cpumask.h 注释:Cpumasks 提供了代表系统中 CPU 集合的位图,一位放置一个 CPU 序号。我们已经在 Kernel entry point 部分,函数 boot_cpu_init 中看到了一点 cpumask。这个函数将第一个启动的 cpu 上线、激活等等……

set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);

set_cpu_possible 是一个在系统启动时任意时刻都可插入的 cpu ID 集合。cpu_present 代表了当前插入的 CPUs。cpu_onlinecpu_present 的子集,表示可调度的 CPUs。这些掩码依赖于 CONFIG_HOTPLUG_CPU 配置选项,以及 possible == presentactive == online 选项是否被禁用。这些函数的实现很相似,检测第二个参数,如果为 true,就调用 cpumask_set_cpu ,否则调用 cpumask_clear_cpu

有两种方法创建 cpumask。第一种是用 cpumask_t。定义如下:

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

它封装了 cpumask 结构,其包含了一个位掩码 bits 字段。DECLARE_BITMAP 宏有两个参数:

  • bitmap name;
  • number of bits.

并以给定名称创建了一个 unsigned long 数组。它的实现非常简单:

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

其中 BITS_TO_LONGS

#define BITS_TO_LONGS(nr)       DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

因为我们专注于 x86_64 架构,unsigned long 是8字节大小,因此我们的数组仅包含一个元素:

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

NR_CPUS 宏表示的是系统中 CPU 的数目,且依赖于在 include/linux/threads.h 中定义的 CONFIG_NR_CPUS 宏,看起来像这样:

#ifndef CONFIG_NR_CPUS
        #define CONFIG_NR_CPUS  1
#endif

#define NR_CPUS         CONFIG_NR_CPUS

第二种定义 cpumask 的方法是直接使用宏 DECLARE_BITMAPto_cpumask 宏,后者将给定的位图转化为 struct cpumask *

#define to_cpumask(bitmap)                                              \
        ((struct cpumask *)(1 ? (bitmap)                                \
                            : (void *)sizeof(__check_is_bitmap(bitmap))))

可以看到这里的三目运算符每次总是 true__check_is_bitmap 内联函数定义为:

static inline int __check_is_bitmap(const unsigned long *bitmap)
{
        return 1;
}

每次都是返回 1。我们需要它只是因为:编译时检测一个给定的 bitmap 是一个位图,换句话说,它检测一个 bitmap 是否有 unsigned long * 类型。因此我们传递 cpu_possible_bits 给宏 to_cpumask ,将 unsigned long 数组转换为 struct cpumask *

cpumask API

因为我们可以用其中一个方法来定义 cpumask,Linux 内核提供了 API 来处理 cpumask。我们来研究下其中一个函数,例如 set_cpu_online,这个函数有两个参数:

  • CPU 数目;
  • CPU 状态;

这个函数的实现如下所示:

void set_cpu_online(unsigned int cpu, bool online)
{
	if (online) {
		cpumask_set_cpu(cpu, to_cpumask(cpu_online_bits));
		cpumask_set_cpu(cpu, to_cpumask(cpu_active_bits));
	} else {
		cpumask_clear_cpu(cpu, to_cpumask(cpu_online_bits));
	}
}

该函数首先检测第二个 state 参数并调用依赖它的 cpumask_set_cpucpumask_clear_cpu。这里我们可以看到在中 cpumask_set_cpu 的第二个参数转换为 struct cpumask *。在我们的例子中是位图 cpu_online_bits,定义如下:

static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;

函数 cpumask_set_cpu 仅调用了一次 set_bit 函数:

static inline void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp)
{
        set_bit(cpumask_check(cpu), cpumask_bits(dstp));
}

set_bit 函数也有两个参数,设置了一个给定位(第一个参数)的内存(第二个参数或 cpu_online_bits 位图)。这儿我们可以看到在调用 set_bit 之前,它的两个参数会传递给

  • cpumask_check;
  • cpumask_bits.

让我们细看下这两个宏。第一个 cpumask_check 在我们的例子里没做任何事,只是返回了给的参数。第二个 cpumask_bits 只是返回了传入 struct cpumask * 结构的 bits 域。

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

现在让我们看下 set_bit 的实现:

 static __always_inline void
 set_bit(long nr, volatile unsigned long *addr)
 {
         if (IS_IMMEDIATE(nr)) {
                asm volatile(LOCK_PREFIX "orb %1,%0"
                        : CONST_MASK_ADDR(nr, addr)
                        : "iq" ((u8)CONST_MASK(nr))
                        : "memory");
        } else {
                asm volatile(LOCK_PREFIX "bts %1,%0"
                        : BITOP_ADDR(addr) : "Ir" (nr) : "memory");
        }
 }

这个函数看着吓人,但它没有看起来那么难。首先传参 nr 或者说位数给 IS_IMMEDIATE 宏,该宏调用了 GCC 内联函数 __builtin_constant_p

#define IS_IMMEDIATE(nr)    (__builtin_constant_p(nr))

__builtin_constant_p 检查给定参数是否编译时恒定变量。因为我们的 cpu 不是编译时恒定变量,将会执行 else 分支:

asm volatile(LOCK_PREFIX "bts %1,%0" : BITOP_ADDR(addr) : "Ir" (nr) : "memory");

让我们试着一步一步来理解它如何工作的:

LOCK_PREFIX 是个 x86 lock 指令。这个指令告诉 CPU 当指令执行时占据系统总线。这允许 CPU 同步内存访问,防止多核(或多设备 - 比如 DMA 控制器)并发访问同一个内存cell。

BITOP_ADDR 转换给定参数至 (*(volatile long *) 并且加了 +m 约束。+ 意味着这个操作数对于指令是可读写的。m 显示这是一个内存操作数。BITOP_ADDR 定义如下:

#define BITOP_ADDR(x) "+m" (*(volatile long *) (x))

接下来是 memory。它告诉编译器汇编代码执行内存读或写到某些项,而不是那些输入或输出操作数(例如,访问指向输出参数的内存)。

Ir - 寄存器操作数。

bts 指令设置一个位字符串的给定位,存储给定位的值到 CF 标志位。所以我们传递 cpu 号,我们的例子中为 0,给 set_bit 并且执行后,其设置了在 cpu_online_bits cpumask 中的 0 位。这意味着第一个 cpu 此时上线了。

当然,除了 set_cpu_* API 外,cpumask 提供了其它 cpumasks 操作的 API。让我们简短看下。

附加的 cpumask API

cpumaks 提供了一系列宏来得到不同状态 CPUs 序号。例如:

#define num_online_cpus()	cpumask_weight(cpu_online_mask)

这个宏返回了 online CPUs 数量。它读取 cpu_online_mask 位图并调用了 cpumask_weight 函数。cpumask_weight 函数使用两个参数调用了一次 bitmap_weight 函数:

  • cpumask bitmap;
  • nr_cpumask_bits - 在我们的例子中就是 NR_CPUS
static inline unsigned int cpumask_weight(const struct cpumask *srcp)
{
	return bitmap_weight(cpumask_bits(srcp), nr_cpumask_bits);
}

并计算给定位图的位数。除了 num_online_cpus,cpumask还提供了所有 CPU 状态的宏:

  • num_possible_cpus;
  • num_active_cpus;
  • cpu_online;
  • cpu_possible.

等等。

除了 Linux 内核提供的下述操作 cpumask 的 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' 字节数大小;

还有很多。

链接

Linux嵌入式必考必会 文章被收录于专栏

"《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。

全部评论

相关推荐

09-24 16:22
已编辑
深圳大学 Java
字节广告:1.Java基本数据类型?什么时候包装?什么时候解除包装?自动包装是编译期间做的还是运行期间做的?2.Integer a=1;Integer b=1;a==b;返回的是true还是false?3.Synchronized关键字的作用?JVM对Synchronized做了哪些优化?4.自旋锁,轻量级锁,重量级锁?Java的ThreadLocal类?ThreadLocalMap的key是什么引用?软引用和强引用的区别?ThreadLocal内存泄漏的问题?5.volatile?单处理器有可见性问题吗?如果没有volatile关键字的话,怎么处理可见性问题?6.Java内存模型?CAS操作?怎么规避ABA问题?CAS实现一个乐观锁,给出代码设计?乐观锁和悲观锁的区别?7.操作系统的进程和线程?操作系统的线程和Java的线程有何区别与联系?8.事务的基本特性?InnoDB隔离级别?什么是可重复读?可重复读和读已提交的区别?MVCC原理?9.MySOL的聚集索引?是什么数据结构?B+树和B树的区别?聚集索引如果用平衡二叉树来实现会有什么问题?算法最短路径(bfs)求步数,写完后再加了一个输出最短路径美团基研:1.实习收获2.什么是JVM?3.JVM如何进行内存管理?4.GC流程?Full GC的原因?你说到的Minor GC,如果频繁的Minor GC会导致什么问题?5.CMS和G1?6.MySQL什么时候加索引?7.我有一个sql执行比较慢,怎么办?如果我已经加了索引还是很慢呢?8.为什么选择B+树作为索引结构?9.MySQL的事务隔离级别?分别介绍一下?为什么innodb默认可重复读?MySQL为什么要设置隔离级别?算法重排链表腾讯广告:实习相关挑一个项目介绍,项目提问Java:说一下ThreadLocal。项目中有用到?主要使用场景?使用threadlocal的时候要注意哪些问题?Java的内存模型堆和栈的区别常用的垃圾回收方法解释一下分代收集算法MySQL:常见存储引擎?myisam和innodb 的区别?分别应用的场景是什么?事务的隔离级别,分别介绍应用场景。MVCC,undo log ,redo log 聚集索引,覆盖索引查询什么时候不走索引?手撕:lc387一题sql
点赞 评论 收藏
分享
时间:120min题型:编程*4(20‘+25’+25‘+30’)ACM模式通过了大概80分左右,已约面1、有一个长为n的只有0、1组成的字符串s。需要对s执行k次转置操作。要想使得s的字典序最小,求出最小字典序s。 任意一个位置的0变成1或者1变成0称为一次转置。字典序的比较方法:比较第一个不同的字符。输入第一行为s的长度n和转置操作次数k,第二行为只有'0'和'1'组成的字符串s,输出为k次操作后字典序最小的字符串s2、有一个n个点、n-1条边的树。如果树上存在一个点w,使得原始的树上存在边(u,w)和(w,v),那么可以添加一条边(u,v)。求最多可以添加多少条边。树是指一张任意两个点都连通、且不存在环的一张图。 输入第一行为树上的点数n,之后n-1行,第i行为树上第i条边的节点ui和vi。输出为最多可以添加的边数。3、有一个长度为n的数组a1,a2,...,an,每次询问一个空间[l,r],计算数组a的所有长度大于等于l且小于等于r的子数组之和的最大值是多少。 输入第一行为数组中元素数量n和询问次数q,第二行为数组元素a1,a2,...,an,之后q行每行输入l和r表示询问区间。 输出q行,表示对于每次询问的答案。(对时间复杂度要求比较高)4、有一个长度为n的数组a1,a2,...,an,求出有多少个严格单调递减的子序列。结果可能很大,对10^9+7取模后再输出。【如果对你有帮助能给我送个花花吗】 #字节求职进展汇总#
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务