Android系统功耗优化之Alarm - 从AlarmManager到Linux kernel

1 Overview

对于系统功耗优化,时常可以看到alarm唤醒频繁,或者alarm timer持锁时间过长的问题,对于这样的情况Android的各个版本也都有持续性的优化,对于alarm来说,简而言之都是加强管控,尽可能减少唤醒,集中批量处理。

2 AlarmManager

AlarmManager提供接口供应用根据自己的需求,来设置alarm以及对应的处理方法

frameworks/base/core/java/android/app/AlarmManager.java

alarm目前有几种类型

  • RTC_WAKEUP
    /**
     * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
     * (wall clock time in UTC), which will wake up the device when
     * it goes off.
     */
    public static final int RTC_WAKEUP = 0;
  • RTC
    /**
     * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
     * (wall clock time in UTC).  This alarm does not wake the
     * device up; if it goes off while the device is asleep, it will not be
     * delivered until the next time the device wakes up.
     */
    public static final int RTC = 1;
  • ELAPSED_REALTIME_WAKEUP
    /**
     * Alarm time in {@link android.os.SystemClock#elapsedRealtime
     * SystemClock.elapsedRealtime()} (time since boot, including sleep),
     * which will wake up the device when it goes off.
     */
    public static final int ELAPSED_REALTIME_WAKEUP = 2;
  • ELAPSED_REALTIME
    /**
     * Alarm time in {@link android.os.SystemClock#elapsedRealtime
     * SystemClock.elapsedRealtime()} (time since boot, including sleep).
     * This alarm does not wake the device up; if it goes off while the device
     * is asleep, it will not be delivered until the next time the device
     * wakes up.
     */
    public static final int ELAPSED_REALTIME = 3;
  • RTC_POWEROFF_WAKEUP
    /**
     * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
     * (wall clock time in UTC), which will wake up the device when
     * it goes off. And it will power on the devices when it shuts down.
     * Set as 5 to make it be compatible with android_alarm_type.
     * @hide
     */
    public static final int RTC_POWEROFF_WAKEUP = 5;

其特点用途如下

  • RTC和ELAPSED_REALTIME 的差异在于前者使用绝对时间,后者使用相对时间来设置

  • WAKEUP类型的alarm在超时时如果系统处于待机休眠状态,则会唤醒系统,非WAKEUP类型的只能等到下一次系统唤醒的时候再被处理

  • RTC_POWEROFF_WAKEUP 基本就是用来给关机闹钟,或者定时开机用

提供的设置接口有

  • set(int type, long triggerAtMillis, PendingIntent operation)
    设置一次性的
  • setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
    设置可重复执行的
  • setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
    设置可重复执行的,并且没有精确的时间要求

第三个参数就是Alarm对应的超时处理方法,

除了PendingIntent还有OnAlarmListener类型

public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,

        Handler targetHandler)

以上的API在小于19的情况下,可以正常工作,因为从Android 4.4开始,set 或者 setRepeating创建的alarm将会变得不准确,原因是做了唤醒对齐, 时间不敏感的放一起触发。

这时候可以用setWindow(), 这个api设置的是一个时间范围,可以确保alarm在这个时间范围内触发
如果需要非常精确的时间,可以使用setExact()

在Android 6.0 API 23 开始,DOZE 又会导致上述方法设置的alarm无法生效

标准 AlarmManager 闹铃(包括 setExact() 和 setWindow())推迟到下一维护时段。
    如果您需要设置在低电耗模式下触发的闹铃,请使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。
    一般情况下,使用 setAlarmClock() 设置的闹铃将继续触发 — 但系统会在这些闹铃触发之前不久退出低电耗模式。

这时候可以使用setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)

取消设置的接口

对应到设置的方法,cancel两种类型的超时处理方法

    /**
     * Remove any alarms with a matching {@link Intent}.
     * Any alarm, of any type, whose Intent matches this one (as defined by
     * {@link Intent#filterEquals}), will be canceled.
     *
     * @param operation IntentSender which matches a previously added
     * IntentSender. This parameter must not be {@code null}.
     *
     * @see #set
     */
    public void cancel(PendingIntent operation) {
        if (operation == null) {
            final String msg = "cancel() called with a null PendingIntent";
            if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
                throw new NullPointerException(msg);
            } else {
                Log.e(TAG, msg);
                return;
            }
        }

        try {
            mService.remove(operation, null);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * Remove any alarm scheduled to be delivered to the given {@link OnAlarmListener}.
     *
     * @param listener OnAlarmListener instance that is the target of a currently-set alarm.
     */
    public void cancel(OnAlarmListener listener) {
        if (listener == null) {
            throw new NullPointerException("cancel() called with a null OnAlarmListener");
        }

        ListenerWrapper wrapper = null;
        synchronized (AlarmManager.class) {
            if (sWrappers != null) {
                wrapper = sWrappers.get(listener);
            }
        }

        if (wrapper == null) {
            Log.w(TAG, "Unrecognized alarm listener " + listener);
            return;
        }

        wrapper.cancel();
    }

3 AlarmManagerService

AlarmManagerService提供接口给AlarmManager,实现上述接口,另外也实现了系统对于Alarm的处理逻辑,包括一些优化

从Android 4.4开始 Alarm默认是非精确的,也可以指定采用精确模式。在非精确模式下,Alarm是批量提醒的,每个Alarm会被根据其触发时间以及最大触发时间,加入到不同的batch中去,同一个batch中的alarm会同时触发,这也是low power的优化。对于

frameworks/base/services/core/java/com/android/server/AlarmManagerService.java

SET

setImpl 是最常用的方法
比如setAndAllowWhileIdle的实现

    public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
            PendingIntent operation) {
        setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
                operation, null, null, null, null, null);
    }

setImpl() 会把RTC类型的转换成ELAPSED_REALTIME类型,也就是说最终到底层的只有 ELAPSED_REALTIME_WAKEUP和ELAPSED_REALTIME, 也就是id 2和3

final long nominalTrigger = convertToElapsed(triggerAtTime, type);

其调用流程如下

setImpl()
    setImplLocked()
        setImplLocked()
            rescheduleKernelAlarmsLocked()
                setlocked()
                    set()

Service处理逻辑

在添加alarm的时候,比如先添加了一个15s的wakeup alarm(假设其就是下一个wakeup类型的alarm),再添加一个10s的wakeup alarm

在调用进行到 setImplLocked的时候, 15s的alarm所在的batch已在mAlarmBatches 数组中,这个时候来了10s的,那么10s的batch就会位于mAlarmBatches中15s的前面,之后调用rescheduleKernelAlarmsLocked(), 进而把位于第一个的10s设置下去

如果是先设15s的后面来了一个20s的,实际上20s的在这一次是不会被设置下去的,需要等到其前面的超时之后,alarm thread遍历20s的这个已经排在最前,才会被设置下去


private void setImplLocked(int type, long when, long whenElapsed, long maxWhen, long interval,  
        PendingIntent operation, boolean isStandalone, boolean doValidate,  
        WorkSource workSource) {  
    /**创建一个alarm,其中各参数的含义如下: 
     * type 闹钟类型 ELAPSED_REALTIME、RTC、RTC_WAKEUP等 
     * when 触发时间 UTC类型,绝对时间,通过System.currentTimeMillis()得到 
     * whenElapsed 相对触发时间,自开机算起,含休眠,通过SystemClock.elapsedRealtime()得到 
     * maxWhen 最大触发时间 
     * interval 触发间隔,针对循环闹钟有效 
     * operation 闹钟触发时的行为,PendingIntent类型 
     */  
    Alarm a = new Alarm(type, when, whenElapsed, maxWhen, interval, operation, workSource);  
    //根据PendingIntent删除之前已有的同一个闹钟  
    removeLocked(operation);  
  
    boolean reschedule;  
    //尝试将alarm加入到合适的batch中,如果alarm是独立的或者无法找到合适的batch去容纳此alarm,返回-1  
    int whichBatch = (isStandalone) ? -1 : attemptCoalesceLocked(whenElapsed, maxWhen);  
    if (whichBatch < 0) {  
        //没有合适的batch去容纳alarm,则新建一个batch  
        Batch batch = new Batch(a);  
        batch.standalone = isStandalone;  
        //将batch加入mAlarmBatches中,并对mAlarmBatches进行排序:按开始时间升序排列  
        reschedule = addBatchLocked(mAlarmBatches, batch);  
    } else {  
        //如果找到合适了batch去容纳此alarm,则将其加入到batch中  
        Batch batch = mAlarmBatches.get(whichBatch);  
        //如果当前alarm的加入引起了batch开始时间和结束时间的改变,则reschedule为true  
        reschedule = batch.add(a);  
        if (reschedule) {  
            //由于batch的起始时间发生了改变,所以需要从列表中删除此batch并重新加入、重新对batch列表进行排序  
            mAlarmBatches.remove(whichBatch);  
            addBatchLocked(mAlarmBatches, batch);  
        }  
    }  
  
    if (DEBUG_VALIDATE) {  
        if (doValidate && !validateConsistencyLocked()) {  
            Slog.v(TAG, "Tipping-point operation: type=" + type + " when=" + when  
                    + " when(hex)=" + Long.toHexString(when)  
                    + " whenElapsed=" + whenElapsed + " maxWhen=" + maxWhen  
                    + " interval=" + interval + " op=" + operation  
                    + " standalone=" + isStandalone);  
            rebatchAllAlarmsLocked(false);  
            reschedule = true;  
        }  
    }  
  
    if (reschedule) {  
        rescheduleKernelAlarmsLocked();  
    }  
}  
    void rescheduleKernelAlarmsLocked() {
        // Schedule the next upcoming wakeup alarm.  If there is a deliverable batch
        // prior to that which contains no wakeups, we schedule that as well.
        long nextNonWakeup = 0;
        if (mAlarmBatches.size() > 0) {
            final Batch firstWakeup = findFirstWakeupBatchLocked();
            final Batch firstBatch = mAlarmBatches.get(0);
            final Batch firstRtcWakeup = findFirstRtcWakeupBatchLocked();
            if (firstWakeup != null && mNextWakeup != firstWakeup.start) {
                mNextWakeup = firstWakeup.start;
                mLastWakeupSet = SystemClock.elapsedRealtime();
                setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start);
            }

CANCEL

cancel方法都会调用到AlarmManagerService中的 remove

boolean remove(final PendingIntent operation, final IAlarmListener listener)

相比与set,cancel并没有直接调用到底层的所谓cancel的api,kernel也没有提供cancel的接口

假设设置了一个10s的wakeup alarm, 在超时之前cancel,实际上是从超时的执行列表中移除10s alarm对应的处理方法,那这个alarm是怎么处理的呢?

  • 首先会从batch中移除这个alarm

  • alarm thread 会调用到rescheduleKernelAlarmsLocked()把移除的这个alarm的下一个作为next,重新写到底层

4 JNI 层

frameworks/base/services/core/jni/com_android_server_AlarmManagerService.cpp

在JNI层,可以看到id 2和3对应的分别是 CLOCK_BOOTTIME_ALARM 和 CLOCK_BOOTTIME

static const clockid_t android_alarm_to_clockid[N_ANDROID_TIMERFDS] = {
    CLOCK_REALTIME_ALARM,
    CLOCK_REALTIME,
    CLOCK_BOOTTIME_ALARM,
    CLOCK_BOOTTIME,
    CLOCK_MONOTONIC,
    CLOCK_REALTIME,
};

调用timerfd的接口设置下去

int AlarmImpl::set(int type, struct timespec *ts)
{
    if (static_cast<size_t>(type) > ANDROID_ALARM_TYPE_COUNT) {
        errno = EINVAL;
        return -1;
    }

    if (!ts->tv_nsec && !ts->tv_sec) {
        ts->tv_nsec = 1;
    }
    /* timerfd interprets 0 = disarm, so replace with a practically
       equivalent deadline of 1 ns */

    struct itimerspec spec;
    memset(&spec, 0, sizeof(spec));
    memcpy(&spec.it_value, ts, sizeof(spec.it_value));

    return timerfd_settime(fds[type], TFD_TIMER_ABSTIME, &spec, NULL);
}

5 Linux Kernel 层

很核心的概念就是对于alarm timer 或者是hrtimer,用户空间只能设置设置一个下来,新的会把旧的覆盖掉, 比如你通过AlarmManager设置了一个15s以后的wakeup 类型的alrm事件,再设置一个10s以后的,那么timerfd会把先前设置的15s的cancel掉,再设置10s的

timerfd

~/fs/timerfd.c

timerfd_settime 这个系统调用内核的调用路径如下, 根据类型的不同,分为alarm和hrtimer

do_timerfd_settime()
    timerfd_setup()
        alarm_init()        ~/kernel/time/alarmtimer.c
        alarm_start()
        hrtimer_init()        ~/kernel/time/hrtimer.c
        hrtimer_start()
static int do_timerfd_settime(int ufd, int flags, 
		const struct itimerspec *new,
		struct itimerspec *old)
{
	struct fd f;
	struct timerfd_ctx *ctx;
	int ret;

	if ((flags & ~TFD_SETTIME_FLAGS) ||
	    !timespec_valid(&new->it_value) ||
	    !timespec_valid(&new->it_interval))
		return -EINVAL;

	ret = timerfd_fget(ufd, &f);
	if (ret)
		return ret;
	ctx = f.file->private_data;

	if (!capable(CAP_WAKE_ALARM) && isalarm(ctx)) {
		fdput(f);
		return -EPERM;
	}

	timerfd_setup_cancel(ctx, flags);

	/*
	 * We need to stop the existing timer before reprogramming
	 * it to the new values.
	 */
	for (;;) {
		spin_lock_irq(&ctx->wqh.lock);

		if (isalarm(ctx)) {
			if (alarm_try_to_cancel(&ctx->t.alarm) >= 0)
				break;
		} else {
			if (hrtimer_try_to_cancel(&ctx->t.tmr) >= 0)
				break;
		}
		spin_unlock_irq(&ctx->wqh.lock);
		cpu_relax();
	}

	/*
	 * If the timer is expired and it's periodic, we need to advance it
	 * because the caller may want to know the previous expiration time.
	 * We do not update "ticks" and "expired" since the timer will be
	 * re-programmed again in the following timerfd_setup() call.
	 */
	if (ctx->expired && ctx->tintv.tv64) {
		if (isalarm(ctx))
			alarm_forward_now(&ctx->t.alarm, ctx->tintv); //如果是Alarm timer类型,先cancel前一个alarm类型的
		else
			hrtimer_forward_now(&ctx->t.tmr, ctx->tintv);
	}

	old->it_value = ktime_to_timespec(timerfd_get_remaining(ctx));
	old->it_interval = ktime_to_timespec(ctx->tintv);

	/*
	 * Re-program the timer to the new value ...
	 */
	ret = timerfd_setup(ctx, flags, new); //开始设置新的timer

	spin_unlock_irq(&ctx->wqh.lock);
	fdput(f);
	return ret;
}

alarm timer

AlarmTimer 参考https://www.cnblogs.com/arnoldlu/p/7145879.html 这一篇就好

值得注意的是 alarmtimer_suspend方法,在进入睡眠之前,遍历alarm_bases->timerqueue,取最近一次timer的expires;将此expires写入RTC定时器,RTC超时后会唤醒系统。



static int alarmtimer_suspend(struct device *dev)
{
...
    rtc = alarmtimer_get_rtcdev();-------------------------------------获取RTC设备
    /* If we have no rtcdev, just return */
    if (!rtc)
        return 0;

    /* Find the soonest timer to expire*/
    for (i = 0; i < ALARM_NUMTYPE; i++) {------------------------------遍历ALARM_REALTIME和ALARM_BOOTTIME两个alarm_base,取各自最近expires
        struct alarm_base *base = &alarm_bases[i];
        struct timerqueue_node *next;
        ktime_t delta;

        spin_lock_irqsave(&base->lock, flags);
        next = timerqueue_getnext(&base->timerqueue);
        spin_unlock_irqrestore(&base->lock, flags);
        if (!next)
            continue;
        delta = ktime_sub(next->expires, base->gettime());
        if (!min.tv64 || (delta.tv64 < min.tv64))---------------------比较两次expires,取最小者
            min = delta;
    }
    if (min.tv64 == 0)
        return 0;

    if (ktime_to_ns(min) < 2 * NSEC_PER_SEC) {-----------------------如果expires小于2秒,保持系统唤醒2秒,并中断suspend流程。
        __pm_wakeup_event(ws, 2 * MSEC_PER_SEC);
        return -EBUSY;
    }

    /* Setup an rtc timer to fire that far in the future */
    rtc_timer_cancel(rtc, &rtctimer);-------------------------------取消当前rtctimer
    rtc_read_time(rtc, &tm);
    now = rtc_tm_to_ktime(tm);
    now = ktime_add(now, min);--------------------------------------获取RTC时间,将rtc_timer转换成ktimer_t,将RTC时间加上AlarmTimer超时。

    /* Set alarm, if in the past reject suspend briefly to handle */
    ret = rtc_timer_start(rtc, &rtctimer, now, ktime_set(0, 0));---设置rtctimer
    if (ret < 0)
        __pm_wakeup_event(ws, 1 * MSEC_PER_SEC);
    return ret;
}




参考文章

https://www.cnblogs.com/leipDao/p/8203684.html
https://www.cnblogs.com/arnoldlu/p/7145879.html
https://blog.csdn.net/wh_19910525/article/details/44303039

猜你喜欢

转载自blog.csdn.net/memory01/article/details/83244444