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