前言
所謂日誌就是記下工作的過程,也就是小學時候老師經常叫我們寫的日記,要求我們記錄今天做了哪些事啊,遇到了什麼有趣的事啊,等等。計算機中程序的運行就像我們人類活動一樣會遇到各種各種的事件,程序員也會像小學老師一樣要求程序寫日記,當有一天在一個月黑風高的晚上,這個程序突然跑死了,系統中看不到這個程序在運行了,這個時候程序員就會跑過去,打開程序的日誌仔細研究,檢查程序在什麼地方發生了bug, 出現了什麼類型的bug。然後,幹掉這個bug😃。
當然啦,我們寫程序的時候可以使用printf將信息打印到屏幕以監測程序的運行,爲啥需要日誌管理呢?你要這麼想,我們開發的過程中一般都是在PC上開發,當然可以將程序信息打印到標準輸出上啦,可是我們寫的程序並不是運用在PC上的哦,我們將寫好的程序放到嵌入式設備中運行,在大多少嵌入式設備都不會將程序的運行信息輸出到標準輸出上,所以我們需要將信息寫到日誌文件中,記錄程序運行的過程,方便程序員後期對產品的維護和技術支持。
這篇文章的大致結構就是首先學習Linux下的日誌管理系統,然後c語言實現自己的日誌管理😏
Linux下的日誌系統
Linux環境中系統運行的日誌信息一般記錄在/var/log/syslog文件中,以/var/log/*.log爲擴展名的文件就是日誌文件,比如:
那我們應該如何操作才能將日誌信息記錄到這個目錄下呢??
系統自帶的日誌操作API
- openlog
打開日誌設備,可以顯示調用也可以不調用,因爲syslog函數會自動自動調用openlog。
void openlog(const char *ident, int option, int facility);
參數分析:
ident:日誌標籤,表明本條日誌是誰產生的,一般填該程序的名稱。
option:選項,表明日誌設備應該做什麼工作。其宏定義如下:
LOG_CONS :如果將信息發送給 syslogd 守護進程時發生錯誤,直接將相關信息輸出到終端。
LOG_NDELAY: 立即打開與系統日誌的連接(通常情況下,只有在產生第一條日誌信息的情況下才會
打開與日誌系統的連接)
LOG_ODELAY :類似於 LOG_NDELAY 參數,與系統日誌的連接只有在 syslog 函數調用時纔會
創建
LOG_PERROR :在將信息寫入日誌的同時,將信息發送到標準錯誤輸出
LOG_PID: 每條日誌信息中都包含進程號
facility:指定記錄消息程序的類型,與 syslogd 守護進程的配置文件 syslog.conf 中的 facility 對應。可取如下值:
LOG_AUTH :認證系統(login、su、getty等)
LOG_AUTHPRIV :同 LOG_AUTH 但只登陸到所選擇的單個用戶可讀的文件中。
LOG_CRON :cron 守護進程
LOG_DAEMON: 其他系統守護進程,如 routed
LOG_FTP :文件傳輸協議:ftpd、tftpd
LOG_KERN :內核產生的消息
LOG_LPR :系統打印機緩衝池:lpr、lpd
LOG_MAIL :電子郵件系統
LOG_NEWS :網絡新聞系統
LOG_SYSLOG :由 syslogd(8)產生的內部消息
LOG_USER :隨機用戶進程產生的消息
LOG_UUCP :UUCP 子系統
LOG_LOCAL0 ~ LOG_LOCAL7 :本地使用保留
- syslog
向日志設備中寫入日誌。日誌有級別之分,緊急、錯誤、警告等,所以這個函數中需要傳入日誌級別,我們自己實現日誌系統的時候,也需要定義日誌級別,以便分清出哪些是一般的輸出信息,哪些是出錯信息,哪些是提醒信息,一目瞭然。這裏和printf不一樣,printf就是單純地將信息輸出到標準輸出上。
void syslog(int priority, const char *format, …);
參數分析:
priority:前面所說的日誌級別,Linux內核爲其定義了宏定義,如下:
LOG_EMERG: 緊急情況
LOG_ALERT :應該被立即改正的問題,如系統數據庫破壞
LOG_CRIT :重要情況,如硬盤錯誤
LOG_ERR :錯誤
LOG_WARNING :警告信息
LOG_NOTICE :不是錯誤情況,但是可能需要處理
LOG_INFO :情報錯誤
LOG_DEBUG :包含情報的信息,通常指在調試一個程序時使用
format:這個函數是一個可變參數列表的格式,有點像printf的調用方式,比如printf(“打印信息:%s”,str);雙眼號裏面就是format.
…:第三個參數就是str,想要打印的字符串。在編寫自己的日誌函數時需要用到這種方式。😃
- closelog
看函數名就知道這是個關閉日誌設備的函數!
void closelog(void);
測試一下系統日誌設備中寫日誌
#include<stdio.h>
#include<syslog.h>
int main(int argc, char **argv)
{
char *progname = "syslog_test";
openlog("log_test", LOG_CONS | LOG_PID, 0);//打開日誌設備,並將標籤設置爲log_test
syslog(LOG_INFO, "Program '%s'start running\n", progname);//寫入一個程序開始信息
syslog(LOG_WARNING, "Program '%s' running with a warnning message\n", progname );//寫入一個提醒信息
syslog(LOG_EMERG, "Program '%s' running with a emergency message\n", progname );//寫入一個錯誤信息
syslog(LOG_INFO, "Program '%s' stop running\n", progname);//寫入一個程序結束信息
closelog();//關閉日誌設備
return 0;
}
~
終端輸入tail /var/log/syslog
查看剛剛寫入的日誌信息
其實,操作Linux下的日誌管理系統很簡單,但是在實際項目中我們不會將所有的程序信息都輸出到系統日誌文件中,而是通過使用GNU C提供的標準庫函數和系統調用爲每個程序實現專用的日誌函數和日誌文件。
自定義日誌函數
- 需要實現的日誌格式:日期+時間+日誌等級+函數+行號+信息
- 不再多說了,直接上代碼
頭文件
+ log.h
#ifndef LOG_H_
#define LOG_H_
/*定義日誌等級,使用枚舉*/
enum LogLevel
{
ERROR = 0,
WARN,
INFO,
DEBUG,
};
int mylog(const char *function, int line, enum LogLevel level, const char *fmt, ...);
#endif
c文件
#include<stdarg.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<time.h>
#include<sys/time.h>
#include<sys/file.h>
#include<sys/types.h>
#include<errno.h>
#include<libgen.h>
#include"log.h"
#ifndef LOGLEVEL
#define LOGLEVEL DEBUG
#endif
#define MAXSIZE_LOG 1024//日誌文件的大小
//通過s_loginfo數組即可獲取等級對應的字符串
static const char* s_loginfo[] =
{
"ERROR",
"WARN",
"INFO",
"DEBUG",
};
/* 測試程序
* */
int main(int argc,char **argv)
{
mylog(__func__,__LINE__,DEBUG,"this is debug information");
mylog(__func__,__LINE__,INFO,"this is info information");
mylog(__func__,__LINE__,WARN,"this is WARN information");
mylog(__func__,__LINE__,ERROR,"this is error information");
}
int mylog(const char *function, int line, enum LogLevel level, const char *fmt, ...)//含有可變參數的函數
{
char *tmp;
time_t t;
struct tm *p;
struct timeval tv;
int len;
char *progname;
int millsec;
FILE *fp;
char buf[100];
char log_buf[200];
char time_buf[32];
va_list arg_list;
int millisec;
off_t filesize;
int fd = -1;//文件描述符
memset(buf,0,sizeof(buf));
//開始應用可變參數
va_start(arg_list,fmt);//指向可變參數表中的第一個參數
vsnprintf(buf,sizeof(buf),fmt,arg_list);//將可變參數輸出到buf中
//獲取事件戳
t = time(NULL);
p = localtime(&t);
gettimeofday(&tv,NULL);
millisec = (int)(tv.tv_usec/100);
//製作時間的格式:年月日時分秒毫秒
snprintf(time_buf,32,"%04d-%02d-%02d %02d:%02d:%02d:%03d",p->tm_year+1900,p->tm_mon+1,p->tm_mday,p->tm_hour,p->tm_min,p->tm_sec,millisec);
if(level > LOGLEVEL)//日誌等級不存在則返回
{
return -1;
}
//製作日誌格式:時間+函數+行+信息
len = snprintf(log_buf,sizeof(buf),"[%s][%s][%s:%d]:%s",time_buf,s_loginfo[level], function, line, buf);
//打開或者創建日誌文件
if((fp = fopen("mylog.txt","a+"))==NULL)
{
printf("fopen mylog.txt failed:%s\n",strerror(errno));
return -1;
}
fd = fileno(fp);
if(fd < 0)
{
printf("fileno error:%s\n",strerror(errno));
return -1;
}
//給日誌文件上鎖,防止在併發程序中共用日誌文件
if(flock(fd,LOCK_EX) ==-1 )
{
printf("flock LOCK_EX failed:%s\n",strerror(errno));
return -1;
}
//剛打開文件的時候文件指針默認在文件頭,所以先將文件指針偏移到文件尾
if(filesize=lseek(fd,0,SEEK_END) ==-1 )
{
printf("lseek failed:%s\n",strerror(errno));
return -1;
}
//控制文件不要無限制增長,設置文件最大容量,到達就清空
if(filesize == MAXSIZE_LOG)
{
if(ftruncate(fd,0) ==-1 )//清空文件
{
printf("ftruncate failed:%s\n",strerror(errno));
return -1;
}
}
//調用庫函數將日誌信息寫入日誌文件
fprintf(fp,"%s\n",log_buf);
//可變參數的應用區結束
va_end(arg_list);
//釋放文件鎖
if(flock(fd,LOCK_UN) ==-1 )
{
printf("flock LOCK_UN failed:%s\n",strerror(errno));
return -1;
}
//關閉文件流描述符
fclose(fp);
return 0;
}
對上面代碼用到的知識點進行總結
- 第一:可變參數的使用方法va_list、va_start、va_end
這個三個函數都是stdarg.h的宏定義,linux下可以使用locate stdarg.h
命令查找頭文件所在的位置:
//重定義char *
typedef char * va_list;
//獲取類型佔用的空間長度,最小佔用長度爲int的整數倍
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//獲取可變參數列表的第一個參數的地址
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//獲取可變參數的當前參數,返回指定類型並將指針指向下一參數
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//清空va_list可變參數列表
#define va_end(ap) ( ap = (va_list)0 )
- 第二:定義一個結構體指針數組,數組的每一個下標表示數組中每一個字符串的首地址,所以我們
s_loginfo[0],s_loginfo[1],s_loginfo[2],s_loginfo[3]
這樣來指向字符串首地址。
static const char* s_loginfo[] =
{
"ERROR",
"WARN",
"INFO",
"DEBUG",
};
- 第三:snprintf、vsnprintf、printf區別和應用
#include <stdio.h>
//輸出到標準輸出
int printf(const char *format, ...);
//輸出到文件
int fprintf(FILE *stream, const char *format, ...);
//將格式化的數據輸出到字符串str中,我們一般使用這個庫函數來定製想要的字符格式
int sprintf(char *str, const char *format, ...);
//按size大小輸出到字符串str中
int snprintf(char *str, size_t size, const char *format, ...);
上面的庫函數和下面的庫函數一一對應,只不過最後一個參數需要動態地獲取
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
- 第四:系統調用open和標準庫函數fopen
這兩個函數最大的區別就是系統調用和庫函數,open返回的的是一個文件描述符fd,fd是每個進程控制塊維護的一張表,這個表裏面默認有1024個文件描述符,通過這個文件描述符可以直接找到磁盤的的文件所在的位置。但是,fopen是一個庫函數,它返回的是一個指向文件結構的指針,這個文件結構中包含磁盤文件的索引fd,fopen不能直接對磁盤文件進行操作,先要寫入io緩衝區,最後由內核寫到磁盤上。我們可以使用以下以下兩個函數進型fp和fd之間進行轉換:
//將一個文件流中對應打開的文件描述符fd返回
int fileno(FILE *stream);
// 給定一個文件描述符和mode創建一個使用該描述符對應的流,與fileno相反
FILE *fdopen(int fd, const char *mode);
- 第五fopen的第二個參數mode:
mode 打開模式:
r 只讀方式打開一個文本文件
rb 只讀方式打開一個二進制文件
w 只寫方式打開一個文本文件
wb 只寫方式打開一個二進制文件
a 追加方式打開一個文本文件
ab 追加方式打開一個二進制文件
r+ 可讀可寫方式打開一個文本文件
rb+ 可讀可寫方式打開一個二進制文件
w+ 可讀可寫方式創建一個文本文件
wb+ 可讀可寫方式生成一個二進制文件
a+ 可讀可寫追加方式打開一個文本文件
ab+ 可讀可寫方式追加一個二進制文件
- 第六flock的鎖類型宏定義
LOCK_SH 建立共享鎖定。多個進程可同一時候對同一個文件作共享鎖定。
LOCK_EX 建立相互排斥鎖定。一個文件同一時候僅僅有一個相互排斥鎖定。
LOCK_UN 解除文件鎖定狀態。