The most important part of Linux kernel process switching is the macro definition of switch_to. The following describes in detail from several aspects:
(1) Embedded Assembly
(2) memory destruction Descriptor (Compiler Optimization)
(3) What are the signs of process switching?
(4) What is the mark of stack switching?
(5) Why does switch_to provide three parameters?
(6) How to pass Assembly parameters?
With these issues, let's take a general look at the code.
# Define switch_to (prev, next, last )/
Do {/
/*/
* Context-switching clobbers (thoroughly defeated) all registers, So we clobber/
* Them explicitly, via unused output variables ./
* (Eax and EBP is not listed because EBP is saved/restored/
* Explicitly for wchan access and eax is the return value/
* _ Switch_to ())/
*//
Unsigned long EBX, ECx, EDX, ESI, EDI ;/
/
ASM volatile ("pushfl/n/t"/* save flags *//
"Pushl % EBP/n/t"/* save EBP *//
"Movl % ESP, % [prev_sp]/n/t"/* save ESP *//
"Movl % [next_sp], % ESP/n/t"/* restore ESP *//
"Movl $ 1f, % [prev_ip]/n/t"/* save EIP *//
"Pushl % [next_ip]/n/t"/* restore EIP *//
"JMP _ switch_to/N"/* regparm call *//
"1:/t "/
"Popl % EBP/n/t"/* restore EBP *//
"Popfl/N"/* restore flags *//
/
/* Output parameters *//
: [Prev_sp] "= m" (prev-> thread. SP ),/
/* M indicates putting the variable into the memory, that is, putting the variable stored in [prev_sp] into the memory, and then writing the variable into Prev-> thread. sp *//
[Prev_ip] "= m" (prev-> thread. IP ),/
"= A" (last ),/
/* = Indicates the output, and a indicates to put the last variable into ax, eax = last *//
/
/* Clobbered output registers :*//
"= B" (EBX), "= C" (ECx), "= D" (EDX ),/
/* Put the B variable into EBX, and C to put ECx, d to edX, s to put Si, and D to put EDI *//
"= S" (Esi), "= D" (EDI )/
/
/* Input parameters :*//
: [Next_sp] "M" (next-> thread. SP ),/
/* Next-> put thread. SP into the memory [next_sp] * //
[Next_ip] "M" (next-> thread. IP ),/
/
/* Regparm parameters for _ switch_to ():*//
[Prev] "a" (prev ),/
/* Eax = Prev edX = next *//
[Next] "D" (next )/
/
:/* Reloaded segment REGISTERS *//
"Memory ");/
} While (0)
The above code is mainly embedded assembly. Here is a brief introduction:
1. Embedded Assembly syntax
_ ASM _ violate _ ("movl % 1, % 0": "= r" (result): "M" (input ));
_ ASM _ violate _ ("command template": Output part: input part );
"Movl % 1, % 0" is the instruction template; "% 0" and "% 1" represent the instruction operands, called placeholders, embedded sink
Compile the C language expressions to correspond to the instruction operands. The C language table is enclosed in parentheses after the instruction template.
In this example, there are only two values: "result" and "input". They are displayed in the order they correspond to the command operand "% 0", respectively ",
"% 1" corresponds to; Note the order: the first c expression corresponds to "% 0"; the second expression corresponds to "% 1", and the class
Push, there are up to 10 operands, respectively, with "% 0", "% 1 ".... "% 9" indicates. There is
A string enclosed in quotation marks. The content of a string is a limitation or requirement on the operand. The
The limit string is "= r", where "=" indicates that "result" is the output operand, and "R" indicates that "result" must be"
Associated with a general register, first read the value of the operand into the Register, and then use the corresponding register in the instruction
Not the "result" itself. Of course, after executing the command, you need to save the value in the register to the variable "result ".
It seems like the command directly performs operations on the "result". In fact, GCC actually performs implicit processing, so we can write less
Some commands. The "R" in front of "input" indicates that the expression needs to be put into a register first and then used in the instruction
This register is used for calculation.
(2) memory destruction Descriptor (Compiler Optimization)
The memory access speed is far less than the CPU processing speed. To improve the overall performance of the machine, hardware high-speed cache is introduced on the hardware.
Cache to accelerate memory access. In addition, commands in modern CPUs are not necessarily executed in strict order.
Related commands can be executed in disorder to make full use of the CPU command line and improve the execution speed. The above is the hardware
Level optimization. Let's look at optimization at the software level: one is optimized by the programmer when writing code, and the other is optimized by the compiler
. Common Methods for Compiler optimization are: caching memory variables to registers; adjusting command order to make full use
CPU command line. It is common to re-Sort read/write commands.
When optimizing regular memory, these optimizations are transparent and efficient. Optimized by the compiler or hard
The solution to the problem caused by component re-sorting is that the hardware (or other processors) must be in a specific order.
Sets the memory barrier between executed operations. Linux provides a macro to solve the execution of the compiler.
Sequence problems.
Void barrier (void)
This function notifies the compiler to insert a memory barrier, but it is invalid for hardware. The compiled code will insert the current CPU register
All the modified values are stored in the memory, and the data needs to be read again from the memory.
Memory descriptor informs GCC:
L 1) do not re-sort the embedded assembly instructions in this section with the preceding commands; that is, execute the embedded assembly code.
Previously, all the commands before it were executed.
L 2) Do not cache the variables to registers, because this Code may use memory variables, and these memory changes
The amount will change in an unpredictable way, So GCC inserts the necessary code first to cache the changes to the Register
The value is written back to the memory. If you access these variables later, you need to re-access the memory.
If the Assembly command modifies the memory, but GCC itself does not notice it, because there is no description in the output part
You need to add "Memory" in the description section to tell GCC that the memory has been modified. After GCC learns this information,
Before this command, insert the necessary command to write the variable values in the cache to the register first.
If you want to use these variables again later.
(3) process switchover flag ----- switch of the SP pointer
Because process switching is the process descriptor switching, let's take a look at how we locate the address of a process descriptor. See the following assembly code:
MoV $0xffffe000, % ECx
Andl % ESP, % ECx
Movl % ECx, P
After the above code is executed, p Stores the address of the thread_info structure of the currently running process, but we use the address of the Process descriptor at most, therefore, the kernel has designed the current macro to calculate the pointer to the process descriptor:
MoV $0xfffe000, % ECx
Andl % ESP, % ECx
Movl (% ECx), P
Because the offset of the task field in thread_info is 0, after executing the preceding three commands, p is the descriptor pointer of the currently running process.
As we can see, as long as you know ESP, the process is uniquely identified, So ESP is a sign of process switching.
(4) signs of stack switching --- EBP (stack low pointer)
Undoubtedly, the bottom pointer of the stack must be a sign of stack switching.
(5) Three switch_to Parameters
Process switching generally involves three processes. For example, process a is switched to process B and process B is started to execute. However, when process a resumes execution, process C is usually used instead of process B. Note the switch_to call: switch_to (prev, next, Prev). You can see that last is the prev. The call method is as follows: process a-> process B switch_to (A, B,) there are three main parameters:
Enter two parameters: PREV: the process before the switch, next: the process after the switch, and output one: Last: the process before the switch.
Note that all three variables are local variables. In the system stack, the value of the variables will not change after switching to another process.
Before process a switches to B, the value of eax is Prev, that is, a; the value of edX is next, that is, B, and the value of eax is last, that is,
When the third parameter is not considered, switch from C to A and switch from Kernel stack to stack a. In this case, Prev and nexxt in a point to A and B respectively, the reference of Process C is lost. In this case, the third parameter comes in handy. After switching process A, C is saved to eax. After switching to a, the output part "= A" (last) writes the eax value to the last part, that is, Prev, therefore, the values of Prev and next are C and B.
(6) How to pass Assembly parameters?
The Assembly uses registers to pass parameters. eax and EDX are used here, So JMP is similar to call, but the difference between JMP and call is, call will have some registers of the hardware automatic stack pressure, such as CS: IP. Here, the stack IP is pressed manually to simulate the call. In _ switch_to, return with return. Why don't we need to call? The reason is that after _ switch_to is returned, we are preparing to run other processes. That is to say, we should run the next process instead of the current process. If call is used, the IP address of the current process is pressed, then _ switch_to will run the current process after returning, this is inconsistent with the idea of running the next process. Therefore, we manually press the IP address of the next process, when _ switch_to is returned, the next IP address is the next process.