Linux内核中的的原子变量分析
Linux内核中有几个东西是比较常见的,原子变量就是其中之一。其实之前我也没有专门去折腾过这些东西,毕竟就那么几行代码,只要知道这个意思就行了。也就仅限于知道有一条汇编指令叫lock。用这个可以保证数据操作的原子性。但前几天因为某些原因翻了一下RCU的代码,RCU的代码在指针赋值的时候并未lock。所以有些奇怪。经过一翻折腾,终于有了一些答案。现写出来与大家一起分享,高手就不用看了,属于小菜级的文章。
首先我们从一小段代码入手:
2 int atomic_set()
3 {
4 int a = 0x1111;
5 int b = a;
6 return 0;
7 }
简单吧?
反汇编后:
0x00000000
0x00000001
0x00000003
0x00000006
0x0000000d
0x00000010
0x00000013
0x00000018
0x00000019
上面这段汇编是未加任何优化选项的。
大家不用看函数压栈的代码,就可以看出核心的指令是:
0x00000006
0x0000000d
0x00000010
第一条就是简单的赋值。
第二条是为第三条操作做准备的,因为不能内存到内存,这个如果不知道的话可以回去看看汇编的书籍。
第三条也就是简单的赋值。
现在的主要问题变成了mov这条指令的原子性了。OK。
翻intel手册可以看到有如下一句话:
对以下内存操作的执行总是原子的:
读或写一个字节.
读或写一个16位边界对齐的的字。
读或写一个32位边界对齐的双字。
事实上Linux内核中也是这么做的:
static inline void atomic_set(atomic_t *v, int i)
{
v->counter = i;
}
OK,让我们来看看这个:
static inline void atomic_inc(atomic_t *v)
{
asm volatile(LOCK_PREFIX "incl %0"
: "+m" (v->counter));
}
至于LOCK_PREFIX大家直接当这个就是一条lock指令就成了,当不是SMP的情况下直接就当它不存在就得了。
为什么要加lock呢?这里有个权威解释,取自IA-32手册:
The LOCK prefix can be prepended only to the following instructions and only to those
forms of the instructions where the destination operand is a memory operand: ADD,
ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB,
SUB, XOR, XADD, and XCHG. If the LOCK prefix is used with one of these instructions
and the source operand is a memory operand, an undefined opcode exception (#UD)
may be generated. An undefined opcode exception will also be generated if the LOCK
prefix is used with any instruction not in the above list. The XCHG instruction always
asserts the LOCK# signal regardless of the presence or absence of the LOCK prefix.
所以这个你得无条件接受,没有什么好说的。
但是在单核的情况下不用lock也是没关系的。
OK,看完这段话,相当于整个原子操作都搞明白了。
LOCK_PREFIX这个玩意我也看了一下:
#define LOCK_PREFIX_HERE \
".section .smp_locks,\"a\"\n" \
".balign 4\n" \
".long 671f - .\n" /* offset */ \
".previous\n" \
"671:"
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
搞来搞去还就是lock指令,不过真不知道作者他搞这么一堆东西是为了啥。上面那一堆的意思就是为每一个lock操作追加一个变量,至于追加的这个变量到底是用来干嘛的,让我们来看一看。
先给出一条函数调用路线:
init_module->load_module->post_relocation->module_finalize->alternatives_smp_module_add->alternatives_smp_unlock
这个我们简单分析一下就行了:
static void alternatives_smp_unlock(const s32 *start, const s32 *end,
u8 *text, u8 *text_end)
{
const s32 *poff;
if (noreplace_smp)
return;
mutex_lock(&text_mutex);
for (poff = start; poff < end; poff++) {
u8 *ptr = (u8 *)poff + *poff;
if (!*poff || ptr < text || ptr >= text_end)
continue;
/* turn lock prefix into DS segment override prefix */
if (*ptr == 0xf0)
text_poke(ptr, ((unsigned char []){0x3E}), 1);
};
mutex_unlock(&text_mutex);
}
关键落在这个地方:
/* turn lock prefix into DS segment override prefix */
if (*ptr == 0xf0)
text_poke(ptr, ((unsigned char []){0x3E}), 1);
根据我自已看代码的上下文,因为我上面给出的只是个函数调用关系,具体大家可以自行查看代码,我的理解是:
当你编译内核增加SMP选项的时候会在操作前面加了lock指令,这个前面已经说过了,但是在运行的时候,也就是执行module_init的时候会调用上面给出那个函数链,在这个函数链中会根据探测的结果判断你是否真的是多核或多CPU,如果是就不管了,继续加lock指令,如果不是,就调用最终那一句text_poke去掉text段中那个lock锁的代码。
感觉这个技巧还是有用的,尤其是在单核的CPU上,linux内核中本来用原子操作就比较多,能省一点是一点。
Linux内核中的技巧太多啦,已经见怪不怪了。
抽根烟先,下回见。