目录
为什么要引入网络编程进行远程打印?
针对开发板调试中串口打印的局限性,建议采用网络日志收集方案替代传统串口输出。
- 我们的程序是在开发板上运行,以前 printf 打印信息从串口打印出来。如果有成百上千个设备要同时去测试的话,那就要接成百上千条串口线,太麻烦了。所以说用串口线打印,一个是麻烦,另外一个是不好管理。
- 还有串口的打印非常慢,当应用程序加入了成百上千条串口打印之后,就会导致程序运行得非常慢。
- 发布一个程序之后,肯定会把这些打印信息去掉,就会导致调试的程序和真正发布的程序效果是不一样的,就会掩盖很多的问题。
所以我们要引入网络编程,把打印信息通过网络传输到某一台机器上,在那台机器上进行观察。
框架与管理
功能架构:
其他函数在进行打印调试输出的时候使用DebugPrint函数,DebugPrint函数调用底层的打印结构体(封装在stdout.c和netprint.c中)。
功能:
- 模块化架构:通过T_DebugOpr结构体抽象不同输出方式。
- 支持多输出通道管理;包括串口通道(stdout)以及网络打印(netprint)
- 提供动态通道开关接口(SetDebugChannel)。
- 对于网络打印(netprint.c),使用环形缓冲区存储日志信息,当客户端(日志)连接之后,使用网络通信传递日志信息。
- 实现日志分级控制(0-7级)。
- 如果我们用stdou.c来打印的话,肯定很快,但是用netprint来打印的话还会涉及客户端和服务端,所以不会一开始马上就打印出来,所以我们要先把数据存入buffer,当客户端连接之后,再使用网络通信传输数据给客户端。
- 关于第5点理解:我们参照内核printk的实现的功能,对每条信息实现设置和显示打印级别。
debug层结构
在debug_manager.h中定义结构体;包含name,初始化(对网络通信的初始化),print(在debug_manager.c中先对变参形式的数据进行处理,参考printf,使用vsprintf存入到一个缓冲区中),Exit,canUsed(用于判断当前打印渠道)。
cpp
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即可)
cpp
#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?
- 日志传输时如果使用TCP传输显然更安全并且可以保证服务端接收的顺序。如果采用UDP则不可能会出现日志块丢失的情况。但是日志块丢失一般出现在网络环境不好的情况下,如果网络环境出现问题即使使用TCP也会因为Tcp的超时重传机制,出现传输拥堵的问题,最终可能导致进程异常结束。
- 如果使用UDP来传输,传输效率上会很快。
- 在对日志完整性要求不是很高,在可靠的局域网环境下可以使用UDP。
开发版作为server端还是cllient端?
- 烧入开发板的程序当做 server 端,因为开发板上你可以随便执行应用程序,然后你想在哪一台机器上打印,就用那台机器登录开发板就可以了。ssh [email protected]
- **被动服务模式:**开发板作为Server时持续监听固定端口(如8888),客户端可随时连接获取日志。这种设计符合UDP无连接特性优势,无需维护长连接状态
- **资源占用优化:**Server端只需初始化一次网络模块,避免了Client端频繁建立连接的开销。
- 单个Server可同时响应多个Client请求,便于团队协作调试
核心机制
- 采用16KB环形缓冲区实现异步日志传输
- 双线程模型:发送线程(NetDebugSendThread)和接收线程(NetDebugRecvThread)
- 条件变量(pthread_cond_t)实现线程间高效同步
实现细节
在对UDP日志服务初始化的时候
- UDP Socket创建与绑定(端口号通过SERVER_PORT宏定义)
- 动态分配16KB打印缓冲区(PRINT_BUF_SIZE宏控制)
- 双线程模型建立(发送/接收线程分离)
打印函数的实现
其他文件调用DebugPrint打印的时候会调用各个渠道的Print;首先将数据放入到环形缓冲区中;之后使用条件变量唤醒发送线程。发送线程平时处于休眠状态(客户端连接之后并且有数据进入到唤醒缓冲区中),等待唤醒。唤醒后,如果有客户端连接并且环形缓冲区中有数据,则将环形缓冲区中的数据取出来放入到发送缓冲区中,通过sendto函数向客户端发送打印数据,直到环形缓冲区中没有数据。NetDebugRecvThread用于接收来自客户端的设置与控制命令。
为什么要使用环形缓冲区?
由于环形缓冲区的读写分开特性,当两个线程进行通信的时候,可以采用环形缓冲区进行交流,一个进程读取,一个进程写入,由于读写的位置不同,并不需要加锁进行并发控制,也就减少了锁的时间开销
在多进行几次打印后会发现客户端不再打印信息,原因在于条件变量仅进行通知操作也要加上互斥量。
线程的条件变量在唤醒和休眠的时候要获得互斥量:条件本身是由互斥量保护的。线程在改变条件状态之前必须要首先锁住互斥量。-------------------------------------参考APUE p332。
- 原子性保证:
pthread_cond_wait()
必须与互斥量配合才能实现"解锁-等待-加锁"的原子操作 - 互斥量保护共享状态:条件变量的等待/通知机制必须基于某个共享状态的改变,该状态需要互斥量保护
cpp
pthread_mutex_lock(&g_tSendMutex);
pthread_cond_signal(&g_tSendConVar);
pthread_mutex_unlock(&g_tSendMutex);
debug_manager.c
全局日志级别参照内核的实现,使用8个打印级别。

SetDebugLevel
:解析"dbglevel=X"格式命令,设置全局日志级别阈值(0-7)SetDebugChannel
:处理"通道名=0/1"指令,动态启用/禁用特定输出通道。
在main.c中首先初始化日志系统,因为后续初始化会调用日志调试系统。
netprint_client.c
基于UDP协议的网络调试客户端程。主要用于发送控制命令和接收日志信息。需要单独编译,通过开发板ip地址进行连接。
