内核的休眠流程

内核休眠流程

当安卓合适的时机(wakeup_count = save_count && inpr == 0),与内核一致,在没有wakeup事件的时候,走进一步的流程,发echo mem > /sys/power/state命令后,内核开始处理休眠流程

./kernel/power/main.c 
state_store
    ->pm_suspend
        ->enter_state

休眠流程:

按照内核划分的休眠测试环节分为,FREEZER(进程冻结),DEVICES(设备休眠),PLATFORM(关闭不必要的中断),CPUS(关闭noboot cpu),core(关闭cpu中断,通知cpus进入休眠处理流程)几个阶段

kernel/power/suspend.c

enter_state
    suspend_prepare(suspend_state_t state)
    error = suspend_devices_and_enter(state);

test_freezer(冻结流程)

suspend_prepare(suspend_state_t state)    
        pm_prepare_console //如果需要的话重定向内核的kmsg  ??为什么要做切换
        error = pm_notifier_call_chain(pm_suspend_prepare); //用途一:有些设备需要在freeze进程之前就suspend;用途二:如果有些设备的reseym动作需要较多的延时么么resume的时候会在进程恢复之前,会阻止所有进程的恢复?更有甚者需要设备等待某个进程的数据才能resume; 
        //例子如下:drivers/video/fbdev/omap2/omapfb/dss/core.c
        error = suspend_freeze_processes(); //进程冻结,将用户进程和内核线程置于“可控”的暂停状态

在此对进程冻结进行展开描述

为什么需要进程冻结

  1. 会破坏文件系统,在系统创建hibernate image到cpu down之间,如果有进程还在修改文件系统的内容,这将会导致系统恢复之后无法完全恢复文件系统;
  2. 有可能导致创建hibernation image失败,创建hibernation image需要足够的内存空间,这期间如果还有进程在申请内存,就可能导致创建失败
  3. 干扰设备的resume和suspend,在cpu down之前,device suspend期间,如果进程还在访问设备,尤其是访问竞争资源,就有可能引起设备suspend异常
  4. 有可能导致进程感知休眠。系统休眠的理想状态下是所有任务对休眠过程无感知,睡眠之后会自动恢复工作,有些进程,需要所有cpu online才能正常工作,如果进程不冻结,那么休眠过程中就会异常;

冻结进程的主要流程

suspend_freeze_processes
——>    freeze_processes()   -> pm_freezing = true   
                         ->try_to_freeze_tasks ->  freeze_task ->  fake_signal_wake_up(p); 冻结用户进程



->freeze_kernel_threads()  ->pm_nosig_freezing = true 
                           ->try_to_freeze_tasks -> freeze_task ->    freeze_workqueues_begin();   冻结workqueue;
                                                                    wake_up_state(p, task_interruptible); 冻结内核线程

冻结的对象:可以被调度执行的实体,包括用户进程,内核线程和workqueue.

1.用户进程冻结流程

用户进程默认是可以被冻结的,借助信号处理机制,设置任务的tif_sigpending位,但不传递系统,然后唤醒任务;这样任务在返回用户态时就会进入信号处理流程,检查系统freeze状态,并做相应的处理;

fake_signal_wake_up -> signal_wake_up(p, 0) -> signal_wake_up_state(t, resume ? task_wakekill : 0); 
                                                    -> set_tsk_thread_flag(t, tif_sigpending);

信号的处理时机

理解信号异步机制的关键是信号的响应时机,我们对一个进程发送一个信号后,其实并没有硬中断发生,只是简单把信号挂载到目标进程的信号pending队列上去,信号真正得到执行的时机是进程执行完异常/中断返回到用户态的时刻;

让信号看起来是一个异步中断的关键是,正常的用户进程是会频繁的在用户态和内核态之间切换的(这种切换包括:系统调用,缺页异常,系统中断..),所以信号能很快得到执行。这样也带来一个问题,内核进程是不响应信号的,除非它刻意的去查询。所以通常情况下我们无法通过kill命令去杀死一个内核进程;

信号响应时机:

信号响应时机

arch/arm64/kernel/signal.c    
do_notify_resume -> do_signal(regs) -> get_signal
                                            task_work_run
                                            try_to_freeze

                                    -> handle_signal

2.内核线程冻结

内核线程和workqueue默认是不能被冻结的,少数内核线程和workqueue在创建的时指定了freezable标志。这些任务需要对freeze状态进行判断,当系统进入freezing时,可以通过调用freezing来判断freezing状态,并主动调用,try_to_freeze进入冻结;

代码范例如下:
static int autohotplug_thread_task(void *data)
{
    set_freezable();
    while (1) {
            if (freezing(current)) {
                    if (try_to_freeze())
                            continue;
            }    

            if (hotplug_enable)
                    autohotplug_governor_judge();

            set_current_state(task_interruptible);
            schedule();
            if (kthread_should_stop())
                    break;
            set_current_state(task_running);
    }    

    return 0;
}

3.workqueue冻结

work_queue通过max_active属性,如果max_active=0则不能入队新的work,所有的work延后执行;

freeze_workqueues_begin ->     workqueue_freezing = true; 
                            pwq_adjust_max_active(pwq); 
                        if (!freezable || !workqueue_freezing){
                                pwq->max_active = wq->saved_max_active;
                        } else {
                             pwq->max_active = 0;
                        }

schedule_work -> queue_work -> queue_work_on ->
 __queue_work 
    ...
    如果还没有达到max_active,将work挂载到worklist
          if (likely(pwq->nr_active < pwq->max_active)) {
                trace_workqueue_activate_work(work);
                pwq->nr_active++;
                worklist = &pwq->pool->worklist;
                if (list_empty(worklist))
                        pwq->pool->watchdog_ts = jiffies;
       否则将work挂载到临时队列 pwq->delayed_works
            } else {
                work_flags |= work_struct_delayed;
                worklist = &pwq->delayed_works;
            }                       
  ...

需要注意事项:

  1. workqueue的冻结只针对带有WQ_FREEZABLE;所以如果是schedule_work这种默认使用的是system_wq类型的workqueue冻结进程的时候并不会进行冻结;
  2. 对于schedule_delay_work的动作,如果休眠的时候不需要执行,则需要进行cancel_delayed_work_sync的动作,防止阻止系统休眠;

冻结进程何时被打断

进程怎么阻止休眠:无论通过wake_lock接口,__pm_stay_awake等,最终调用的都是wakeup_source_activate,都是增加inpr表示有事件在处理不准休眠;

wakeup_source_activate
    /* increment the counter of events in progress. */
    cec = atomic_inc_return(&combined_event_count);   也即解下来要讲的变量inpr

在系统休眠过程中:上层会判断cnt == saved_count, inpr == 0;底层也是如此
 bool pm_wakeup_pending(void)                                                                                     
 {                                                                                                                
     unsigned long flags;                                                                                     
     bool ret = false;                                                                                        

     spin_lock_irqsave(&events_lock, flags);                                                                  
     printk("++%s %d++\n",__func__, events_check_enabled);                                                    
     if (events_check_enabled) {                                                                              
             unsigned int cnt, inpr;                                                                          

             split_counters(&cnt, &inpr);    
             用户空间还有内核都是判断这两个变量;如果写入的值和cnt不同,说明读写的过程中出现产生了events;
             inpr表示用唤醒事件在处理,这两种都会阻止系统休眠                                                                
             ret = (cnt != saved_count || inpr > 0);                                                          
             events_check_enabled = !ret;                                                                     
             printk("+++%s cnt:%d saved_count:%d inpr:%d ret:%d++\n",__func__, cnt, saved_count, inpr, ret);  
     }                                                                                                        
     spin_unlock_irqrestore(&events_lock, flags);                                                             

     if (ret) {                                                                                               
             pr_info("pm: wakeup pending, aborting suspend\n");                                               
             pm_print_active_wakeup_sources();                                                                
     }                                                                                                        

     return ret || pm_abort_suspend;                                                                          
 }

device_suspend(设备休眠)

suspend_devices_and_enter
--> platform_suspend_begin
    suspend_console();
    dpm_suspend_start(pmsg_suspend);
        -->dpm_prepare(state);
        -->dpm_suspend(state);
            device_suspend(dev);    
                -->async_schedule(async_suspend, dev); 
                //在设备中如果通过device_enable_async_suspend设置过,async_schedule使设备走异步的休眠唤醒流程;
                -->__device_suspend(dev, pm_transition, false);
                        dpm_watchdog_set//设置超时watchdog,若休眠时间过长,则打印打钱的设置和函数栈默认超时时间为120s

                -->    dpm_run_callback(callback, dev, state, info);    
                        由于设备模型有bus、driver、device等多个层级,而suspend接口可能由任意一个层级实现。这里的优先顺序
                        是指,只要优先级高的层级注册了suspend,就会优先使用它,而不会使用优先级低的prepare。优先顺序为:dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm(这个优先顺序同样适用于其它callbacks)。 
                echo 1 > /sys/power/pm_print_times //在每个设备发起休眠唤醒流程的时候就能打印出来
                --> async_synchronize_full (等待所有异步动作都做完) 

platform(关闭不需要唤醒的中断)

suspend_enter(state, &wakeup);
    -->platform_suspend_prepare(state);        
       dpm_suspend_late(pmsg_suspend);
       platform_suspend_prepare_late(state);
       dpm_suspend_noirq(pmsg_suspend)
               -->cpuidle_pause();
               device_wakeup_arm_wake_irqs();
                    如果设备具有唤醒功能(dev->power.can_wakeup,device_init_wake 的时候就会使能)那么设置唤醒标志(irqd_wakeup_state)
                    -->enable_irq_wake->irq_set_irq_wake->set_irq_wake_real->irq_set_wake->sunxi_irq_set_wake 
                    -->arisc_set_wakeup_source传递到cpus
                  suspend_device_irqs();
                    -->没有唤醒标志的 enable_irq_wake,  request_irq(irq, xxx_isr, flag | IRQF_NO_SUSPEND`, xxx, xxx)的时候,此时中断会被清除,此时切断的是irq device和irq controller之间的联系
                        这两者的区别在于enable_irq_wake还会走到synchronize_irq(irq),等待中断处理完毕

              device_suspend_noirq(dev) //处理关闭中断后的事情    
       platform_suspend_prepare_noirq(state)            

cpus(关闭no bootcpu)

disable_nonboot_cpus() //关闭no boot cpu
    freeze_secondary_cpus(0)
            _cpu_down(cpu, 1, cpuhp_offline);

core(关闭cpu中断,通知arisc走休眠处理流程)

arch_suspend_disable_irqs(); //此时关闭的是irq controller和cpu的联系,在这个时候中断便不能唤醒cpu了;
     local_irq_disable();
syscore_suspend();//主要调用sched_clock_suspend,timekeeping_suspend,irq_gc_suspend,fw_suspend,cpu_pm_suspend
          //echo Y > /sys/module/kernel/parameters/initcall_debug 通过这个命令可以看到相关的调用
    suspend_ops->enter(state);    
        sunxi_suspend_enter//向小cpus传递电源相关参数
            arm_cpuidle_suspend
                 cpuidle_ops[cpu].suspend(index)
                    psci_cpu_suspend_enter
                        cpu_suspend(3, psci_suspend_finisher)
                    svc:psci_fn_cpu_suspend

arisc(小cpu的休眠处理流程)

extended_super_standby_entry(pmessage)
    esstandby_process_init(request, para, config) 
        dram进入自刷新;
        将系统相关的clk切换到低频状态
        将系统相关的电源关闭
    get_wakeup_src(para)
        轮询检测中断源

休眠唤醒流程

suspend_resume_trace

思考题:

1:进行如下操作后

echo 1 > /sys/power/wake_lock

echo mem > /sys/power/state

此时系统能否休眠??

能,events_check_enabled此时的值为false,故系统会直接进入休眠状态;只有上层程序在调用save_wakeup_count的时候,会去使能events_check_enabled,从这个层面讲,休眠的时候需不需要被唤醒事件打断,其实是看用户自身是否需要这种同步机制;

有正在处理的事件inpr,为什么还要cnt作为判断进入休眠唤醒的标志呢?

一般而言inpr已经足够表示系统有事件在进行,不要进入休眠;引入这个条件笔者认为只是为了能更新该值,保证wakeup_event都能被统计到;
当cnt != save_cnt 
打印如下 last active wakeup source: axp22_wakeup_source;表明是上次是被谁影响到休眠

2: 为什么即有request_irq(irq, xxx_isr, flag | IRQF_NO_SUSPEND, xxx, xxx)又有,enanle_irq_wake?

http://www.wowotech.net/forum/viewtopic.php?id=20
这里有相关的讨论,大意是
这两个方法,做的是同一件事情,都是控制“request line和irq controller之间的联系”。
IRQF_NO_SUSPEND是旧方法;
enable_irq_wake是新方法。
之有的driver都调用这两个接口,有两种可能:一是为兼容;而是不太理解,为保险起见,都调用。
从代码上看request的时候带的参数IRQF_OF_SUSPEND只能在内核休眠的时候起到阻止休眠的功能,而enable_irq_wake则可以在深度休眠后仍然能唤醒;
当休眠到core阶段的时候cpu的irq和fiq都已关闭,但仍保存着wakeup信号线,通过wakeup信号线唤醒从而唤醒cpu,当cpu中断使能开启后再响应中断
不建议使用IRQF_NO_SUSPEND的方式,原因有三个,一:对于电平触发的会在唤醒的是时候由于中断处理线程得不到调度导致pending存在,阻塞系统唤醒;
二:enable_irq_wake能通过disable_irq_wake的方式动态处理开关是否需要唤醒系统,
或者在probe的时候通过device_init_wakeup的方式添加设备唤醒系统的属性,这样无需调用enable_irq_wake就能使能设备休眠唤醒系统属性 
三:enable_irq_wake的设备,中断底半部处理函数即使调用sleep函数,系统仍会等到中断处理完成才睡眠不会漏处理

3:怎么对休眠流程进行分解debug

内核在休眠的流程中加入了trace point可以根据这个去定位是哪个流程去定位哪个流程异常:
    echo 1 > /sys/kernel/debug/tracing/events/power/suspend_resume/enable
    echo > /sys/kernel/debug/tracing/trace //清空trace
    echo 1 > /sys/kernel/debug/tracing/tracing_on
    按power键进入休眠的流程
    按power键唤醒
    cat  /sys/kernel/debug/tracing/trace
    查看在哪个流程就退出

如当怀疑休眠流程在device_suspend中挂死的;
可以echo devices > /sys/power/pm_test
然后修改dpm_suspend设备的休眠流程,让其逐一进入休眠后退出,观察是哪个设备休眠唤醒后系统异常;
其他处于内核阶段的类似;

echo N > /sys/module/printk/parameters/console_suspend //休眠的时候保持终端打开    
如果没有异常,那么可以怀疑是否由于cpus中dram进入自刷新后异常导致;或者系统电上下电导致;或者时钟频率不稳导致;    
在cpus中挂死,即走到arisc的休眠流程中,表现为串口log处于深度休眠,且无任何异常打印;
在重启的log中:
可以观察rtc寄存器的值:正常的值如下:
[371]rtc[0] value = 0x00000000
[374]rtc[1] value = 0x00000000
[377]rtc[2] value = 0x00000000
[380]rtc[3] value = 0x0000a101
[383]rtc[4] value = 0x00000000
[386]rtc[5] value = 0x00000000
此时观察rtc[3],查看相关代码定位挂死的地方
如怀疑dram导致的异常,可进行DRAM_CRC的校验,如下校验1G的空间
usage:
        echo  1 0x40000000 0x40000000 > sys/devices/platform/soc/soc@03000000:arisc/dram_crc_paras

        1:           enable
        0x40024080:  dram crc start address
        0xf0000:     dram crc length(byte)
venus-a1:/ # cat ./sys/devices/platform/soc/soc@03000000:arisc/dram_crc_result
dram info:
 enable 0
  error 0
 total count 0
error count 0
src:40000000
len:0x100000

4.查看内核锁

cat /sys/kernel/debug/wakeup_sources
如果debug下没有节点,需先进行挂载:mount -t debugfs none /mnt ,然后在mnt目录下查看
早期的linux版本,3.4
cat /proc/wakelocks    

5.查看唤醒源

cat /sys/power/pm_wakeup_irq 这个会保存深度休眠的时候唤醒系统的中断号
配合 cat /proc/interrupts可获取当前唤醒系统的唤醒源是哪个
原理如下
在supend_device_irq的流程中能够wakeup系统的都会被置上IRQD_WAKEUP_ARMED的标志,然后就关闭中断;如果在中断处理流程里面的
会通过synchronize_irq等待中断线程处理完;
resume_irq的时候后重新enable_irq进入中断处理流程,通过pm_system_irq_wakeup标记第一次的唤醒源;

6.具有唤醒系统能力设备代码demo

static int gpio_keys_probe(struct platform_device *pdev)
{
    ......
    button->wakeup = of_property_read_bool(pp, "wakeup-source"); //获取dts中配置的设备是否需要从深度休眠中唤醒

    button->gpio = of_get_gpio_flags(pp, 0, (enum of_gpio_flags*)&flags); 

    bdata->gpiod = gpio_to_desc(button->gpio);                          

    irq = gpiod_to_irq(bdata->gpiod); 

    request_irq(irq, gpio_key_irq_handle, IRQF_TRIGGER_RISING, "gpio-key", bdata); //如果想缺省bdata可以使用devm_request_irq,api,无需IRQF_NO_SUSPEND
    ......
    device_init_wakeup(&pdev->dev, wakeup);//为设备注册wakeup_source,申明设备为设备添加可唤醒标志

    if (device_may_wakeup(dev)) {
    for (i = 0; i < pdata->nbuttons; i++) {
    struct gpio_button_data *bdata = &ddata->data[i];
    error = dev_pm_set_wake_irq(dev, bdata->irq);//把中断号配置到wakeup_source中,这样在休眠的时候系统会帮你将该中断设置为深度休眠可唤醒源
    }
    }
        ......
}

7.项目经验总结

1. 硬件需确保电源信号稳定性;
2. 在项目开发过程中,休眠唤醒稳定性测试,在每次迭代的时候尽量做到一周进行一次多台机器休眠唤醒老化测试,测试老化的时候,应加大负载,如同时播放视频;
   以便增加问题的复现概率

参考资料

linux进程冻结技术

Linux电源管理(6)_Generic PM之Suspend功能

Concurrency Managed Workqueue之(一):workqueue的基本概念

Linux 的并发可管理工作队列机制探讨

linux workqueue

中断唤醒系统流程