惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
Lohrmann on Cybersecurity
K
Kaspersky official blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
P
Proofpoint News Feed
量子位
S
Schneier on Security
AWS News Blog
AWS News Blog
N
Netflix TechBlog - Medium
T
Threat Research - Cisco Blogs
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
L
LINUX DO - 热门话题
T
The Exploit Database - CXSecurity.com
A
Arctic Wolf
S
Securelist
T
Tailwind CSS Blog
T
Tor Project blog
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
I
InfoQ
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
CXSECURITY Database RSS Feed - CXSecurity.com
C
Cisco Blogs
D
DataBreaches.Net
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tenable Blog
The GitHub Blog
The GitHub Blog
V
Vulnerabilities – Threatpost
V
Visual Studio Blog
博客园 - 叶小钗
F
Full Disclosure
Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
Engineering at Meta
Engineering at Meta
S
SegmentFault 最新的问题
The Last Watchdog
The Last Watchdog
H
Hacker News: Front Page
阮一峰的网络日志
阮一峰的网络日志
D
Docker
Spread Privacy
Spread Privacy
T
The Blog of Author Tim Ferriss
S
Security @ Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
MyScale Blog
MyScale Blog
腾讯CDC
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 Protobuf又一坑 - C++标准和ABI兼容性 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 C++20 Text Formatting/fmtlib 适配问题小记 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) [Rust] 实现一个线程安全且迭代器可以保存的链表 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
近期的一个协程流程BUG
2017-01-03 · via I'm OWenT

blog-website

最近一直没什么时间整理近期碰到的问题,今天思考了一下之前碰到的一个临时处理的BUG,顺便写点东西清理一下思路。

其实严格来说这个BUG更应该是一个流程试用问题,不过这个问题应该是需要能在协程库里检测并抛出错误来。

libcopp的执行流程

这个BUG涉及最底层context的执行流程,这个协程库切入有两个接口(start和resume),截止目前为止,这两个接口其实是对等的,然后有一个切出接口(yield)。切入的时候需要记录调用者的执行上下文,用于在切出时上下文切出到哪里。问题是各种情况下的这个上下文保存的执行状态。

然后再来看上下文切换的代码,第一部分是首次切入的入口(调用start函数)。

void coroutine_context_base::coroutine_context_callback(::copp::fcontext::transfer_t src_ctx) {
    assert(src_ctx.data);
    if (NULL == src_ctx.data) {
        abort();
        return;
    }

    // copy jump_src_data_t in case it's destroyed later
    jump_src_data_t jump_src = *reinterpret_cast<jump_src_data_t *>(src_ctx.data);

    // this must in a coroutine
    coroutine_context_base *ins_ptr = jump_src.to_co;
    assert(ins_ptr);
    if (NULL == ins_ptr) {
        abort();
        return;
    }

    // update caller of to_co
    ins_ptr->caller_ = src_ctx.fctx;

    // save from_co's fcontext and switch status
    if (UTIL_CONFIG_NULLPTR != jump_src.from_co) {
        jump_src.from_co->callee_ = src_ctx.fctx;
        int from_status = status_t::EN_CRS_RUNNING; // from coroutine change status from running to ready
        jump_src.from_co->status_.compare_exchange_strong(from_status, status_t::EN_CRS_READY);
    }

    // this_coroutine
    set_this_coroutine_context(ins_ptr);

    // run logic code
    ins_ptr->run_and_recv_retcode(jump_src.priv_data);

    ins_ptr->status_.store(status_t::EN_CRS_FINISHED);
    // jump back to caller
    ins_ptr->yield();
}

最重要的是save from_co’s fcontext and switch status下面的这一段。这里的作用就是切入后要把跳入前的上下文保存到来源的协程中。

然后是恢复的流程(调用resume/yield函数):

void coroutine_context_base::jump_to(fcontext::fcontext_t &to_fctx, stack_context &from_sctx, stack_context &to_sctx,
                                     jump_src_data_t &jump_transfer) UTIL_CONFIG_NOEXCEPT {

    copp::fcontext::transfer_t res;
    jump_src_data_t *jump_src;
    int from_status;
    bool swap_success;
// can not use any more stack now
// can not initialize those vars here

#ifdef COPP_MACRO_USE_SEGMENTED_STACKS
    assert(&from_sctx != &to_sctx);
    // ROOT->A: jump_transfer.from_co == NULL, jump_transfer.to_co == A, from_sctx == A.caller_stack_, skip backup segments
    // A->B.start(): jump_transfer.from_co == A, jump_transfer.to_co == B, from_sctx == B.caller_stack_, backup segments
    // B.yield()->A: jump_transfer.from_co == B, jump_transfer.to_co == NULL, from_sctx == B.callee_stack_, skip backup segments
    if (UTIL_CONFIG_NULLPTR != jump_transfer.from_co) {
        __splitstack_getcontext(jump_transfer.from_co->callee_stack_.segments_ctx);
        if (&from_sctx != &jump_transfer.from_co->callee_stack_) {
            memcpy(&from_sctx.segments_ctx, &jump_transfer.from_co->callee_stack_.segments_ctx, sizeof(from_sctx.segments_ctx));
        }
    } else {
        __splitstack_getcontext(from_sctx.segments_ctx);
    }
    __splitstack_setcontext(to_sctx.segments_ctx);
#endif
    res = copp::fcontext::copp_jump_fcontext(to_fctx, &jump_transfer);
    if (NULL == res.data) {
        abort();
        return;
    }
    jump_src = reinterpret_cast<jump_src_data_t *>(res.data);
    assert(jump_src);

    /**
     * save from_co's fcontext and switch status
     * we should use from_co in transfer_t, because it may not jump from jump_transfer.to_co
     *
     * if we jump sequence is A->B->C->A.resume(), and if this call is A->B, then
     * jump_src->from_co = C, jump_src->to_co = A, jump_transfer.from_co = A, jump_transfer.to_co = B
     * and now we should save the callee of C and set the caller of A = C
     *
     * if we jump sequence is A->B.yield()->A, and if this call is A->B, then
     * jump_src->from_co = B, jump_src->to_co = NULL, jump_transfer.from_co = A, jump_transfer.to_co = B
     * and now we should save the callee of B and should change the caller of A
     *
     */

    // update caller of to_co if not jump from yield mode
    if (UTIL_CONFIG_NULLPTR != jump_src->to_co) {
        jump_src->to_co->caller_ = res.fctx;
    }

    if (UTIL_CONFIG_NULLPTR != jump_src->from_co) {
        jump_src->from_co->callee_ = res.fctx;
        from_status = jump_src->from_co->status_.load();
        if (status_t::EN_CRS_RUNNING == from_status) {
            jump_src->from_co->status_.compare_exchange_strong(from_status, status_t::EN_CRS_READY);
        } else if (status_t::EN_CRS_FINISHED == from_status) {
            // if in finished status, change it to exited
            jump_src->from_co->status_.store(status_t::EN_CRS_EXITED);
        }
    }

    // private data
    jump_transfer.priv_data = jump_src->priv_data;

    // this_coroutine
    set_this_coroutine_context(jump_transfer.from_co);
    // resume running status of from_co
    if (NULL != jump_transfer.from_co) {
        from_status = jump_transfer.from_co->status_.load();
        swap_success = false;
        while (!swap_success && status_t::EN_CRS_READY == from_status) {
            swap_success = jump_transfer.from_co->status_.compare_exchange_strong(from_status, status_t::EN_CRS_RUNNING);
        }
    }
}

同样,需要关注的部分是update caller of to_co if not jump from yield mode后面的保存上下下文的部分。

所以到这里可以看到第一个麻烦的地方,流程上的两种(start/resume和yield)和上下文切换的两种(首次和后续)并不匹配;第二个麻烦的地方在于只有在切入完成以后才能拿到切入时切入方的上下文。所以导致执行流程比较复杂。

执行流程

首先是正常的执行流程。

外部->协程A(yield)[->外部(resume)->协程A(yield)]…->外部

这时候,首次切入的时候,由首次的回调函数保存上下文。yield时,相当于切入的copp_jump_fcontext完成return,而后由后面的代码完成把上下文保存进协程的工作。resume的时候,相当于yield的copp_jump_fcontext完成return,然后用同样的方式保存上下文。

其次是一个协程启动另一个协程,然后yield回前一个,走类似链式的流程,也是类似栈的流程。

外部->协程A(start)->协程B(yield)->协程A(yield)->外部

这种流程在携程B会记录caller为A的切出点,那么切回时流程也是走上面第一种情况一样的流程。

问题流程

这两种流程最常用,并且已经使用很久了并不会有什么问题。然后第三种情况是最近一个特殊流程导致的。

外部->协程A(start)->协程B(resume)->协程A(yield)->…

按最早的设计,协程A通过resume或者start接口第二次切入后,会更新协程A的caller为协程B(注释里写得状态信息更完整一些,但是考虑的调用链更长)。但是现在精简下调用链和这个执行流程,就不容易发现,这也是不正确的。因为这时候再也切不回最外部的调用者了。

然后我的第一反应是,这样的话可以尝试后续的切入流程不再更新caller,只在第一次切入时更新caller。但是这样也不正确,因为假设协程B第二次通过resume切入协程A的时候,如果没有更新调用信息,那么其实协程A已经运行了一段代码了,而协程B里记录的还是老的值,这样协程B如果使用yield那么也是不对的。

其实我还考虑过,协程的caller保存协程指针而不只是上下文。但是这样也是有问题的,一个是数据保存的流程和生命周期很难管理,其次是碰到复杂的调用流程的话,还是没法通知某些协程通知caller。

解决方案

所以最终最安全的解决方案还是加个flag禁止掉这种调用。即一旦一个协程被切入了,只能通过yield切出。而同理,没有被切入的协程,必须通过start/resume切入。

不过其实会触发这个BUG还有另一个原因,libatbus有一处优化是,如果发送的消息目标是自己,那么会通过递归的方式直接回调接收事件(这也能够使发送给自己的消息少一次copy)。然后这个事件能导致协程调用resume。

但是根据实际的使用情况,自己发给自己的消息是非常少见的,并且对外发豆油一次copy,发给自己减少掉这次copy意义不是很大。另外解包的CPU消耗应该本身比copy要高得多,所以我打算果断时间对libatbus给自己发消息的时序也做一次优化,模拟成异步消息。这样也能解决一些隐性的前期以来问题。

当然有限还是先加libcopp的这个patch和单元测试。也是之前的单元测试没有覆盖到这种情况才导致现在才发现这种流程BUG。

Written with StackEdit.