73、深入理解 Linux 内核中的 Per-CPU 变量

深入理解 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 变量,我们可以开发出更高效、更稳定的内核模块和应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值