Linux下学习日志管理系统并实现日志功能

前言

所谓日志就是记下工作的过程,也就是小学时候老师经常叫我们写的日记,要求我们记录今天做了哪些事啊,遇到了什么有趣的事啊,等等。计算机中程序的运行就像我们人类活动一样会遇到各种各种的事件,程序员也会像小学老师一样要求程序写日记,当有一天在一个月黑风高的晚上,这个程序突然跑死了,系统中看不到这个程序在运行了,这个时候程序员就会跑过去,打开程序的日志仔细研究,检查程序在什么地方发生了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 解除文件锁定状态。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章