• 在鸿蒙系统搭建一个操作系统的日志模块


    操作系统的日志模块,对整个系统其实并没有什么用处,但是对于开发者,这个功能模块是必不可少的。写程序是编码+调试的过程,调试可能占据着整个开发周期的大头。而日志调试法,也是用的最多的调试方法,所以一个好用可靠的日志子系统对操作系统来说是很重要的。

    鸿蒙的日志系统的实现:log driver + log daemon + log api。

    log driver是日志的仓库,所有用户进程通过log api向log driver写入日志数据,log daemon是日志守护进程,负责从log driver读取日志保存到文件中。

    log api


    log api主要是供应用程序调用,向内核日志缓冲区写入日志数据。log api的源代码主要是下面两个文件。

    code-1.0asehiviewdfxinterfacesinnerkitshiloghiview_log.h code-1.0asehiviewdfxframeworkshilog_litefeaturedhiview_log.c

    code-1.0asehiviewdfxinterfacesinnerkitshiloghiview_log.h

    // 日志定义了5个级别,优先级从低到高依次是:debug、info、warn、error、fatal。
    typedef enum {
        /** Debug level to be used by {@link HILOG_DEBUG} */
        LOG_DEBUG = 3,
        /** Informational level to be used by {@link HILOG_INFO} */
        LOG_INFO = 4,
        /** Warning level to be used by {@link HILOG_WARN} */
        LOG_WARN = 5,
        /** Error level to be used by {@link HILOG_ERROR} */
        LOG_ERROR = 6,
        /** Fatal level to be used by {@link HILOG_FATAL} */
        LOG_FATAL = 7,
    } LogLevel;
    // 划分的5个大系统模块
    typedef enum {
        /** DFX */
        HILOG_MODULE_HIVIEW = 0,
        /** System Ability Manager */
        HILOG_MODULE_SAMGR,
        /** Update */
        HILOG_MODULE_UPDATE,
        /** Ability Cross-platform Environment */
        HILOG_MODULE_ACE,
        /** Third-party applications */
        HILOG_MODULE_APP,
        /** Maximum number of modules */
        HILOG_MODULE_MAX
    } HiLogModuleType;
    // 打印日志,系统建议不直接调用这个函数,而是使用下面的宏定义
    int HiLogPrint(LogType type, LogLevel level, unsigned int domain, const char* tag, const char* fmt, ...)
        __attribute__((format(os_log, 5, 6)));
    
    #define HILOG_DEBUG(type, ...) ((void)HiLogPrint(LOG_CORE, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, __VA_ARGS__))
    #define HILOG_INFO(type, ...) ((void)HiLogPrint(LOG_CORE, LOG_INFO, LOG_DOMAIN, LOG_TAG, __VA_ARGS__))
    #define HILOG_WARN(type, ...) ((void)HiLogPrint(LOG_CORE, LOG_WARN, LOG_DOMAIN, LOG_TAG, __VA_ARGS__))
    #define HILOG_ERROR(type, ...) ((void)HiLogPrint(LOG_CORE, LOG_ERROR, LOG_DOMAIN, LOG_TAG, __VA_ARGS__))
    #define HILOG_FATAL(type, ...) ((void)HiLogPrint(LOG_CORE, LOG_FATAL, LOG_DOMAIN, LOG_TAG, __VA_ARGS__))

    LOG_CORE是3,used by core service and framework。LOG_DOMAIN是0。LOG_TAG是null。下面展开,看一下函数HiLogPrintf()的实现

    int HiLogPrint(LogType bufID, LogLevel prio, unsigned int domain, const char *tag, const char *fmt, ...)
    {
        int ret;
        va_list ap;
        va_start(ap, fmt);
        // 直接调用HiLogPrintArgs()
        ret = HiLogPrintArgs(bufID, prio, domain, tag, fmt, ap);
        va_end(ap);
        return ret;
    }
    
    int HiLogPrintArgs(LogType bufID, LogLevel prio, unsigned int domain, const char *tag, const char *fmt, va_list ap)
    {
        int ret;
        // 创建1KB日志缓冲区
        char buf[LOG_BUF_SIZE] = {0};
        bool isDebugMode = 1;
        unsigned int bufLen;
    
    #ifdef OHOS_RELEASE
        isDebugMode = 0;
    #endif
        // 向buffer的开头写入domain tag
        if (snprintf_s(buf, sizeof(buf), sizeof(buf) - 1, "%c %05X/%s: ", g_logLevelInfo[prio], (domain & DOMAIN_FILTER),
            tag) == -1) {
            return 0;
        }
    
        bufLen = strlen(buf);
        // domain tag的最大长度是64子字节
        if (bufLen >= MAX_DOMAIN_TAG_SIZE) {
            return 0;
        }
        // 按照设定的格式把日志内容写入到buffer
        HiLog_Printf(buf + bufLen, LOG_BUF_SIZE - bufLen, LOG_BUF_SIZE - bufLen - 1, isDebugMode, fmt, ap);
    
    #ifdef LOSCFG_BASE_CORE_HILOG
        ret = HiLogWriteInternal(buf, strlen(buf) + 1);
    #else
        // 获取/dev/hilog驱动设备文件的文件描述符
        if (g_hilogFd == -1) {
            g_hilogFd = open(HILOG_DRIVER, O_WRONLY);
        }
        // 向驱动写入日志数据
        ret = write(g_hilogFd, buf, strlen(buf) + 1);
    #endif
        return ret;
    }

    log daemon

    log daemon负责从日志仓库读取数据,写入到文件。这个就是init.cfg中配置的service apphilogcat。

    code-1.0/base/hiviewdfx/services/hilogcat_lite/apphilogcat

    以上是hilogcat模块的代码路径。

    code-1.0/base/hiviewdfx/services/hilogcat_lite/apphilogcat/hiview_applogcat.c

    int main(int argc, const char **argv)
    {
    #define HILOG_PERMMISION 0700
    #define HILOG_TEST_ARGC 2
        int fd;
        int ret;
        FILE *fpWrite = NULL;
        // 不带参数执行可执行文件,如果是release版本,直接返回
        // 即,release版本默认不启动log服务
        if (argc == 1) {
    #ifdef OHOS_RELEASE
            return 0;
    #endif
        }
        // release版本可以通过传入参数来启动日志服务
        if (argc == HILOG_TEST_ARGC) {
            HILOG_ERROR(LOG_CORE, "TEST = %d,%s,%d
    ", argc, "hilog test", argc);
            return 0;
        }
        // 打开设备文件/dev/hilog
        fd = open(HILOG_DRIVER, O_RDONLY);
        if (fd < 0) {
            printf("hilog fd failed fd=%d
    ", fd);
            return 0;
        }
        // 打开第一个日志文件
        FILE *fp1 = fopen(HILOG_PATH1, "at");
        if (fp1 == NULL) {
            close(fd);
            printf("open err fp1=%p
    ", fp1);
            return 0;
        }
        // 打开第二个日志文件
        FILE *fp2 = fopen(HILOG_PATH2, "at");
        if (fp2 == NULL) {
            fclose(fp1);
            close(fd);
            printf("open err fp2=%p
    ", fp2);
            return 0;
        }
        // 选择一个没有写满用于进行日志记录,优先使用第一个日志文件
        // 如果两个都满了,会把第一个内容擦除,然后返回
        fpWrite = SelectWriteFile(&fp1, fp2);
        if (fpWrite == NULL) {
            printf("SelectWriteFile open err fp1=%p
    ", fp1);
            return 0;
        }
        while (1) {
            char buf[HILOG_LOGBUFFER] = {0};
            // 从设备文件hilog读取一条日志(可以从下一节的log driver看到,每次读操作,确实只返回一条日志数据)
            ret = read(fd, buf, HILOG_LOGBUFFER);
            // 如果读取的长度小于一条日志的长度,就丢弃
            if (ret < sizeof(struct HiLogEntry)) {
                continue;
            }
            struct HiLogEntry *head = (struct HiLogEntry *)buf;
    
            time_t rawtime;
            struct tm *info = NULL;
            unsigned int sec = head->sec;
            rawtime = (time_t)sec;
            // 如果日志时间无效,也丢掉
            /* Get GMT time */
            info = gmtime(&rawtime);
            if (info == NULL) {
                continue;
            }
            buf[HILOG_LOGBUFFER - 1] = '';
            // 按照下面这种格式打印日志到终端
            printf("%02d-%02d %02d:%02d:%02d.%03d %d %d %s
    ", info->tm_mon + 1, info->tm_mday, info->tm_hour, info->tm_min,
                info->tm_sec, head->nsec / NANOSEC_PER_MIRCOSEC, head->pid, head->taskId, head->msg);
            // 日志数据按照下面这种格式写入到文件
            ret =
                fprintf(fpWrite, "%02d-%02d %02d:%02d:%02d.%03d %d %d %s
    ", info->tm_mon + 1, info->tm_mday, info->tm_hour,
                    info->tm_min, info->tm_sec, head->nsec / NANOSEC_PER_MIRCOSEC, head->pid, head->taskId, head->msg);
            // 重新选择一个文件,准备进行下一条日志写入
            // 写入没有满的文件,如果两个文件都满了,就进行交替写入(不会出现一直写入一个文件的情况)
            // select file, if file1 is full, record file2, file2 is full, record file1
            fpWrite = SwitchWriteFile(&fp1, &fp2, fpWrite);
            if (fpWrite == NULL) {
                printf("[FATAL]File cant't open  fp1=%p, fp2=%p
    ", fp1, fp2);
                return 0;
            }
        }
        return 0;
    }

    hilog driver

    code-1.0/kernel/liteos_a/kernel/common/los_hilog.c

    hilog是内核的一个组件,可以在编译的时候,选择是否要包含此组件。

    #define HILOG_BUFFER 1024
    #define HILOG_DRIVER "/dev/hilog"

    驱动文件的名字hilog,驱动日志缓冲区的大小是1KB。

    STATIC struct file_operations_vfs g_hilogFops = {
        HiLogOpen,  /* open */
        HiLogClose, /* close */
        HiLogRead,  /* read */
        HiLogWrite, /* write */
        NULL,       /* seek */
        NULL,       /* ioctl */
        NULL,       /* mmap */
        NULL, /* poll */
        NULL, /* unlink */
    };
    
    int HiLogDriverInit(VOID)
    {
        HiLogDeviceInit();
        return register_driver(HILOG_DRIVER, &g_hilogFops, DRIVER_MODE, NULL);
    }

    定义文件操作函数,并调用register_driver()进行驱动注册。register_driver是开源库NuttX里面的函数。

    驱动初始化

    static void HiLogDeviceInit(void)
    {
        // 分配1KB的内核内存空间
        g_hiLogDev.buffer = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, HILOG_BUFFER);
        if (g_hiLogDev.buffer == NULL) {
            dprintf("In %s line %d,LOS_MemAlloc fail", __FUNCTION__, __LINE__);
        }
        // 初始化wq
        init_waitqueue_head(&g_hiLogDev.wq);
        // lite OS 互斥锁初始化
        LOS_MuxInit(&g_hiLogDev.mtx, NULL);
        // 当前日志数据写入的位置(在环形缓冲区中)
        g_hiLogDev.writeOffset = 0;
        // 当前日志数据开始的位置(在环形缓冲区中)
        g_hiLogDev.headOffset = 0;
        // 当前写入日志数据的大小
        g_hiLogDev.size = 0;
        // 日志的条数,其实count是日志条数的2倍,日志头和日志内容会分别被记录一次
        g_hiLogDev.count = 0;
    }

    主要进行log字符设备数据结构的变量赋值。

    写入数据

    // buffer是要写入的数据,bufLen是日志数据的长度
    static ssize_t HiLogWrite(FAR struct file *filep, const char *buffer, size_t bufLen)
    {
        (void)filep;
        // 要写入的日志是否超出了kernel log buffer的长度(1KB)
        if (bufLen + sizeof(struct HiLogEntry) > HILOG_BUFFER) {
            dprintf("input too large
    ");
            return -ENOMEM;
        }
    
        return HiLogWriteInternal(buffer, bufLen);
    }
    
    int HiLogWriteInternal(const char *buffer, size_t bufLen)
    {
        struct HiLogEntry header;
        int retval;
        // 获取mtx互斥锁,因为涉及对日志缓冲区的读写操作
        (VOID)LOS_MuxAcquire(&g_hiLogDev.mtx);
        // 检查剩余空间是否足够写入这条日志数据
        // 如果剩余空间不够,通过移动日志开头游标来释放空间,每次移动一条日志的长度(保证尽量保留多一些日志)
        HiLogCoverOldLog(bufLen);
        // 生成这条日志的头部数据
        HiLogHeadInit(&header, bufLen);
        // 写入这条日志头到环形日志缓冲区中
        retval = HiLogWriteRingBuffer((unsigned char *)&header, sizeof(header));
        if (retval) {
            retval = -ENODATA;
            goto out;
        }
        // 处理日志数据结构中的:日志大小、写游标、日志数量字段
        HiLogBufferInc(sizeof(header));
        // 写入这条日志的日志内容到日志环形缓冲区中
        retval = HiLogWriteRingBuffer((unsigned char *)(buffer), header.len);
        if (retval) {
            retval = -ENODATA;
            goto out;
        }
        // 处理日志数据结构中的:日志大小、写游标、日志数量字段
        HiLogBufferInc(header.len);
    
        retval = header.len;
    
    out:
        // 日志缓冲区操作完毕,释放互斥锁mtx
        (VOID)LOS_MuxRelease(&g_hiLogDev.mtx);
        if (retval > 0) {
            // 发送中断,尝试唤醒日志读取进程
            wake_up_interruptible(&g_hiLogDev.wq);
        }
        if (retval < 0) {
            dprintf("write fail retval=%d
    ", retval);
        }
        return retval;
    }

    读取数据

    static ssize_t HiLogRead(FAR struct file *filep, char *buffer, size_t bufLen)
    {
        size_t retval;
        struct HiLogEntry header;
    
        (void)filep;
        // 设置中断,如果size不大于0,即日志环形缓冲区的日志已经读完,则进入休眠状态,等待wq唤醒
        // 否则,继续执行下面的程序
        wait_event_interruptible(g_hiLogDev.wq, (g_hiLogDev.size > 0));
        // 获取互斥锁mtx,因为下面涉及对日志缓冲区的读写操作
        (VOID)LOS_MuxAcquire(&g_hiLogDev.mtx);
        // 从内核日志缓冲区读取一条日志的head部分
        retval = HiLogReadRingBuffer((unsigned char *)&header, sizeof(header));
        if (retval < 0) {
            retval = -EINVAL;
            goto out;
        }
        // 如果用户程序开辟的日志缓冲区小于将要读取的这条日志的长度,打印kenrel日志到终端,并直接返回
        // 日志头中记录着这条日志的长度,所以读取了日志头之后,就知道了这条日志的长度
        if (bufLen < header.len + sizeof(header)) {
            dprintf("buffer too small,bufLen=%d, header.len=%d,%d
    ", bufLen, header.len, header.hdrSize, header.nsec);
            retval = -ENOMEM;
            goto out;
        }
        // 处理日志数据结构中的:日志数量字段、日志大小、日志读取开始位置
        HiLogBufferDec(sizeof(header));
        // 将这条日志的head数据,写回用户程序空间的内存缓冲区中
        retval = HiLogBufferCopy((unsigned char *)buffer, bufLen, (unsigned char *)&header, sizeof(header));
        if (retval < 0) {
            retval = -EINVAL;
            goto out;
        }
        // 将这条日志的内容数据,写回用户程序空间的内存缓冲区中
        retval = HiLogReadRingBuffer((unsigned char *)(buffer + sizeof(header)), header.len);
        if (retval < 0) {
            retval = -EINVAL;
            goto out;
        }
        // 处理日志数据结构中的:日志数量字段、日志大小、日志读取开始位置
        HiLogBufferDec(header.len);
        // 返回实际读取的日志长度
        retval = header.len + sizeof(header);
    out:
        // 日志缓冲区操作完毕,释放互斥锁mtx
        (VOID)LOS_MuxRelease(&g_hiLogDev.mtx);
        return retval;
    }

    当用户程序从驱动读取日志时,驱动每次返回一条日志数据。

    此处用到开源库NuttX

    内核互斥锁

    code-1.0kernelliteos_akernelincludelos_mux.h


    互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子API之上实现的,这对内核用户是不可见的。

    对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。

    互斥锁比当前的内核信号量选项更快,并且更加紧凑。

    环形缓冲区


    环形缓冲区,虽然叫环形,实际存储区域是一段连续的物理内存,有一个起始地址,有一个结束地址。所谓环形是通过算法实现的,表现出来像是一个环形存储区,可以一直写。这里的日志环形缓冲区,是通过两个游标,一个是read开始的位置,一个write开始的位置,在这段有限的区域里面,通过不断移动这两个游标,当写到存储区的结尾时,再充存储区的开始位置继续写,通过移动read游标来释放空间,保证可以有足够的空间来写入数据。

    中断


    中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

    wait_event_interruptible()/wake_up_interruptible()

    重要数据结构
    一条日志

    code-1.0/base/hiviewdfx/interfaces/innerkits/hilog/hiview_log.h

    struct HiLogEntry {
        unsigned int len;
        unsigned int hdrSize;
        unsigned int pid : 16;
        unsigned int taskId : 16;
        unsigned int sec;
        unsigned int nsec;
        unsigned int reserved;
        char msg[0];
    };
    

    一条日志的数据格式就像上面这样,包括:日志长度、hdrSize(理解为日志头部数据的大小)、输出日志的进程ID、任务ID、时间秒、时间纳秒、保留字段、消息内容。

    限制

    日志系统的规格:

    1.log daemon
     ● 日志缓冲区大小是2KB
     ● 单条日志的最大长度是2KB
     ● 双日志文件机制,单个文件最大2KB
    2.log driver
     ● 日志环形缓冲区的大小是1KB

    作者:韩童

    想了解更多内容,请访问: 51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com

  • 相关阅读:
    四则运算C语言程序
    雅思听听app
    What is 软件工程
    Python(Head First)学习笔记:六
    Python(Head First)学习笔记:五
    Python(Head First)学习笔记:四
    Python(Head First)学习笔记:三
    Python(Head First)学习笔记:二
    Python(Head First)学习笔记:一
    一名前端Web架构师的成长之路(转载)
  • 原文地址:https://www.cnblogs.com/HarmonyOS/p/14357960.html
Copyright © 2020-2023  润新知