Android帝国之日志系统--logd、logcat

本文概要

这是Android系统进程系列的第四篇文章,本文以自述的方式来介绍logd进程,通过本文您将了解到logd进程存在的意义,以及日志系统的实现原理。(文中的代码是基于android13)

Android系统进程系列的前三篇文章如下:

我是init进程

Android-属性服务的设计魅力

Android帝国之进程杀手–lmkd

我是谁

init:“大家好,我是你们的老朋友init进程,今天我把我的出生最早的孩子‘logd进程‘介绍给各位认识,logd那我就把舞台交给你了,不要紧张,你的二弟‘logd进程‘在介绍自己的时候表现的非常棒,父亲相信你你也可以做的更棒!“

logd:“大家好啊!我是logd进程,可以直接叫我logd,‘logd’这不是一个单词而是‘log daemon’的缩写,翻译为中文是‘日志守护进程’。”

一个进程迫不及待地说:“不好意思logd,我先插句话,我想起来了,我们每个进程都有打印日志的需求,在java层使用Log.iLog.dLog.eLog类的方法来打印日志,在native层用ALOGIALOGD等方法来打印日志,这些打印日志的方法是不是都和你有关?”

“是的,刚刚这位进程提到的这些打印日志的方法都和我有关,别看你们在使用的时候特别简单,其实这些日志信息在到达我这的时候要经过‘千山万水的旅程‘。回归正题,我的名字叫logd(日志守护进程),主要的功能是给除了init、logd进程的所有用户空间进程提供日志打印的功能,这些日志信息我会收集起来,以便日志消费者进行消费(如logcat展示日志信息)

大家对我应该有了一个初步的印象了,我虽然不像你们人类的伟人那么伟大,但是我觉得有必要介绍下我的出生。

我的出生

我出生在一个"单亲家庭",我只有父亲没有母亲,我的父亲是init进程,我的父亲是一个“不称职的父亲“,为啥这样说呢,它对于我何时创建、创建后叫啥名字等这些信息,它统统不知道,你们说它负责吗,我只需要把这些信息用init脚本语言配置好后交给它即可,剩下的事就全权交给它了,脚本语言配置的信息如下:

//文件路径:/system/logging/logd/logd.rc//logd是进程的名字,/system/bin/logd 代表当init进程fork logd成功后,需要执行的可执行文件
service logd /system/bin/logd//下面三个socket分别代表需要创建的三个server socketsocket logd stream 0666 logd logdsocket logdr seqpacket 0666 logd logdsocket logdw dgram+passcred 0222 logd logd//kmsg代表内核会把内核的日志信息存储在这个文件中file /proc/kmsg rfile /dev/kmsg wuser logdgroup logd system package_info readproccapabilities SYSLOG AUDIT_CONTROLpriority 10task_profiles ServiceCapacityLowonrestart setprop logd.ready false省略其他信息......//文件路径:/system/core/rootdir/init.rc,下面的内容是该文件的其中一部分//on init:代表init触发器触发的时候会执行下面的各种命令
on init省略其他命令......//start logd:start命令会创建进程,logd与上面logd.rc中service后面的logd一致start logd

我的出生是不是非常的简单啊,这其实是脚本语言和init进程的功劳,可以点击 我是init进程 这篇文章来了解init进程的知识、以及如何通过脚本语言来创建子进程

当init把我创建成功后,执行下面的方法后,我才可以为各进程提供日志功能。

//文件路径:/system/logging/logd/main.cpp
int main(int argc, char* argv[]) {// We want EPIPE when a reader disconnects, not to terminate logd.signal(SIGPIPE, SIG_IGN);省略其他代码.....return EXIT_SUCCESS;
}

从雏形说起

在介绍我是谁的时候提到了我的主要功能是给除了init、logd进程的所有用户空间进程提供日志打印的功能,这些日志信息我会收集起来,以便日志消费者进行消费(如logcat展示日志信息),我给这个功能起了一个高大上的名字日志系统,那我接下来就来介绍下日志系统的实现原理,这应该也是大家最感兴趣的内容了。

我计划先从日志系统的“雏形”讲起,从雏形讲起的主要原因:一方面是我在设计日志系统的时候也是从雏形开始设计,雏形先有一个大概的模块划分以及模块之间的相互关系,进而再对每个模块内部进行详细设计,最终才有了日志系统;第二方面是遵循你们人类学习知识的一个过程,你们人类在学习知识的时候比如学习一门编程语言的时候需要先看下大纲都有哪些内容,进而再有针对性的去学习相应章节的内容,而不要一上来就开始抠细节。

雏形起源

“我先问大家一个问题:如何实现一个进程内的日志打印、显示功能呢?给大家几分钟的考虑时间。”

一个进程高高的举起了手:“这个我会啊,要实现进程内的日志打印、显示功能这还是很简单的,首先需要用到生产者/消费者模式,日志打印功能就是日志生产者,日志显示功能就是日志消费者。这就是大概的思路了。”

“这位仁兄回答的很好,我用一幅图来总结下实现该功能的逻辑”

如上图,进程内实现日志打印、显示功能主要涉及到以下三部分:

  1. 日志生产者:进程中的各线程调用相应的方法把需要打印的日志放入日志队列
  2. 日志队列:主要用来存放日志,是按日志存放的先后顺序存放在队列中,需要做好同步处理
  3. 日志消费者:显示日志的模块从日志队列中把日志按时间先后顺序取出来,进行显示

雏形形成

日志系统解决的是进程之间的日志打印、显示需求,它和进程内日志打印、显示的雏形基本是差不多的,只不过前者日志生产者是其他进程,而日志消费者也是其他进程,日志队列是logd进程。如下图:

如上图,日志系统雏形涉及到以下三部分:

  1. 日志生产者:日志的生产者是其他进程,通过进程通信的方式把日志传递到日志收集分发中心
  2. 日志收集分发中心:位于logd进程内,其中日志队列是用来存放收集到的日志,同时还会把收集到的日志分发给日志消费者
  3. 日志消费者:日志的消费者也是其他进程,同样也是通过进程通信的方式把日志传递给消费者

日志生产者会有多个同时日志消费者也有多个,日志收集分发中心日志生产者日志消费者形成了日志系统的雏形,那我就从这三部分来介绍雏形是如何一步步衍化为日志系统的。

日志收集分发中心

日志收集分发中心从名字上大家就能明白它的作用,日志收集分发中心存在于我logd进程内,主要作用是从日志生产者生产的日志收集起来,同时把收集到的日志分发给日志消费者。日志收集分发中心是日志系统的最重要的模块,它的设计的好坏会严重影响到日志系统,因此在设计的时候我logd花了非常大的精力,日志收集分发中心又可以分为三部分:日志队列日志收集中心日志分发中心,因此会从这三部分入手来进行介绍。

日志队列

日志队列从名字上就可以显而易见知道它是用来存储日志的,这个日志队列可不是像你们想象的那种简单的日志队列,它面对的环境可是非常复杂的:首先存储的日志是非常非常多的,可不是仅仅只存储一个进程内的日志,它存储的是除了init和logd之外所有的用户空间进程的所有日志;其次每时每刻都会有各种日志需要存储。因此日志队列的性能、存储量的大小会影响到日志系统。

日志队列有以下几个要求:

  1. 日志队列中的日志需要存储在内存中,不会因为日志消费者消费了对应日志,该日志就从日志队列中删除
  2. 可以通过命令来控制日志队列的状态比如清除某些日志等
  3. 日志队列中存储的日志总量是有最大限制的,不可能无上限的存下去如果这样肯定会出现内存溢出的
日志分类

先从一个问题讲起

如果日志队列存储的日志总量达到了最大上限,那采取的措施肯定是需要先把旧的日志清除掉以便腾出更多的存储空间,但是如果按这个策略执行的话,就有可能出现一个问题:假如有一些重要的日志比如crash日志,并且它们在日志队列中是最老的日志,那如果达到日志上限的时候,就会把这些重要的日志清理掉。这个问题肯定会遭到开发者骂娘的,明明出现了crash,但是却没有抓到。其实这个问题如果是完全解决掉,那是完全不可能的,因为存储上限的存在,老的日志肯定会被清除掉,对于这个问题能做的就是让这种概率降到最低最低并且对于crash这种类型的日志尽量保存更多的日志,那如何能做到呢?

logd自信的说:“先不要慌,遇到问题咱们就想办法,办法总比问题多。”

一个进程说:“我想到一个办法:能不能消费者消费掉了对应日志后,把对应日志从队列中清除掉,这样的话就可以大大的降低这个问题的概率。”

logd:“这个办法行不通,因为日志队列中的日志,它面对的消费者是很多的,日志队列中的日志对于所有消费者来说是公共资源,如果因为某个消费者消费了对应日志而把这些日志删掉,那其他的消费者不就取不到这些被删掉的日志了。这肯定是有问题的。”

这个进程接着问“那如果把存储日志的上限值设置大些呢?”

“这个确实可以降低问题概率,但是我觉得这个概率还没到更低更低。咱们可以从查找问题的根源入手,找到问题根源进而想对策。”

问题的根源其实是不管什么样的日志(crash、kernel、app日志)都混杂在一起,就是因为它们都混杂在一起,所有的日志都共用一个最大上限值。那如果我们采用分而治之的策略呢,分而治之就是对日志进行分类,每种类型的日志分别有自己的内存最大上限值,因为每种类型的日志使用了自己的内存上限值,那就可以保存更多的日志了,进而就可以非常大非常大的降低此问题的概率。同时对日志分类也可以带来别的好处比如消费者可以针对性的关注自己关心的类别日志,在生产者生产日志的时候可以增加权限控制(比如某种类型的日志只有生产者经过权限校验后才可以把它生产的日志放入队列)。

可以把日志分类为LOG_ID_MAINLOG_ID_RADIOLOG_ID_EVENTSLOG_ID_SYSTEMLOG_ID_CRASHLOG_ID_SECURITYLOG_ID_KERNEL,其中LOG_ID_MAIN是app进程的日志(开发者在开发app的时候打印的日志就是这种类型),LOG_ID_EVENTS是event类型日志(比如Activity生命周期),LOG_ID_SYSTEM是systemserver进程的日志,LOG_ID_CRASH是崩溃类型日志,LOG_ID_KERNEL是kernel类型日志。

下面是代码,有兴趣可以看下:

//文件路径:/system/logging/liblog/include/android/log.h
typedef enum log_id {LOG_ID_MIN = 0,//app进程的日志/** The main log buffer. This is the only log buffer available to apps. */LOG_ID_MAIN = 0,/** The radio log buffer. */LOG_ID_RADIO = 1,//event类型日志,比如activity生命周期之类的/** The event log buffer. */LOG_ID_EVENTS = 2,//systemserver进程的日志,比如AMS/** The system log buffer. */LOG_ID_SYSTEM = 3,//崩溃日之惠/** The crash log buffer. */LOG_ID_CRASH = 4,/** The statistics log buffer. */LOG_ID_STATS = 5,/** The security log buffer. */LOG_ID_SECURITY = 6,//kernel日志/** The kernel log buffer. */LOG_ID_KERNEL = 7,LOG_ID_MAX,/** Let the logging function choose the best log target. */LOG_ID_DEFAULT = 0x7FFFFFFF
} log_id_t;

每种类型的日志在初始化的时候都会分配最大内存上限值,如下代码:

//文件路径:/system/logging/logd/SimpleLogBuffer.cpp , SimpleLogBuffer是其中一种日志队列
void SimpleLogBuffer::Init() {//log_id_for_each方法会遍历上面 所有的日志分类log_id_for_each(i) {//调用SetSize方法设置每种类型的最大内存值if (!SetSize(i, GetBufferSizeFromProperties(i))) {SetSize(i, kLogBufferMinSize);}}省略代码......
}bool SimpleLogBuffer::SetSize(log_id_t id, size_t size) {省略代码......auto lock = std::lock_guard{logd_lock};max_size_[id] = size;return true;
}
简单日志队列

logd:“介绍完日志的分类,问大家个问题,你们认为简单的日志队列应该是啥样的?”

一个进程说:“这题我会啊,简单日志队列首先可以选用列表list来存储日志,新的日志会加入到list的尾部位置,越是老的日志越是位于list的头部位置。”

logd:“这位兄弟答的非常正确,我刚好也设计了一个类来实现简单日志队列的功能,它的名字是SimpleLogBuffer。我用一张图总结了下它的结构和工作流程。”

SimpleLogBuffer的结构和工作流程图:

如上图,SimpleLogBuffer的属性logs_是一个list队列,list中存放的元素是LogBufferElement,LogBufferElement包含了日志信息及log_id(它是上面介绍的日志分类的id)等其他信息。属性max_size_是一个数组,数组的索引是log_id(日志分类id),它存储了每种类型日志的内存最大上限值。

当有新的LogBufferElement数据加入logs_队列的时候,会从max_size_取到该类别日志的最大上限值,进而去检测该类型的日志总量是否达到了最大上限值,达到了就开始清理老的日志。

下面是对应代码,有兴趣可以看下:

//文件路径:/system/logging/logd/SimpleLogBuffer.cpp//添加日志到队列,log_id日志类别id
int SimpleLogBuffer::Log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid, pid_t tid,const char* msg, uint16_t len) {省略代码......auto lock = std::lock_guard{logd_lock};//每条日志都对应一个sequence,从1开始每次加1auto sequence = sequence_.fetch_add(1, std::memory_order_relaxed);//log_id, realtime, uid, pid, tid, sequence, msg, len生成LogBufferElement对象LogInternal(LogBufferElement(log_id, realtime, uid, pid, tid, sequence, msg, len));return len;
}//添加LogBufferElement到队列
void SimpleLogBuffer::LogInternal(LogBufferElement&& elem) {log_id_t log_id = elem.log_id();//添加到logs_队列中logs_.emplace_back(std::move(elem));stats_->Add(logs_.back().ToLogStatisticsElement());//如若达到上限尝试去清除老的日志MaybePrune(log_id);//通知监听者有新日志reader_list_->NotifyNewLog(1 << log_id);
}void SimpleLogBuffer::MaybePrune(log_id_t id) {unsigned long prune_rows;//ShouldPrune返回true则代表需要清理该日志类别的旧日志if (stats_->ShouldPrune(id, max_size_[id], &prune_rows)) {//清理旧日志,id为日志类别,prune_rows需要清理多少行Prune(id, prune_rows, 0);}
}bool SimpleLogBuffer::Prune(log_id_t id, unsigned long prune_rows, uid_t caller_uid) {省略代码......return true;
}
压缩功能的日志队列

简单日志队列正常工作是完全没有任何问题的,但是为了在有限的内存下存储更多的日志,我设计了具有压缩功能的日志队列,它的名字是SerializedLogBuffer,下面是它结构和工作流程图:

如上图,SerializedLogBuffer的属性logs_它是一个数组,数组的索引是log_id(日志类别id),它的每个元素是一个list队列,队列中包含的元素是SerializedLogChunk
SerializedLogChunk是一个具有日志压缩功能的类,它的每个元素是SerializedLogEntry,SerializedLogChunk在创建的时候会分配一个size,当要写入的SerializedLogEntry超过了这个size值后,就会把当前的SerializedLogChunk的contents_进行压缩,压缩后的数据存放在compressed_log_

具体代码如下,有兴趣可以看下:

//文件路径:/system/logging/logd/SerializedLogBuffer.cpp//存放日志,log_id日志类别id
int SerializedLogBuffer::Log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid, pid_t tid,const char* msg, uint16_t len) {省略代码......//生成sequence,每个日志都对应一个sequenceauto sequence = sequence_.fetch_add(1, std::memory_order_relaxed);auto lock = std::lock_guard{logd_lock};//调用LogToLogBuffer开始加入日志auto entry = LogToLogBuffer(logs_[log_id], max_size_[log_id], sequence, realtime, uid, pid, tid,msg, len);stats_->Add(entry->ToLogStatisticsElement(log_id));//若超过上限值,则开始清除该日志类别的老的日志MaybePrune(log_id);//通知监听者有新的日志可以读取了reader_list_->NotifyNewLog(1 << log_id);return len;
}//开始加入日志
static SerializedLogEntry* LogToLogBuffer(std::list<SerializedLogChunk>& log_buffer,size_t max_size, uint64_t sequence, log_time realtime,uid_t uid, pid_t pid, pid_t tid, const char* msg,uint16_t len) {//若为empty,则push一个SerializedLogChunk,它的大小是max_size / SerializedLogBuffer::kChunkSizeDivisorif (log_buffer.empty()) {log_buffer.push_back(SerializedLogChunk(max_size / SerializedLogBuffer::kChunkSizeDivisor));}auto total_len = sizeof(SerializedLogEntry) + len;//若最后的SerializedLogChunk没有空间存储当前日志if (!log_buffer.back().CanLog(total_len)) {//调用FinishWriting方法会对最后的SerializedLogChunk进行压缩log_buffer.back().FinishWriting();//往log_buffer重新push一个SerializedLogChunklog_buffer.push_back(SerializedLogChunk(max_size / SerializedLogBuffer::kChunkSizeDivisor));}//调用SerializedLogChunk的Log方法把新日志写入return log_buffer.back().Log(sequence, realtime, uid, pid, tid, msg, len);
}//文件路径:/system/logging/logd/SerializedLogChunk.cpp
SerializedLogEntry* SerializedLogChunk::Log(uint64_t sequence, log_time realtime, uid_t uid,pid_t pid, pid_t tid, const char* msg, uint16_t len) {auto new_log_address = contents_.data() + write_offset_;auto* entry = new (new_log_address) SerializedLogEntry(uid, pid, tid, sequence, realtime, len);memcpy(entry->msg(), msg, len);write_offset_ += entry->total_len();highest_sequence_number_ = sequence;return entry;
}

不管是SerializedLogBuffer还是SimpleLogBuffer都会在我logd启动后,选择其中一种作为日志系统的日志队列,当然还有一种类型的日志队列ChattyLogBuffer(不是很常用就不赘述了),相关代码如下:

//文件路径:/system/logging/logd/main.cpp
int main(int argc, char* argv[]) {省略其他代码......//获取logd.buffer_type属性对应的值,默认值是serializedstd::string buffer_type = GetProperty("logd.buffer_type", "serialized");// LogBuffer is the object which is responsible for holding all log entries.LogBuffer* log_buffer = nullptr;//根据buffer_type的值,来对日志队列log_buffer进行初始化,一般情况下都会初始化SerializedLogBuffer的日志队列if (buffer_type == "chatty") {log_buffer = new ChattyLogBuffer(&reader_list, &log_tags, &prune_list, &log_statistics);} else if (buffer_type == "serialized") {log_buffer = new SerializedLogBuffer(&reader_list, &log_tags, &log_statistics);} else if (buffer_type == "simple") {log_buffer = new SimpleLogBuffer(&reader_list, &log_tags, &log_statistics);} else {LOG(FATAL) << "buffer_type must be one of 'chatty', 'serialized', or 'simple'";}省略其他代码......
}
总结

日志队列为了在有限的内存能够存放更多的日志:首先采用分而治之的办法,对日志进行了分类分为LOG_ID_MAINLOG_ID_RADIOLOG_ID_EVENTSLOG_ID_SYSTEMLOG_ID_CRASHLOG_ID_SECURITYLOG_ID_KERNEL,每种类型的日志分别有自己的内存最大上限值,因为每种类型的日志使用了自己的内存上限值,那就可以保存更多的日志了;其次设计了具有压缩功能的日志队列SerializedLogBuffer,它可以对日志进行压缩,这样就能存储更多的日志了。

日志收集中心

日志收集中心的作用就是把生产者生产的日志收集起来放入日志队列中,因为日志的生产者是位于其他进程,而日志收集中心是位于logd进程,因此要想把它们生产的日志收集起来就需要一个收集渠道,你们人类有句话是那样说的“要想富先修路”,因此要想收集日志,就需要把收集渠道先“修好”。

收集渠道

收集渠道其实就是解决生产日志进程如何与logd进程通信,进程通信的方法有socket、signal、binder、共享内存等,咱们还是按照老惯例先结合咱们的使用场景进而在决定使用哪种方法。咱们的使用场景是:生产者把生产的日志传递给logd进程,对于传递过程有以下要求:首先是传递是串行的,也就是只要收到日志我就会直接放入日志队列中,我不需要关心是按照时间或者别的因素对放入日志队列的日志进行排序;其次是对于传递快慢没有要求必须非常快;最后传递渠道是一对多的关系,也就是logd进程是server端,日志生产进程是client端,也就是C/S模式。

因此基于以上使用场景,收集渠道应该使用socket通信方式,socket首先是C/S模式,logd是socket的server端,其他生产者进程是socket的client端;其次socket通信是串行的,需要传递了一个数据再传递下个数据。而binder通信方式虽然也可以做到C/S模式,但是它的通信是并行的,也就是会出现多个数据同时传递,如若采用binder通信就需要对于收到的日志按时间或者别的因素重新排序,而我可不想做这种出力不讨好的工作。

收集渠道会启动一个名字为logdw的server socket,server socket启动后还会启动一个单独的线程,不断循环监听日志生产者发送过来的日志。

收集渠道的相关代码如下,有兴趣可以看下:

//文件路径:/system/logging/logd/main.cpp
int main(int argc, char* argv[]) {省略其他代码......// LogListener listens on /dev/socket/logdw for client// initiated log messages. New log entries are added to LogBuffer// and LogReader is notified to send updates to connected clients.// 实例化LogListenerLogListener* swl = new LogListener(log_buffer);//调用StartListener方法会启动一个线程并且不断循环监听收到的日志数据if (!swl->StartListener()) {return EXIT_FAILURE;}省略其他代码......
}//文件路径:/system/logging/logd/LogListener.cpp
//GetLogSocket()方法会把server socket名字为logdw的server获取到,对socket_进行初始化
LogListener::LogListener(LogBuffer* buf) : socket_(GetLogSocket()), logbuf_(buf) {}//启动一个线程,调用ThreadFunction方法
bool LogListener::StartListener() {if (socket_ <= 0) {return false;}auto thread = std::thread(&LogListener::ThreadFunction, this);thread.detach();return true;
}//该方法会进入循环,不断地从socket client端读取日志信息
void LogListener::ThreadFunction() {prctl(PR_SET_NAME, "logd.writer");while (true) {HandleData();}
}void LogListener::HandleData() {省略代码......//把从socket client端获取的日志信息放入logbuf中logbuf_->Log(logId, header->realtime, cred->uid, cred->pid, header->tid, msg,((size_t)n <= UINT16_MAX) ? (uint16_t)n : UINT16_MAX);
}int LogListener::GetLogSocket() {//server socket的名字是logdwstatic const char socketName[] = "logdw";int sock = android_get_control_socket(socketName);省略代码......return sock;
}
收集协议

既然收集渠道已经“修好了”,那来定下在渠道上传递的协议吧,协议格式如下:

//文件路径:system/logging/liblog/include/android/log.h
struct __android_log_message {/** Must be set to sizeof(__android_log_message) and is used for versioning. */size_t struct_size;//日志类别id/** {@link log_id_t} values. */int32_t buffer_id;//优先级/** {@link android_LogPriority} values. */int32_t priority;//tag 日志对应的tag/** The tag for the log message. */const char* tag;//暂时用不到,可以忽略/** Optional file name, may be set to nullptr. */const char* file;/** Optional line number, ignore if file is nullptr. */uint32_t line;//日志具体信息/** The log message itself. */const char* message;
};

日志分发中心

日志分发中心的作用是把日志队列中的日志分发给日志消费者,而日志消费者是位于其他进程与logd不是同一进程,因此也需要一个“分发渠道”把日志队列中的日志分发出去。

分发渠道

与收集渠道类似,分发渠道也使用socket通信,主要原因是:首先日志队列会对应多个日志消费者,这明显是C/S模式;其次日志消费者它是非常省心的,只是一味的接收发送过来的日志即可也不需要对收到的日志进行排序等处理,因此需要串行传递日志,而socket发送接收数据是串行的。基于以上原因分发渠道选用了socket通信。

收集渠道会启动一个名字为logdr的server socket,server socket启动后还会启动一个单独的线程,不断循环监听与logdr建立连接的client端并且把它保存。日志队列中的日志就会通过socket通信发送到client端。

对应代码如下:

//文件路径:/system/logging/logd/main.cpp
int main(int argc, char* argv[]) {省略其他代码......// LogReader listens on /dev/socket/logdr. When a client// connects, log entries in the LogBuffer are written to the client.//创建LogReader实例LogReader* reader = new LogReader(log_buffer, &reader_list);//调用startListener方法,会创建一个线程,不断循环去监听建立连接的client端if (reader->startListener()) {return EXIT_FAILURE;}省略其他代码......
}//文件路径:/system/logging/logd/LogReader.cpp
LogReader::LogReader(LogBuffer* logbuf, LogReaderList* reader_list): SocketListener(getLogSocket(), true), log_buffer_(logbuf), reader_list_(reader_list) {}//获取server socket
int LogReader::getLogSocket() {//server socket name logdrstatic const char socketName[] = "logdr";int sock = android_get_control_socket(socketName);if (sock < 0) {sock = socket_local_server(socketName, ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_SEQPACKET);}return sock;
}//若有socket client连接的话,会调用这个方法,进而把client保存
bool LogReader::onDataAvailable(SocketClient* cli) {static bool name_set;省略代码......return true;
}

总结

日志队列日志收集中心日志分发中心它们三部分组成了日志收集分发中心。日志队列对日志进行了分类,同时还为了能够在有限的内存下存储更多的日志,设计了具有压缩功能的日志队列–SerializedLogBuffer;日志收集中心会启动一个名为logdw的server socket,用来不断地监听日志生产者发送的日志并且会把收到的日志放入日志队列中;日志分发中心也会启动一个名为logdr的server socket,用来不断地监听建立socket连接的client端,并且会把日志队列中的日志发送给这些client。

日志生产者

我为了让日志生产者非常非常容易的生产日志,我把底层的实现细节全部都给隐藏掉,使用者只需要调用简单的方法就可以生产一条日志,并且把这条日志通过socket发送到logd进程的日志收集中心。比如Java层只需要简单的调用Log.i(tag,msg)的方法就可以生产一条main类型的日志,systemserver进程的代码只需要调用Slog.i(tag,msg)方法就以生产一条system类型的日志,native层代码只需要调用ALOGI(msg)方法就可以生产一条main类型的日志。

像上面的log.iSlog.i这些方法最终都会调用到__android_log_buf_write(/system/logging/liblog/logger_write.cpp)方法,而像ALOGI等native方法最终会调用到__android_log_print/system/logging/liblog/logger_write.cpp),无论是__android_log_buf_write还是__android_log_print方法殊途同归最终调用到__android_log_write_log_message(/system/logging/liblog/logger_write.cpp)方法把封装好的日志信息通过socket(socket与名字为logdw的server建立连接)发送到logd的日志收集中心。

如下代码:

//文件路径:/system/logging/liblog/logger_write.cppint __android_log_buf_write(int bufID, int prio, const char* tag, const char* msg) {省略代码......//构造__android_log_message对象__android_log_message log_message = {sizeof(__android_log_message), bufID, prio, tag, nullptr, 0, msg};//由于代码量太大,关于__android_log_write_log_message及后续的代码就不贴出来了__android_log_write_log_message(&log_message);return 1;
}int __android_log_print(int prio, const char* tag, const char* fmt, ...) {ErrnoRestorer errno_restorer;if (!__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE)) {return -EPERM;}va_list ap;__attribute__((uninitialized)) char buf[LOG_BUF_SIZE];va_start(ap, fmt);vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);va_end(ap);//构造__android_log_message对象,日志类别为LOG_ID_MAIN__android_log_message log_message = {sizeof(__android_log_message), LOG_ID_MAIN, prio, tag, nullptr, 0, buf};//由于代码量太大,关于__android_log_write_log_message及后续的代码就不贴出来了__android_log_write_log_message(&log_message);return 1;
}

日志消费者

最常用的日志消费者就是logcat了,在终端输入adb logcat命令后,终端上就会显示出所有的日志,是不是非常的简单好用啊,其实在执行adb logcat命令后,会创建一个logcat进程,这个进程会与logd进程的日志分发中心logdr server socket建立连接,从而把传递过来日志显示在终端。

相应代码如下:

//文件路径:/system/logging/logcat/logcatd ,在终点输入 adb logcat命令后会执行logcatd shell脚本文件
#! /system/bin/sh
省略代码......
//下面代码会执行logcat.cpp的main方法,同时会把adb logcat携带的参数传递过去
exec logcat "${ARGS[@]}"//文件路径:/system/logging/logcat/logcat.cpp
int main(int argc, char** argv) {Logcat logcat;return logcat.Run(argc, argv);
}int Logcat::Run(int argc, char** argv) {省略代码......//从logd接收日志并显示while (!max_count_ || print_count_ < max_count_) {struct log_msg log_msg;int ret = android_logger_list_read(logger_list.get(), &log_msg);省略代码......//若是二进制则走这if (print_binary_) {WriteFully(&log_msg, log_msg.len());} else {//显示拿到的日志ProcessBuffer(&log_msg);if (blocking && output_file_ == stdout) fflush(stdout);}}return EXIT_SUCCESS;
}

总结

日志收集分发中心日志生产者日志消费者三部分组成了日志系统

日志收集分发中心位于logd进程,logd进程会被init进程创建,日志收集分发中心又可以分为三部分日志收集中心日志分发中心日志队列。日志收集中心会启动一个名字为logdw的server socket,等待日志生产者建立连接,收到日志后会把它们放入日志队列中;日志队列为了在有限的内存下存储更多了日志,对日志进行了分类,同时也设计了具有压缩功能的日志队列SerializedLogBuffer从而可以存储更多的日志;日志分发中心也会启动一个名字为logdr的server socket,如果日志消费者(比如logcat)想要消费日志那就需要与之建立socket连接,进而收到日志进行显示等处理。

日志生产者只需要调用LogSlogALOGI等方法,就可以生产一条日志,并且把这条日志通过socket发送到日志收集中心。

日志消费者比如logcat使用起来也非常方便,只需要在终端输入adb logcat命令即可把日志显示在终端,adb logcat命令会创建一个logcat进程,这个进程会与日志分发中心建立socket连接,进而接收日志并且显示。

好了关于日志系统,关于我logd进程的介绍到此为止,感谢大家的观看。

欢迎大家关注我的公众号–牛晓伟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/215378.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在OpenWRT软路由系统部署uhttpd搭建web服务器实现远程访问——“cpolar内网穿透”

文章目录 前言1. 检查uhttpd安装2. 部署web站点3. 安装cpolar内网穿透4. 配置远程访问地址5. 配置固定远程地址 前言 uhttpd 是 OpenWrt/LuCI 开发者从零开始编写的 Web 服务器&#xff0c;目的是成为优秀稳定的、适合嵌入式设备的轻量级任务的 HTTP 服务器&#xff0c;并且和…

docker-compose的介绍与使用

一、docker-compose 常用命令和指令 1. 概要 默认的模板文件是 docker-compose.yml&#xff0c;其中定义的每个服务可以通过 image 指令指定镜像或 build 指令&#xff08;需要 Dockerfile&#xff09;来自动构建。 注意如果使用 build 指令&#xff0c;在 Dockerfile 中设置…

RHEL网络服务器

目录 1.时间同步的重要性 2.配置时间服务器 &#xff08;1&#xff09;指定所使用的上层时间服务器。 (2&#xff09;指定允许访问的客户端 (3&#xff09;把local stratum 前的注释符#去掉。 3.配置chrony客户端 &#xff08;1&#xff09;修改pool那行,指定要从哪台时间…

Python常见面试知识总结(一):迭代器、拷贝、线程及底层结构

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 今天来总结一下Python和C语言中常见的面试知识&#xff0c;欢迎大家一起前来探讨学习~ 【一】Python中迭代器的概念&#xff1f; 可迭代对象是迭代器、生成器和装饰器的基础。简单来说&#xff0c;可以使用for来循环遍历…

[古剑山2023] pwn

最近这个打stdout的题真多。这个比赛没打。拿到附件作了一天。 choice 32位&#xff0c;libc-2.23-i386&#xff0c;nbytes初始值为0x14,读入0x804A04C 0x14字节后会覆盖到nbytes 1个字节。当再次向v1读入nbytes字节时会造成溢出。 先写0x14p8(0xff)覆盖到nbytes然后溢出写传…

记录一次postgresql临时表丢失问题

项目相关技术栈 springboot hikari连接池pgbouncerpostgresql数据库 背景 为了优化一个任务执行的速度&#xff0c;我将任务的sql中部分语句抽出生成临时表&#xff08;create temp table tempqw as xxxxxxxxx&#xff09;&#xff0c;再和其他表关联&#xff0c;提高查询速…

三翼鸟2023辉煌收官, 定盘2024高质量棋局

最近在不同平台上接连看到这样的热搜话题&#xff1a;用时间胶囊记录2023的自己、2023年度问答、2023十大网络流行语公布… 显然&#xff0c; 2023年进入最后一个月&#xff0c;时间匆匆&#xff0c;这也意味着又到了总结过去和规划未来的时候。拿到结果、取得成绩当然是对202…

短视频引流获客系统:引领未来营销的新潮流

在这个信息爆炸的时代&#xff0c;短视频已经成为了人们获取信息的主要渠道之一。而随着短视频的火爆&#xff0c;引流获客系统也逐渐成为了营销领域的新宠。本文将详细介绍短视频引流获客系统的开发流程以及涉及到的技术&#xff0c;让我们一起来看看这个引领未来营销的新潮流…

华清远见作业第二十四天

使用消息队列完成两个进程之间相互通信 代码 #include<stdio.h> #include<string.h> #include<stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/msg.h> #in…

k8s一键部署uniswap

1、拉取uniswap源码 github地址 2、编写Dockerfile并打镜像 # Set the base image FROM node:18.10.0# WORKDIR /usr/src/app/ WORKDIR /home/gateway# Copy files COPY ./ /home/gateway/# Dockerfile author / maintainer LABEL maintainer"Michael Feng <mikehummi…

Java最全面试题专题---2、Java集合容器(2)

Map接口 说一下 HashMap 的实现原理&#xff1f; HashMap概述&#xff1a; HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键。此类不保证映射的顺序&#xff0c;特别是它不保证该顺序恒久不变。 HashMap的数据…

C语言-枚举

常量符号化 用符号而不是具体的数字来表示程序中的数字 枚举 用枚举而不是定义独立的const int变量 枚举是一种用户定义的数据类型&#xff0c;他用关键词enum以如下语法来声明&#xff1a; enum枚举类型名字{名字0&#xff0c;…&#xff0c;名字n}&#xff1b; 枚举类型名…

外包干了3年,技术退步太明显了。。。。。

先说一下自己的情况&#xff0c;本科生生&#xff0c;18年通过校招进入武汉某软件公司&#xff0c;干了差不多3年的功能测试&#xff0c;今年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能…

6_CSS布局之浮动的应用

day06_CSS布局之浮动的应用 本课目标&#xff08;Objective&#xff09; 理解什么是浮动掌握浮动的三种机制掌握浮动的案例应用 1 CSS 布局的三种机制 CSS 提供了 3 种机制来设置盒子的摆放位置&#xff0c;分别是普通流&#xff08;标准流&#xff09;、浮动和定位。 普通流…

HarmonyOS开发:回调实现网络的拦截

前言 基于http封装的一个网络库&#xff0c;里面有一个知识点&#xff0c;在初始化的时候&#xff0c;可以设置请求头拦截和请求错误后的信息的拦截&#xff0c;具体案例如下&#xff1a; et.getInstance().init({netErrorInterceptor: new MyNetErrorInterceptor(), //设置全…

web网络安全

web安全 一&#xff0c;xss 跨站脚本攻击(全称Cross Site Scripting,为和CSS&#xff08;层叠样式表&#xff09;区分&#xff0c;简称为XSS)是指恶意攻击者在Web页面中插入恶意javascript代码&#xff08;也可能包含html代码&#xff09;&#xff0c;当用户浏览网页之时&…

关于北京医学sci论文翻译

在医学领域&#xff0c;翻译论文是一项非常重要的工作。医学论文的翻译需要准确、专业、严谨&#xff0c;同时也需要考虑到医学领域的特殊性和复杂性。那么&#xff0c;如何翻译医学论文呢&#xff1f;北京医学SCI论文翻译哪家好呢&#xff1f; 首先&#xff0c;需要具备专业的…

多目标跟踪数据集

目录 DanceTrack数据集 自己改进的可视化代码: DanceTrack数据集 DanceTrack 是一个大规模的多对象跟踪数据集。用于在遮挡、频繁交叉、同样服装和多样化身体姿态条件下对人进行跟踪。强调运动分析在多对象跟踪中的重要性。 GitHub地址:https://github.com/DanceTrack/Dan…

python自动化测试实战 —— 单元测试框架

软件测试专栏 感兴趣可看&#xff1a;软件测试专栏 自动化测试学习部分源码 python自动化测试相关知识&#xff1a; 【如何学习Python自动化测试】—— 自动化测试环境搭建 【如何学习python自动化测试】—— 浏览器驱动的安装 以及 如何更…

swing快速入门(五)

注释很详细&#xff0c;直接上代码 上一篇 本篇新增内容&#xff1a; 1.布局管理器BorderLayout 2.自适应尺寸方法pack() import java.awt.*; public class swing_test_3 {public static void main(String[] args) {Frame framenew Frame("演示BorderLayout");//…