查看原文
其他

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

牛晓伟 郭霖
2024-08-23


/   今日科技快讯   /

据 Thaipbsworld 报道,泰国总理赛塔・他威信(Srettha Thavisin)已责成数字经济和社会部、税务局和警方调查拼多多旗下电商平台 Temu 是否遵守泰国法律并缴纳所需税款。

赛塔表示,外国电商平台的涌入对泰国零售业构成挑战,因为它们资金雄厚、拥有先进技术,使泰国本土企业难以竞争。他还警告称,任何帮助这些外国平台逃税的泰国官员都将受到严厉处罚。

中国电商巨头 Temu 已悄然进军泰国市场,提供高达 90% 的中国制造商品折扣。Temu 于 7 月 31 日在泰国推出服务,成为其继菲律宾和马来西亚之后进军东南亚的第三个国家。

/   作者简介   /

本篇文章来自牛晓伟的投稿,文章主要分享了Android帝国之日志系统--logd、logcat,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

原文地址:
https://mp.weixin.qq.com/s/8LrY0OKcglBfIBjUfavvPw

/  本文概要   /

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

/   我是谁   /

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

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

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

“是的,刚刚这位进程提到的这些打印日志的方法都和我有关,别看你们在使用的时候特别简单,其实这些日志信息在到达我这的时候要经过‘千山万水的旅程‘。回归正题,我的名字叫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 socket
    socket logd stream 0666 logd logd
    socket logdr seqpacket 0666 logd logd
    socket logdw dgram+passcred 0222 logd logd
    //kmsg代表内核会把内核的日志信息存储在这个文件中
    file /proc/kmsg r
    file /dev/kmsg w
    user logd
    group logd system package_info readproc
    capabilities SYSLOG AUDIT_CONTROL
    priority 10
    task_profiles ServiceCapacityLow
    onrestart 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_MAIN、LOG_ID_RADIO、LOG_ID_EVENTS、LOG_ID_SYSTEM、LOG_ID_CRASH、LOG_ID_SECURITY、LOG_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开始每次加1
    auto 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,每个日志都对应一个sequence
    auto 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::kChunkSizeDivisor
    if (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一个SerializedLogChunk
        log_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属性对应的值,默认值是serialized
    std::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_MAIN、LOG_ID_RADIO、LOG_ID_EVENTS、LOG_ID_SYSTEM、LOG_ID_CRASH、LOG_ID_SECURITY、LOG_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.

    // 实例化LogListener
    LogListener* 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的名字是logdw
    static 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 logdr
    static 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.i、Slog.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.cpp

int __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连接,进而收到日志进行显示等处理。

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

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

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

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
原创:写给初学者的Jetpack Compose教程,高级Layout
Harmony OS鸿蒙应用开发,学一下如何使用系统路由表

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
郭霖
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存