深入理解 Linux 内核中的 Per-CPU 变量
1. 引言
在大型 NUMA 多核系统中,处理每个 CPU 的数据能确保数据安全和良好的性能,因为此时缓存效应不再是需要关注的问题。本文将详细介绍 Per-CPU 变量的使用,包括其分配、初始化、释放以及读写操作,还会通过示例和内核中的实际用例来加深理解。
2. 操作 Per-CPU 变量的基本规则
在处理 Per-CPU 变量时,必须使用内核提供的辅助方法(宏和 API),而不能直接访问它们,这与引用计数和原子操作符的使用方式类似。下面将分两部分讨论 Per-CPU 数据的辅助 API 和宏:首先是如何分配、初始化和释放 Per-CPU 数据项;其次是如何对其进行读写操作。
3. 分配、初始化和释放 Per-CPU 变量
Per-CPU 变量大致分为静态分配和动态分配两种类型。
-
静态分配
:静态分配的 Per-CPU 变量在编译时就分配了内存,通常使用
DEFINE_PER_CPU()
或
DECLARE_PER_CPU()
宏。使用
DEFINE_PER_CPU()
可以一次性完成变量的分配和初始化。例如,将一个名为
pcpa
的整数静态分配为 Per-CPU 变量,并自动初始化为 0:
#include <linux/percpu.h>
DEFINE_PER_CPU(int, pcpa); // signature: DEFINE_PER_CPU(type, name)
在一个有四个 CPU 核心的系统中,初始化后这个 Per-CPU 变量的概念表示如图 13.9 所示。但实际实现要复杂得多。
-
动态分配
:动态分配 Per-CPU 数据可以通过
alloc_percpu()
或
alloc_percpu_gfp()
包装宏实现。只需传递要分配的对象的数据类型,对于
alloc_percpu_gfp()
还需传递 GFP 掩码:
alloc_percpu[_gfp](type [,gfp]);
这些包装宏调用的底层
__alloc_per_cpu[_gfp]()
例程通过
EXPORT_SYMBOL_GPL()
导出,因此只有在模块以 GPL 兼容许可证发布时才能使用。通过上述例程分配的内存必须使用
void free_percpu(void __percpu *__pdata)
API 释放。
4. 对 Per-CPU 变量进行 I/O 操作(读写)
关键问题是如何访问(读取)和更新(写入)Per-CPU 变量。内核提供了几个辅助例程来实现这一点,下面通过一个简单的例子来理解。
-
静态定义并访问 Per-CPU 变量
:静态定义一个整数 Per-CPU 变量
pcpa
,并在后续访问和打印其当前值。由于是 Per-CPU 变量,检索的值将根据当前代码运行的 CPU 核心自动计算。示例代码如下:
DEFINE_PER_CPU(int, pcpa);
int val;
[ ... ]
val = get_cpu_var(pcpa);
pr_info("cpu0: pcpa = %+d\n", val); // critical section, must be atomic!
put_cpu_var(pcpa);
get_cpu_var()
和
put_cpu_var()
这对宏允许我们安全地检索或修改给定 Per-CPU 变量的 Per-CPU 值。需要注意的是,
get_cpu_var()
和
put_cpu_var()
之间的代码实际上是一个临界区,即原子上下文,此时内核抢占被禁用,任何阻塞(或睡眠)操作都是不允许的。如果在这个临界区执行阻塞操作,会导致内核错误。
-
示例:在临界区使用可能阻塞的 API
:以下代码展示了在
get_cpu_var()
/
put_cpu_var()
对的宏中使用
vmalloc()
进行内存分配可能导致的问题:
void *p;
val = get_cpu_var(pcpa); // disables kernel preemption!
void *vp=vmalloc(1024*1024); // vmalloc(), vfree() are possibly blocking!
mdelay(1); // mdelay(), printk() are non-blocking
vfree(vp);
pr_info("cpu1: pcpa = %+d\n", val);
put_cpu_var(pcpa); // enables kernel preemption
运行上述代码会出现内核错误,因为在原子上下文中执行了可能阻塞的
vmalloc()
操作。实际上,
get_cpu_var()
宏会调用
preempt_disable()
禁用内核抢占,而
put_cpu_var()
会调用
preempt_enable()
恢复。
-
其他操作
:
get_cpu_var()
是一个左值,可以直接进行操作,例如递增 Per-CPU 的
pcpa
变量:
get_cpu_var(pcpa) ++;
put_cpu_var(pcpa);
还可以通过
per_cpu()
宏安全地检索当前 Per-CPU 值:
per_cpu(var, cpu);
要检索系统中每个 CPU 核心的 Per-CPU
pcpa
变量,可以使用以下代码:
for_each_online_cpu(i) {
val = per_cpu(pcpa, i);
pr_info(" cpu %2d: pcpa = %+d\n", i, val);
}
此外,还可以使用
[raw_]smp_processor_id()
宏来确定当前运行的 CPU 核心。
5. 处理 Per-CPU 变量指针的宏
内核还提供了处理需要作为 Per-CPU 的变量指针的例程,如
{get,put}_cpu_ptr()
和
per_cpu_ptr()
宏。这些宏在处理 Per-CPU 数据结构时经常使用,用于安全地检索当前运行的 CPU 核心上的结构指针。
6. 示例:Per-CPU 内核模块
下面通过一个示例内核模块来演示如何使用 Per-CPU 变量。该模块定义并使用了两个 Per-CPU 变量:一个静态分配并初始化的 Per-CPU 整数,以及一个动态分配的 Per-CPU 数据结构。
// ch13/3_lockfree/percpu/percpu_var.c
[ ... ]
/*--- The percpu variables, an integer 'pcpa' and a data structure --- */
/* This percpu integer 'pcpa' is statically allocated and initialized to 0;
* one integer instance will be allocated per CPU core! */
DEFINE_PER_CPU(int, pcpa);
/* This "driver context" per-cpu structure will be dynamically allocated
* via alloc_percpu() */
static struct drv_ctx {
int tx, rx; /* here, as a demo, we just use these two members,
ignoring the rest */
[ ... ]
} *pcp_ctx;
[ ... ]
static int __init init_percpu_var(void)
{
[ ... ]
/* Dynamically allocate the per-cpu structures; one structure instance
* will be allocated per CPU core! (If you want to specify the GFP flags,
then use the alloc_percpu_gfp(type, gfp) macro instead) */
ret = -ENOMEM;
pcp_ctx = (struct drv_ctx __percpu *) alloc_percpu(struct drv_ctx);
if (!pcp_ctx) {
[ ... ]
}
该模块创建了两个内核线程
thrd_0
和
thrd_1
,并使用
sched_setaffinity()
API 将
thrd_0
绑定到 CPU 0,将
thrd_1
绑定到 CPU 1。由于这些是 Per-CPU 变量,并且线程在不同的核心上运行,因此可以在不使用任何锁的情况下并发更新它们。内核线程的工作例程如下:
/* Our kernel thread worker routine [...] */
static int thrd_work(void *arg)
{
int i, val;
long thrd = (long)arg;
struct drv_ctx *ctx;
[ ... ]
/* Set CPU affinity mask to 'thrd', which is either 0 or 1 */
if (set_cpuaffinity(thrd) < 0) {
[ ... ]
} [ … ]
SHOW_CPU_CTX();
if (thrd == 0) { /* our kthread #0 runs on CPU 0 */
for (i=0; i<THRD0_ITERS; i++) {
/* Operate on our perpcu integer */
val = ++ get_cpu_var(pcpa);
pr_info(" thrd_0/cpu0: pcpa = %+d\n", val);
put_cpu_var(pcpa);
/* Operate on our perpcu structure */
ctx = get_cpu_ptr(pcp_ctx);
ctx->tx += 100;
pr_info(" thrd_0/cpu0: pcp ctx: tx = %5d, rx = %5d\n",
ctx->tx, ctx->rx);
put_cpu_ptr(pcp_ctx);
}
} else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */
for (i=0; i<THRD1_ITERS; i++) {
/* Operate on our perpcu integer */
val = -- get_cpu_var(pcpa);
pr_info(" thrd_1/cpu1: pcpa = %+d\n", val);
put_cpu_var(pcpa);
/* Operate on our perpcu structure */
ctx = get_cpu_ptr(pcp_ctx);
#if 0
/* If we try and run a blocking API within the per-CPU
* 'critical section', we land up with a kernel bug; hence,
* it's commented out by default. */
void *vp=vmalloc(1024*1024); // vmalloc(), vfree() are possibly
blocking!
mdelay(1); // mdelay(), printk()
are non-blocking
vfree(vp);
#endif
ctx->rx += 200;
pr_info(" thrd_1/cpu1: pcp ctx: tx = %5d, rx = %5d\n",
ctx->tx, ctx->rx);
put_cpu_ptr(pcp_ctx);
}
}
disp_our_percpu_vars();
pr_info("Our kernel thread #%ld exiting now...\n", thrd);
return 0;
}
要运行该模块,只需进入其源文件夹并输入
./run
。运行时的效果很有趣,可以在
sudo dmesg
的输出中看到每个 CPU 核心上的 Per-CPU 数据项的值。
7. 内核中 Per-CPU 变量的使用案例
Per-CPU 变量在 Linux 内核中被广泛使用,下面介绍两个有趣的用例。
-
在 x86 上通过 Per-CPU 数据实现
current
宏
:在 x86 架构上,
current
宏用于引用当前正在执行的线程的任务结构。将其实现为 Per-CPU 数据项可以确保无锁访问,从而提高性能。实现代码如下:
// arch/x86/include/asm/current.h
[ ... ]
DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(current_task);
}
#define current get_current()
DECLARE_PER_CPU()
宏将
current_task
声明为
struct task_struct *
类型的 Per-CPU 变量。
get_current()
内联函数调用
this_cpu_read_stable()
辅助函数读取当前 CPU 核心上的
current
值。
current_task
变量在上下文切换代码中更新,在 x86 上的更新位置为
arch/x86/kernel/process_64.c:__switch_to()
。
-
在网络接收路径上使用 Per-CPU 数据
:在早期,网络接收路径将数据包排队到全局数据结构中,这会导致严重的性能瓶颈,因为对该结构的访问需要使用锁(自旋锁),竞争可能会很高,性能会逐渐下降。现在,网络开发者改用 Per-CPU 数据来存储全局接收队列数据结构(
softnet_data
):
// net/core/dev.c
[ … ]
/*
* Device drivers call our routines to queue packets here. We empty the
* queue in the local softnet handler.
*/
DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);
EXPORT_PER_CPU_SYMBOL(softnet_data);
[ … ]
网络接收软中断
NET_RX_SOFTIRQ
函数的相关代码也在
net/core/dev.c
中。
综上所述,Per-CPU 变量在 Linux 内核中是一种强大的技术,能够提高数据访问的性能和安全性。通过合理使用静态和动态分配方式,以及正确的读写操作方法,可以充分发挥其优势。同时,内核中的实际用例也展示了其在不同场景下的应用价值。
总结
通过本文的介绍,我们深入了解了 Linux 内核中 Per-CPU 变量的使用。从基本的分配、初始化和释放操作,到复杂的读写操作和内核中的实际应用,我们看到了 Per-CPU 变量在提高性能和确保数据安全方面的重要作用。在实际开发中,我们可以根据具体需求选择合适的分配方式和操作方法,充分利用 Per-CPU 变量的优势。同时,要注意在原子上下文中避免使用可能阻塞的操作,以防止内核错误的发生。
流程图:Per-CPU 变量操作流程
graph TD;
A[开始] --> B{选择分配方式};
B -->|静态分配| C[使用 DEFINE_PER_CPU 或 DECLARE_PER_CPU];
B -->|动态分配| D[使用 alloc_percpu 或 alloc_percpu_gfp];
C --> E[初始化变量];
D --> E;
E --> F{进行读写操作};
F -->|读取| G[使用 get_cpu_var 或 per_cpu];
F -->|写入| H[使用 get_cpu_var 并操作];
G --> I[使用 put_cpu_var 结束读取];
H --> I;
I --> J{是否需要释放};
J -->|是| K[使用 free_percpu 释放内存];
J -->|否| L[结束];
K --> L;
表格:Per-CPU 变量操作总结
| 操作类型 | 方法 | 示例代码 | 注意事项 |
|---|---|---|---|
| 静态分配 | DEFINE_PER_CPU |
DEFINE_PER_CPU(int, pcpa);
| 编译时分配内存并初始化 |
| 动态分配 | alloc_percpu/alloc_percpu_gfp |
alloc_percpu[_gfp](type [,gfp]);
| 需使用 GPL 兼容许可证,分配后需释放 |
| 读取 | get_cpu_var/per_cpu |
val = get_cpu_var(pcpa);
val = per_cpu(pcpa, i);
| get_cpu_var 和 put_cpu_var 之间为原子上下文,避免阻塞操作 |
| 写入 | get_cpu_var 操作 |
get_cpu_var(pcpa) ++;
| 同样需注意原子上下文 |
| 释放 | free_percpu |
free_percpu(void __percpu *__pdata);
| 释放动态分配的内存 |
深入理解 Linux 内核中的 Per-CPU 变量
8. 操作步骤总结
为了更清晰地展示如何使用 Per-CPU 变量,下面将操作步骤进行详细总结:
-
静态分配步骤
:
1. 包含必要的头文件:
#include <linux/percpu.h>
2. 使用
DEFINE_PER_CPU
宏定义变量:
DEFINE_PER_CPU(int, pcpa);
-
动态分配步骤
:
1. 包含必要的头文件:
#include <linux/percpu.h>
2. 使用
alloc_percpu
或
alloc_percpu_gfp
宏分配内存:
pcp_ctx = (struct drv_ctx __percpu *) alloc_percpu(struct drv_ctx);
3. 检查分配是否成功:
if (!pcp_ctx) { ... }
4. 使用完后释放内存:
free_percpu(pcp_ctx);
-
读取操作步骤
:
1. 使用
get_cpu_var
或
per_cpu
宏读取值:
val = get_cpu_var(pcpa);
或
val = per_cpu(pcpa, i);
2. 在
get_cpu_var
和
put_cpu_var
之间避免阻塞操作
3. 使用
put_cpu_var
结束读取:
put_cpu_var(pcpa);
-
写入操作步骤
:
1. 使用
get_cpu_var
宏获取变量:
get_cpu_var(pcpa);
2. 对变量进行操作:
get_cpu_var(pcpa) ++;
3. 使用
put_cpu_var
结束操作:
put_cpu_var(pcpa);
9. 常见问题及解决方法
在使用 Per-CPU 变量时,可能会遇到一些常见问题,下面是一些问题及解决方法:
| 问题 | 原因 | 解决方法 |
| ---- | ---- | ---- |
| 内核错误:在原子上下文中使用阻塞操作 | 在
get_cpu_var
和
put_cpu_var
之间使用了可能阻塞的 API,如
vmalloc
| 避免在原子上下文中使用可能阻塞的操作,确保代码在非阻塞的情况下执行 |
| 无法使用
sched_setaffinity
函数 | 该函数未导出 | 在旧版本内核中可以使用
kallsyms_lookup_name
获取函数地址,但从 5.7 版本开始该 API 也被取消导出。可以使用辅助 Bash 脚本从
/proc/kallsyms
中获取函数地址并传递给模块 |
| 内存泄漏 | 动态分配的内存未正确释放 | 使用
free_percpu
函数释放动态分配的内存 |
10. 性能分析
Per-CPU 变量在性能方面具有显著优势,主要体现在以下几个方面:
-
减少锁竞争
:由于每个 CPU 核心都有自己独立的变量副本,多个线程可以在不同的核心上并发地访问和修改这些变量,而无需使用锁,从而避免了锁竞争带来的性能开销。
-
提高缓存命中率
:每个 CPU 核心可以将自己的变量副本缓存到本地缓存中,减少了内存访问的延迟,提高了缓存命中率。
-
降低上下文切换开销
:在多线程环境中,使用 Per-CPU 变量可以减少线程之间的同步操作,降低上下文切换的开销,提高系统的整体性能。
11. 应用场景
Per-CPU 变量适用于以下几种应用场景:
-
计数器
:在多线程环境中,每个线程可以独立地更新自己的计数器,避免了使用锁来保护计数器的更新操作,提高了性能。
-
统计信息收集
:每个 CPU 核心可以独立地收集自己的统计信息,如 CPU 使用率、网络流量等,最后将这些信息汇总,避免了锁竞争。
-
数据缓存
:每个 CPU 核心可以维护自己的数据缓存,提高数据访问的速度。
12. 注意事项
在使用 Per-CPU 变量时,需要注意以下几点:
-
原子上下文
:在
get_cpu_var
和
put_cpu_var
之间的代码是原子上下文,禁止使用可能阻塞的操作,否则会导致内核错误。
-
内存管理
:动态分配的内存必须使用
free_percpu
函数释放,避免内存泄漏。
-
兼容性
:使用
alloc_percpu
和
alloc_percpu_gfp
时,需要确保模块以 GPL 兼容许可证发布。
流程图:Per-CPU 变量使用注意事项
graph TD;
A[开始使用 Per-CPU 变量] --> B{选择操作类型};
B -->|分配| C{分配方式};
C -->|静态| D[使用 DEFINE_PER_CPU 或 DECLARE_PER_CPU];
C -->|动态| E[使用 alloc_percpu 或 alloc_percpu_gfp];
E --> F[确保 GPL 兼容许可证];
D --> G{进行读写操作};
F --> G;
G -->|读取| H[使用 get_cpu_var 或 per_cpu];
H --> I[进入原子上下文];
I --> J{是否有阻塞操作};
J -->|是| K[避免阻塞操作,否则内核错误];
J -->|否| L[使用 put_cpu_var 结束读取];
G -->|写入| M[使用 get_cpu_var 并操作];
M --> I;
L --> N{是否动态分配};
K --> N;
N -->|是| O[使用 free_percpu 释放内存];
N -->|否| P[结束];
O --> P;
表格:Per-CPU 变量使用注意事项总结
| 注意事项类型 | 详细说明 |
|---|---|
| 原子上下文 |
在
get_cpu_var
和
put_cpu_var
之间禁止使用可能阻塞的操作,如
vmalloc
|
| 内存管理 |
动态分配的内存必须使用
free_percpu
函数释放,避免内存泄漏
|
| 兼容性 |
使用
alloc_percpu
和
alloc_percpu_gfp
时,模块需以 GPL 兼容许可证发布
|
总结
Per-CPU 变量是 Linux 内核中一种强大的技术,它通过为每个 CPU 核心提供独立的变量副本,避免了锁竞争,提高了系统的性能和数据安全性。本文详细介绍了 Per-CPU 变量的分配、初始化、读写操作以及内核中的实际应用,同时分析了其性能优势和适用场景。在实际开发中,我们可以根据具体需求选择合适的分配方式和操作方法,充分利用 Per-CPU 变量的优势。但需要注意在原子上下文中避免使用可能阻塞的操作,确保内存的正确管理和模块的兼容性。通过合理使用 Per-CPU 变量,我们可以开发出更高效、更稳定的内核模块和应用程序。
超级会员免费看

935

被折叠的 条评论
为什么被折叠?



