前記
在Android開發中,logcat是我們不可或缺的調試工具,我一直有個疑問,logcat的到底是在哪里存儲著呢,帶著這個疑問,開始探究Android日志系統.
參考文獻: Android日志系統驅動程序Logger源代碼分析
沒有下載源碼的同學可以參考 Google git
概述
Android的日志系統是一個設備,在類Unix中,設備不一定要對應于物理設備,叫做偽設備.
我們知道Linux系統中萬物皆文件,設備也是一種文件,即類型為c或b的文件,在/dev/log/目錄下,我們可以找到幾個文件:
日志的讀寫總體流程:
- 開機之后,Logger的初始化
- 將Log寫入RingBuffer
2.1 App(包括使用NDK)調用Logger輸出log
2.2 Logger將Log寫入RingBuffer - 讀取RingBuffer
3.1 shell中使用logcat
3.2 Logger讀取RingBuffer
RingBuffer的結構
首先需要了解一下RingBuffer中存儲Log時的數據結構,方便下文的理解
總體結構:
Logger中持有RingBuffer的Logger_log:
// kernel/common/drivers/staging/android/logger.c
struct logger_log{
unsigned char * buffer; //持有的RingBuffer緩沖區
struct miscdevice misc; //Logger使用的設備,可看出Logger屬于misc(混雜設備)
wait_quene_head_t wq; //用于保存正在等待讀取日志的進程
struct list_head readers; //正在讀去日志的進程
struct mutex mutex; //一個保護log并發訪問的互斥量
size_t w_off; //當前寫入位置的偏移(即下一條往哪寫)
size_t head; //讀取時應該從RingBuffer環形緩沖的哪個位置開始讀
size_t size; //log的size
}
Ringbuffer中的基本單元Logger_entry:
// kernel/common/drivers/staging/android/logger.h
struct logger_entry{
__u16 len; // 有效負載的長度
__u16 __pad; // 對齊結構體
__s32 pid; // log所屬的進程
__s32 tid; // log所屬的線程
__s32 sec; // log發出的時間
__s32 nsec; // log發出的納秒時間
char msg[0]; // 有效負載
}
讀取Log的結構體:
struct logger_reader {
struct logger_log * log; // 指向一個Logger設備 如main/system/event
struct list_head list; // Logger_log中的
size_t r_off; // 當前讀取位置的偏移
bool r_all; // reader能否讀取所有log
int r_ver; // reader ABI版本
};
Logger的初始化過程
分為三步:
- 定義
- 初始化
- 注冊
定義四個日志設備
/*
* Defines a log structure with name 'NAME' and a size of 'SIZE' bytes, which
* must be a power of two, and greater than
* (LOGGER_ENTRY_MAX_PAYLOAD + sizeof(struct logger_entry)).
*/
#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE) \
static unsigned char _buf_ ## VAR[SIZE]; \
static struct logger_log VAR = { \
.buffer = _buf_ ## VAR, \
.misc = { \
.minor = MISC_DYNAMIC_MINOR, \
.name = NAME, \
.fops = &logger_fops, \
.parent = NULL, \
}, \
.wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq), \
.readers = LIST_HEAD_INIT(VAR .readers), \
.mutex = __MUTEX_INITIALIZER(VAR .mutex), \
.w_off = 0, \
.head = 0, \
.size = SIZE, \
};
DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN, 256*1024)
DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS, 256*1024)
DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO, 256*1024)
DEFINE_LOGGER_DEVICE(log_system, LOGGER_LOG_SYSTEM, 256*1024)
static struct logger_log *get_log_from_minor(int minor)
{
if (log_main.misc.minor == minor)
return &log_main;
if (log_events.misc.minor == minor)
return &log_events;
if (log_radio.misc.minor == minor)
return &log_radio;
if (log_system.misc.minor == minor)
return &log_system;
return NULL;
}
初始化日志設備
static int __init logger_init(void)
{
int ret;
ret = init_log(&log_main);
if (unlikely(ret))
goto out;
ret = init_log(&log_events);
if (unlikely(ret))
goto out;
ret = init_log(&log_radio);
if (unlikely(ret))
goto out;
ret = init_log(&log_system);
if (unlikely(ret))
goto out;
out:
return ret;
}
device_initcall(logger_init);
注冊設備
注冊設備的源碼在kernel/common/drivers/char/misc.c中,可以在Google Git中查看
int misc_register(struct miscdevice * misc)
{
struct miscdevice *c;
dev_t dev;
int err = 0;
INIT_LIST_HEAD(&misc->list);
mutex_lock(&misc_mtx);
list_for_each_entry(c, &misc_list, list) {
if (c->minor == misc->minor) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
}
if (misc->minor == MISC_DYNAMIC_MINOR) {
int i = DYNAMIC_MINORS;
while (--i >= 0)
if ( (misc_minors[i>>3] & (1 << (i&7))) == 0)
break;
if (i<0) {
mutex_unlock(&misc_mtx);
return -EBUSY;
}
misc->minor = i;
}
if (misc->minor < DYNAMIC_MINORS)
misc_minors[misc->minor >> 3] |= 1 << (misc->minor & 7);
dev = MKDEV(MISC_MAJOR, misc->minor);
misc->this_device = device_create(misc_class, misc->parent, dev, NULL,
"%s", misc->name);
if (IS_ERR(misc->this_device)) {
err = PTR_ERR(misc->this_device);
goto out;
}
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add(&misc->list, &misc_list);
out:
mutex_unlock(&misc_mtx);
return err;
}
注冊之后的設備就可以在/dev/log/下找到了,并且用戶空間即可讀寫這些設備和驅動程序進行交互.
寫入過程
寫入過程分為兩個環節, 首先是用戶空間調用Logger打印日志, 然后是Logger寫入日志的實現原理
APP(NDK)調用Logger輸出Log
使用java代碼調用log
這種方式大家肯定都很熟悉, 不再多說.
在NDK中打印log
- 定義自己的 LOG_TAG 宏;
- 包含頭文件 system/core/include/cutils/log.h ;
代碼示例:
#define LOG_TAG "MY LOG TAG"
#include <cutils/log.h>
LOGV("This is the log printed by LOGV in android user space.");
日志級別跟java接口差不多:
/*
* Android log priority values, in ascending priority order.
*/
typedef enum android_LogPriority {
ANDROID_LOG_UNKNOWN = 0,
ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */
ANDROID_LOG_VERBOSE,
ANDROID_LOG_DEBUG,
ANDROID_LOG_INFO,
ANDROID_LOG_WARN,
ANDROID_LOG_ERROR,
ANDROID_LOG_FATAL,
ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */
} android_LogPriority;
Logger寫入RingBuffer
用戶空間調用了打印log的方法之后, logger就會將其寫入到RingBuffer中,主要分為五步:
- 構造需要寫入RingBuffer的結構體;
- 如果日志覆蓋了之前的, 調整讀寫指針;
- 調用do_write_log把logger_entry寫入RingBuffer;
- 調用do_write_log_from_user把priority,tag, msg寫入RingBuffer;
- 喚醒阻塞等待日志更新的reader進程;
/*
* logger_aio_write - our write method, implementing support for write(),
* writev(), and aio_write(). Writes are our fast path, and we try to optimize
* them above all else.
*/
ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t ppos)
{
//第一步
struct logger_log *log = file_get_log(iocb->ki_filp);
size_t orig = log->w_off;
struct logger_entry header;
struct timespec now;
ssize_t ret = 0;
now = current_kernel_time();
header.pid = current->tgid;
header.tid = current->pid;
header.sec = now.tv_sec;
header.nsec = now.tv_nsec;
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
/* null writes succeed, return zero */
if (unlikely(!header.len))
return 0;
mutex_lock(&log->mutex);
//第二步
/*
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
*/
fix_up_readers(log, sizeof(struct logger_entry) + header.len);
//第三步
do_write_log(log, &header, sizeof(struct logger_entry));
while (nr_segs-- > 0) {
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */
len = min_t(size_t, iov->iov_len, header.len - ret);
/* write out this segment's payload */
//第四步
nr = do_write_log_from_user(log, iov->iov_base, len);
if (unlikely(nr < 0)) {
log->w_off = orig;
mutex_unlock(&log->mutex);
return nr;
}
iov++;
ret += nr;
}
mutex_unlock(&log->mutex);
//第五步
/* wake up any blocked readers */
wake_up_interruptible(&log->wq);
return ret;
}
每個要寫入的日志的結構形式:
struct logger_entry | priority | tag | msg
其中,priority、tag 和 msg 這三個段的內容是由 iov 參數從用戶空間傳遞下來的,分別對應 iov 里面的三個元素。而 logger_entry 是由內核空間來構造的,
struct logger_entry header;
struct timespec now;
now = current_kernel_time();
header.pid = current->tgid;
header.tid = current->pid;
header.sec = now.tv_sec;
header.nsec = now.tv_nsec;
header.len = min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
讀取
Shell中使用logcat
先看logcat的用法, 查看logcat --help: 一般用法 [adb] logcat [<option>] ... [<filter-spec>] ...
參數包括:
-s 設置過濾器,過濾標簽
-f <filename> 把緩存輸出到<filename>, 如果不設置默認輸出到stdout, 在Shell中也可以 adb logcat > filename來重定向到filename文件中
-r [<kbytes>] 日志文件的最大尺寸,需要和-f配合使用,單位為KB. 超過這個尺寸后將原文件轉移到filename.1,若有filename.1則將其轉移到filename.2,以此類推.默認16KB
-n <count> 日志文件的最多個數,默認為4
-v <format> 設置日志輸出格式 Brief process tag thread raw time threadtime long
默認格式brief: 優先級/標簽(進程ID):日志信息
process格式: 優先級 (進程ID) : 日志信息
tag格式: 優先級 / 標簽 : 日志信息
thread格式: 優先級 ( 進程ID : 線程ID) 標簽 : 日志內容
raw格式: 只輸出日志信息, 不附加任何其他 信息, 如 優先級 標簽等
time格式: 日期 時間 優先級 / 標簽 (進程ID) : 進程名稱 : 日志信息
-C 清除緩存
-d 將ring buffer中所有內容輸出后直接退出,不阻塞
-t <count> 輸出最近的count條日志,不阻塞
-t '<time>' 輸出time之后的日志,不阻塞 時間格式 'MM-DD hh:mm:ss.mmm'
-T <count> 阻塞版 -t
-T '<time>' 阻塞版 -t
-g 獲取log設備的ring buffer大小
-b <buffer> 指定使用的日志緩沖區, 可選多個, 默認 -b main -b system -b crash,還可以選擇:events, crash or all.
-B 輸出二進制
-S 輸出統計信息
-G <size> 設置Ring Buffer的尺寸,單位為K或M
-p print prune white and ~black list. Service is specified as
UID, UID/PID or /PID. Weighed for quicker pruning if prefix
with ~, otherwise weighed for longevity if unadorned. All
other pruning activity is oldest first. Special case ~!
represents an automatic quicker pruning for the noisiest
UID as determined by the current statistics.
-P '<list> ...' set prune white and ~black list, using same format as
printed above. Must be quoted.
-p和-P兩個參數沒看懂不知道怎么用,網上也沒查到,希望知道的大神不吝賜教..
過濾項解析 filter-spec:
格式: <tag>:[priority], 默認的過濾項為 " *:I ";
優先級級別:
--V : Verbose
--D : Debug
--I : Info
--W : warning
--E : Error
--F : Fatal
--S : Super all outputs
可設置多個過濾項, 如 adb logcat ActivityManager:D dalvikvm:I *:S,做或運算,注意,要有 *:S才能正確輸出
當然還可以在終端中使用重定向 > 和grep等命令來過濾
Logger的讀取過程
Logger從RingBuffer中讀取日志的過程只要分為:
- 打開設備文件;
- 判斷是否有新日志可讀, 判斷方法:讀寫指針是否在同一位置. 若無新日志,根據打開方式判斷是否需要休眠,等待喚醒;
- 有新日志, 讀取日志
- 調整指針
其中在第三步在日志的讀取過程, 又分為:
3.1 獲取有效負載長度, 在logger_entry中的前兩個字節, 需要判斷是否一個在首一個在尾;
3.2 logger_entry長度固定,可得日志記錄總長度;
3.3 調用do_read_log_to_user函數來執行真正的讀取動作(把內核空間的RingBuffer指定內容拷貝到用戶空間的內存緩沖區即可);
代碼如下:
/*
* logger_read - our log's read() method
*
* Behavior:
*
* - O_NONBLOCK works
* - If there are no log entries to read, blocks until log is written to
* - Atomically reads exactly one log entry
*
* Optimal read size is LOGGER_ENTRY_MAX_LEN. Will set errno to EINVAL if read
* buffer is insufficient to hold next entry.
*/
static ssize_t logger_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
struct logger_reader *reader = file->private_data;
struct logger_log *log = reader->log;
ssize_t ret;
DEFINE_WAIT(wait);
start:
while (1) {
prepare_to_wait(&log->wq, &wait, TASK_INTERRUPTIBLE);
mutex_lock(&log->mutex);
ret = (log->w_off == reader->r_off);
mutex_unlock(&log->mutex);
if (!ret)
break;
if (file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
break;
}
if (signal_pending(current)) {
ret = -EINTR;
break;
}
schedule();
}
finish_wait(&log->wq, &wait);
if (ret)
return ret;
mutex_lock(&log->mutex);
/* is there still something to read or did we race? */
if (unlikely(log->w_off == reader->r_off)) {
mutex_unlock(&log->mutex);
goto start;
}
/* get the size of the next entry */
ret = get_entry_len(log, reader->r_off);
if (count < ret) {
ret = -EINVAL;
goto out;
}
/* get exactly one entry from the log */
ret = do_read_log_to_user(log, reader, buf, ret);
out:
mutex_unlock(&log->mutex);
return ret;
}
static ssize_t do_read_log_to_user(struct logger_log *log,
struct logger_reader *reader,
char __user *buf,
size_t count)
{
size_t len;
/*
* We read from the log in two disjoint operations. First, we read from
* the current read head offset up to 'count' bytes or to the end of
* the log, whichever comes first.
*/
len = min(count, log->size - reader->r_off);
if (copy_to_user(buf, log->buffer + reader->r_off, len))
return -EFAULT;
/*
* Second, we read any remaining bytes, starting back at the head of
* the log.
*/
if (count != len)
if (copy_to_user(buf + len, log->buffer, count - len))
return -EFAULT;
reader->r_off = logger_offset(reader->r_off + count);
return count;
}
總結
回顧最開始的疑問,Log到底是存儲在哪里呢,當然是在驅動程序Logger持有的RingBuffer中,RingBuffer是維護在內核空間中的一個環形緩沖區,一旦關機,就沒有了如果需要收集Log信息存儲起來,可以使用logcat [-d] -f filename [-r maxSize] [-n maxCount] 命令將其存儲在一系列文件中.但是普通用戶身份只能查看自己進程的log, shell可以查看所有三方應用, system, root..