Wakeup in XNU

本文做者:段家顺前端

苹果在 iOS 13 的时候,在内核中加入了一个新的性能衡量指标wakeup,同时因为这个指标而被系统杀死的应用数不胜数,其中也包括咱们经常使用的微信淘宝等。而这个指标彻底是由 XNU 内核统计的,因此咱们很难经过日志等普通手段去准确的定位问题,因此这里经过另外一种思路去解决这个问题。git

为何要统计 wakeup

要定位这个问题,首先咱们须要知道这个指标的目的是什么。github

XNU 中,对性能的指标有CPU、内存、IO,而wakeup属于 CPU 的性能指标,同时属于 CPU 指标的还有 CPU 使用率,下面是XNU中对其限制的定义。微信

/* * Default parameters for CPU usage monitor. * * Default setting is 50% over 3 minutes. */
#define DEFAULT_CPUMON_PERCENTAGE 50
#define DEFAULT_CPUMON_INTERVAL (3 * 60)
复制代码
#define TASK_WAKEUPS_MONITOR_DEFAULT_LIMIT 150 /* wakeups per second */
#define TASK_WAKEUPS_MONITOR_DEFAULT_INTERVAL 300 /* in seconds. */

/* * Level (in terms of percentage of the limit) at which the wakeups monitor triggers telemetry. * * (ie when the task's wakeups rate exceeds 70% of the limit, start taking user * stacktraces, aka micro-stackshots) */
#define TASK_WAKEUPS_MONITOR_DEFAULT_USTACKSHOTS_TRIGGER 70
复制代码

总结来讲,当 CPU 使用率在3分钟内均值超过50%,就认为过分使用CPU,当wakeup在300秒内均值超过150次,则认为唤起次数过多,同时在阈值的70%水位内核会开启监控。markdown

CPU 使用率咱们很容易理解,使用率越高,电池寿命越低,并且并非线性增长的。那么wakeup又是如何影响电池寿命的呢?网络

首先咱们须要看看ARM架构中对于 CPU 功耗问题的描述:架构

Many ARM systems are mobile devices and powered by batteries. In such systems, optimization of power use, and total energy use, is a key design constraint. Programmers often spend significant amounts of time trying to save battery life in such systems. 
复制代码

因为ARM被大量使用于低功耗设备,而这些设备每每会由电池来做为驱动,因此 ARM 在硬件层面就对功耗这个问题进行了优化设计。app

Energy use can be divided into two components: 

- Static 
Static power consumption, also often called leakage, occurs whenever the core logic or RAM blocks have power applied to them. In general terms, the leakage currents are proportional to the total silicon area, meaning that the bigger the chip, the higher the leakage. The proportion of power consumption from leakage gets significantly higher as you move to smaller fabrication geometries. 

- Dynamic 
Dynamic power consumption occurs because of transistor switching and is a function of the core clock speed and the numbers of transistors that change state per cycle. Clearly, higher clock speeds and more complex cores consume more power. 
复制代码

功耗能够分为2种类型,即静态功耗与动态功耗。异步

静态功耗指的是只要 CPU 通上电,因为芯片没法保证绝对绝缘,因此会存在“漏电”的状况,并且越大的芯片这种问题越严重,这也是芯片厂家为何拼命的研究更小尺寸芯片的缘由。这部分功耗因为是硬件自己决定的,因此咱们没法去控制,而这种类型功耗占比不大。ide

动态功耗指的是 CPU 运行期间,接通时钟后,执行指令所带来的额外开销,而这个开销会和时钟周期频率相关,频率越高,耗电量越大。这也就说明了苹果为何会控制 CPU 使用率,而相关研究(Facebook 也作过)也代表,CPU 在20如下和20以上的能耗几乎是成倍的增长。

CPU 使用率已经可以从必定程度上限制电池损耗问题了,那么wakeup又是什么指标呢?

wakeup 是什么

要了解wakeup是什么,首先要知道ARM低功耗模式的2个重要指令WFIWFE

ARM assembly language includes instructions that can be used to place the core in a low-power state. The architecture defines these instructions as hints, meaning that the core is not required to take any specific action when it executes them. In the Cortex-A processor family, however, these instructions are implemented in a way that shuts down the clock to almost all parts of the core. This means that the power consumption of the core is significantly reduced so that only static leakage currents are drawn, and there is no dynamic power consumption. 
复制代码

经过这2个指令进入低功耗模式后,时钟将会被关闭,这个 CPU 将不会再执行任何指令,这样这个 CPU 的动态能耗就没有了。这个能力的实现是由和 CPU 核心强绑定的空转线程idle thread实现的,有意思的是XNU中的实现较为复杂,而Zircon中则很是直接暴力:

__NO_RETURN int arch_idle_thread_routine(void*) {
  for (;;) {
    __asm__ volatile(“wfi”);
  }
}
复制代码

在 XNU 中,一个 CPU 核心的工做流程被归纳为以下状态机:

/* * -------------------- SHUTDOWN * / ^ ^ * _/ | \ * OFF_LINE ---> START ---> RUNNING ---> IDLE ---> DISPATCHING * \_________________^ ^ ^______/ / * \__________________/ */
复制代码

wakeup则表示的是,从低功耗模式唤起进入运行模式的次数。

wakeup 如何统计的

ARM异常系统

CPU 时钟被关闭了,那么又要怎么唤起呢?这就涉及到 CPU 的异常系统。

在 ARM 中,异常和中断的概念比较模糊,他把全部会引发 CPU 执行状态变动的事件都称为异常,其中包括软中断,debug 中断,硬件中断等。

从触发时机上能够区分为同步异常与异步异常。这里指的同步异步并非应用程序的概念,这里同步指的是拥有明确的触发时机,好比系统调用,缺页中断等,都会发生在明确的时机,而异步中断,则彻底无视指令的逻辑,会强行打断指令执行,好比 FIQ 和 IRQ,这里比较典型的是定时器中断。

异常系统有不少能力,其中一个重要的能力就是内核态与用户态切换。ARM的执行权限分为4个等级,EL0,EL1,EL2,EL3。其中 EL0 表明用户态,而 EL1 表明内核态,当用户态想要切换至内核态的时候,必须经过异常系统进行切换,并且异常系统只能向同等或更高等级权限进行切换。

那么这么多类型的异常,又是如何响应的呢?这里就涉及到一个异常处理表(exception table),在系统启动的时候,须要首先就去注册这个表,在XNU中,这个表以下:

.section __DATA_CONST,__const
    .align 3
    .globl EXT(exc_vectors_table)
LEXT(exc_vectors_table)
    /* Table of exception handlers.
         * These handlers sometimes contain deadloops. 
         * It's nice to have symbols for them when debugging. */
    .quad el1_sp0_synchronous_vector_long
    .quad el1_sp0_irq_vector_long
    .quad el1_sp0_fiq_vector_long
    .quad el1_sp0_serror_vector_long
    .quad el1_sp1_synchronous_vector_long
    .quad el1_sp1_irq_vector_long
    .quad el1_sp1_fiq_vector_long
    .quad el1_sp1_serror_vector_long
    .quad el0_synchronous_vector_64_long
    .quad el0_irq_vector_64_long
    .quad el0_fiq_vector_64_long
    .quad el0_serror_vector_64_long
复制代码

wakeup 计数

那么咱们回过头来看看wakeup计数的地方:

/* * thread_unblock: * * Unblock thread on wake up. * Returns TRUE if the thread should now be placed on the runqueue. * Thread must be locked. * Called at splsched(). */
boolean_t thread_unblock( thread_t thread, wait_result_t wresult) {
	  // . . .
    boolean_t aticontext, pidle;
    ml_get_power_state(&aticontext, &pidle);

     /* Obtain power-relevant interrupt and “platform-idle exit" statistics. * We also account for “double hop” thread signaling via * the thread callout infrastructure. * DRK: consider removing the callout wakeup counters in the future * they’re present for verification at the moment. */

    if (__improbable(aticontext /* . . . */)) {
        // wakeup ++
    }
    // . . .
}
复制代码

而这里的aticontext则是经过ml_at_interrupt_context获取的,其含义则是是否处于中断上下文中。

/* * Routine: ml_at_interrupt_context * Function: Check if running at interrupt context */
boolean_t ml_at_interrupt_context(void) {
    /* Do not use a stack-based check here, as the top-level exception handler * is free to use some other stack besides the per-CPU interrupt stack. * Interrupts should always be disabled if we’re at interrupt context. * Check that first, as we may be in a preemptible non-interrupt context, in * which case we could be migrated to a different CPU between obtaining * the per-cpu data pointer and loading cpu_int_state. We then might end * up checking the interrupt state of a different CPU, resulting in a false * positive. But if interrupts are disabled, we also know we cannot be * preempted. */
    return !ml_get_interrupts_enabled() && (getCpuDatap()->cpu_int_state != NULL);
}
复制代码

那么cpu_int_state标记又是在何时设置上去的呢?只有在locore.S中,才会更新该标记:

str        x0, [x23, CPU_INT_STATE]            // Saved context in cpu_int_state
复制代码

同时发现以下几个方法会配置这个标记:

el1_sp0_irq_vector_long
el1_sp1_irq_vector_long
el0_irq_vector_64_long
el1_sp0_fiq_vector_long
el0_fiq_vector_64_long
复制代码

结合上述的异常处理表的注册位置,与ARM官方文档的位置进行对比,能够发现:

这几个中断类型均为 FIQ 或者 IRQ,也就是硬中断。由此咱们能够判断,wakeup必然是由硬中断引发的,而像系统调用,线程切换,缺页中断这种并不会引发wakeup

进程统计

由上能够看出,wakeup实际上是对CPU核心唤起次数的统计,和应用层的线程与进程彷佛绝不相干。但从程序执行的角度思考,若是一个程序一直在运行,就不会进入等待状态,而从等待状态唤醒,确定是由于某些异常中断,好比网络,vsync 等。

在 CPU 核心被唤醒后,在当前 CPU 核心执行的线程会进行wakeup++,而系通通计维度是应用维度,也就是进程维度,因此会累计该进程下面的全部线程的wakeup计数。

queue_iterate(&task->threads, thread, thread_t, task_threads) {
        info->task_timer_wakeups_bin_1 += thread->thread_timer_wakeups_bin_1;
        info->task_timer_wakeups_bin_2 += thread->thread_timer_wakeups_bin_2;
}
复制代码

因此在咱们代码中,若是在2个不一样线程启用用一样的定时器,wakeup是同一个线程起2个定时器的2倍(一样的定时器在底层实际上是一颗树,注册一样的定时器实际只注册了一个)。

用户层获取该统计值则能够经过以下方式:

#include <mach/task.h>
#include <mach/mach.h>

BOOL GetSystemWakeup(NSInteger *interrupt_wakeup, NSInteger *timer_wakeup) {
    struct task_power_info info = {0};
    mach_msg_type_number_t count = TASK_POWER_INFO_COUNT;
    kern_return_t ret = task_info(current_task(), TASK_POWER_INFO, (task_info_t)&info, &count);
    if (ret == KERN_SUCCESS) {
        if (interrupt_wakeup) {
            *interrupt_wakeup = info.task_interrupt_wakeups;
        }
        if (timer_wakeup) {
            *timer_wakeup = info.task_timer_wakeups_bin_1 + info.task_timer_wakeups_bin_2;
        }
        return true;
    }
    else {
        if (interrupt_wakeup) {
            *interrupt_wakeup = 0;
        }
        if (timer_wakeup) {
            *timer_wakeup = 0;
        }
        return false;
    }
}
复制代码

wakeup 治理

从以上分析来看,咱们只须要排查各类硬件相关事件便可。

从实际排查结果来看,目前只有定时器或者拥有定时能力的类型是最广泛的场景。

好比NSTimerCADisplayLinkdispatch_semaphore_waitpthread_cond_timedwait等。

关于定时器,咱们尽可能复用其能力,避免在不一样线程去建立一样的定时能力,同时在回到后台的时候,关闭不须要的定时器,由于大部分定时器都是UI相关的,关闭定时器也是一种标准的作法。

关于 wait 类型的能力,从方案选择上避免轮询的方案,或者增长轮询间隔时间,好比能够经过 try_wait,runloop或者 EventKit 等能力进行优化。

监控与防劣化

一旦咱们知道了问题缘由,那么对问题的治理比较简单,然后续咱们须要创建持续的管控等长效措施才能够。

在此咱们能够简单的定义一些规则,而且嵌入线下监控能力中:

  • 定时器时间周期小于1s的,在进入后台须要进行暂停
  • wait 类型延迟小于1s,而且持续使用10次以上的状况须要进行优化

总结

wakeup因为是 XNU 内核统计数据,因此在问题定位排查方面特别困难,因此从另外一个角度去解决这个问题反而是一种更好的方式。

同时从 XNU 中对 CPU 功耗的控制粒度能够看出,苹果在极致的优化方面作的很好,在自身的软件生态中要求也比较高。电量问题在短期内应该不会有技术上的突破,因此咱们自身也须要多思考如何减小电池损耗。

本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!

相关文章
相关标签/搜索