线程切换函数schedule的实现

栏目: C · 发布时间: 5年前

内容简介:版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! 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什么技术的,完全不在一个频道的。我自己当然是关注底层技术和不关注业务逻辑的人,这点我还是敢于承认的。

浙江温州皮鞋湿,下雨进水不会胖!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Egret——HTML5游戏开发指南

Egret——HTML5游戏开发指南

张鑫磊 等 / 电子工业出版社 / 2016-3 / 85

《Egret——HTML5游戏开发指南》由浅入深,在讲解游戏开发基础的同时提供众多实战案例供读者学习。《Egret——HTML5游戏开发指南》章节内容包含Egret基础概念及基础图形图像处理方法、网络相关操作、移动设备适配、性能优化、文本动画相关知识、调试技巧、DragonBones骨骼动画系统和P2物理引擎等。通过《Egret——HTML5游戏开发指南》,读者可以了解并掌握HTML5游戏开发技能......一起来看看 《Egret——HTML5游戏开发指南》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具