在linux/arch/x86/include/asm/switch_to.h
中,有宏switch_to
的定义,真正的线程切换奇迹读取的关键线就是这样(直到Linux 4.7改变时):
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 */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
命名的操作数具有内存约束,如[prev_sp] "=m" (prev->thread.sp)
。除非__switch_canary
已定义(然后使用CONFIG_CC_STACKPROTECTOR
加载和存储),否则%ebx
被定义为空。
我理解它是如何工作的,比如内核堆栈指针备份/恢复,以及push next->eip
和jmp __switch_to
在函数末尾带有ret
指令的方式,实际上是与真实ret
指令匹配的“伪”调用指令,有效地使next->eip
成为下一个线程的返回点。
我不明白的是,为什么黑客?为什么不只是call __switch_to
,然后是ret
,jmp
到next->eip
,这更干净,读者友好。
答案 0 :(得分:6)
这样做有两个原因。
一个是允许[next_ip]
的操作数/寄存器分配的完全灵活性。如果您希望能够在jmp %[next_ip]
之后执行call __switch_to
,则必须将%[next_ip]
分配给非易失性寄存器(即,通过ABI定义,在进行函数调用时保留其值)。
这引入了编译器优化能力的限制,context_switch()
('调用者' - 使用switch_to()
)的结果代码可能不尽如人意。但为了什么好处?
嗯 - 这就是第二个原因,真的,没有,因为call __switch_to
相当于:
pushl 1f
jmp __switch_to
1: jmp %[next_ip]
即。它推送返回地址;您最终会得到一个序列push
/ jmp
(== call
)/ ret
/ jmp
,如果您不想返回这个地方(并且此代码没有),您通过“伪造”一个电话来保存代码分支,因为您只需要执行push
/ jmp
/ ret
。代码在这里使尾递归。
是的,这是一个小优化,但避免分支减少延迟和延迟对于上下文切换至关重要。