如何在系统挂起流程中玩出白屏问题

我们的这次实验的目标是:能在系统挂起唤醒中玩出一个白屏。为啥不搞个panic出来?panic出现和修好都太容易,不好玩。显示异常才好玩呢,没日志只有现象,hiahiahia~

好了知己知彼才能百战百胜,先看看系统挂起的简单流程。

系统挂起的流程

系统挂起流程主要分为两个部分:挂起和唤醒。挂起的入口在suspend_enter,基本流程下面这样的:
如何在系统挂起流程中玩出白屏问题_第1张图片

上图大概是大家能见到系统挂起流程图中最简单的一个了吧。然而在非arm、x86这种使用广泛的平台,这其中看似简单的每一步都可能暗藏杀机。不管是系统挂起还是系统休眠,信号一定是上层传进来的,假如在某个场景硬件无缘无故自己睡下去了,虽然看上去和内核很相关但锅真的不是内核的。收到上层传进来的系统睡眠信号之后,首先是冻结用户进程同步文件系统,为了避免数据不一致问题。之后的冻结设备、保存cpu现场、非boot cpu下电等等,每一步都有可能出错,非常刺激。可能某个显卡、网卡、输入设备睡眠做的有问题设备没法冻结起来,可能是cpu现场保存的位置溢出了或者数组越界了,可能是某个外设不能下电导致cpu不能断电,因为cpu在等设备;可能固件初始化完成后没法跳到内核里面来,cpu现场恢复的有问题,甚至可能进程恢复之后都访问零地址,内核把他们全都杀死了。每一个报错,都是一个加深理解内核的机会,多好玩。好了回到本文重点,想要做一个白屏出来,最直接的肯定是在驱动的唤醒流程里改动,因为cpu和平台相关的唤醒并没有显示参与,而且动唤醒架构代码容易起不来那就没办法演示啦。下面大概展开讲讲驱动睡眠或者唤醒流程,这两个流程是对称的,搞清楚一个另一个自然明白。

驱动挂起流程

5分钟搞清楚设备挂起大致流程

对内核熟悉的童鞋们肯定清楚,从核心代码执行到驱动代码的一般都要通过好几个钩子函数。正常分析,就是从入口一点点查找,然后慢慢往下分析。今天,我们就让驱动说话,告诉我们它是怎么被调用起来的。得到调用链之后,就像拿到了一篇文章的纲领,后面再填充内容就简单很多啦。首先选择一个熟悉的驱动,找到这个驱动的挂起函数。挂起函数在内核中的接口叫suspend,查找这个关键字就即可找到啦。

$ grep --color "suspend =" drivers/gpu/drm/loongson/ -rn
drivers/gpu/drm/loongson/loongson_drv.c:1013:	.suspend = loongson_pmops_suspend,
$ git diff drivers/gpu/drm/loongson/loongson_drv.c
diff --git a/drivers/gpu/drm/loongson/loongson_drv.c b/drivers/gpu/drm/loongson/loongson_drv.c
index 5629b2d7d4ff..82fa3aa5dd80 100644
--- a/drivers/gpu/drm/loongson/loongson_drv.c
+++ b/drivers/gpu/drm/loongson/loongson_drv.c
@@ -941,6 +941,7 @@ static int loongson_pmops_suspend(struct device *dev)
        struct pci_dev *pdev = to_pci_dev(dev);
        struct drm_device *drm_dev = pci_get_drvdata(pdev);
 
+ dump_stack();
        return loongson_drm_suspend(drm_dev);
 }

示例中,suspend钩子上挂的是loongson_pmops_suspend。在函数中加上栈打印,编译运行就能得到一个这样的函数栈。在更换原有的内核之前有一个小的tips,一定保证机器上有俩内核,因为每一个对内核的改动都可能会起不来。

[ 38.527113] CPU: 1 PID: 3177 Comm: kworker/u8:12 Tainted: G W 4.19.0-loongson-shiwen #1672
[ 38.527117] Hardware name: HT706 TR4191/B20-3a40, BIOS V4.0 12/14/2020
[ 38.527131] Workqueue: events_unbound async_run_entry_fn
[ 38.527134] Stack : 0000000000000000 0000000000000001 0000000000000000 0000000000000001
[ 38.527139] 0000000000000000 0000000000000000 0000000000000001 0000000000000040
[ 38.527142] 0000000000000000 0000000000000000 0000000000000001 746e657665203a65
[ 38.527146] ffffffff80209234 ffffffff80209224 ffffffff81460000 0000000000000000
[ 38.527149] 0000000000000000 ffffffff812d0000 0000000000000000 0000000000000002
[ 38.527153] 980000025c05e0f8 0000000000000000 ffffffff81264730 ffffffff812d0000
[ 38.527156] 000000000000000c 9800000254e97950 0000000000006000 980000025d72c000
[ 38.527160] 9800000254e94000 9800000254e97b70 980000025d9b0d80 ffffffff80e6ea24
[ 38.527163] 980000025c014600 980000025c01c000 0000000000000000 0000000000000002
[ 38.527167] 980000025c05e0f8 ffffffff80218f24 0000000000000001 ffffffff80e6ea24
[ 38.527170] ...
[ 38.527174] Call Trace:
[ 38.527183] [] show_stack+0x94/0x140
[ 38.527192] [] dump_stack+0x94/0xd0
[ 38.527209] [] loongson_pmops_suspend+0x1c/0x38 [loongson]
[ 38.527219] [] pci_pm_suspend+0x7c/0x188
[ 38.527227] [] dpm_run_callback.isra.5+0x20/0x70
[ 38.527231] [] __device_suspend+0x16c/0x3a0
[ 38.527235] [] async_suspend+0x2c/0xd8
[ 38.527238] [] async_run_entry_fn+0x50/0x128
[ 38.527243] [] process_one_work+0x23c/0x440
[ 38.527246] [] worker_thread+0x164/0x5d8
[ 38.527250] [] kthread+0x128/0x130
[ 38.527254] [] ret_from_kernel_thread+0x14/0x1c

首先一眼看去,__device_suspend在这个函数长得就很像驱动挂起的入口。加上打印,就可能看到其他驱动的挂起函数和他们被调起来的逻辑啦。另外栈底压得函数能看出来和kthread worker有关,说明驱动挂起内核使用工作队列这样的方式调起来的。工作队列在驱动这边用的很常见,主要分为队列初始化和队列调用两部分,工作队列的初始化一般是放在驱动的init或者probe函数,调用是在任何一个需要的位置。虽说在执行顺序是并发不可控的,但从调用的位置还是能找到点东西。工作队列的调度要求把队列所完成的功能函数当参数传递进去,从查找这个功能函数名方式接着往上查,直到能查到系统挂起的最开始入口。
__device_suspend函数主要做很多安全检查,然后是设备掉电前的准备,最终挨个扫描调用驱动提前注册好的suspend函数。

    if (dev->pm_domain) {
        info = "power domain ";
        callback = pm_op(&dev->pm_domain->ops, state);
        goto Run;
    }

    if (dev->type && dev->type->pm) {
        info = "type ";
        callback = pm_op(dev->type->pm, state);
        goto Run;
    }

    if (dev->class && dev->class->pm) {
        info = "class ";
        callback = pm_op(dev->class->pm, state);
        goto Run;
    }

    if (dev->bus) {
        if (dev->bus->pm) {
            info = "bus ";
            callback = pm_op(dev->bus->pm, state);
        } else if (dev->bus->suspend) {
            pm_dev_dbg(dev, state, "legacy bus ");
            error = legacy_suspend(dev, state, dev->bus->suspend,
                        "legacy bus ");
            goto End;
        }
    }

 Run:
    if (!callback && dev->driver && dev->driver->pm) {
        info = "driver ";
        callback = pm_op(dev->driver->pm, state);
    }

    error = dpm_run_callback(callback, dev, state, info);

Run就是挨个调用驱动中的suspend了,看着遍历的代码大概能看出来dev是按照树或者链表的形式管理的。OK,先挨个把suspend函数打出来看看遍历的先后顺序是个什么顺序。

diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c
index 4abd7c6531d9..f6151ab60b25 100644
--- a/drivers/base/power/main.c
+++ b/drivers/base/power/main.c
@@ -1793,6 +1793,7 @@ static int __device_suspend(struct device *dev, pm_message_t state, bool async)
                callback = pm_op(dev->driver->pm, state);
        }
 
+ printk("SHIWEN MESSAGe: in %s, callback=%#x\n", __func__, callback);
        error = dpm_run_callback(callback, dev, state, info);

实验机器上得到的地址在system.map里面对比即可得到函数名称。遍历的先后顺序分别是:

  • serial_suspend
  • input_dev_suspend
  • backlight_suspend
  • platform_pm_suspend
  • usb_dev_suspend
  • scsi_bus_suspend
  • led_suspend
  • hda_codec_pm_suspend
  • rtc_suspend
  • ata_port_pm_suspend
  • pci_pm_suspend
  • usb_dev_suspend

好了,花费不到10分钟拿到了驱动挂起的整个流程,还绝对正确,为自己鼓掌!上述函数的调用也都不是线性的,是穿插多次调用的,因为像scsi、platform、pci每扫一个bus,就需要对每个扫到的device调用一下对应的suspend。

添加白屏逻辑

接下来就是修改代码得到一个白屏啦。无任何理论基础前提,想到两个办法:一是提前点亮屏幕或者延迟显示数据的准备;二是自己写一个白屏进去。不管采用那种,看起来都需要在显卡驱动里面做点事,先看看显卡驱动的resume流程咯。lspci查看显卡类型,这次实验是在龙芯机上做的,显卡是龙芯集显。看看龙芯集显的resume流程,然后再看看是不是能加点东西。不停的grep看钩子函数挂的是哪个,或者直接在驱动里找带resume的函数。很快就找到了龙芯显卡resume入口——loongson_drm_resume

int loongson_drm_resume(struct drm_device *dev)
{
    u32 r;
    u64 gpu_addr;
    struct loongson_bo *lbo;
    struct drm_framebuffer *drm_fb;
    struct loongson_framebuffer *lfb;
    struct loongson_device *ldev = dev->dev_private;
    
    if (dev->switch_power_state == DRM_SWITCH_POWER_OFF)
        return 0;
    
    console_lock();
    
    mutex_lock(&dev->mode_config.fb_lock);
    drm_for_each_fb (drm_fb, dev) {
        lfb = to_loongson_framebuffer(drm_fb);
        lbo = gem_to_loongson_bo(lfb->obj);
        r = loongson_bo_reserve(lbo, false);
        if (unlikely(r))
            continue;
        
        loongson_bo_pin(lbo, TTM_PL_FLAG_VRAM, &gpu_addr);
        loongson_bo_unreserve(lbo);
    }
    mutex_unlock(&dev->mode_config.fb_lock);
    
    loongson_encoder_resume(ldev);
    drm_helper_resume_force_mode(dev);
    drm_kms_helper_poll_enable(dev); 
    loongson_fbdev_set_suspend(ldev, 0);
    loongson_connector_resume(ldev);
    console_unlock();
    
    return 0;
}

gem和bo是显示缓冲区相关的,根据传入的dev,先获取到显示的framebuffer、gem和bo这些显示缓冲区相关变量。之后唤醒encoder,encoder是显示器解码器,把显存中的像素点解码成显示器需要的信号。随之设置显示mode相关参数,使能输出轮询唤醒fbdev,最后是唤醒显示器,backlight设备的唤醒是放在唤醒显示器里。先试试看把显示器的的唤醒放在前面看看。

diff --git a/drivers/gpu/drm/loongson/loongson_drv.c b/drivers/gpu/drm/loongson/loongson_drv.c
index 5629b2d7d4ff..cb77c482a473 100644
--- a/drivers/gpu/drm/loongson/loongson_drv.c
+++ b/drivers/gpu/drm/loongson/loongson_drv.c
@@ -905,6 +905,7 @@ int loongson_drm_resume(struct drm_device *device        console_lock();
 
+ loongson_connector_resume(ldev);
        mutex_lock(&dev->mode_config.fb_lock);
        drm_for_each_fb (drm_fb, dev) {

实验结果证明,没有任何变化。不应该啊,难道是没有调用?直接把connector的resume注释掉试试看?
嗯,没有任何变化。没道理啊,难道是没有调用?或者真正的点亮并不是内核调用的?找到驱动设置bl的位置打印试试看。

[ 3286.496606] CPU: 2 PID: 2246 Comm: backlight_helpe Tainted: G W 4.19.0-loongson-3-desktop-shiwen #1663
[ 3286.496611] Hardware name: HT706 TR4191/B20-3a40, BIOS V4.0 12/14/2020
[ 3286.496615] Stack : 0000000000000000 0000000000000001 0000000000000000 0000000000000001
[ 3286.496623] 0000000000000000 0000000000000000 0000000000000001 0000000000000000
[ 3286.496628] 0000000000000448 3230322f34312f32 0000000000000030 ffffffff81260000
[ 3286.496633] 0000000000000001 ffffffff8144ba15 ffffffffffffffff 3134525420363037
[ 3286.496637] ffff000000000000 ffffffff812d0000 0000000000000000 98000002557fd018
[ 3286.496642] 98000002572ebe60 00000001202fedf8 000000c00003e8c8 00000001204e0000
[ 3286.496647] 0000000000000001 0000000000000000 0000000000006000 9800000255e28000
[ 3286.496652] 98000002572e8000 98000002572ebb70 000000c000001680 ffffffff80e6ea24
[ 3286.496657] 0000000000000000 0000000000000000 0000000000000000 98000002557fd018
[ 3286.496661] 98000002572ebe60 ffffffff80218f24 0000000000000001 ffffffff80e6ea24
[ 3286.496666] ...
[ 3286.496671] Call Trace:
[ 3286.496685] [] show_stack+0x94/0x140
[ 3286.496697] [] dump_stack+0x94/0xd0
[ 3286.496718] [] loongson_connector_pwm_set+0x54/0xf8 [loongson]
[ 3286.496728] [] loongson_connector_backlight_update+0x44/0xa8 [loongson]
[ 3286.496741] [] backlight_device_set_brightness+0x74/0xc8
[ 3286.496746] [] brightness_store+0x40/0x58
[ 3286.496755] [] kernfs_fop_write+0xd0/0x1f0
[ 3286.496762] [] __vfs_write+0x28/0x170
[ 3286.496767] [] vfs_write+0xb4/0x1e8
[ 3286.496771] [] ksys_write+0x60/0x100
[ 3286.496778] [] syscall_common+0x34/0x58

从syscall调下来的,那点亮屏幕肯定是上层干的,单单改内核像做到延迟点亮屏幕看来是走不通了。那就试试看延迟显卡的resume,不让console挂起,倒是延迟了显卡数据的准备,显示的界面是挂起之前的console而且一闪而过,并不是想象中白屏之类的明显异常界面,是不是可以在其他驱动里面加延时呢?
根据dmesg能明显的看出来在drm设备之前被唤醒的是usb、wifi和sata等,wifi和sata最好不要动,数据量太大了,可能还没搞出来白屏系统就崩溃了。那最好欺负的就是你了——usb驱动,试试看在usb唤醒函数里加延迟试试看。
嗯,现在到时能看到一个明显的显示残留了,但是看起来还是不够严重。通过观察发现,显示残留看到的是系统挂起前最后显示的内容。也就是说,显存里的东西是suspend保存进去的。如果想要看上去更严重一点,我们需要把resume之后把显存清掉,或者是显卡suspend时候不要保存显存里的东西。最简单暴力的方式是,显卡不要suspend了,对于设备来说是直接断电没有任何保存的过程。
好了,这下看上去严重多了,这勉勉强强算是做出了一个概率白屏了。

后面还可以做什么

目前白屏的实现是把显卡suspend去掉,迫使显卡resume之后恢复出来的显存是未知数据,从而导致的显示异常。至于什么样子的异常,这点不可控制。可以尝试通过修改fb控制异常显示的内容,做一个闪烁显示出来?或者修改色深,做一个炫彩异常?尝试控制光标等等,都是很有趣。
最后献上一首,挂起调试之歌,请笑纳。
休眠调试一定要仔细,

内核日志太少不要急,

拿到日志后慢慢看。

每一行的报错不要遗。

通用驱动先看设备的问题,

然后再和上游代码比一比。

上游能用回头来看自己。

碰到内存错误不要慌,

数据溢出越界先查起。

成百上千测试跑不过,

稳定测试日志要整齐。

如果显示异常调试方法,

请一定要介绍给我。

谢谢大家,欢迎补充。

你可能感兴趣的:(linux内核的一些事,内核玩起来)