线程切换函数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什么技术的,完全不在一个频道的。我自己当然是关注底层技术和不关注业务逻辑的人,这点我还是敢于承认的。

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


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

查看所有标签

猜你喜欢:

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

Music Recommendation and Discovery

Music Recommendation and Discovery

Òscar Celma / Springer / 2010-9-7 / USD 49.95

With so much more music available these days, traditional ways of finding music have diminished. Today radio shows are often programmed by large corporations that create playlists drawn from a limited......一起来看看 《Music Recommendation and Discovery》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具