Linux内核的PER_CPU机制
参考书《Linux内核模块开发技术指南》
1.原理
在多核CPU的情况下,为了提高CPU并发执行的效率,对于某些不是必须要在核间进行同步访问的资源,可以为每一个CPU创建一个副本,让每个CPU都访问自身的数据副本,而不是通过加锁等互斥手段访问内存中的同一数据,这样可以提高并发执行效率。例如:操作系统中有很多个进程,这些进程会在队列中,等待调度到某个CPU上运行,如果将这些进程放在一个队列中,各CPU需要访问这个队列并从队列取出进程运行时,则需要加锁操作以进行多核间的访问互斥,如下图所示。
由于访问进程所在的队列时需要加锁,上图中的四个CPU会发生资源竞争。同一时刻如果多个CPU想要同时访问队列,则只能有一个CPU能够访问成功,其他CPU则会等待,直到访问队列的CPU退出访问(这样的方式效率不高)。此时,如果创建多个队列,队列的数量和CPU核心数相同,一个队列和一个CPU绑定。让所有进程均匀分散在各队列中,而每个CPU只访问和自身绑定的队列,这时由于各队列是CPU的私有数据,CPU不会访问到不和自身绑定的队列,就不用通过加锁进行资源互斥,如此可大大提高运行效率。如下图所示。
以上描述的为每个CPU都创建一个私有数据副本的方式被称为PER_CPU。除了无需加锁外,PER_CPU还可以提高CPU的Cache利用。如果某些资源不是必须要在核间进行同步访问,使用PER_CPU是一个好的选择。需要注意的是,使用PER_CPU并不是意味着在任何情况下都无需进行加锁,在上图中,如果一个进程从CPU的私有队列迁移到另一个CPU的私有队列中,是需要加锁进行互斥的,因为这涉及到跨CPU的资源访问。
2.相关接口
开发人员能够通过内核提供的接口使用PER_CPU机制,这些接口在内核源码的include/linux/percpu-defs.h头文件和include/linux/percpu.h头文件中声明。这里将其分为两组接口:静态声明PER_CPU接口和动态分配PER_CPU接口。
(1)静态声明
PER_CPU的静态声明接口指的是PER_CPU变量使用的内存空间在编译期间分配,这些接口如下所示。
DECLARE_PER_CPU(type, name)
该接口用于创建PER_CPU变量。第一个参数type是变量类型,第二个参数name是变量名。例如要为每个CPU分配一个类型为long,变量名为test的变量,通过DECLARE_PER_CPU(long, test)可以实现。
DEFINE_PER_CPU(type, name)
该接口用于声明PER_CPU变量,参数和DECLARE_PER_CPU的参数一致。
get_cpu_var(var)
该接口用于获取当前CPU的PER_CPU变量,参数var是变量名,返回值是对应的PER_CPU变量。
put_cpu_var(var)
该接口和get_cpu_var配合使用,在PER_CPU变量访问完成时调用。
(2)动态分配
PER_CPU的动态分配指的是PER_CPU变量的内存空间在内核运行期间动态分配,相关接口如下所示。
alloc_percpu(type)
该接口用于分配PER_CPU变量,参数type是变量类型。函数的返回值就是分配的PER_CPU指针变量。
void free_percpu(void __percpu *__pdata)
该接口用于释放PER_CPU变量,其参数是PER_CPU的指针变量,一般由alloc_percpu分配。
per_cpu_ptr(ptr, cpu)
该接口用于获取某个CPU的私有PER_CPU变量。第一个参数ptr是PER_CPU变量,一般由alloc_percpu分配。第二个参数cpu是CPU编号。执行完该接口后,返回的就是编号为cpu的处理器的私有PER_CPU指针变量。
3.示例程序
本节通过一个简单的示例程序来说明如何使用PER_CPU的接口。假设PC上有两个CPU,分别为CPU0和CPU1,示例程序通过alloc_percpu分配一个PER_CPU变量,然后通过per_cpu_ptr分别获取并设置两个CPU对应的PER_CPU变量的值,然后将两个CPU的PER_CPU变量的值分别打印出来。源码如下:
#include <linux/module.h>
#include <linux/percpu.h>//要使用PER_CPU相关接口需引入该头文件
//自定义结构体
struct my_test_struct
{int a;
};
//声明一个类型为my_test_struct的PER_CPU变量
static struct my_test_struct *percpu_test;
//加载函数
static int test_percpu_init(void)
{
//动态分配PER_CPU变量
percpu_test = alloc_percpu(struct my_test_struct); per_cpu_ptr(percpu_test, 0)->a = 1; //将CPU0中的变量a的值设为1per_cpu_ptr(percpu_test, 1)->a = 2; //将CPU1中的变量a的值设为2
//分别打印出两个CPU的PER_CPU变量值printk("cpu 0:a=%d\n", per_cpu_ptr(percpu_test, 0)->a);printk("cpu 1:a=%d\n", per_cpu_ptr(percpu_test, 1)->a);return 0;
}
//卸载函数
static void test_percpu_exit(void)
{free_percpu(percpu_test); //释放分配的PER_CPU变量
}
module_init(test_percpu_init);
module_exit(test_percpu_exit);
MODULE_LICENSE("GPL"); //要使用PER_CPU变量需要声明GPL许可协议
编译、加载上述模块后,执行dmesg -c命令打印调试信息,会分别打印出两个CPU的PER_CPU变量值,这两个值分别为1和2。如下图所示。