1. Saída de informações de depuração para a tela
1.1 Redação geral
Quando normalmente escrevemos código, definitivamente haverá alguma saída de informações de depuração:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char szFileName[] = "test.txt";
FILE *fp = fopen(szFileName, "r");
if (fp == NULL)
{
// 文件打开失败,提示错误并退出
printf("open file(%s) error.\n", szFileName);
exit(0);
}
else
{
// 文件打开成功,进行相应的文件读/写操作
}
return 0;
}
Suponha que não haja nenhum arquivo test.txt no diretório atual. Quando o programa for executado na linha 7, ele deve retornar NULL.Neste momento, por meio das informações de depuração da linha 11, podemos nos ajudar a descobrir com precisão o motivo da saída do programa: verifica-se que o arquivo falhou ao abrir.
E se houver um arquivo test.txt no diretório atual, mas não for legível?
Também gera erro de arquivo aberto (test.txt)
Nesse caso, como localizar rapidamente a causa da falha na abertura do arquivo? Podemos considerar o uso de errno .
1.2 Usando errno
errno é o último código de erro para o sistema de registro. O código de erro é um valor int definido em errno.h.
#include <errno.h> // errno 头文件
#include <string.h> // strerror 头文件
// 文件打开失败,提示错误并退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));
Execute main.exe novamente após a modificação:
E se o código contiver muitas informações de depuração? Não podemos saber onde essas informações são impressas de uma só vez, então pensamos, podemos também imprimir o nome do arquivo e o local da linha do código-fonte onde as informações de depuração atuais estão localizadas, para que fique claro à primeira vista. Com base nisso, tem-se o conteúdo de 1.3.
1.3 Macros integradas do compilador
Existem várias macros padrão predefinidas no padrão ANSI C:
__LINE__
: Insira o número da linha do código-fonte atual no código-fonte__FILE__
: Insira o nome do arquivo de origem atual no arquivo de origem__FUNCTION_
: Insira o nome da função atual no arquivo de origem__DATE__
: insere a data de compilação atual no arquivo de origem__TIME__
: insere o tempo de compilação atual no arquivo de origem__STDC__
: Quando o programa é obrigado a seguir estritamente o padrão ANSI C, o sinalizador recebe um valor de 1__cplusplus
: Este identificador é definido ao escrever um programa C++
Portanto, modificamos a instrução de saída assim:
// 文件打开失败,提示错误并退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n",
__FILE__,
__FUNCTION__,
__LINE__,
szFileName,
errno, strerror(errno));
A partir das informações do log, podemos obter com precisão: a 16ª linha da função principal no arquivo main.c relatou um erro e a causa do erro foi Permissão negada
Comparado com antes, pode realmente nos ajudar a localizar o problema com precisão, mas não podemos escrever um printf tão longo todas as vezes, certo? Existe uma maneira de ser preguiçoso?
1.4 Use macros variáveis para gerar informações de depuração
1.4.1 Introdução às macros variáveis
Use macros variadic para passar argumentos variadic. Você pode estar familiarizado com o uso de argumentos variadic em funções, como:
void printf(formato const char*, ...);
Na versão de 1999 do padrão ISO C, as macros podem ser definidas com funções semelhantes a varargs. A sintaxe de uma macro é semelhante à de uma função, como segue:
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- O número padrão (
...
) refere-se a parâmetros variáveis __VA_ARGS__
As macros são usadas para aceitar um número variável de argumentos
Quando esse tipo de macro é chamado, ele (aqui se refere ao número padrão ...
) é expresso como zero ou mais símbolos (incluindo vírgulas dentro), até o final do parêntese direito. Quando invocadas, no corpo da macro (corpo da macro), essas coleções de sequências de símbolos substituirão os identificadores _VA_ARGS_ dentro de . Quando a chamada para a macro é expandida, os argumentos reais são passados para printf
.
Comparado ao padrão ISO C, o GCC sempre suportou macros complexas, usando uma sintaxe diferente que permite dar aos parâmetros variáveis um nome como qualquer outro parâmetro. Por exemplo o seguinte exemplo:
#define DEBUG(format, args...) printf(format, args)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- Isso é exatamente o mesmo que o exemplo de macro "ISO C" acima, mas é mais legível e fácil de descrever
No padrão C, você não pode omitir um parâmetro variádico, mas pode passar um parâmetro vazio. Por exemplo, a seguinte chamada de macro é ilegal em "ISO C" porque não há vírgula após a string:
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
DEBUG("hello world.\n"); // 非法调用
}
O GCC permite que você ignore varargs inteiramente neste caso. No exemplo acima, ainda haverá problemas com a compilação, pois após a macro ser expandida, haverá uma vírgula extra após a string dentro dela. Para resolver esse problema, o GCC usa uma ##
operação especial. O formato de escrita é:
#define DEBUG(format, args...) printf(format, ##args)
-
Aqui, se o parâmetro variável for omitido ou vazio,
##
a ação fará com que o pré-processador retire a vírgula que o precede -
Se você fornecer alguns parâmetros variáveis ao chamar a macro, a definição da macro funcionará bem, colocará esses parâmetros variáveis após a vírgula
1.4.2 Usando macros variáveis para gerar informações de depuração
Com o básico de 1.4.1, podemos modificar o código assim:
#define DEBUG(format, args...) \
printf("[%s][%s:%d] "format"\n", \
__FILE__, \
__FUNCTION__, \
__LINE__, \
##args)
// 文件打开失败,提示错误并退出
DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
- Por meio de macros variáveis, o problema de escrever informações de depuração muito longas é perfeitamente resolvido
Depois que o problema de escrever muito tempo é resolvido, há um novo problema: e se eu quiser saber quando uma determinada informação de depuração é impressa?
Vamos aprender sobre o conteúdo relacionado ao tempo no Linux.
2. Funções relacionadas ao tempo no Linux
2.1 Uma estrutura que representa o tempo
Observando os arquivos de cabeçalho "/usr/include/time.h" e "/usr/include/bits/time.h", podemos encontrar as quatro estruturas a seguir representando "tempo":
/* Returned by `time'. */
typedef __time_t time_t;
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
struct timespec
{
__time_t tv_sec; /* Seconds. */
long int tv_nsec; /* Nanoseconds. */
};
struct tm
{
int tm_sec; /* Seconds. [0-59] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year. 自 1900 起的年数 */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year. [0-365] */
int tm_isdst; /* DST. 夏令时 */
#ifdef __USE_BSD
long int tm_gmtoff; /* Seconds east of UTC. */
__const char *tm_zone; /* Timezone abbreviation. */
#else
long int __tm_gmtoff; /* Seconds east of UTC. */
__const char *__tm_zone; /* Timezone abbreviation. */
#endif
};
time_t
É um inteiro longo usado para representar "segundos"struct timeval
A estrutura usa "segundos e microssegundos" para representar o tempostruct timespec
A estrutura usa "segundos e nanossegundos" para representar o tempostruct tm
Use diretamente "segundos, minutos, horas, dias, meses, anos" para expressar o tempo
2.2 Obter a hora atual
// 可以获取精确到秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的秒数
time_t time(time_t *t);
// 可以获取精确到微秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的微秒数
int gettimeofday(struct timeval *tv, struct timezone *tz);
// 可以获取精确到纳秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的纳秒数
int clock_gettime(clockid_t clk_id, struct timespec *tp)
A forma de usar é a seguinte:
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
int main()
{
time_t lTime;
time(&lTime);
printf("lTime : %ld\n", lTime);
struct timeval stTimeVal;
gettimeofday(&stTimeVal, NULL);
printf("stTimeVal : %ld\n", stTimeVal.tv_sec);
struct timespec stTimeSpec;
clock_gettime(CLOCK_REALTIME, &stTimeSpec);
printf("stTimeSpec : %ld\n", stTimeSpec.tv_sec);
return 0;
}
- Podemos obter a hora atual com três precisões diferentes através das três funções acima
Notas:
- POSIX.1-2008 marca gettimeofday() como obsoleto, recomendando o uso de clock_gettime(2).
- Além disso, alguém fez um teste. Ao usar gettimeofday duas vezes seguidas, haverá um fenômeno de "tempo para trás" com uma pequena probabilidade. O tempo obtido pela segunda chamada da função é menor ou anterior ao tempo obtido pelo primeira chamada. .
- A função gettimeofday não é tão estável e nem tão precisa quanto as horas ou o relógio, mas são usadas de maneira semelhante.
- O relógio tem um limite de tempo, que é de 596,5+ horas, o que geralmente é suficiente para lidar.
- Processos como o ntpd podem modificar a hora do sistema, causando erros de temporização.
- De acordo com discussões on-line, coisas como interrupções de TSC e HPET podem fazer com que o tempo de parede do sistema seja revertido. Isso deve estar relacionado à implementação específica do sistema. Resumindo, a função gettimeofday não garante a precisão fornecida, nem garante a hora exata do sistema. O resultado que ela retorna é "o melhor palpite do sistema no tempo de parede".
- Se possível, tente usar clock_gettime (CLOCK_MONOTONIC), mas nem todos os sistemas implementam posix realtime, como mac os x.
- 所以现在应该用:int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
CLOCK_MONOTONIC:Relógio que não pode ser ajustado e representa tempo monotônico desde algum ponto inicial não especificado.
2.3 Conversão entre segundos, milissegundos, microssegundos, nanossegundos
- 1 segundo = 1000 milissegundos
- 1 milissegundo = 1000 microssegundos
- 1 microssegundo = 1000 nanossegundos
então:
- 1 segundo = 1000.000 microssegundos (um milhão de microssegundos)
- 1 segundo = 1000.000.000 nanossegundos (bilhões de nanossegundos)
De segundos a milissegundos, milissegundos a microssegundos e microssegundos a nanossegundos são tempos de 1000, ou seja, a relação de mais três 0s.
Outra: leva cerca de 2 a 4 nanossegundos para o microprocessador de um computador pessoal executar uma instrução (como somar dois números), portanto, o programa só precisa ter precisão de nanossegundos.
2.4 Saída formatada de tempo
-
Primeiro converta
struct timeval
oustruct timespec
para segundos representados por time_t:struct timeval stTimeVal; gettimeofday(&stTimeVal, NULL); time_t lTime = stTimeVal.tv_sec;
-
Use as funções do sistema para converter time_t para
struct tm
:struct tm stTime; localtime_r(&lTime, &stTime); // 注意,localtime_r 的第二个参数是入参
-
Saída formatada:
char buf[128]; // 自定义输出格式:YYYY-MM-DD hh:mm:ss snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d", stTime.tm_year + 1900, stTime.tm_mon + 1, stTime.tm_mday, stTime.tm_hour, stTime.tm_min, stTime.tm_sec); puts(buf);
Existem 4 funções para converter time_t em struct tm, a saber:
- struct tm *gmtime(const time_t *timep);
- struct tm *gmtime_r(const time_t *timep, struct tm *resultado);
- struct tm *localtime(const time_t *timep);
- struct tm *localtime_r(const time_t *timep, struct tm *resultado);
A diferença entre funções como localtime e funções como localtime_r é: o valor de retorno obtido por localtime existe em uma variável struct tm estática, que pode ser sobrescrita por chamadas de localtime subsequentes. Se quisermos evitar a substituição, podemos fornecer uma variável do tipo struct tm por nós mesmos, usar a função localtime_r para passar o endereço da variável que definimos e salvar o resultado nela, para evitar a substituição.
Portanto, pode-se ver que as funções gmtime e localtime não são thread-safe, e devem ser usadas com cautela em programação multi-thread!
2.5 Obter tempo em milissegundos
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <string.h>
char *GetMsecTime()
{
static char buf[128];
time_t lTime = 0;
struct timeval stTimeVal = {0};
struct tm stTime = {0};
gettimeofday(&stTimeVal, NULL);
lTime = stTimeVal.tv_sec;
localtime_r(&lTime, &stTime);
snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d.%.3d",
stTime.tm_year + 1900,
stTime.tm_mon + 1,
stTime.tm_mday,
stTime.tm_hour,
stTime.tm_min,
stTime.tm_sec,
stTimeVal.tv_usec / 1000); // 微秒 -> 毫秒
return buf;
}
int main()
{
puts(GetMsecTime());
return 0;
}
- Observe que o buf retornado por esta função é modificado por static e não é thread-safe
2.6 Adicionar informações de tempo nas informações de depuração
#define DEBUG(format, args...) \
printf("%s [%s][%s:%d] "format"\n", \
GetMsecTime(), \
__FILE__, \
__FUNCTION__, \
__LINE__, \
##args)
Até agora, aperfeiçoamos o formato de saída das informações de depuração e precisamos considerar como enviar as informações de depuração para o arquivo de log.
3. Envie as informações de depuração para o arquivo de log
3.1 Nível de registro
Log4J define 8 níveis de Log (excluindo OFF e ALL, pode ser dividido em 6 níveis), e a prioridade de alto a baixo é: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL.
-
OFF: o nível mais alto, usado para desligar todos os logs
-
FATAL: Indica que cada evento de erro grave fará com que o aplicativo seja encerrado. Este nível é relativamente alto, erros graves, você pode parar o programa diretamente neste nível
-
ERRO: Indica que, embora ocorra um evento de erro, ele ainda não afeta a operação contínua do sistema. Imprimir informações de erro e exceção, se você não quiser gerar muitos logs, pode usar este nível
-
WARN: Indica que um possível erro irá ocorrer.Algumas informações não são uma mensagem de erro, mas algumas dicas também são dadas ao programador.
-
INFO: Imprima algumas informações de seu interesse ou importantes. Isso pode ser usado para exibir algumas informações importantes sobre a execução do programa no ambiente de produção, mas não pode ser abusado para evitar a impressão de muitos logs
-
DEBUG: Usado principalmente para imprimir algumas informações em execução durante o processo de desenvolvimento
-
TRACE: nível de log muito baixo, geralmente não usado
-
ALL: o nível mais baixo, usado para abrir todos os logs
Log4J recomenda usar apenas quatro níveis, a prioridade de alta para baixa é ERROR, WARN, INFO, DEBUG. Nosso programa abaixo também será codificado em torno desses quatro níveis de log.
Cole o código-fonte primeiro e explique-o em detalhes quando tiver tempo~
3.2 Código fonte
3.2.1 log.h
#ifndef __LOG_H__
#define __LOG_H__
#ifdef __cplusplus
extern "C"
{
#endif
// 日志路径
#define LOG_PATH "./Log/"
#define LOG_ERROR "log.error"
#define LOG_WARN "log.warn"
#define LOG_INFO "log.info"
#define LOG_DEBUG "log.debug"
#define LOG_OVERFLOW_SUFFIX "00" // 日志溢出后的文件后缀,如 log.error00
#define LOG_FILE_SIZE (5*1024*1024) // 单个日志文件的大小,5M
// 日志级别
typedef enum tagLogLevel
{
LOG_LEVEL_ERROR = 1, /* error级别 */
LOG_LEVEL_WARN = 2, /* warn级别 */
LOG_LEVEL_INFO = 3, /* info级别 */
LOG_LEVEL_DEBUG = 4, /* debug级别 */
} LOG_LEVEL_E;
typedef struct tagLogFile
{
char szCurLog[64];
char szPreLog[64];
} LOG_FILE_S;
#define PARSE_LOG_ERROR(format, args...) \
WriteLog(LOG_LEVEL_ERROR, __FILE__, __FUNCTION__, __LINE__, format, ##args)
#define PARSE_LOG_WARN(format, args...) \
WriteLog(LOG_LEVEL_WARN, __FILE__, __FUNCTION__, __LINE__, format, ##args)
#define PARSE_LOG_INFO(format, args...) \
WriteLog(LOG_LEVEL_INFO, __FILE__, __FUNCTION__, __LINE__, format, ##args)
#define PARSE_LOG_DEBUG(format, args...) \
WriteLog(LOG_LEVEL_DEBUG, __FILE__, __FUNCTION__, __LINE__, format, ##args)
extern void WriteLog
(
LOG_LEVEL_E enLogLevel,
const char *pcFileName,
const char *pcFuncName,
int iFileLine,
const char *format,
...
);
#ifdef __cplusplus
}
#endif
#endif
3.2.2 log.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h> // va_stat 头文件
#include <errno.h> // errno 头文件
#include <time.h> // 时间结构体头文件
#include <sys/time.h> // 时间函数头文件
#include <sys/stat.h> // stat 头文件
#include "log.h"
static LOG_FILE_S gstLogFile[5] =
{
{"", ""},
{
/* error级别 */
LOG_PATH LOG_ERROR, // ./Log/log.error
LOG_PATH LOG_ERROR LOG_OVERFLOW_SUFFIX // ./Log/log.error00
},
{
/* warn级别 */
LOG_PATH LOG_WARN, // ./Log/log.warn
LOG_PATH LOG_WARN LOG_OVERFLOW_SUFFIX // ./Log/log.warn00
},
{
/* info级别 */
LOG_PATH LOG_INFO, // ./Log/log.info
LOG_PATH LOG_INFO LOG_OVERFLOW_SUFFIX // ./Log/log/info00
},
{
/* debug级别 */
LOG_PATH LOG_DEBUG, // ./Log/log.debug
LOG_PATH LOG_DEBUG LOG_OVERFLOW_SUFFIX // ./Log/log.debug00
},
};
static void __Run_Log
(
LOG_LEVEL_E enLogLevel,
const char *pcFileName,
const char *pcFuncName,
int iFileLine,
const char *format,
va_list vargs
)
{
FILE *logfile = NULL;
logfile = fopen(gstLogFile[enLogLevel].szCurLog, "a");
if (logfile == NULL)
{
printf("open %s error[%d](%s).\n", gstLogFile[enLogLevel].szCurLog, errno, strerror(errno));
return;
}
/* 获取时间信息 */
struct timeval stTimeVal = {0};
struct tm stTime = {0};
gettimeofday(&stTimeVal, NULL);
localtime_r(&stTimeVal.tv_sec, &stTime);
char buf[768];
snprintf(buf, 768, "%.2d-%.2d %.2d:%.2d:%.2d.%.3lu [%s][%s:%d] ",
stTime.tm_mon + 1,
stTime.tm_mday,
stTime.tm_hour,
stTime.tm_min,
stTime.tm_sec,
(unsigned long)(stTimeVal.tv_usec / 1000),
pcFileName,
pcFuncName,
iFileLine);
fprintf(logfile, "%s", buf);
vfprintf(logfile, format, vargs);
fprintf(logfile, "%s", "\r\n");
fflush(logfile);
fclose(logfile);
return;
}
static void __LogCoverStrategy(char *pcPreLog) // 日志满后的覆盖策略
{
int iLen = strlen(pcPreLog);
int iNum = (pcPreLog[iLen - 2] - '0') * 10 + (pcPreLog[iLen - 1] - '0');
iNum = (iNum + 1) % 10;
pcPreLog[iLen - 2] = iNum / 10 + '0';
pcPreLog[iLen - 1] = iNum % 10 + '0';
}
void WriteLog
(
LOG_LEVEL_E enLogLevel,
const char *pcFileName,
const char *pcFuncName,
int iFileLine,
const char *format,
...
)
{
char szCommand[64]; // system函数中的指令
struct stat statbuff;
if (stat(gstLogFile[enLogLevel].szCurLog, &statbuff) >= 0) // 如果存在
{
if (statbuff.st_size > LOG_FILE_SIZE) // 如果日志文件超出限制
{
printf("LOGFILE(%s) > 5M, del it.\n", gstLogFile[enLogLevel].szCurLog);
snprintf(szCommand, 64, "cp -f %s %s", gstLogFile[enLogLevel].szCurLog, gstLogFile[enLogLevel].szPreLog);
puts(szCommand);
system(szCommand); // 将当前超出限制的日志保存到 log.error00 中
snprintf(szCommand, 64, "rm -f %s", gstLogFile[enLogLevel].szCurLog);
system(szCommand); // 删掉 log.error
printf("%s\n\n", szCommand);
// 如果 log.error 超出 5M 后,将依次保存在 log.error00、log.error01、... 中
__LogCoverStrategy(gstLogFile[enLogLevel].szPreLog);
}
}
else // 如果不存在,则创建
{
printf("LOGFILE(%s) is not found, create it.\n\n", gstLogFile[enLogLevel].szCurLog);
snprintf(szCommand, 64, "touch %s", gstLogFile[enLogLevel].szCurLog);
system(szCommand);
}
va_list argument_list;
va_start(argument_list, format);
if (format)
{
__Run_Log(enLogLevel, pcFileName, pcFuncName, iFileLine, format, argument_list);
}
va_end(argument_list);
return;
}
3.3.3 principal.c
#include <stdio.h>
#include <unistd.h> // sleep 头文件
#include "log.h"
int main()
{
for (int i = 0; i < 5; i++)
{
PARSE_LOG_ERROR("我是第 %d 条日志", i+1);
}
return 0;
}
3.3.4 Tutorial
Coloque log.h, log.c, main.c no mesmo diretório
E crie um novo diretório de Log
compilar, executar