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

梦想的初衷 提交于 2021-02-01 19:50:09

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

 

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

 

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

 

log api


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

code-1.0\base\hiviewdfx\interfaces\innerkits\hilog\hiview_log.h code-1.0\base\hiviewdfx\frameworks\hilog_lite\featured\hiview_log.c

code-1.0\base\hiviewdfx\interfaces\innerkits\hilog\hiview_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\n", argc, "hilog test", argc);
        return 0;
    }
    // 打开设备文件/dev/hilog
    fd = open(HILOG_DRIVER, O_RDONLY);
    if (fd < 0) {
        printf("hilog fd failed fd=%d\n", fd);
        return 0;
    }
    // 打开第一个日志文件
    FILE *fp1 = fopen(HILOG_PATH1, "at");
    if (fp1 == NULL) {
        close(fd);
        printf("open err fp1=%p\n", fp1);
        return 0;
    }
    // 打开第二个日志文件
    FILE *fp2 = fopen(HILOG_PATH2, "at");
    if (fp2 == NULL) {
        fclose(fp1);
        close(fd);
        printf("open err fp2=%p\n", fp2);
        return 0;
    }
    // 选择一个没有写满用于进行日志记录,优先使用第一个日志文件
    // 如果两个都满了,会把第一个内容擦除,然后返回
    fpWrite = SelectWriteFile(&fp1, fp2);
    if (fpWrite == NULL) {
        printf("SelectWriteFile open err fp1=%p\n", 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] = '\0';
        // 按照下面这种格式打印日志到终端
        printf("%02d-%02d %02d:%02d:%02d.%03d %d %d %s\n", 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\n", 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\n", 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\n");
        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\n", 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\n", 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.0\kernel\liteos_a\kernel\include\los_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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!