# Per CPU变量

内核中通常会定义很多的pcpu变量，这样有几个好处

* 增加数据访问的并发量
* 减少数据访问的时延

从定义上就可以看出pcpu变量就是每个cpu都有某个变量的副本，各自访问各自的。那在实现上是怎么做的呢？我们今天就来看一下。

## 如何定义

我们先来看静态pcpu变量是如何定义的。

通常我们定义一个pcpu变量使用这样的语句。

```
DEFINE_PER_CPU(int, numa_node);
```

这样就定义了一个int类型，名字为numa\_node的变量。接下来就深入研究一下。

### DEFINE\_PER\_CPU

```
#define DEFINE_PER_CPU_SECTION(type, name, sec)				\
	__PCPU_ATTRS(sec) __typeof__(type) name
#endif

#define DEFINE_PER_CPU(type, name)					\
	DEFINE_PER_CPU_SECTION(type, name, "")
```

看最后一行，最终也就是定义了一个type类型，名字是name的变量。感觉和普通的变量没有什么区别。那区别在哪里呢？对了，就在\_\_PCPU\_ATTRS宏里面。

### \_\_PCPU\_ATTRS

```
#define __PCPU_ATTRS(sec)						\
	__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))	\
	PER_CPU_ATTRIBUTES

#define PER_CPU_BASE_SECTION ".data..percpu"
```

好了，这个比较明确了，就是给定义的变量添加了一个section的修饰符。section是什么概念呢？你可以理解为同一个section的变量会放在同一块存储区。一会儿我们再来看这个概念。

### 展开后

```
DEFINE_PER_CPU(int, numa_node);

=

__attribute__((section(".data..percpu")))  \
    __typeof__(int) numa_node;
```

这样看或许能够清楚一些。

pcpu变量和普通变量定义时的差别在于pcpu变量被安排在了一个指定的section中。

PS： 可以用make xxx.i将某个使用DEFINE\_PER\_CPU()定义了变量的文件展开，就能验证了。

### 放在哪

已经看到变量定义在某一个section了，但是还是不死心，想要看看究竟是怎么放的。

好吧，我就带你来看看。

首先在include/asm-generic/vmlinux.lds.h中定义了PERCPU\_INPUT。

```
#define PERCPU_INPUT(cacheline)						\
	__per_cpu_start = .;						\
	. = ALIGN(PAGE_SIZE);						\
	*(.data..percpu..page_aligned)					\
	. = ALIGN(cacheline);						\
	__per_cpu_hot_start = .;					\
	*(SORT_BY_ALIGNMENT(.data..percpu..hot.*))			\
	__per_cpu_hot_end = .;						\
	. = ALIGN(cacheline);						\
	*(.data..percpu..read_mostly)					\
	. = ALIGN(cacheline);						\
	*(.data..percpu)						\
	*(.data..percpu..shared_aligned)				\
	PERCPU_DECRYPTED_SECTION					\
	__per_cpu_end = .;
```

你看，凡是.data..percpu开头的都包含在这个定义内了。

在同一个文件中又定义了一个包含这个定义的定义PERCPU\_SECTION。

```
#define PERCPU_SECTION(cacheline)					\
	. = ALIGN(PAGE_SIZE);						\
	.data..percpu	: AT(ADDR(.data..percpu) - LOAD_OFFSET) {	\
		PERCPU_INPUT(cacheline)					\
	}
```

而这个PERCPU\_SECTION定义最终包含在文件arch/x86/kernel/vmlinux.lds.S中。这就是我们最后链接vmlinux时使用的脚本。

这样通过DEFINE\_PER\_CPU()定义的变量，就在链接的时候有了固定的位置。（所以这算全局变量。）

## 如何访问

定义看完了，来看看要怎么访问。

还是看刚才的numa\_node变量，我们要访问某cpu上的变量时通过如下的语句。

```
per_cpu(numa_node, cpu);
```

好了，那来看看都是些什么吧

### per\_cpu()

```
#define per_cpu(var, cpu)	(*per_cpu_ptr(&(var), cpu))
```

```
#define per_cpu_ptr(ptr, cpu)						\
({									\
	__verify_pcpu_ptr(ptr);						\
	SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu)));			\
})
```

卧槽，这么长。不着急，仔细看其实有的不用。比如这个\_\_verif\_pcpu\_ptr()，一看就是用来检测了，就先跳过吧。关键是这个SHIFT\_PERCPU\_PTR。

### per\_cpu\_offset()

先从软柿子开始捏，per\_cpu\_offset()就一个数组。

```
extern unsigned long __per_cpu_offset[NR_CPUS];

#define per_cpu_offset(x) (__per_cpu_offset[x])
```

然后就没这么简单了，好像是系统在初始化的时候会设置这个数组的值。不过好在原理比较简单，就是给每一个cpu划分了一个区域，区域的起始地址就保存在这个数组里。

### SHIFT\_PERCPU\_PTR()

```
/*
 * Add an offset to a pointer.  Use RELOC_HIDE() to prevent the compiler
 * from making incorrect assumptions about the pointer value.
 */
#define SHIFT_PERCPU_PTR(__p, __offset)					\
	RELOC_HIDE(PERCPU_PTR(__p), (__offset))
```

RELOC\_HIDE:分别传入了变量的一个指针，和刚才我们看到的offset。

得，继续。

### PERCPU\_PTR()

```
#define PERCPU_PTR(__p)							\
	(TYPEOF_UNQUAL(*(__p)) __force __kernel *)((__force unsigned long)(__p))
```

感觉就是定义了\_\_p类型的变量？没懂，先这样吧。

### RELOC\_HIDE()

```
#define RELOC_HIDE(ptr, off)					\
  ({ unsigned long __ptr;					\
     __ptr = (unsigned long) (ptr);				\
    (typeof(ptr)) (__ptr + (off)); })
```

又是一个很长，但是其实很简单的定义。是什么呢？ 你看最后一行，其实就是一个指针加上了offset。

### 展开后

```
per_cpu(numa_node, cpu)

=

*(&numa_node + per_cpu_offset(cpu))
```

好了，这样看可能可以清楚一些。

但是呢，还是有点不清楚，让我再来把实现的细节讲一下。这样，我估计你就可以彻底理解了。

## 背后的实现

先透露一下，其实pcpu变量的实现就是给每个cpu都分配一块内存，并且记录下每块区域和原生区域之间的offset。所以访问的时候就是直接通过原生变量的地址加上目标cpu变量区域的offset就可以了。

貌似有点饶，来看一张图吧。

```
 __per_cpu_start +-------------------+  -+-   -+-
                 |                   |   |     |
                 |                   |   |     |
                 |                   |   |     |
 __per_cpu_end   +-------------------+   |     |
                                         |     |
                                         |     |
                                         |     |
                                               |
                 Group0                        |
  pcpu_base_addr +-------------------+   per_cpu_offset(2)
                 |cpu0               |         |    
                 |                   |   |     |    
                 |                   |   |     |
                 +-------------------+   |     |
                 |cpu1               |   |     |
                 |                   |   |      
                 |                   |   |     per_cpu_offset(3)                        
                 +-------------------+  -+-                             
                 |cpu2               |         |
                 |                   |         |
                 |                   |         |
                 +-------------------+         |
                                               |
                                               |
                                               |
                                               |
                 Group1                        |
                 +-------------------+        -+-
                 |cpu3               |
                 |                   |
                 |                   |
                 +-------------------+
                 |cpu4               |
                 |                   |
                 |                   |
                 +-------------------+ -+-
                 |cpu5               |  |
                 |                   |  pcpu_unit_size
                 |                   |  |
                 +-------------------+ -+-
```

其中\_\_per\_cpu\_start和\_\_per\_cpu\_end就是刚才我们看到的那个section的定义。所有pcpu变量都会被保存在这个地址空间内。

在系统启动的时候，会给每一个cpu分配各自的pcpu变量内存空间。并计算出每个区域相对于\_\_per\_cpu\_start的offset。

具体的代码在setup\_per\_cpu\_areas()函数中，有兴趣的可以再深入研究。具体实现就不在这里展开了。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://richardweiyang-2.gitbook.io/kernel-exploring/00-index-3/static_pcpu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
