首先注明,标题描述的修改效果并不具备普适性,仅仅是针对我刚买的新本子的特殊处理,但文中涉及的一些思路和技巧应该还是对处理解决类似的问题有一定的帮助。

对比视频

问题描述

趁着 618 活动新买了台红米 air 笔记本,cpu 是 i7-10510Y,希望可以日常使用 Linux,尝试了多个发行版基本都能正常使用,唯一的问题就是启动时,当在 GRUB 选择系统页面选择系统后,画面会停留在 GURB 主题背景页面长达 30 秒,然后才能继续开机流程。

尝试了最新的 Ubuntu 系统,也是类似的问题,唯一的区别是 Ubuntu 下是选择系统后电脑黑屏 30 秒然后出现加载信息。

查看 dmesg 信息可以看到如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[    0.244480] Simple Boot Flag at 0x44 set to 0x1
[ 0.244480] ACPI: bus type PCI registered
[ 0.244480] acpiphp: ACPI Hot Plug PCI Controller Driver version: 0.5
[ 0.244480] PCI: MMCONFIG for domain 0000 [bus 00-ff] at [mem 0xe0000000-0xefffffff] (base 0xe0000000)
[ 0.244480] PCI: MMCONFIG at [mem 0xe0000000-0xefffffff] reserved in E820
[ 0.244480] PCI: Using configuration type 1 for base access
[ 0.244621] ENERGY_PERF_BIAS: Set to 'normal', was 'performance'
[ 0.248307] Kprobes globally optimized
[ 0.248314] HugeTLB registered 1.00 GiB page size, pre-allocated 0 pages
[ 0.248314] HugeTLB registered 2.00 MiB page size, pre-allocated 0 pages
[ 0.248314] ACPI: Added _OSI(Module Device)
[ 0.248314] ACPI: Added _OSI(Processor Device)
[ 0.248314] ACPI: Added _OSI(3.0 _SCP Extensions)
[ 0.248314] ACPI: Added _OSI(Processor Aggregator Device)
[ 0.248314] ACPI: Added _OSI(Linux-Dell-Video)
[ 0.248314] ACPI: Added _OSI(Linux-Lenovo-NV-HDMI-Audio)
[ 0.248314] ACPI: Added _OSI(Linux-HPI-Hybrid-Graphics)
[ 0.358456] ACPI: 17 ACPI AML tables successfully acquired and loaded
[ 0.362525] ACPI: EC: EC started
[ 0.362527] ACPI: EC: interrupt blocked

[ 30.376923] No Local Variables are initialized for Method [ECMD]

[ 30.376927] Initialized Arguments for Method [ECMD]: (1 arguments defined for method invocation)
[ 30.376928] Arg0: 0000000036f30172 Integer 000000000000001A

[ 30.376940] ACPI Error: Aborting method \_SB.PCI0.LPCB.H_EC.ECMD due to previous error (AE_AML_LOOP_TIMEOUT) (20200925/psparse-531)
[ 30.376960] fbcon: Taking over console
[ 30.376972] ACPI Error: Aborting method \_TZ.FNCL due to previous error (AE_AML_LOOP_TIMEOUT) (20200925/psparse-531)
[ 30.376986] ACPI Error: Aborting method \_TZ.FN00._OFF due to previous error (AE_AML_LOOP_TIMEOUT) (20200925/psparse-531)
[ 30.376997] ACPI Error: Aborting method \_SB.PCI0.LPCB.H_EC._REG due to previous error (AE_AML_LOOP_TIMEOUT) (20200925/psparse-531)
[ 30.377037] ACPI: EC: EC_CMD/EC_SC=0x66, EC_DATA=0x62
[ 30.377038] ACPI: EC: Boot ECDT EC used to handle transactions
[ 30.379873] ACPI: [Firmware Bug]: BIOS _OSI(Linux) query ignored
[ 30.426569] ACPI: Dynamic OEM Table Load:
[ 30.426598] ACPI: SSDT 0xFFFF89BC00C45800 000507 (v02 PmRef Cpu0Ist 00003000 INTL 20160527)
[ 30.429939] ACPI: \_PR_.PR00: _OSC native thermal LVT Acked
[ 30.433359] ACPI: Dynamic OEM Table Load:
[ 30.433378] ACPI: SSDT 0xFFFF89BC011D2C00 0003FF (v02 PmRef Cpu0Cst 00003001 INTL 20160527)
[ 30.436652] ACPI: Dynamic OEM Table Load:
[ 30.436670] ACPI: SSDT 0xFFFF89BC0148D6C0 0000BA (v02 PmRef Cpu0Hwp 00003000 INTL 20160527)
[ 30.439724] ACPI: Dynamic OEM Table Load:
[ 30.439742] ACPI: SSDT 0xFFFF89BC00C41800 000628 (v02 PmRef HwpLvt 00003000 INTL 20160527)
[ 30.443600] ACPI: Dynamic OEM Table Load:
[ 30.443622] ACPI: SSDT 0xFFFF89BC011C1000 000D14 (v02 PmRef ApIst 00003000 INTL 20160527)
[ 30.448401] ACPI: Dynamic OEM Table Load:
[ 30.448419] ACPI: SSDT 0xFFFF89BC011D0000 000317 (v02 PmRef ApHwp 00003000 INTL 20160527)
[ 30.451771] ACPI: Dynamic OEM Table Load:
[ 30.451788] ACPI: SSDT 0xFFFF89BC011D2800 00030A (v02 PmRef ApCst 00003000 INTL 20160527)
[ 30.461796] ACPI: Interpreter enabled

通过传递 acpi=off 参数给内核可以跳过卡住的过程,但是开机后触摸板不可使用,且发热严重,根据日志错误信息查阅大量资料后,基本都是说升级 BIOS 以解决 ACPI 的错误,但是这款笔记本没有 BIOS 更新而且很有可能以后也不会有。在多处发帖与网友交流后,进行了如下尝试

添加启动时内核参数

尝试了各种仿冒 Windows 的字符串,以及尝试添加 nomodeset 参数以禁用内核的显卡驱动,这些在某些安装 Linux 系统有问题的机型上会比较有用,但是我这台机器无效。

尝试修复 ACPI 字节码

主要参考:

根据 generic 大佬的方法,尝试修改 ACPI 字节码,但是反编译时获得的代码含有大量反编译失败的错误,无法继续修改……

尝试不同版本的内核

先后尝试了 5.4、5.8、5.10、5.12 等内核,情况完全一样,直到尝试了安装4.x的内核,发现启动时就不会卡那30秒了,虽然本子的触摸板、喇叭和麦克风都不能用了,但也说明确实可以通过修改kernel来解决/绕过/缓解这个问题。

分析错误日志

查看dmesg日志的第27行,可以看到有AE_AML_LOOP_TIMEOUT的错误描述。

找到 Linux Kernel 5.12 的内核源码,通过搜索 “AE_AML_LOOP_TIMEOUT” 这个错误的定义,顺藤摸瓜找到如下定义:
root/include/acpi/acconfig.h

1
2
3
/* Maximum time (default 30s) of While() loops before abort */

#define ACPI_MAX_LOOP_TIMEOUT 30

所以在出现某种错误的时候才会有卡住30秒的表现——那么是不是把这里的值改小,就可以缩短加载内核时卡住的时间,这样只要影响不大我也就能接受这样用下去了。

之后又在acpica/acpica: documents/changes.txt这里看到了如下说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
29 September 2017. Summary of changes for version 20170929:


1) ACPICA kernel-resident subsystem:

Redesigned and implemented an improved ASL While() loop timeout
mechanism. This mechanism is used to prevent infinite loops in the kernel
AML interpreter caused by either non-responsive hardware or incorrect AML
code. The new implementation uses AcpiOsGetTimer instead of a simple
maximum loop count, and is thus more accurate and constant across
different machines. The default timeout is currently 30 seconds, but this
may be adjusted later.

Renamed the ACPI_AML_INFINITE_LOOP exception to AE_AML_LOOP_TIMEOUT to
better reflect the new implementation of the loop timeout mechanism.

也就是说,在 2017年9月29日 的时候,ACPICA内核驻留子系统重新设计并实现了一个改进的 ASL While() 循环超时机制,用于防止内核中的无限循环 AML 解释器中由无响应的硬件或不正确的 AML 代码造成的无限循环(之前是 ACPI_AML_INFINITE_LOOP),并设置了默认的超时时间是 30 秒,而不是使用之前的最大循环数,因此可以在不同的机器上更加准确和稳定,这个值之后也可能会被调整——通过日志可以看出,我的机器上在出现 AE_AML_LOOP_TIMEOUT 异常之前总共只耗费了 300 多毫秒,也就是说,正常情况下所有操作都是可以在一秒内完成的。

于是我决定把上面代码中定义的 30 改为 3 试试看效果,也就是只允许内核的 AML 解释器循环等待 3 秒钟。

修改、编译内核

参考 Linux Kernel 的官方教程:KernelBuild

  1. 下载内核源码
    访问https://www.kernel.org/,点击所需版本的内核源码压缩包下载链接:
    tarball.webp

  2. 解压源码

    1
    2
    tar -xf linux-5.xx.tar.xz
    cd linux-5.xxx/
  3. 安装依赖

    1
    2
    # 安装依赖,可能不全,可以根据错误信息补
    sudo apt-get install libncurses5-dev gcc make git exuberant-ctags bc libssl-dev flex bison
  4. 从系统中获取配置文件

    1
    2
    3
    cp /boot/config-`uname -r`* .config
    # 将配置文件中未设置的选项自动设置为默认值
    make defconfig

此时可以修改源码,除了上文提到的对root/include/acpi/acconfig.h的修改,我还修改了.config文件中的CONFIG_LOCALVERSION字段的值,用于区分没有修改的内核。

  1. 编译安装
    1
    2
    3
    4
    5
    6
    7
    8
    # 可以使用 make -jn 指定编译时使用的线程数以加快编译,在我的机器上可以从20分钟缩短至5分钟
    make

    # 安装编译好的内核到系统中
    sudo make modules_install install

    # 更新启动项,以在下次启动时应用新内核
    sudo update-grub2

对比效果

可以用 systemd-analyze 命令查看开机耗时情况(硬件启动、加载器、内核、用户程序),另外还有 systemd-analyze blame 比较常用,用于看看开机时各个服务的启动耗时。
修改前的开机耗时:
before
修改后:
after
先这样用了,有空再慢慢折腾,看看以后会不会官方更新BIOS或者内核升级可以彻底解决问题~~

传送门:回到视频

上面的操作中,使用了make && make install这种源码编译安装的方式,但这是非常不提倡不正规的行为,至于原因我比较认同这个回答:为何linux安装程序会很麻烦? - invalid s的回答 - 知乎。所以下一篇文章中,我将介绍如何将我们修改过的内核直接编译为 debian 系通用的 deb 安装包,使我们可以方便地利用系统包管理工具对编译好的内核进行安装和删除等管理操作,以及如何利用 GitHub Actions 帮我们自动化编译 deb 包,而不用花费时间精力在本地创建编译环境以及自己编译 🐕