欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

高通8937平台手机关机概率卡住问题分析

程序员文章站 2022-07-14 15:45:33
...

异常现象:

        高通8937平台手机执行开关机压力测试出现概率性无法关机,平均复现概率为1/400次, 问题出现时手机黑屏,看上去像死机,但充电灯在亮着.

分析:

        拿到机器发现adb还可以使用,那既然adb还可以用,说明系统并没有发生kernel panic,只是由于某种原因导致关机Thread发生了stuck,跟死锁的现象有点像,问题是概率发生并且概率比较低,能想到比较合适的分析方法就是在关机流程中加trace记录下整个流程,看到底卡在了哪里,缩小代码分析范围。 这里有一个问题:为什么卡住了,wdt不会去复位呢?开始我也没明白,后面才发现,是卡在device_unlock函数被调度走了,不是在spinlock内,喂狗线程还在正常运行,所以也就不会wdt time out 了.

Debug 难点:

1、Android层关机流程走完后会把filesystem设为read only,所以内置log工具只能抓到Android 层的log,对于kernel层log需要使用uart 飞线抓取,但是问题复现是概率很低的,通常需要跑一个晚上才可能复现,所以也就是说必须要一直连着uart 工具,直到问题复现才有可能抓到复现时候的klog.

2、虽然用uart抓很麻烦,但是终究是一种方法,遗憾的是,加了log后问题出现概率大大降低,从1/400降低到< 1/1500,再次提升了复现问题的难度,而且打开uart 输出后测试8*2000次无复现!如果无法复现的话,基本上是无解的,目前考虑的这种方法抓log失败了,只能想另外的方法途径。

3、加log会增加delay,我们认为此问题跟delay相关性比较大,另外出问题时候adb还是可以用的,w同学建议可以尝试在kernel关机流程函数加计数器(下面代码红色部分)追踪,浮现问题后,用adb把计数器值拉出来看(丰富debug手段多么重要!),因为计数器只是赋值语句,相比log的时间开销来说很小的,这样可以大大增加复现概率!经过一晚上的压力测试,发现果然有效果,定位到thread卡在kernel关机流程的 device_shutdown() 函数.

void device_shutdown(void)
{
	struct device *dev, *parent;

	spin_lock(&devices_kset->list_lock);
	/*
	 * Walk the devices list backward, shutting down each in turn.
	 * Beware that device unplug events may also start pulling
	 * devices offline, even as the system is shutting down.
	 */
	while (!list_empty(&devices_kset->list)) {
		dev = list_entry(devices_kset->list.prev, struct device,
				kobj.entry);
				
                dev_p = dev->kobj.name;

		/*
		 * hold reference count of device's parent to
		 * prevent it from being freed because parent's
		 * lock is to be held
		 */
		parent = get_device(dev->parent);
		get_device(dev);
		/*
		 * Make sure the device is off the kset list, in the
		 * event that dev->*->shutdown() doesn't remove it.
		 */
		list_del_init(&dev->kobj.entry);
		spin_unlock(&devices_kset->list_lock);

                flag1++;

		/* hold lock to avoid race with probe/release */
		if (parent)
			device_lock(parent);
		device_lock(dev);

                flag2++;
        
		/* Don't allow any more runtime suspends */
		pm_runtime_get_noresume(dev);
		pm_runtime_barrier(dev);
		
                flag3++;
        
		if (dev->bus && dev->bus->shutdown) {
			if (initcall_debug)
				dev_info(dev, "shutdown\n");
			dev->bus->shutdown(dev);
		} else if (dev->driver && dev->driver->shutdown) {
			if (initcall_debug)
				dev_info(dev, "shutdown\n");
			dev->driver->shutdown(dev);
		}

		device_unlock(dev);
		if (parent)
			device_unlock(parent);

		put_device(dev);
		put_device(parent);

                flag4++;

		spin_lock(&devices_kset->list_lock);
	}
	spin_unlock(&devices_kset->list_lock);
}

device_shutdown 函数主要干的事情就是遍历系统设备链表中各个dev,**pm runtime,依次删除掉,然后执行各个dev对关机动作感兴趣的shutdown回调函数.

第一晚的测试只是定位到了具体位置,后面还需要进一步定位具体卡在那一句代码上,经过w同学的反复努力,终于抓到卡住的如下具体代码位置,就是flag1 和 flag2之间,而此时dev_p是bam_dumx_ch_0.2,好像跟rmnet有关,这是属于qcom私有的东西,具体不知干什么用的...

if (parent)
    device_lock(parent);
device_lock(dev);

卡住的地方很诡异,看上去像死锁... 前面费了很大力气抓到了卡住的具体代码,目的是想知道事故现场的具体情况,以及给高通提供最精确的信息,但是目前看到这段貌似死锁的代码也无法进一步分析为什么会这样?按道理如果是单线程就不应该会出现卡在lock上, java层关机会发一个shutdown广播,难道还有别的内核线程也在执行dev的操作导致死锁?猜想根本原因是还有其他的路径再执行设备卸载操作,由于某种原因导致卸载失败又没有释放lock,所以当device shutdown的时候再次去获取lock就导致死锁了...已经抓ramdump给高通(若是能自己分析ramdump就多一个debug手段了),希望他们可以离线分析出root cause,遗憾的是,到目前为止,现在还没有有价值的分析结果...

到这里为止,以现有的知识积累跟调试手段已经无法再进一步分析根本原因,但问题还得解决,既然直的来不了,就只能想办法来弯的,想办法绕过去,有两种思路:

1、既然加log难以浮现,可以尝试在loop中加mdelay的方式,看是否可以解决问题,此法改动风险最小,但是被动而且运气成分高,不够可靠.

2、新开一个内核线程用于监视device_shutdown函数是否卡住,如果这个函数执行时间超过 5s(正常不到1s跑完),那么就认为已经被stuck,就执行强制restart,有点类似于给这个函数加了一个wdt...此法主动出击,应该比上一种方法可靠,但是改动相对大风险高,而且简单粗暴,在目前这种山穷水尽也没有其它更好的办法的情况下, 只能背水一战赌一把了.

经过大量的压力测试发现,上面的NO.1方法最终没有扛住10*5000次的压力测试,加delay降低了复现概率,但是大量长时间的压力测试时候还是会出现。NO.2方法还在验证中...

下面简单介绍下NO.2的实现方式:

简单流程图:

高通8937平台手机关机概率卡住问题分析

1、新建一个内核线程,当作device_shutdown的监视器:

static int sd_counter = 0;
static struct task_struct *sd_thd = NULL;

static void start_monitor_kthread(void)
{
    sd_counter = 0;
    sd_thd = kthread_run(ds_monitor_thd, NULL, "ds_m_thd");
    if (IS_ERR(sd_thd)) {
        pr_emerg("xxx# %s: -%d create fail!\n", __func__, __LINE__);
        sd_thd = NULL;
    }
}

线程回调入口,起一个while(1)循环,如果5s后sd_counter计数器没清零就执行强制reboot.

static int ds_monitor_thd(void *args) 
{    	 
    do {
         mdelay(5000);
         if(sd_counter > 0) {
            pr_emerg("xxx# %s: Oops! force shutdown or reboot ! sd_counter:%d\n", __func__, sd_counter);
            force_reboot();
            break;
        }
    } while(1);
	
    return 0;
}

2、强制重启force_reboot的实现,其实只要执行machine_xxx就行了,这样的做法只是为了尽量按原有代码路径执行.

void force_reboot(void)
{
    char *cmd = reboot_cmd_buf;
    
    //原有关机流程中的动作,这里保留,以达到影响最小.
    migrate_to_reboot_cpu();
    syscore_shutdown();
    
    pr_emerg("xxx# %s: -%d-machine goto restart or shutdown!:%s\n", __func__, __LINE__, cmd);
    
    //判断是reboot还是shutdonw,执行不同的操作.
    if(reboot_cmd == LINUX_REBOOT_CMD_RESTART2 
    || reboot_cmd == LINUX_REBOOT_CMD_RESTART) {
        machine_restart(cmd);
    }
    else {
        machine_power_off();
    }
}

3、在device_shutdown()中启动该线程,同时加上计数器:

void device_shutdown(void)
{
	struct device *dev, *parent;

	start_monitor_kthread(); //启动监视线程
...
	while (!list_empty(&devices_kset->list)) {
...
		sd_counter++; //计数器++
...
	}
	spin_unlock(&devices_kset->list_lock);

	sd_counter = 0; //说明该函数跑完了,计数器清零.
}

从上面代码可以得出,如果device_shutdown函数在5s内没有执行完成导致sd_counter计数器没有被清零,则ds_m_thd 线程会进入force_reboot,强制系统重启或者关机,以达到不卡住的问题出现,因为正常情况下device_shutdown会在1s内执行完成,所以正常情况下新加的监视线程也就没有影响了,有点类似是给这个函数单独加了一个watch dog,规定时间没喂狗就被狗咬..