• 记一次坎坷的调试|Mosquitto通过TLS连接EMQ时阻塞的问题


    最近两天在调试一个关于嵌入式Linux系统环境时,在系统开机之后,Mosquitto通过tls连接MQTT服务器(EMQ)时,创建MQTT连接总是阻塞的问题,现记录一下调试过程及解决问题的步骤。

    先说下开发调试环境:

    • 硬件平台:EXP imx.6ull
    • 内核版本:4.1.15
    • rootfs:基于buildroot创建
    • mosquitto:2.0.11
    • openssl:1.1.1
    • MQTT服务器:支持TLS服务的EMQ

    问题表象

    linux系统开机之后,出现shell登录提示符之后,调用mosquitto_connect和EMQ建立基于TLS的连接,mosquitto_connect调用之后阻塞,大约90秒,该函数调用才会返回,并且报错。之后,mosquitto会触发重连机制,再次连接EMQ服务器,连接成功。这时,如果重新发起向EMQ的连接请求,mosquitto_connect不会阻塞。

    起初怀疑的几点原因:

    1. 系统启动之后,网络还未初始化完成,就调用了mosquitto_connect,连接EMQ可能失败。尝试测试方法:完善网络初始化流程,优化mosquitto_connect调用时序,经测试,无效。
    2. 怀疑EMQ服务器问题,尝试测试方法:使用其他设备测试对同一EMQ发起TLS连接,没有问题。
    3. 开机时,系统时间未同步(1970-01-01 08:00:00),联网之后,ntpd服务才会同步网络时间到本地,所以中间会导致mqtt连接阻塞,经过思考,如果是系统时间的问题,mosquitto_connect应该返回TLS校验证书失效的错误提示,而不是阻塞。
    4. 怀疑应用程序其他部分干扰mosquitto_connect的调用流程,使用mosquitto_pub直接向EMQ发布消息,发现现象一样。

    上述几轮怀疑和测试之后,调试陷入了僵局,我有些郁闷,经过思考之后,使出了杀手锏:就是最笨的,也是最有效的方法:向mosquitto_connect执行流程插入调试日志。其实,有些时候,看似最笨的方法,却是最为有效的调试方法,只要方向对,花些时间和精力也是值得的。

    调试步骤

    按照mosquitto_connect的调用流程,增加日志,确定阻塞位置,这里有一个调试的小技巧,就是先主干,再细节,即,先在需要调试的主路径上的关键位置添加日志,然后,编译调试,从而可以确定问题的大体位置,然后,在子流程上再次添加日志,再编译,在调试,这样一步一步递归下去,就会较为快速的定位问题位置。应用该方法,我最终确定了mosquitto_connect阻塞问题位置是:

    SSL_CTX *ssl_ctx = SSL_CTX_new(TLS_client_method());
    
    • 1

    简单介绍一下SSL_CTX_new,其主要是openssl创建用于TLS通信的控制块,为了确认问题,我编写了测试程序,再次确认是不是该函数导致的问题。代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
     
    void ssl_test(void)
    {
        printf("ssl_test:TP0.\n");
        SSL_CTX *ssl_ctx = SSL_CTX_new(TLS_client_method());
     
        (void)ssl_ctx;
        printf("ssl_test:TP1.\n");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    程序很简单,就是为了确认罪魁祸首是不是SSL_CTX_new。Linux开机后,立即执行改测试函数,首先打印"ssl_test:TP0",大约90秒之后,打印"ssl_test:TP1.”,问题位置确认。

    发现问题

    如果你是一个SSL/TLS机制小白的话,或者说,只了解SSL/TLS交互流程的话,分析SSL_CTX_new的实现原理有点强人所难,我属于这类人,所以,调试再次陷入困境。

    就在一筹莫展之际,我发现了一个有趣的现象:发现终端只要打印一行:

    random: nonblocking pool is initialized
    
    • 1

    SSL_CTX_new就会返回,多次测试之后,确认这两者的先后关系。可见,random的初始化和SSL_CTX_new的阻塞存在关系。之后的调试就比较顺利了,实在是山重水复疑无路,柳暗花明又一村

    通过查阅资料,发现关于random: nonblocking pool is initialized有以下几种主流的解决思路:

    1. Linux随机数nonblocking pool快速初始化

    2. random: nonblocking pool is initialized,只是开机不打印该提示,没有实际解决问题

    3. Linux随机数nonblocking pool快速初始化

    经过分析,1、3和该问题的现象基本一致:可以确定,4.1.15版本的内核random驱动存在问题,导致系统启动时,random初始较慢(90s),而SSL_CTX_new在初始化的时候,用到了random,其需要等待random初始化完成,才能继续执行,从而导致SSL_CTX_new阻塞,最终导致mosquitto_connect阻塞。

    解决问题

    修改random内核驱动代码:drivers/char/random.c,找到函数add_interrupt_randomness,修改如下的代码:

    原始代码:

     if ((fast_pool->count < 64) &&
                !time_after(now, fast_pool->last + HZ))
            return;
    
    • 1
    • 2
    • 3

    修改后的代码:

     if ((fast_pool->count < 64) &&
             !time_after(now, fast_pool->last + HZ) &&
             nonblocking_pool.initialized)
         return;
    
    • 1
    • 2
    • 3
    • 4

    注意:本文内核版本是4.1.15,经过查阅内核代码,发现在较新的内核版本,该bug已解决!

    编译,替换内核,经测试问题解决!

    后记

    反观这个问题的解决过程,不知道你发下了没有,一开始,问题的现象和引起问题的根源,可以用俗语“八竿子打不着”来形容,每次感觉问题快要解决了,最后,还是竹篮打水一场空,那种失落感、焦虑感会越来越浓。其实,事后分析解决问题的过程,不难得出解决问题的关键首先,保持冷静,确定方向,然后,使用科学的方法,坚持不懈的向前推进,相信自己,最后,问题最终会被解决,只是时间的问题

  • 相关阅读:
    c++中c风格的字符串
    typescript24-类型推论
    架构师范文(AI写作)两篇
    讲解嵌入式软件中超时机制设计
    C++ 基础一
    Vue监测数据改变原理
    【Postman&JMeter】使用Postman和JMeter进行signature签名
    vue 使用Dialog对话框使用过程中出现灰色遮罩问题
    灰狼算法Grey Wolf Optimizer跑23个经典测试函数|含源码
    创邻科技Galaxybase—激活数据要素的核心引擎
  • 原文地址:https://blog.csdn.net/linux_embedded/article/details/126100266