Atomic variable analysis in Linux Kernel
There are several things in the Linux kernel that are common, and atomic variables are one of them. In fact, I have never tried these things before. After all, there are just a few lines of code, as long as I know what this means. It is limited to knowing that there is an assembly command called lock. This ensures the atomicity of data operations. But a few days ago, for some reason, I flipped through the RCU code. The RCU code was not locked when the pointer was assigned. So it's strange. Finally, I had some answers. Now I want to share it with you, so you don't have to read it. It's a little dish-level article.
First, let's start with a short piece of code:
2 int atomic_set ()
3 {
4 int A = 0x1111;
5 Int B =;
6 RETURN 0;
7}
Simple?
After disassembly:
0x00000000 <atomic_set + 0>: Push % EBP
0x00000001 <atomic_set + 1>: mov % ESP, % EBP
0x00000003 <atomic_set + 3>: Sub $0x10, % ESP
0x00000006 <atomic_set + 6>: movl $0x1111, 0xfffffff8 (% EBP)
0x0000000d <atomic_set + 13>: mov 0xfffffff8 (% EBP), % eax
0x00000010 <atomic_set + 16>: mov % eax, 0 xfffffffc (% EBP)
0x00000013 <atomic_set + 19>: mov $0x0, % eax
0x00000018 <atomic_set + 24>: Leave
0x00000019 <atomic_set + 25>: Ret
The Assembly above does not contain any optimization options.
You can see that the core commands are:
0x00000006 <atomic_set + 6>: movl $0x1111, 0xfffffff8 (% EBP)
0x0000000d <atomic_set + 13>: mov 0xfffffff8 (% EBP), % eax
0x00000010 <atomic_set + 16>: mov % eax, 0 xfffffffc (% EBP)
The first is simple assignment.
The second step is to prepare for the third operation, because the memory cannot reach the memory. If you do not know this, you can go back and check the compiled books.
The third is simple assignment.
Now the main problem is the atomicity of the mov command. OK.
The intel manual shows the following sentence:
The execution of the following memory operations is always atomic:
Read or write a byte.
Read or write a 16-bit boundary aligned word.
Read or write a 32-bit boundary aligned dual word.
In fact, this is also done in the Linux kernel:
Static inline void atomic_set (atomic_t * V, int I)
{
V-> counter = I;
}
OK. Let's take a look at this:
Static inline void atomic_inc (atomic_t * V)
{
ASM volatile (lock_prefix "incl % 0"
: "+ M" (V-> counter ));
}
As for lock_prefix, when this is a lock command, it will become. If it is not SMP, it will be done directly if it does not exist.
Why should I add lock? Here is an authoritative explanation taken from the IA-32 manual:
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.
So you have to accept it unconditionally, and there is nothing to say.
However, it does not matter if you do not need to use lock for a single core.
OK. After reading this passage, we can understand the entire atomic operation.
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 ;"
It's the lock command, but I really don't know what the author is doing with such a bunch of things. The above pile means to append a variable for each lock operation. As to what the appended variable is for, let's take a look.
First, a function call route is provided:
Init_module-> load_module-> post_relocation-> module_finalize-> alternatives_smp_module_add-> alternatives_smp_unlock
Here is a simple analysis:
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 );
}
The key lies in this place:
/* Turn lock prefix into Ds segment override prefix */
If (* PTR = 0xf0)
Text_poke (PTR, (unsigned char []) {0x3e}), 1 );
According to the context of my own code, because the above is just a function call relationship, you can view the code on your own. My understanding is:
When you compile the kernel and add the SMP option, the lock command will be added before the operation. As mentioned earlier, that is, when module_init is executed, the function chain shown above will be called. In this function chain, you will determine whether you are actually multi-core or multi-CPU Based on the test results, continue to add the lock command. If not, call the final sentence text_poke to remove the lock code in the text segment.
It seems that this technique is still useful, especially on single-core CPUs. There are many atomic operations in the Linux kernel, saving a little bit.
There are too many tips in the Linux kernel.
Smoke a cigarette first.