bthread源码剖析(三): 汇编语言实现的上下文切换

简介: 上回书说道,TaskGroup的run_main_task()有三大关键函数,剩余一个sched_to()没有展开详谈。那在今天的sched_to()源码探秘之旅开始之前呢,首先高能预警,本文会涉及到汇编语言,所以请大家坐稳扶好!

上回书说道,TaskGroup的run_main_task()有三大关键函数,剩余一个sched_to()没有展开详谈。那在今天的sched_to()源码探秘之旅开始之前呢,首先高能预警,本文会涉及到汇编语言,所以请大家坐稳扶好


TaskGroup::sched_to()


sched_to()是用来进行上下文(Context)切换的。先看下sched_to()的代码,然后再解读:


inline void TaskGroup::sched_to(TaskGroup** pg, bthread_t next_tid) {
    TaskMeta* next_meta = address_meta(next_tid);
    if (next_meta->stack == NULL) {
        ContextualStack* stk = get_stack(next_meta->stack_type(), task_runner);
        if (stk) {
            next_meta->set_stack(stk);
        } else {
            // stack_type is BTHREAD_STACKTYPE_PTHREAD or out of memory,
            // In latter case, attr is forced to be BTHREAD_STACKTYPE_PTHREAD.
            // This basically means that if we can't allocate stack, run
            // the task in pthread directly.
            next_meta->attr.stack_type = BTHREAD_STACKTYPE_PTHREAD;
            next_meta->set_stack((*pg)->_main_stack);
        }
    }
    // Update now_ns only when wait_task did yield.
    sched_to(pg, next_meta);
}


通过传入的参数:next_tid找到TM:next_meta,和对应的ContextualStack信息:stk。


然后给next_meta设置栈stk。


最后调用另外一个重载的sched_to(),声明如下:


void TaskGroup::sched_to(TaskGroup** pg, TaskMeta* next_meta);


定义:


void TaskGroup::sched_to(TaskGroup** pg, TaskMeta* next_meta) {
    TaskGroup* g = *pg;
    // Save errno so that errno is bthread-specific.
    const int saved_errno = errno;
    void* saved_unique_user_ptr = tls_unique_user_ptr;
    TaskMeta* const cur_meta = g->_cur_meta;
    const int64_t now = butil::cpuwide_time_ns();
    const int64_t elp_ns = now - g->_last_run_ns;
    g->_last_run_ns = now;
    cur_meta->stat.cputime_ns += elp_ns;
    if (cur_meta->tid != g->main_tid()) {
        g->_cumulated_cputime_ns += elp_ns;
    }
    ++cur_meta->stat.nswitch;
    ++ g->_nswitch;



记录一些数据。继续看代码,判断下一个的TM(next_meta)和当前TM(cur_meta)如果不是同一个,就去切换栈。


// Switch to the task
    if (__builtin_expect(next_meta != cur_meta, 1)) {
        g->_cur_meta = next_meta;
        // Switch tls_bls
        cur_meta->local_storage = tls_bls;
        tls_bls = next_meta->local_storage;
        if (cur_meta->stack != NULL) {
            if (next_meta->stack != cur_meta->stack) {
                jump_stack(cur_meta->stack, next_meta->stack);
                // probably went to another group, need to assign g again.
                g = tls_task_group;
            }
        }
        // else because of ending_sched(including pthread_task->pthread_task)
    } else {
        LOG(FATAL) << "bthread=" << g->current_tid() << " sched_to itself!";
    }


tls_bls表示的是TM(bthread)内的局部存储。先做还原,并且赋值成下一个TM的局部存储。接着执行jump_stack()去切换栈。


上面的大if结束之后,去执行TG的remain回调函数(如果设置过)。


while (g->_last_context_remained) {
        RemainedFn fn = g->_last_context_remained;
        g->_last_context_remained = NULL;
        fn(g->_last_context_remained_arg);
        g = tls_task_group;
    }
    // Restore errno
    errno = saved_errno;
    tls_unique_user_ptr = saved_unique_user_ptr;
    *pg = g;


jump_stack()


定义在src/bthread/stack_inl.h 中


inline void jump_stack(ContextualStack* from, ContextualStack* to) {
    bthread_jump_fcontext(&from->context, to->context, 0/*not skip remained*/);
}


bthread_jump_fcontext()其实是汇编函数,在bthread/context.cpp中,功能就是进行栈上下文的切换(跳转)。与之配套的还有一个bthread_make_fcontext(),负责创建bthread的栈上下文。这两个函数是实现栈上下文切换的核心。它们的代码其实并非brpc的原创,而是出自开源项目libcontext。libcontext是boost::context的简化实现。打开bthread/context.h可以看到版权声明:


libcontext - a slightly more portable version of boost::context

Copyright Martin Husemann 2013. Copyright Oliver Kowalke 2009. Copyright Sergue E. Leontiev 2013. Copyright Thomas Sailer 2013. Minor modifications by Tomasz Wlostowski 2016.

Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)


其实另外一个C++协程的开源项目libgo中的Context也脱胎于此。


在context.cpp中,定义了各种平台的bthread_jump_fcontext()/


bthread_make_fcontext()实现。__asm代码块是C语言文件中编写汇编语言代码的写法。


#if defined(BTHREAD_CONTEXT_PLATFORM_linux_x86_64) && defined(BTHREAD_CONTEXT_COMPILER_gcc)
__asm (
".text\n"
".globl bthread_jump_fcontext\n"
".type bthread_jump_fcontext,@function\n"
".align 16\n"
"bthread_jump_fcontext:\n"
"    pushq  %rbp  \n"
"    pushq  %rbx  \n"
"    pushq  %r15  \n"
"    pushq  %r14  \n"
"    pushq  %r13  \n"
"    pushq  %r12  \n"
"    leaq  -0x8(%rsp), %rsp\n"
"    cmp  $0, %rcx\n"
"    je  1f\n"
"    stmxcsr  (%rsp)\n"
"    fnstcw   0x4(%rsp)\n"
"1:\n"
"    movq  %rsp, (%rdi)\n"
"    movq  %rsi, %rsp\n"
"    cmp  $0, %rcx\n"
"    je  2f\n"
"    ldmxcsr  (%rsp)\n"
"    fldcw  0x4(%rsp)\n"
"2:\n"
"    leaq  0x8(%rsp), %rsp\n"
"    popq  %r12  \n"
"    popq  %r13  \n"
"    popq  %r14  \n"
"    popq  %r15  \n"
"    popq  %rbx  \n"
"    popq  %rbp  \n"
"    popq  %r8\n"
"    movq  %rdx, %rax\n"
"    movq  %rdx, %rdi\n"
"    jmp  *%r8\n"
".size bthread_jump_fcontext,.-bthread_jump_fcontext\n"
".section .note.GNU-stack,\"\",%progbits\n"
);


这里的汇编是AT&T汇编,和Intel汇编语法不同。比如这里的mov操作,在从左到右看的。movq和popq的q表示操作的单位是四字(64位),如果是32位系统,则是movl和popl了。


pushq  %rbp
    pushq  %rbx
    pushq  %r15
    pushq  %r14
    pushq  %r13
    pushq  %r12

常规操作,就是把函数调用方的相关寄存器入栈,也就是保存调用方的运行环境。在当前函数执行结束之后要从栈中还原数据到相应的寄存器中,从而让调用方继续执行。所以末尾有出栈操作。


在入栈之后:


leaq  -0x8(%rsp), %rsp


表示:rsp 栈顶寄存器下移 8 字节,为FPU 浮点运算预留。


另外值得一提的是bthread_jump_fcontext()函数在调用的时候是传入了3个参数,但是定义的bthread_jump_fcontext()是可以接收4个参数的。也正是因为这个第4个参数,导致了代码里有了2次跳转,分别跳转到1和2处。


先看一下函数参数和寄存器的关系:


寄存器 对应参数
%rdi 第1个参数
%rsi 第2个参数
%rdx 第3个参数
%rcx 第4个参数


在leaq指令之后,开始判断第四个参数的值。


cmp  $0, %rcx
    je  1f
    stmxcsr  (%rsp)    // 保存当前MXCSR内容到rsp指向的位置
    fnstcw   0x4(%rsp) // 保存当前FPU状态字到rsp+4指向的位置
1:

如果第四个参数为0则直接跳转到1处(1在这里是一个标记,可以直接jump到对应的代码位置,类似C语言中的goto用法)。也就是跳过stmxcsrfnstcw这两个指令。


对于我们的场景而言,没有第四个参数也就不需要管这个。继续:


1:
    movq  %rsp, (%rdi)
    movq  %rsi, %rsp


我们知道%rdi%rsi表示的是第一个参数和第二个参数,也就是:&from->context 和 to->context。


这两个movq指令表示的就是栈切换的核心操作,将当前的栈指针(%rsp)存储到第一个参数所指向的内存中。然后将第二个参数的值赋值给栈指针。修改栈指针,就是更改了栈顶,也就是进行了实际的栈切换操作。


接着是不太重要的代码,还是和第四个参数有关的:


cmp  $0, %rcx
    je  2f
    ldmxcsr  (%rsp)
    fldcw  0x4(%rsp)
2:


也就是说如果第4个参数是0,则跳转到2。跳过的两条指令ldmxcsrfldcw可以理解为是之前stmxcsrfnstcw那两个指令的逆操作(也就是还原一下)。


2:
    leaq  0x8(%rsp), %rsp


%rsp 栈顶寄存器上移 8 字节,恢复为 FPU 浮点运算预留空间。


接着还原从栈中各个寄存器,因为是栈,所以逆向出栈。


popq  %r12
    popq  %r13
    popq  %r14
    popq  %r15
    popq  %rbx
    popq  %rbp


在这6个popq之后还有一个popq,和前面的pushq是没有对应关系的。


popq  %r8


是将bthread_jump_fcontext()返回之后要执行的指令地址,放到 %r8 寄存器中。展

开一下谈谈,在函数A调用函数B的时候,会先把函数的返回值入栈,然后再把函数B的参数入栈。所以对应逆操作,在函数参数都出栈之后,继续出栈的数据就是函数的返回地址!


movq  %rdx, %rax
    movq  %rdx, %rdi


%rdx表示的是函数的第三个参数,也就是是否:skip remained,当前都是0。先后存入到%rax%rdi中。


%rax寄存器表示的是返回值。


%rdi表示的是函数第一个参数。也就是给切换完栈之后要调用的函数,准备参数。


jmp  *%r8


跳转到返回地址,即调用方在调用完bthread_jump_fcontext()后,继续执行的指令位置。

相关文章
|
人工智能 自然语言处理 物联网
用最直白的方式,带你搞懂大模型“预训练” ——为什么今天的AI模型“学什么都快”?
预训练让AI先“通读万卷书”,掌握语言和图像的通用规律,再通过微调“因材施教”,快速学会专业技能。本文用通俗比喻解析其原理,带你零代码打造专属智能客服,开启人人可用的大模型时代。
575 0
|
10月前
|
存储 机器学习/深度学习 算法
|
存储 安全 C++
C++ 原子变量atomic variable
原子变量是 C++11 引入的一种同步机制,用于多线程环境中的无锁、线程安全操作。其操作不可分割,避免了数据竞争和不一致问题。原子变量位于 `&lt;atomic&gt;` 头文件中,支持多种类型如 `std::atomic&lt;T&gt;` 和特化类型。基本用法包括定义原子变量、加载、存储、交换及比较交换操作。内存顺序(如 `std::memory_order_seq_cst`)用于控制内存访问顺序和可见性,适用于不同场景。原子变量常用于线程安全的计数器和标志位等。
|
存储 消息中间件 缓存
Lustre架构介绍的阅读笔记-NFS兼容性
Lustre是分布式NFS系统,融合了分布式系统和NFS特性。它支持线性扩展容量和性能,提供POSIX语义,隐藏复杂存储细节。关键技术涉及分布式计算、缓存、锁、事务、通信(RPC、消息队列、同步/异步模式)、选举、任务调度、健康检查、负载均衡、集群管理和QoS。数据一致性、复制(副本、EC)、热点管理及多种上层协议(如NFS、S3)也是重点。分布式存储通过扩容提升读写带宽和IOPS。
625 1
|
Linux 测试技术 调度
Linux调度器何时需触发抢占?—— 从hackbench谈起
作者:何惟禹 吴一昊一、背景:性能之战“不服跑个分”虽然已经沦为手机行业的调侃用语,但在操作系统领域仍然是最重要的评价方式之一。本文的故事也源于一次 Alinux3 与 CentOS8 的一次跑分的较量。当然比分较量并不是目的,更重要的是发现存在的回归缺陷并进行修复,最终让 Alinux3 全方位持平或超过 CentOS8。在本次较量中,我们使用 hackbench 作为跑分软件,我们在测试过程中
3279 0
Linux调度器何时需触发抢占?—— 从hackbench谈起
crash工具学习 —— percpu相关的一些用法
crash工具学习 —— percpu相关的一些用法
|
存储 Unix 程序员
面向 C++ 的现代 CMake 教程(三)(1)
面向 C++ 的现代 CMake 教程(三)
797 1
|
存储 缓存 算法
比Bloom Filter节省25%空间!Ribbon Filter在Lindorm中的应用
本文研究了一种新的过滤器Ribbon Filter,并将其集成到Lindorm中
45898 11
比Bloom Filter节省25%空间!Ribbon Filter在Lindorm中的应用
|
存储 JavaScript 前端开发
JavaScript:DOM事件
JavaScript:DOM事件
254 0
|
存储 监控 C++
vs下dump文件的生成
vs下dump文件的生成
721 0