基于android_n的log机制浅析_下篇

在上篇中从上层使用Log的java接口讲到了liblog中,最终通过socket将要打印的Log交给了logd模块处理,本篇就继续来看一下logd模块内部对Log的处理过程。

Logd的启动

Logd模块是单独跑在一个进程中的,该进程是直接由init进程fork出来的子进程。

shell@M3X:/ $ ps | grep zygote
root      343   1     2137408 86344 poll_sched 0000000000 S zygote64
root      344   1     1579728 75304 poll_sched 0000000000 S zygote
shell@M3X:/ $ ps | grep logd                                                   
logd      268   1     27136  3724  sigsuspend 0000000000 S /system/bin/logd
shell@M3X:/ $ ps | grep init                                                   
root      1     0     13112  1364  SyS_epoll_ 0000000000 S /init
system    293   1     13316  1644  dev_char_r 0000000000 S /system/bin/ccci_mdinit
system    295   1     12284  1636  dev_char_r 0000000000 S /system/bin/ccci_mdinit

通过ps可以看到logd进程和zygote一样,他们的ppid都是1,而1正是init进程的pid,所以也印证了他们都是由init进程fork出来的结论,当然你也可以去查看一下系统中的init.rc文件,里面也详细描述了启动logd的时机。

Logd启动后的入口是/system/core/logd/main.cpp中的main方法,本文聚焦上层打印Log后Logd的处理过程,因此省略部分代码:

int main(int argc, char *argv[]) {
    
    ...

    // LogBuffer is the object which is responsible for holding all
    // log entries.

    logBuf = new LogBuffer(times);

    // LogReader listens on /dev/socket/logdr. When a client
    // connects, log entries in the LogBuffer are written to the client.

    LogReader *reader = new LogReader(logBuf);
    if (reader->startListener()) {
        exit(1);
    }

    // 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 *swl = new LogListener(logBuf, reader);
    // Backlog and /proc/sys/net/unix/max_dgram_qlen set to large value
    if (swl->startListener(600)) {
        exit(1);
    }

    ...
    
}

上面的代码在Logd进程启动后执行,省略了部分初始化工作的代码,这里主看到构造了一个LogBuffer类型的对象logBuf,它的作用是真正存放写入的log。然后构造了一个LogReader类型的对象reader,并将上面构造的logBuf当做参数传给reader,它的作用主要是监听LogBuffer中log的变化或者是否有进程想要读取logd中的log,在有新的log写入时能够被及时通知到,然后可以通过socket通知log的读取方将log取出,通过其startListener()方法开始监听socket是否有请求(比如adb shell logcat)。接下来构造了一个LogListener类型的对象swl,并将reader作为构造方法的参数传递给它,它的作用是监听是否有进程通过socket向logd发起了写入log的请求,通过startListener(600)开始监听。这里的600是socket中的backlog值,具体的含义可以参考这篇文章。 这里可以理解为logd本身运行在一个独立的进程,他是socket的服务端,跟它对接的client进程有两种角色,一种是将log往logd中写的程序,另一种是往logd中读log的程序。

往Logd中写数据

上面分析完后logd已经正常在系统中跑起来了,并且开始监听指定的socket是否有相应的请求。现在就可以接着上篇的调用栈往下看,当有程序需要打印log时,logd时如何处理的:

LogListener.cpp

bool LogListener::onDataAvailable(SocketClient *cli) {
    static bool name_set;
    if (!name_set) {
        prctl(PR_SET_NAME, "logd.writer");
        name_set = true;
    }

    char buffer[sizeof_log_id_t + sizeof(uint16_t) + sizeof(log_time)
        + LOGGER_ENTRY_MAX_PAYLOAD];
    struct iovec iov = { buffer, sizeof(buffer) };

    char control[CMSG_SPACE(sizeof(struct ucred))] __aligned(4);
    struct msghdr hdr = {
        NULL,
        0,
        &iov,
        1,
        control,
        sizeof(control),
        0,
    };

    int socket = cli->getSocket();

    // To clear the entire buffer is secure/safe, but this contributes to 1.68%
    // overhead under logging load. We are safe because we check counts.
    // memset(buffer, 0, sizeof(buffer));
    ssize_t n = recvmsg(socket, &hdr, 0);
    
    ...

    if (logbuf->log((log_id_t)header->id, header->realtime,
            cred->uid, cred->pid, header->tid, msg,
            ((size_t) n <= USHRT_MAX) ? (unsigned short) n : USHRT_MAX) >= 0) {
        reader->notifyNewLog();
    }

    return true;
}

这里只保留了部分主要代码。刚刚也说了监听是否有程序有往logd中写入数据的请求是由LogListener来完成的,它本身继承于SocketListener,LogListener会监听/dev/socket/logdw端口,当它监听的socket有请求时会回调LogListener实现的onDataAvailable回调方法。上篇中可以知道待打印的Log是通过socket传递到logd进程的,所以这里的onDataAvailable回调在监听到有请求后首先完成一些数据结构的初始化工作,其实它主要完成了三件事情: 1.通过recvmsg(socket, &hdr, 0);将数据取到hdr结构中。 2.通过logbuf->log((log_id_t)header->id, header->realtime, cred->uid, cred->pid, header->tid, msg, ((size_t) n <= USHRT_MAX) ? (unsigned short) n : USHRT_MAX)方法将log信息存储到logd的缓存中。 3.如果logbuf->log方法正常将log存储到logd的缓存中,那么通过LogReader的notifyNewLog方法唤醒LogReader。

LogBuffer.cpp

int LogBuffer::log(log_id_t log_id, log_time realtime,
                   uid_t uid, pid_t pid, pid_t tid,
                   const char *msg, unsigned short len) {
    if ((log_id >= LOG_ID_MAX) || (log_id < 0)) {
        return -EINVAL;
    }

    LogBufferElement *elem = new LogBufferElement(log_id, realtime,
                                                  uid, pid, tid, msg, len);
    
    ...


    pthread_mutex_lock(&mLogElementsLock);

    // Insert elements in time sorted order if possible
    //  NB: if end is region locked, place element at end of list
    LogBufferElementCollection::iterator it = mLogElements.end();
    LogBufferElementCollection::iterator last = it;
    while (last != mLogElements.begin()) {
        --it;
        if ((*it)->getRealTime() <= realtime) {
            break;
        }
        last = it;
    }

    if (last == mLogElements.end()) {
        mLogElements.push_back(elem);
    } else {
        uint64_t end = 1;
        bool end_set = false;
        bool end_always = false;

        LogTimeEntry::lock();

        LastLogTimes::iterator times = mTimes.begin();
        while(times != mTimes.end()) {
            LogTimeEntry *entry = (*times);
            if (entry->owned_Locked()) {
                if (!entry->mNonBlock) {
                    end_always = true;
                    break;
                }
                if (!end_set || (end <= entry->mEnd)) {
                    end = entry->mEnd;
                    end_set = true;
                }
            }
            times++;
        }

        if (end_always
                || (end_set && (end >= (*last)->getSequence()))) {
            mLogElements.push_back(elem);
        } else {
            mLogElements.insert(last,elem);
        }

        LogTimeEntry::unlock();
    }

    stats.add(elem); //将log加入到一些不同维度的统计数据结构中,并且会唤醒LogReader.
    maybePrune(log_id);
    pthread_mutex_unlock(&mLogElementsLock);

    return len;
}

长话短说,这里也是省略部分代码。在log()方法中首先将log的封装成了一个LogBufferElement实体,这个实体就是log真正在logd中保存的结构化对象了。mLogElements是一个list列表,接下来的while循环主要是要在mLogElements中找到一个合适的地方插入当前的这个LogBufferElement。在mLogElements列表中的log主要是以时间的顺序来排列的,越新的log排在列表的越前面。log插入到mLogElements后,logd就基本完成这条log的“打印”工作了,没错,logd只是把上层应用想要打印的log“打印”到了内存中,然后通过唤醒LogReader将log从内存中读出来后通过socket还给上层应用完成特定的工作的。这里我们还需要特别关注下末尾的maybePrune(log_id);方法,这里的参数log_id就是上篇中也提到过的需要往哪个Buffer写log的Buffer_id,每个Buffer的大小是有限制的,超过限制后logd会对该Buffer中的log做一些清理工作。每个Buffer的大小是在LogBuffer的构造方法中设置的,默认大小为256K,我们可以通过修改persist.logd.size.XXX的值来自定义某个Buffer的大小,其中XXX代表的是Buffer的名字。有如下几种Buffer:

    [LOG_ID_MAIN] = "main",
    [LOG_ID_RADIO] = "radio",
    [LOG_ID_EVENTS] = "events",
    [LOG_ID_SYSTEM] = "system",
    [LOG_ID_CRASH] = "crash",
    [LOG_ID_SECURITY] = "security",
    [LOG_ID_KERNEL] = "kernel",

那么到这里上层应用准备打印的log算是安全“打印”到logd的buffer中了,但这只完成了一半工作,上层应用调用接口打印的Log需要通过不同的工具真正打印出来,比如adb shell logcat。这类工具其实也是一个独立的程序,他们是上面提到的往logd中读数据的上层进程,至于数据读出去后是打印还是其他的方式处理,完全取决于上层工具程序的实现逻辑,所以logd要做的其实就是通知这些程序,有log数据更新了。

从Logd中读数据

在logd的main方法中除了LogListener外还有一个LogReader监听器,LogReader监听的是/dev/socket/logdr端口,并且在构造LogListener时传入的参数是LogReader的对象。所以在监听到有client向/dev/socket/logdr端口发起请求时,LogReader的onDataAvailable方法也会被回调。在onDataAvailable中会将log返回给client处理。同时再回到上面说的LogListener::onDataAvailable方法中提到的完成的三项主要任务,最后一项便是通过LogListener中的LogReader对象的notifyNewLog方法也可以通知logd的client,这些client是另一个角色,他们是往logd中读出数据去处理的进程,而不是往logd中写数据的进程。notifyNewLog方法中通过一系列的调用最终会通过LogBuffer::flushTo方法来向client发送数据:

LogBuffer.cpp

uint64_t LogBuffer::flushTo(
        SocketClient *reader, const uint64_t start,
        bool privileged, bool security,
        int (*filter)(const LogBufferElement *element, void *arg), void *arg) {
    LogBufferElementCollection::iterator it;
    uint64_t max = start;
    uid_t uid = reader->getUid();

    pthread_mutex_lock(&mLogElementsLock);


	...


    for (; it != mLogElements.end(); ++it) {
        LogBufferElement *element = *it;

        if (!privileged && (element->getUid() != uid)) {
            continue;
        }

        if (!security && (element->getLogId() == LOG_ID_SECURITY)) {
            continue;
        }

        if (element->getSequence() <= start) {
            continue;
        }

        // NB: calling out to another object with mLogElementsLock held (safe)
        if (filter) {
            int ret = (*filter)(element, arg);
            if (ret == false) {
                continue;
            }
            if (ret != true) {
                break;
            }
        }

        pthread_mutex_unlock(&mLogElementsLock);

        // range locking in LastLogTimes looks after us
        max = element->flushTo(reader, this, privileged);

        if (max == element->FLUSH_ERROR) {
            return max;
        }

        pthread_mutex_lock(&mLogElementsLock);
    }
    pthread_mutex_unlock(&mLogElementsLock);

    return max;
}

前面写log正是把log写到这里遍历的mLogElements列表中的,所以这里通过遍历它将logd中的log数据取出,但中间会涉及一些log过滤器的实现,关于log过滤器的逻辑后面如果有机会当做专题文章在做记录吧,这里还是聚焦在最基本的log打印流程上。mLogElements存放的是LogBufferElement类型的对象,遍历每一个LogBufferElement并调用LogBufferElement::flushTo()方法完成对每一个log实体的处理工作:

LogBufferElement.cpp

uint64_t LogBufferElement::flushTo(SocketClient *reader, LogBuffer *parent,
                                   bool privileged) {
    struct logger_entry_v4 entry;

    memset(&entry, 0, sizeof(struct logger_entry_v4));

    entry.hdr_size = privileged ?
                         sizeof(struct logger_entry_v4) :
                         sizeof(struct logger_entry_v3);
    entry.lid = mLogId;
    entry.pid = mPid;
    entry.tid = mTid;
    entry.uid = mUid;
    entry.sec = mRealTime.tv_sec;
    entry.nsec = mRealTime.tv_nsec;

    struct iovec iovec[2];
    iovec[0].iov_base = &entry;
    iovec[0].iov_len = entry.hdr_size;

    char *buffer = NULL;

    if (!mMsg) {
        entry.len = populateDroppedMessage(buffer, parent);
        if (!entry.len) {
            return mSequence;
        }
        iovec[1].iov_base = buffer;
    } else {
        entry.len = mMsgLen;
        iovec[1].iov_base = mMsg;
    }
    iovec[1].iov_len = entry.len;

    uint64_t retval = reader->sendDatav(iovec, 2) ? FLUSH_ERROR : mSequence;

    if (buffer) {
        free(buffer);
    }

    return retval;
}

在LogBufferElement::flushTo()方法中将log重新结构化为iovec的结构类型,并通过LogReader::sendDatav()方法通知到上层的应用中,由它们觉得将log打印还是其他处理。

总结

本文只是简单的梳理记录了logd中对整个log从存储到打印的流程。很多细节部分没有展开介绍,受于篇幅所限也没办法展开介绍,想搞清楚logd内部更多有趣细节的朋友们还是阅读代码来的比较畅快淋漓。 其实logd的基本工作模式就是基于两种类型的Client在,针对想要打印log的Client,logd只是将待打印的log写到自己的buffer中,我们真正看到log的打印是由于logd跟另一类(想要处理log的)Client通信的结果。logd将自己buffer中的log通知给想要处理log的这一类Client,由他们来处理这些log,他们可以选择简单的将log打印,也可以做一些其他的更有趣的事情。

小弟才疏学浅,如果有什么错误还请大哥大姐们帮忙指点迷津~ 多谢~