内容简介:版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/89790086
版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/89790086
继续看昨晚的那个setjmp/longjmp实现的 “用户态协作式多线程” (我还是不用 协程 这个词了,这个词太有文化,以至于会被皮鞋老板认为我是在亵渎协程)的demo:
// 基于标准的setjmp/longjmp实现! // 然而我不知道如何才能直接用 PTR_MANGLE 这个宏,所以我使用自己实现内联汇编版本! #include <stdio.h> #include <stdlib.h> #include <string.h> #include <setjmp.h> unsigned char *stack1, *stack2; struct task *tsk1, *tsk2; struct task { jmp_buf ctx; unsigned char *stack; unsigned int seq; char name[32]; }; unsigned long PTR_MANGLE(unsigned long var) { asm ( "movq %1, %%rdx \n" "xor %%fs:0x30, %%rdx\n" "rol $0x11,%%rdx\n" "movq %%rdx, %0\t\n" : "=r" (var) :"0" (var)); return var; } unsigned long PTR_DEMANGLE(unsigned long var) { asm ( "ror $0x11, %0\n" "xor %%fs:0x30, %0" : "=r" (var) : "0" (var)); return var; } void post_schedule(struct task *tsk) { printf("Previous task name:%s [sequence:%d]\n", tsk->name, tsk->seq); } void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } post_schedule(prev); } void func1() { int i = 1; while (i++) { printf("thread 1 :%d\n", i); sleep(1); if (i%3 == 0) { tsk1->seq = i; schedule(tsk1, tsk2); } } } void func2() { int i = 0xffff; while (i--) { printf("thread 2 :%d\n", i); sleep(1); if (i%3 == 0) { tsk2->seq = i; schedule(tsk2, tsk1); } } } #define JB_RBP 1 #define JB_RSP 6 #define JB_PC 7 int main(int unused1, char **unused2) { int i, j; unsigned long *prip1, *prip2; unsigned long *pst1, *pst2, *pbp1, *pbp2; tsk1 = (struct task *)calloc(sizeof(struct task), 1); tsk2 = (struct task *)calloc(sizeof(struct task), 1); stack1 = (unsigned char *)malloc(4096); stack2 = (unsigned char *)malloc(4096); tsk1->stack = stack1 + 4000; tsk2->stack = stack2 + 4000; strncpy(&tsk1->name[0], "task1", 5); strncpy(&tsk2->name[0], "task2", 5); tsk1->seq = 0; tsk2->seq = 0; memset(&tsk1->ctx, 0, sizeof(jmp_buf)); memset(&tsk2->ctx, 0, sizeof(jmp_buf)); i =setjmp(tsk1->ctx); j =setjmp(tsk2->ctx); prip1 = ((unsigned long *)&(tsk1->ctx)) + JB_PC; prip2 = ((unsigned long *)&(tsk2->ctx)) + JB_PC; pst1 = ((unsigned long *)&(tsk1->ctx)) + JB_RSP; pst2 = ((unsigned long *)&(tsk2->ctx)) + JB_RSP; pbp1 = ((unsigned long *)&(tsk1->ctx)) + JB_RBP; pbp2 = ((unsigned long *)&(tsk2->ctx)) + JB_RBP; // 加密需要保护的指针值。 *prip1 = PTR_MANGLE(func1); *pst1 = *pbp1 = PTR_MANGLE(stack1+4000); *prip2 = PTR_MANGLE(func2); *pst2 = *pbp2 = PTR_MANGLE(stack2+4000); longjmp(tsk1->ctx, 2); }
关注一下 schedule 函数:
void post_schedule(struct task *tsk) { printf("Previous task name:%s [sequence:%d]\n", tsk->name, tsk->seq); } void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } post_schedule(prev); }
如果按照代码所述,当task1切换到task2的时候,需要这么如下调用:
schedule(tsk1, tsk2);
按照 C语言常规的逻辑 顺序执行代码,理所当然应该打印如下:
Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task1 [sequence:6]
然而,事实却是:
Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task2 [sequence:65532]
问题出现了!
我在schedule函数的开头和该函数的最后都是在引用 prev指针的字段 为什么结果看起来是不对的?
为什么呢?C语言明明就是顺序执行语句的呀!C语言同一个指针为什么会被改变?如果在Before和After打印的位置打印prev的地址的话,你会发现地址都不一样了!
其实答案很容易想到,答案就是 prev指针不是同一个prev指针!
如果你没有看过 Linux 内核的switch_to宏,没有纠结过为什么该宏拥有三个参数,那么以上的答案可能你马上就能想到,然而,具有讽刺效果的是,看过了switch_to的分析,反而更加懵了,因为这些分析都太复杂了,事情根本就没有那么复杂!
原因就是 寄存器上下文在longjmp中全部切换了,然而 C语言 看不到这种切换! 换句话说, 汇编语言可以对C语言实施降维打击! (通过高维空间突然出现或者消失在低维空间!)
C语言抽象了程序的 业务逻辑 ,隐藏了底层的各种细节,C语言编程根本不需要理解什么寄存器上下文,更不需要知道esp,rsp,r13,eax这些是干什么的。
所以说,如果说一段C代码中间插一段内联汇编,那么这段内联汇编前后的C语句并不能保证是一定可以在逻辑层面上衔接的。
为了显示在Before和After位置的打印处,寄存器上下文已经被切换,我们来看看堆栈的情况。
我们知道,局部变量是在堆栈中保存的,那么我们在schedule函数的Before,After处分别打印一下局部变量ret的地址值,来证明堆栈其实已经不是同一个了:
void schedule(struct task *prev, struct task *next) { int ret; printf("Before switch:[ret is at:%p]\n", &ret); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } printf("After switch:[ret is at:%p]\n", &ret); }
结果如下:
Before switch:[ret is at:0x952184] After switch:[ret is at:0x953194]
那么,如果在switch之后,我依然需要在寄存器上下文切换之前的prev指针怎么办呢?毕竟可能需要执行一些post事务之类的。
办法就是用通用寄存器把prev指针给传递过去。既然要应对汇编语言指令的降维打击,当然需要汇编语言操作C语言不可见的寄存器本身了。
哦,对了,这就是 为什么Linux内核的switch_to需要3个参数的原因!
知道了要用内联汇编,但是到底应该怎么实现呢?也简单!我们只需要看看setjmp/longjmp没有touch到哪些寄存器,然后借用一下即可。这个还算清晰,我们从glibc里就能看出:
https://code.woboq.org/userspace/glibc/sysdeps/x86_64/jmpbuf-offsets.h.html#define JB_RBX 0 #define JB_RBP 1 #define JB_R12 2 #define JB_R13 3 #define JB_R14 4 #define JB_R15 5 #define JB_RSP 6 #define JB_PC 7 #define JB_SIZE (8*8)
嗯,我们发现RCX没有用到,那就用RCX呗,代码如下:
void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { // 切换前,先将变量prev保存在rcx中。 asm ( "movq %0, %%rcx\n" : : "m" (prev)); longjmp(next->ctx, 2); } else { // 切换后,从rcx里恢复到prev变量中。 asm ( "movq %%rcx, %0\n" : "=c" (prev) :); } post_schedule(prev); }
看效果:
... Before task:task2 switching to task:task1! current task sequence:65532 Previous task name:task2 [sequence:65532] thread 1 :4 thread 1 :5 thread 1 :6 Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task1 [sequence:6] thread 2 :65531 thread 2 :65530 thread 2 :65529 Before task:task2 switching to task:task1! current task sequence:65529 Previous task name:task2 [sequence:65529] ...
就是这个意思。
网上能搜到一大堆关于Linux内核task切换时switch_to宏的文章,大致说的都是这个意思,不然一个宏也没啥好分析的,想学内联汇编完全有更好的资源,根本没有必要去分析什么switch_to宏。不是很多人觉得Linux进程调度相当高大上吗?嗯,那是调度,那不是切换, 调度和切换不是一回事! 调度是判断 让谁运行 ,切换是 如何让它运行。 想研究调度的,研究CFS就好。
本文描述的其实是一个相关语言层次的普遍现象:
-
低级语言的逻辑对高级语言不可见,高级语言没有任何手段获取这些逻辑。
比如寄存器这种,C/C++都不能操作,Python和 PHP 试试看? -
高级语言的特性对低级语言不可见,然而低级语言可以通过一些手段获取。
比如类这个概念在汇编就不存在。
上面第一点很有意思,它会带来本文描述的这种奇怪的结果,最终不得不借助于低级语言去搞定。在 Java 中也存在这种辅助的方案,比如Java自省到JVM,比如JNI等等。
说什么XX是最好的语言,说什么业务逻辑比底层技术重要,你要是只学一个什么高级语言,中间件,即便你精通业务逻辑怎么实现,致力于需求分析和满足需求,我敢说你连CPU怎么工作的都不知道。
当然了,就算不穿西装,骨子里的西装人士,也是不care什么技术的,完全不在一个频道的。我自己当然是关注底层技术和不关注业务逻辑的人,这点我还是敢于承认的。
浙江温州皮鞋湿,下雨进水不会胖!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 珊栏函数 iOS之多线程GCD(三)
- java中线程安全,线程死锁,线程通信快速入门
- ObjC 多线程简析(一)-多线程简述和线程锁的基本应用
- Java多线程之线程中止
- Android 的线程和线程池
- iOS 多线程之线程安全
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。