日志系统
1、原有问题
printf是同步阻塞的。如果磁盘 I/O 突然变慢,主线程为了打印一句话可能会卡住几毫秒,导致epoll错过大量网络包- 缺乏上下文:线上出了 Bug,只看到一句
Unknown type,根本不知道是哪个文件、哪一行代码、在什么时间打印的。
2、代码 logger.h和logger.c
2.1 定义宏与接口 (server/logger.h)
logger.h
#define _LOGGER_H_
typedef enum{
DEBUG_LEVEL = 0,
INFO_LEVEL,
WARN_LEVEL,
ERROR_LEVEL
} LogLevel;
void logger_init(const char *filepath);
void async_log(LogLevel level, const char *file, int line, const char *format, ...);
// 【核心】:用宏封装,自动捕获 __FILE__ 和 __LINE__
// __VA_ARGS__ 会把传入的变长参数(比如 "%d", uid)原封不动传给 async_log
#define LOG_DEBUG(...) async_log(DEBUG_LEVEL, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...) async_log(INFO_LEVEL, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARN(...) async_log(WARN_LEVEL, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERROR(...) async_log(ERROR_LEVEL, __FILE__, __LINE__, __VA_ARGS__)
#endif
// 文件名行号自动填
// LOG_INFO("用户 %d 登录", uid);
// LOG_ERROR("连接失败: %s", errmsg);
2.2 实现后台写线程与队列 (server/logger.c)
logger.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdarg.h>
#include<time.h>
#include<pthread.h>
#include"logger.h"
static const char *level_strings[] = {"DEBUG","INFO","WARN","ERROR"};
typedef struct LogNode{
char message[1024];
struct LogNode *next;
} LogNode;
// 后台日志管理器全局变量
static struct {
FILE *fp;
pthread_mutex_t lock;
pthread_cond_t notify;
LogNode *head;
LogNode *tail;
} g_logger;
static void* logger_thread_func(void *arg){
while(1){
pthread_mutex_lock(&g_logger.lock);
while (g_logger.head == NULL)
{
pthread_cond_wait(&g_logger.notify,&g_logger.lock);
}
LogNode *node = g_logger.head;
g_logger.head = node->next;
if(g_logger.head == NULL) g_logger.tail = NULL;
pthread_mutex_unlock(&g_logger.lock);
if(g_logger.fp){
fputs(node->message,g_logger.fp);
fflush(g_logger.fp);
}
free(node);
}
return NULL;
}
void logger_init(const char *filepath){
g_logger.fp = fopen(filepath,"a");
if(!g_logger.fp){
perror(" Failed to open log file");
exit(EXIT_FAILURE);
}
g_logger.head = g_logger.tail =NULL;
pthread_mutex_init(&g_logger.lock,NULL);
pthread_cond_init(&g_logger.notify,NULL);
pthread_t tid;
pthread_create(&tid,NULL,logger_thread_func,NULL);//tid 是线程的"门牌号",创建后线程自己独立运行
LOG_INFO("==========L-CHAT SERVER STARTED==========");
}
void async_log(LogLevel level,const char *file,int line,const char *format,...){
// ↑ ↑
// │ └── 变长参数开始
// └── 最后一个固定参数(格式字符串)
time_t now = time(NULL);
struct tm *t = localtime(&now);
LogNode *node = (LogNode *)malloc(sizeof(LogNode));
if(!node) return;
node->next = NULL;
int offset = snprintf(node->message,sizeof(node->message),//offset:已写入的字节数
"[%04d:%02d:%02d %02d:%02d:%02d] [%s] [%s:%d] ",
t->tm_year+1900, t->tm_mon+1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec,
level_strings[level], file, line);
va_list args;//一个特殊类型,内部存储变长参数的位置信息
va_start(args,format);//定位第一个变长参数:args 现在指向 format 后面的第一个参数(例uid)
//格式化输出到字符串
vsnprintf(node->message+offset, // 目标:从已有内容后面开始写
sizeof(node->message)-offset-1, // 最大可写长度(防溢出)
format, // 格式字符串:例"用户 %d 登录,状态 %d"
args); // 变长参数的实际值
va_end(args); // 结束遍历,清理 args 内部状态(某些架构需要)
strcat(node->message,"\n"); // 在已有字符串末尾追加换行
pthread_mutex_lock(&g_logger.lock);
if(g_logger.tail == NULL){
g_logger.tail = node;
g_logger.head = node;
}
else{
g_logger.tail->next = node;
g_logger.tail = node;
}
pthread_cond_signal(&g_logger.notify);
pthread_mutex_unlock(&g_logger.lock);
// 异步日志(非阻塞)
// 只把字符串复制到队列,立即返回
// 后台线程慢慢写磁盘,不卡业务
}
3、测试结果
实现基本的日志功能