1. 디버깅 정보를 화면에 출력
1.1 일반 글쓰기
일반적으로 코드를 작성할 때 디버깅 정보의 일부 출력이 분명히 있을 것입니다.
#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;
}
현재 디렉터리에 test.txt 파일이 없다고 가정합니다. 프로그램이 7행까지 실행되면 NULL을 반환해야 하는데, 이때 11행의 디버깅 정보를 통해 프로그램이 종료된 이유를 정확하게 찾을 수 있습니다.
현재 디렉터리에 test.txt 파일이 있지만 읽을 수 없는 경우 어떻게 합니까?
열린 파일(test.txt) 오류도 출력
이 경우 파일 열기 실패 원인을 빠르게 찾는 방법은 무엇입니까? errno 사용을 고려할 수 있습니다 .
1.2 errno 사용
errno는 로깅 시스템의 마지막 오류 코드입니다. 오류 코드는 errno.h에 정의된 int 값입니다.
#include <errno.h> // errno 头文件
#include <string.h> // strerror 头文件
// 文件打开失败,提示错误并退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));
수정 후 main.exe를 다시 실행하십시오.
코드에 많은 디버깅 정보가 포함되어 있으면 어떻게 될까요? 이 정보가 어디에 출력되는지 한 번에 알 수 없기 때문에 현재 디버깅 정보가 있는 파일명과 소스코드 줄 위치까지 출력해서 한눈에 알아볼 수 있도록 하면 어떨까 하는 생각을 했습니다. 이를 바탕으로 1.3의 내용이 있다.
1.3 컴파일러 내장 매크로
ANSI C 표준에는 미리 정의된 몇 가지 표준 매크로가 있습니다.
__LINE__
: 소스코드에 현재 소스코드 줄번호 삽입__FILE__
: 현재 소스 파일 이름을 소스 파일에 삽입__FUNCTION_
: 현재 함수 이름을 소스 파일에 삽입__DATE__
: 현재 컴파일 날짜를 소스 파일에 삽입__TIME__
: 현재 컴파일 시간을 소스 파일에 삽입__STDC__
: 프로그램이 ANSI C 표준을 엄격히 따라야 하는 경우 플래그에 값 1을 할당합니다.__cplusplus
: 이 식별자는 C++ 프로그램을 작성할 때 정의됩니다.
따라서 출력 문을 다음과 같이 수정합니다.
// 文件打开失败,提示错误并退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n",
__FILE__,
__FUNCTION__,
__LINE__,
szFileName,
errno, strerror(errno));
로그 정보에서 우리는 다음을 정확하게 얻을 수 있습니다.
이전과 비교하면 확실히 문제를 정확하게 찾는 데 도움이 될 수 있지만 이렇게 긴 printf를 매번 작성할 수는 없겠죠?
1.4 가변 매크로를 사용하여 디버깅 정보 출력
1.4.1 변수 매크로 소개
가변 인수를 전달하려면 가변 매크로를 사용하십시오. 다음과 같이 함수에서 가변 인수를 사용하는 것이 익숙할 수 있습니다.
무효 printf(const char* format, ...);
ISO C 표준의 1999년 버전에서 매크로는 함수와 같은 가변 인수로 정의될 수 있습니다. 매크로의 구문은 다음과 같이 함수의 구문과 유사합니다.
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- 기본 숫자(
...
)는 가변 매개변수를 나타냅니다. __VA_ARGS__
매크로는 가변 개수의 인수를 허용하는 데 사용됩니다.
이러한 종류의 매크로가 호출되면 오른쪽 괄호 ...
끝까지 0개 이상의 기호(내부 쉼표 포함)로 표시됩니다(여기서는 기본 숫자 참조). 호출되면 매크로 본문(매크로 본문)에서 이러한 기호 시퀀스 모음이 내부의 _VA_ARGS_ 식별자를 대체합니다 . 매크로에 대한 호출이 확장되면 실제 인수가 에 전달됩니다 printf
.
ISO C 표준과 비교하여 GCC는 가변 매개변수에 다른 매개변수와 같은 이름을 부여할 수 있는 다른 구문을 사용하여 항상 복잡한 매크로를 지원해 왔습니다. 예를 들면 다음과 같습니다.
#define DEBUG(format, args...) printf(format, args)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- 이것은 위의 "ISO C" 매크로 예제와 정확히 동일하지만 더 읽기 쉽고 설명하기 쉽습니다.
표준 C에서는 가변 매개변수를 생략할 수 없지만 빈 매개변수를 전달할 수 있습니다. 예를 들어 다음 매크로 호출은 문자열 뒤에 쉼표가 없기 때문에 "ISO C"에서 올바르지 않습니다.
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
DEBUG("hello world.\n"); // 非法调用
}
GCC에서는 이 경우 varargs를 완전히 무시할 수 있습니다. 위의 예에서 매크로가 확장된 후 내부 문자열 뒤에 추가 쉼표가 있기 때문에 여전히 컴파일에 문제가 있습니다. 이 문제를 해결하기 위해 GCC는 특별한 ##
작업을 사용합니다. 작성 형식은 다음과 같습니다.
#define DEBUG(format, args...) printf(format, ##args)
-
여기서 변수 매개변수가 생략되거나 비어 있으면
##
작업으로 인해 전처리기가 앞에 있는 쉼표를 제거합니다. -
매크로를 호출할 때 일부 변수 매개변수를 제공하면 매크로 정의가 제대로 작동하고 이러한 변수 매개변수를 쉼표 뒤에 넣습니다.
1.4.2 가변 매크로를 사용하여 디버깅 정보 출력
1.4.1의 기본 사항을 사용하여 다음과 같이 코드를 수정할 수 있습니다.
#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));
- 가변 매크로를 통해 너무 긴 디버깅 정보를 작성하는 문제를 완벽하게 해결합니다.
너무 길게 쓰는 문제가 해결된 후 새로운 문제가 생겼는데 특정 디버깅 정보가 언제 출력되는지 알고 싶다면?
Linux의 시간 관련 콘텐츠에 대해 알아보겠습니다.
2. Linux의 시간 관련 함수
2.1 시간을 나타내는 구조
헤더 파일 "/usr/include/time.h" 및 "/usr/include/bits/time.h"를 보면 "시간"을 나타내는 다음 네 가지 구조를 찾을 수 있습니다.
/* 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
"초"를 나타내는 데 사용되는 긴 정수입니다.struct timeval
구조는 "초 및 마이크로초"를 사용하여 시간을 나타냅니다.struct timespec
구조는 "초 및 나노초"를 사용하여 시간을 나타냅니다.struct tm
시간을 표현하기 위해 "초, 분, 시, 일, 월, 년"을 직접 사용
2.2 현재 시간 얻기
// 可以获取精确到秒的当前距离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)
사용 방법은 다음과 같습니다.
#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;
}
- 위의 세 가지 기능을 통해 세 가지 정밀도로 현재 시간을 얻을 수 있습니다.
노트:
- POSIX.1-2008은 gettimeofday()를 구식으로 표시하고 대신 clock_gettime(2) 사용을 권장합니다.
- 게다가 어떤 사람이 테스트를 했고 gettimeofday를 두 번 연속으로 사용하면 작은 확률로 "뒤로 시간" 현상이 발생합니다. 두 번째 함수 호출로 얻은 시간은 첫 통화. .
- gettimeofday 함수는 시간이나 시계만큼 안정적이지도 정확하지도 않지만 유사하게 사용됩니다.
- 시계에는 일반적으로 대처하기에 충분한 596.5시간 이상이라고 하는 시간 제한이 있습니다.
- ntpd와 같은 프로세스는 시스템 시간을 수정하여 타이밍 오류를 일으킬 수 있습니다.
- 온라인 토론에 따르면 TSC 및 HPET 중단과 같은 것들이 시스템의 벽 시간을 롤백시킬 수 있습니다. 이것은 특정 시스템 구현과 관련이 있어야 합니다. 요컨대, gettimeofday 함수는 제공된 정확도를 보장하지 않으며 시스템의 정확한 시간을 보장하지도 않습니다. 반환되는 결과는 "벽 시간에서 시스템의 최선의 추측"입니다.
- 가능하면 clock_gettime(CLOCK_MONOTONIC)을 사용해 보십시오. 그러나 모든 시스템이 mac os x와 같은 posix 실시간을 구현하는 것은 아닙니다.
- 예를 들어 int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
CLOCK_MONOTONIC: 설정할 수 없는 시계이며 지정되지 않은 시작점 이후 단조로운 시간을 나타냅니다.
2.3 초, 밀리초, 마이크로초, 나노초 간 변환
- 1초 = 1000밀리초
- 1밀리초 = 1000마이크로초
- 1마이크로초 = 1000나노초
그래서:
- 1초 = 1000,000마이크로초(백만 마이크로초)
- 1초 = 1000,000,000나노초(십억 나노초)
초에서 밀리초까지, 밀리초에서 마이크로초까지, 마이크로초에서 나노초까지 모두 1000의 시간, 즉 0이 세 개 더 있는 관계입니다.
또 하나: 개인용 컴퓨터의 마이크로프로세서가 명령(예: 두 개의 숫자 더하기)을 실행하는 데 약 2~4나노초가 걸리므로 프로그램은 나노초까지만 정확하면 됩니다.
2.4 형식화된 시간 출력
-
먼저
struct timeval
or를struct timespec
time_t로 표시되는 초로 변환합니다.struct timeval stTimeVal; gettimeofday(&stTimeVal, NULL); time_t lTime = stTimeVal.tv_sec;
-
시스템 함수를 사용하여 time_t를 다음으로 변환
struct tm
:struct tm stTime; localtime_r(&lTime, &stTime); // 注意,localtime_r 的第二个参数是入参
-
형식화된 출력:
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);
time_t를 struct tm으로 변환하는 4가지 함수가 있습니다.
- struct tm *gmtime(const time_t *timep);
- struct tm *gmtime_r(const time_t *timep, struct tm *result);
- struct tm *localtime(const time_t *timep);
- struct tm *localtime_r(const time_t *timep, struct tm *result);
localtime과 같은 함수와 localtime_r과 같은 함수의 차이점은 다음과 같습니다. localtime에서 얻은 반환 값은 정적 struct tm 변수에 존재하며 후속 localtime 호출에서 덮어쓸 수 있습니다. 덮어쓰기를 방지하려면 스스로 struct tm 유형의 변수를 제공하고 localtime_r 함수를 사용하여 정의한 변수의 주소를 전달하고 그 결과를 저장하여 덮어쓰기를 방지할 수 있습니다.
따라서 gmtime 및 localtime 함수는 스레드로부터 안전하지 않으며 다중 스레드 프로그래밍에서는 주의해서 사용해야 합니다!
2.5 밀리초 시간 얻기
#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;
}
- 이 함수가 반환하는 buf는 정적에 의해 수정되며 스레드로부터 안전하지 않습니다.
2.6 디버깅 정보에 시간 정보 추가
#define DEBUG(format, args...) \
printf("%s [%s][%s:%d] "format"\n", \
GetMsecTime(), \
__FILE__, \
__FUNCTION__, \
__LINE__, \
##args)
지금까지 디버깅 정보의 출력 형식을 완성한 다음 디버깅 정보를 로그 파일에 출력하는 방법을 고려해야 합니다.
3. 디버깅 정보를 로그 파일에 출력
3.1 로그 수준
Log4J는 Log의 8단계(OFF와 ALL을 제외하고 6단계로 나눌 수 있음)를 정의하며 우선 순위는 높음에서 낮음으로 OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL입니다.
-
OFF: 가장 높은 수준으로 모든 로깅을 해제하는 데 사용됩니다.
-
치명적: 각각의 심각한 오류 이벤트로 인해 응용 프로그램이 종료됨을 나타냅니다. 이 수준은 상대적으로 높으며 중대한 오류가 있습니다. 이 수준에서 직접 프로그램을 중지할 수 있습니다.
-
ERROR: 오류 이벤트가 발생하더라도 시스템의 지속적인 운영에 영향을 미치지 않음을 나타냅니다. 오류 및 예외 정보를 인쇄합니다. 너무 많은 로그를 출력하지 않으려면 이 수준을 사용할 수 있습니다.
-
WARN: 잠재적인 오류가 발생할 것임을 나타냅니다. 일부 정보는 오류 메시지가 아니지만 일부 힌트는 프로그래머에게 제공됩니다.
-
INFO: 관심 있거나 중요한 일부 정보를 인쇄합니다. 이것은 프로덕션 환경에서 프로그램 실행에 대한 몇 가지 중요한 정보를 출력하는 데 사용할 수 있지만 너무 많은 로그 인쇄를 피하기 위해 남용될 수는 없습니다.
-
DEBUG: 주로 개발 과정에서 일부 실행 정보를 인쇄하는 데 사용됩니다.
-
TRACE: 매우 낮은 로그 수준, 일반적으로 사용되지 않음
-
ALL: 모든 로깅을 여는 데 사용되는 가장 낮은 수준
Log4J는 네 가지 수준만 사용할 것을 권장합니다. 우선 순위는 높은 것부터 낮은 것까지 ERROR, WARN, INFO, DEBUG입니다. 아래의 프로그램도 이 네 가지 로그 수준을 중심으로 코딩됩니다.
소스코드를 먼저 붙여넣고 시간나실 때 자세히 설명해주세요~
3.2 소스 코드
3.2.1 로그.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 main.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 튜토리얼
같은 디렉토리에 log.h, log.c, main.c 넣기
그리고 새 로그 디렉토리를 만듭니다.
컴파일, 실행