电子书阅读器:基于UDP的网络日志调试系统

发布于:2025-05-31 ⋅ 阅读:(25) ⋅ 点赞:(0)

目录

为什么要引入网络编程进行远程打印?

框架与管理

 debug层结构

stdout.c

netprint.c(重头戏)

明确两个问题:udp和server端的选择

核心机制 

实现细节 

 debug_manager.c

netprint_client.c

为什么要引入网络编程进行远程打印?

针对开发板调试中串口打印的局限性,建议采用网络日志收集方案替代传统串口输出。

  • 我们的程序是在开发板上运行,以前 printf 打印信息从串口打印出来。如果有成百上千个设备要同时去测试的话,那就要接成百上千条串口线,太麻烦了。所以说用串口线打印,一个是麻烦,另外一个是不好管理。
  • 还有串口的打印非常慢,当应用程序加入了成百上千条串口打印之后,就会导致程序运行得非常慢。
  • 发布一个程序之后,肯定会把这些打印信息去掉,就会导致调试的程序和真正发布的程序效果是不一样的,就会掩盖很多的问题。

所以我们要引入网络编程,把打印信息通过网络传输到某一台机器上,在那台机器上进行观察。

框架与管理

功能架构:

        其他函数在进行打印调试输出的时候使用DebugPrint函数,DebugPrint函数调用底层的打印结构体(封装在stdout.c和netprint.c中)。

        功能:

  1. 模块化架构:通过T_DebugOpr结构体抽象不同输出方式。
  2. 支持多输出通道管理;包括串口通道(stdout)以及网络打印(netprint)
  3. 提供动态通道开关接口(SetDebugChannel)。
  4. 对于网络打印(netprint.c),使用环形缓冲区存储日志信息,当客户端(日志)连接之后,使用网络通信传递日志信息。
  5. 实现日志分级控制(0-7级)。
  • 如果我们用stdou.c来打印的话,肯定很快,但是用netprint来打印的话还会涉及客户端和服务端,所以不会一开始马上就打印出来,所以我们要先把数据存入buffer,当客户端连接之后,再使用网络通信传输数据给客户端。
  • 关于第5点理解:我们参照内核printk的实现的功能,对每条信息实现设置和显示打印级别。

 debug层结构

在debug_manager.h中定义结构体;包含name,初始化(对网络通信的初始化),print(在debug_manager.c中先对变参形式的数据进行处理,参考printf,使用vsprintf存入到一个缓冲区中),Exit,canUsed(用于判断当前打印渠道)。

typedef struct DebugOpr{
    char *name;
    int canUsed;
    int (*DebugInit)(void);
    int (*DebugExit)(void);
    int (*DebugPrint)(char *strData);
    struct DebugOpr *ptNext;
}T_DebugOpr, *PT_DebugOpr;

stdout.c

        (无需初始化和退出,打印则直接调用printf即可)

#include <config.h>
#include <debug_manager.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>


static int StdoutDebugPrint(char *strData) {
    /* 标准输出: 直接将输出信息用printf打印即可 */
    printf("%s", strData);

    return strlen(strData);// 返回已经成功打印的字符数
}


// 分配注册一个结构体
static T_DebugOpr g_tStdoutDebugOpr = {
    .canUsed = 1,
    .name = "stdout",
    .DebugPrint = StdoutDebugPrint,
    // 对于标准输出,不需要做什么初始化和退出操作
    // 因此这里直接设置为空即可,当后续需要调用这两个函数的时候需要判断是否存在这两个函数
    // .DebugExit = StdoutDebugExit,
    // .DebugInit = StdoutDebugInit,
};

// 注册结构体
int StdoutInit(void){
    return RegisterDebugOpr(&g_tStdoutDebugOpr);
}


netprint.c(重头戏)

明确两个问题:udp和server端的选择

使用udp还是tcp?

  1. 日志传输时如果使用TCP传输显然更安全并且可以保证服务端接收的顺序。如果采用UDP则不可能会出现日志块丢失的情况。但是日志块丢失一般出现在网络环境不好的情况下,如果网络环境出现问题即使使用TCP也会因为Tcp的超时重传机制,出现传输拥堵的问题,最终可能导致进程异常结束。
  2. 如果使用UDP来传输,传输效率上会很快。
  3. 在对日志完整性要求不是很高,在可靠的局域网环境下可以使用UDP。

开发版作为server端还是cllient端?

  1. 烧入开发板的程序当做 server 端,因为开发板上你可以随便执行应用程序,然后你想在哪一台机器上打印,就用那台机器登录开发板就可以了。ssh root@192.168.5.9
  2. 被动服务模式: 开发板作为Server时持续监听固定端口(如8888),客户端可随时连接获取日志。这种设计符合UDP无连接特性优势,无需维护长连接状态
  3. 资源占用优化: Server端只需初始化一次网络模块,避免了Client端频繁建立连接的开销。
  4. 单个Server可同时响应多个Client请求,便于团队协作调试
核心机制 
  1. 采用16KB环形缓冲区实现异步日志传输
  2. 双线程模型:发送线程(NetDebugSendThread)和接收线程(NetDebugRecvThread)
  3. 条件变量(pthread_cond_t)实现线程间高效同步
实现细节 

在对UDP日志服务初始化的时候        

  1. UDP Socket创建与绑定(端口号通过SERVER_PORT宏定义)
  2. 动态分配16KB打印缓冲区(PRINT_BUF_SIZE宏控制)
  3. 双线程模型建立(发送/接收线程分离)

打印函数的实现

        其他文件调用DebugPrint打印的时候会调用各个渠道的Print;首先将数据放入到环形缓冲区中;之后使用条件变量唤醒发送线程。发送线程平时处于休眠状态(客户端连接之后并且有数据进入到唤醒缓冲区中),等待唤醒。唤醒后,如果有客户端连接并且环形缓冲区中有数据,则将环形缓冲区中的数据取出来放入到发送缓冲区中,通过sendto函数向客户端发送打印数据,直到环形缓冲区中没有数据。NetDebugRecvThread用于接收来自客户端的设置与控制命令。

        为什么要使用环形缓冲区?

由于环形缓冲区的读写分开特性,当两个线程进行通信的时候,可以采用环形缓冲区进行交流,一个进程读取,一个进程写入,由于读写的位置不同,并不需要加锁进行并发控制,也就减少了锁的时间开销

        在多进行几次打印后会发现客户端不再打印信息,原因在于条件变量仅进行通知操作也要加上互斥量。

        线程的条件变量在唤醒和休眠的时候要获得互斥量:条件本身是由互斥量保护的。线程在改变条件状态之前必须要首先锁住互斥量。-------------------------------------参考APUE p332。

  • 原子性保证:pthread_cond_wait()必须与互斥量配合才能实现"解锁-等待-加锁"的原子操作
  • 互斥量保护共享状态:条件变量的等待/通知机制必须基于某个共享状态的改变,该状态需要互斥量保护
    pthread_mutex_lock(&g_tSendMutex);
    pthread_cond_signal(&g_tSendConVar);
    pthread_mutex_unlock(&g_tSendMutex);

 debug_manager.c

全局日志级别参照内核的实现,使用8个打印级别。

  1. SetDebugLevel:解析"dbglevel=X"格式命令,设置全局日志级别阈值(0-7)
  2. SetDebugChannel:处理"通道名=0/1"指令,动态启用/禁用特定输出通道。

 在main.c中首先初始化日志系统,因为后续初始化会调用日志调试系统。

netprint_client.c

基于UDP协议的网络调试客户端程。主要用于发送控制命令和接收日志信息。需要单独编译,通过开发板ip地址进行连接。