# Linux Suspend/Resume 實驗(一)
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 16
On-line CPU(s) list: 0-15
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
CPU family: 6
model: 165
Thread(s) per core: 2
Core(s) per socket: 8
Socket(s): 1
Socket(s): 5
CPU max MHz: 4800.0000
CPU min MHz: 800.0000
BogoMIPS: 5799.77
```
目前正在 PC 上使用 [pm-graph](https://github.com/intel/pm-graph/tree/master) 工具進行 suspend/resume 行為的效能測試與分析,藉此觀察各裝置在休眠與喚醒過程中的時間分布,並找出影響效能的關鍵元件。
首先先來介紹 [pm-graph](https://github.com/intel/pm-graph/tree/master)。
## pm-graph
pm-graph 這個專案可以使用 `sleepgraph` 和 `bootgraph` 這兩個工具,來視覺化 Linux 系統在 `suspend`(休眠)、`resume`(喚醒)與 `boot`(開機)過程中的活動。
針對電源管理模式中的 suspend/resume 效能進行優化極為重要,因為進入與退出低功耗模式所花的時間越長,系統可用時間就越少。
`sleepgraph` 和 `bootgraph` 這些工具擷取 `dmesg` 與 `ftrace` 資料。接著,這些資料會被轉換成時間軸(timeline)與呼叫圖(call graph),用以快速、深入地分析是哪些裝置或 kernel 程序佔用了最多時間。
這些工具會輸出一個 HTML 檔案,這些 HTML 檔可在任何 Linux 瀏覽器中開啟,例如 Firefox 或 Chromium。
## 安裝與設定
**python 安裝步驟**
```shell
sudo apt-get install python python-configparser python-requests linux-tools-common
```
**安裝步驟**
```shell
git clone http://github.com/intel/pm-graph.git
cd pm-graph
sudo make install
```
**Kernel 編譯選項**(所有核心都需要):
```shell
CONFIG_DEVMEM=y
CONFIG_PM_DEBUG=y
CONFIG_PM_SLEEP_DEBUG=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y
CONFIG_KPROBES=y
CONFIG_KPROBES_ON_FTRACE=y
```
## 使用方式
1. 首先,依照前一節的說明設定好 kernel(啟用必要的設定),然後重新編譯、安裝並開機進入該核心,如果原本都有就不用重新編譯 kernel。
1. 開啟終端機,執行以下指令來列出可用的電源模式(power modes):
```shell
sudo ./sleepgraph.py -modes
```
範例:
```
wu@wu-Pro-E500-G6-WS720T:~/pm-graph$ sudo ./sleepgraph.py -modes
[sudo] password for wu:
['freeze', 'mem', 'disk', 'mem-s2idle', 'disk-platform', 'disk-shutdown', 'disk-reboot', 'disk-suspend', 'disk-test_resume']
```
3. 使用其中一種電源模式(例如 mem,對應 S3 suspend)來執行測試:
```shell
sudo ./sleepgraph.py -m mem -rtcwake 15
```
或者使用設定檔進行測試:
```shell
sudo ./sleepgraph.py -config config/suspend.cfg
```
4. 系統進入 suspend,然後在指定時間(這裡是 15 秒)後自動喚醒。
5. 當系統喚醒後,腳本會完成測試流程並在測試目錄中產生輸出檔案。這些檔案會儲存在類似以下格式的子目錄中:
```
suspend-mmddyy-HHMMSS
```
在裡面你會看到這些輸出檔案:
* HTML 分析報告:<主機名>_<電源模式>.html
* 原始 dmesg 輸出:<主機名>_<電源模式>_dmesg.txt
* 原始 ftrace 輸出:<主機名>_<電源模式>_ftrace.txt
可以用 **Firefox 或 Chrome 瀏覽器**打開 .html 檔案來檢視時間線報告。
### 開發者模式
開發者模式會將底層原始函式呼叫資訊加入時間線中。工具會在所有延遲函式(如 sleep、mutex 等)上設定 kprobes,藉此觀察哪些裝置正在等待,以及它們的等待時間點。此外,也會在子系統相關的函式上設置一組 kprobes,以更完整地呈現時間線資訊。
工具還會顯示出一些平常不會出現在時間線中的 kernel 執行緒。這對於了解裝置彼此之間的依賴關係非常有幫助。例如,scsi_eh 執行緒(SCSI 錯誤處理器)是所有 SATA 硬碟在 resume 時所依賴的程序,它必須完成才能讓硬碟繼續 resume。
由於開發者模式會顯示更多細節,時間線會比基本模式大很多,因此**建議搭配 -mindev 參數**來過濾掉太短小的裝置事件區塊,以便更清晰地觀察主要裝置。
範例指令:
```
sudo ./sleepgraph.py -m mem -rtcwake 15 -mindev 1 -dev
```
或使用設定檔方式:
```
sudo ./sleepgraph.py -config config/suspend-dev.cfg
```
### 使用者程序模式
使用者程序模式(proc mode)會將使用者層程序的資訊加入時間線中。這類似 bootchart 工具的功能,bootchart 會將系統開機時 init 程序的執行流程做成圖表,而此工具則是在 suspend/resume 前後進行類似的記錄。
為了讓時間線能顯示出程序的活動資訊,需要在 suspend 前後插入一些延遲,因為程序會在 suspend_prepare 時凍結,在 resume_complete 時解凍。你可以**使用 -predelay 和 -postdelay 參數**來插入這些時間。
另外,也可以使用 -x2 模式(進行兩次 suspend/resume),加上 x2delay,這樣你就能看到 resume 前後,以及兩次 suspend 中間的程序活動狀況。
範例指令如下:
```
sudo ./sleepgraph.py -m mem -rtcwake 15 -x2 -x2delay 1000 -predelay 1000 -postdelay 1000 -proc
```
或使用設定檔方式:
```
sudo ./sleepgraph.py -config config/suspend-proc.cfg
```
## 長時間穩定性測試
評估一個系統健康狀態的最佳方式,是在長時間內反覆執行多次 suspend/resume(休眠/喚醒)測試,並分析其行為表現。
這可以透過 sleepgraph 的 **-multi** 參數來達成。
你只需指定兩個數字:
1. 要執行的測試次數 或 測試持續的天數/小時數/分鐘數
1. 每次測試之間的延遲秒數
你可以加入任何其他選項來產生所需的資料。建議使用 dev 模式收集時間軸資訊,因為 kprobes 對系統效能影響極小,但卻能提供更多深入的資訊。
測試完成後,輸出資料夾中會包含每次測試的子資料夾,以及放在根目錄下的摘要報告頁面。
* summary.html:以表格列出各次測試的資訊與連結
* summary-issues.html:彙整所有測試中偵測到的 kernel 問題
* summary-devices.html:彙整所有裝置的效能資料
```
suspend-xN-{date}-{time}:
summary.html
summary-issues.html
summary-devices.html
suspend-{date}-{time} (1)
suspend-{date}-{time} (2)
...
```
以下是進行測試時常用的重要參數:
`-m mode`
* 指定要啟動的 suspend 模式,例如 mem、freeze、standby(預設為 mem)
`-rtcwake t`
* 使用 rtcwake 指令在 t 秒後自動喚醒系統(預設為 15 秒)
`-gzip(可選)`
* 將 trace 和 dmesg 日誌壓縮成 gzip 格式以節省空間。工具也支援讀取已壓縮的 log 檔,能有效減少多次測試所產生的資料夾大小。
`-dev(可選)`
* 將 kernel 原始碼函式呼叫與執行緒(threads)記錄到時間軸中(預設為停用)
`-multi n d`
* 執行 n 次連續的測試,每次測試之間間隔 d 秒。
所有輸出將會儲存在名為 suspend-xN-{date}-{time} 的新資料夾中。
測試完成後,工具會自動執行 `-summary` 指令產生所有資料的 HTML 摘要頁面,除非你加上 `-skiphtml`。
使用 `-skiphtml` 可以略過時間軸與摘要報表的產生,大幅加快測試流程。
之後你仍可使用 `-summary` 與 `-genhtml` 再次執行來補產報表。
`-skiphtml(可選)`
* 執行測試並擷取 trace 日誌,但略過時間軸與摘要 HTML 檔案的產生。
這能大幅提升整體測試速度。
之後你可以將資料複製到效能更高的主機上,再使用 -summary 與 -genhtml 來產生時間軸與摘要報告。
以下是在測試完成後可使用的相關指令:
`-summary indir`
為一次 -multi 多次測試產生或重新產生摘要報告。
這個指令會在目前的資料夾中建立三個檔案:
* summary.html:以表格方式列出所有測試,依照 kernel/主機/模式分類,並附有對應 HTML 報告的連結
* summary-issues.html:彙整所有測試中在 dmesg 中偵測到的 kernel 問題
* summary-devices.html:彙整所有測試中裝置的 suspend/resume 耗時資料
`-genhtml`
* 搭配 -summary 使用,用來根據 dmesg 和 ftrace 日誌重新產生缺失的 HTML 時間軸檔案。
* 若測試次數非常多,這個動作可能會花上不少時間。
### 使用範例
啟動一次多重測試:
```
sudo ./sleepgraph.py -m mem -rtcwake 10 -dev -gzip -multi 2000 0
```
這個指令會執行 2000 次 suspend/resume 測試,每次之間無延遲,並使用:
* mem 模式(S3 suspend)
* 10 秒後自動喚醒
* 開啟開發者模式(-dev)
* 將 log 壓縮(-gzip)
想加快測試速度,可略過時間軸與摘要報告的產生:
```
sudo ./sleepgraph.py -m mem -rtcwake 10 -dev -gzip -multi 2000 0 -skiphtml
```
### 若要針對既有的 multitest 資料夾產生摘要報告:
```
cd suspend-x2000-{date}-{time}
sleepgraph.py -summary .
```
### 若要補產 timeline 的 HTML 報告:
```
cd suspend-xN-{date}-{time}
sleepgraph.py -summary . -genhtml
```
這會根據每次測試中的 dmesg 和 ftrace 日誌,產出缺失的 HTML 時間軸檔案。
## 配置檔案
config 目錄中包含了各種常見的使用情境範例。不同的電源模式也有對應的設定檔可用:
基本的 suspend/resume + 時間軸(mem/freeze/standby):
* config/suspend.cfg
* config/freeze.cfg
* config/standby.cfg
開發者模式(含 dev timeline):
* config/suspend-dev.cfg
* config/freeze-dev.cfg
* config/standby-dev.cfg
包含呼叫圖(callgraph)的時間軸:
* config/suspend-callgraph.cfg
* config/freeze-callgraph.cfg
* config/standby-callgraph.cfg
proc 模式的 x2 測試範例(使用 mem 模式):
* config/suspend-x2-proc.cfg
編輯時間軸函式的範例(將內建函式匯入 config):
* config/custom-timeline-functions.cfg
對 serio 子系統的除錯設定檔:
* config/debug-serio-suspend.cfg
### 使用範例:
執行一個基本的 mem suspend 測試:
```
sudo ./sleepgraph.py -config config/suspend.cfg
```
執行一個含呼叫圖的 mem suspend 測試:
```
sudo ./sleepgraph.py -config config/suspend-callgraph.cfg
```
執行一個包含 dev 模式詳細資訊的 mem suspend 測試:
```
sudo ./sleepgraph.py -config config/suspend-dev.cfg
```
還有 suspend.cfg 可以設定
## NETFIX
Netfix 是 pm-graph 工具套件中的一個網路修復工具,用來在 Linux 系統執行 suspend/resume(睡眠/喚醒)測試後,自動檢查與恢復網路連線(Wi-Fi、有線)。
(待補)
## 自訂時間軸事件
(待補)
## 在消費型 Linux 作業系統上進行測試
(待補)
## 除錯設定
在閱讀在 [Demystifying Linux Kernel Initialization](https://thenewstack.io/demystifying-linux-kernel-initialization/) `include/linux/init.h` 中定義了 八個 initcall 呼叫階段(等級),用來控制核心啟動時不同行為的初始化執行順序。其定義如下:
```c
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
```
這些不同階段會在 Linux 開機時依序執行,從 pure(0)到 late(7)等級,目的是確保初始化順序正確、安全。
後面的數字為階段編號,s 為"sync"(同步) 的意思
啟用 `initcall_debug` 這個 KNL(核心啟動參數,`Kernel start-up parameter)` 可以讓我們追蹤每個 `initcall` 的執行過程。這對於理解 Linux 核心的開機流程、除錯早期的核心崩潰(kernel panic),以及測量每個 initcall 所花費的初始化時間非常有幫助。
另一個有用的 KNL 參數是 `initcall_blacklist`,它可以讓你指定一組要跳過執行的 initcall 函數清單。這對於除錯內建模組或特定 initcall 行為也非常實用。
**啟用 initcall_debug**,並使用 dmesg 來解析 initcall 資訊:
第一步是確保你的核心有啟用以下兩個選項:
* `CONFIG_PRINTK_TIME`:讓 dmesg 輸出包含每行訊息的時間戳記。
* `CONFIG_KALLSYMS`:允許顯示符號名稱(不然只能看到地址)。
如果這兩個選項未啟用,你就必須自行編譯、安裝並啟動一個新的核心。
一旦這些選項已經啟用,你可以編輯 `/etc/default/grub` 檔案,把下列參數加到 `GRUB_CMDLINE_LINUX` 這一行中:
先輸入
```
sudo nano /etc/default/grub
```
找到 `GRUB_CMDLINE_LINUX` 將後面參數填入 "earlyprintk=vga printk.time=1 initcall_debug"
```
GRUB_CMDLINE_LINUX="earlyprintk=vga printk.time=1 initcall_debug"
```
現在,重新開機系統。系統啟動完成後,執行以下指令來觀察開機期間的 initcall 訊息:
```
dmesg -t -x
```
需要先把目前的核心開機訊息(包含 initcall log)輸出成 dmesg.out 檔案,再執行分析指令。
```
sudo dmesg -t -x > dmesg.out
```
以下這段可以顯示那些執行時間較長的 initcall 函數:
```
cat dmesg.out | grep "initcall" | sed "s/\(.*\)after\(.*\)/\2 \1/g" | sort -n
```
這行指令會:
1. 讀取 dmesg.out(包含 initcall log 的檔案)
1. 找出所有包含 initcall 的行
1. 把每行中 "after XXX usecs" 的耗時資訊移到開頭
1. 最後依照執行時間從小到大排序
範例:
```
7082 usecs kern :debug : initcall hid_init+0x0/0xff0 [usbhid] returned 0
7108 usecs kern :debug : initcall init_encrypted+0x0/0x100 returned 0
11846 usecs kern :debug : initcall jent_mod_init+0x0/0x100 returned 0
15295 usecs kern :debug : initcall hdmi_driver_init+0x0/0xff0 [snd_hda_codec_hdmi] returned 0
16184 usecs kern :debug : initcall inet_init+0x0/0x330 returned 0
20982 usecs kern :debug : initcall serial_pci_driver_init+0x0/0x30 returned 0
23919 usecs kern :debug : initcall msr_init+0x0/0xff0 [msr] returned 0
```
啟用 `initcall_debug` 之後,核心輸出的訊息數量會增加。請確認核心設定中 `CONFIG_LOG_BUF_SHIFT` 被設為 18,這樣可以把日誌緩衝區的大小設定為 256K,以避免訊息被截斷或遺失。
為了加快開機過程,你也可以啟用 `driver_async_probe` 這個核心啟動參數(KNL),它可以讓驅動程式的探測(probe)以非同步模式執行。啟用這個選項後,你可以指定一組驅動名稱,讓它們以非同步方式初始化。
## 第一次測試 ( pm-graph )
首先我先測試 `freeze` 的系統狀態並輸入下列命令:
```
$ sudo ./sleepgraph.py -m freeze -rtcwake 15
```
由下列這張圖可以發現到 `ata1`(SATA 裝置)花了 605.769 ms 進入 `suspend` 狀態是耗時最久的裝置。

根據下面這張圖可以發現裝置名稱:**nvidia @ 0000:01:00.0** `resume` 最久的:
**Total Suspend 時間**:`211.969 ms`
* 其中主體 `suspend` 操作耗時約 `199.800 ms`
**Total Resume 時間**:`548.301 ms`
* `Resume` 操作在黃色區段內明顯佔據了很大一段時間,長達 `535.737 ms`

從這兩張圖可以發現這兩個裝置是整體 **suspend/resume 的 bottleneck**。
為了測試系統在 `mem` 模式下的休眠狀態,我們將 `mem_sleep` 設定為 `deep`,並執行以下指令以進行 `suspend` 操作:
```
$ sudo ./sleepgraph.py -m mem -rtcwake 15
```
```
wu@wu-Pro-E500-G6-WS720T:~/pm-graph$ cat /sys/power/mem_sleep
s2idle [deep]
```
由下列這張圖可以發現到 `ata1`(SATA 裝置)花了 `604.203 ms` 進入 `suspend` 狀態也是一樣是耗時最久的裝置。

根據下面這張圖可以發現裝置名稱:**e1000e @ 0000:00:1f.6** 為 `resume` 最久的:
* `Suspend` 耗時:`110.230 ms`
* `Resume` 耗時:`977.777 ms`

經過多次測試後發現,在系統 resume 的過程中,偶爾會出現雙螢幕其中一邊亮起但無顯示畫面的情況,目前原因尚未明確。但是強制重新初始化 HDMI-0 螢幕輸出流程後就可以顯示:
```
xrandr --output HDMI-0 --off
sleep 1
xrandr --output HDMI-0 --mode 1920x1080 --rate 60 --primary
```
所以推斷可能是 NVIDIA 驅動在 resume(喚醒)階段漏掉了 HDMI encoder 的 re-init 初始化或狀態恢復,因此我輸入了下列這段指令來確認:
```
journalctl -b | grep -i nvidia
```
* journalctl: 查看 systemd 的 system log(包括核心、驅動、服務等日誌)。
* -b: 只顯示本次開機(boot)後的 log。
* |: 管道符號,把 journalctl 的輸出交給後面的 grep。
* grep -i nvidia: 搜尋關鍵字 nvidia
發現到下列這段兩段:
```
五 07 14:57:12 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (II) NVIDIA(0): Setting mode "DP-4: 3440x1440_100 @3440x1440 +0+0 {ViewPortIn=3440x1440, ViewPortOut=3440x1440+0+0}, HDMI-0: 1920x1080_240 @1920x1080 +3440+0 {ViewPortIn=1920x1080, ViewPortOut=1920x1080+0+0}五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): Microstep MAG 276CXF (DFP-4): connected
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): Microstep MAG 276CXF (DFP-4): Internal TMDS
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): Microstep MAG 276CXF (DFP-4): 600.0 MHz maximum pixel clock
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0):
```
可以發現到驅動在 resume 之後掃描 HDMI 輸出埠(DFP-4),發現螢幕已連接,並列出 TMDS 類型與最大頻寬。
```
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (II) NVIDIA(0): Setting mode "DP-4: 3440x1440_100 @3440x1440 +0+0 {ViewPortIn=3440x1440, ViewPortOut=3440x1440+0+0}, HDMI-0: 1920x1080_240 @1920x1080 +3440+0 {ViewPortIn=1920x1080, ViewPortOut=1920x1080+0+0}"
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): DFP-4: disconnected
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): DFP-4: Internal TMDS
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0): DFP-4: 165.0 MHz maximum pixel clock
五 07 16:24:37 wu-Pro-E500-G6-WS720T /usr/libexec/gdm-x-session[1470]: (--) NVIDIA(GPU-0):
```
驅動正在同時設定 DP-4 與 HDMI-0 的顯示模式時,顯示器本來是 connected,但設定顯示模式後,立即變成 **disconnected**,還連 pixel clock 資訊都變了(從 600 降到 165 MHz,顯示為 fallback)
## 第一次測試 ( perfetto )
在進行 Perfetto 測試之前,請先根據官網提供的步驟安裝所需套件。你可以參考這篇文章:[ Quickstart: Record traces on Linux ](https://perfetto.dev/docs/quickstart/linux-tracing)。按照教學完成安裝後,即可開始執行測試。
首先,建立一個用於測試的檔案,檔名為 `xxxxx.cfg`,並將其內容設置為你所需的設定。由於今天的測量目標是 `suspend` 和 `resume`,因此我將檔案命名為 `suspend_resume.cfg`。設定好檔名後,接著填入相應的配置內容,如何設定可以參考檔案中範例和[ TraceConfig ](https://perfetto.dev/docs/reference/trace-config-proto)跟[ TracePacket ](https://perfetto.dev/docs/reference/trace-packet-proto)。
我的 cfg 檔設置如下:
```
buffers {
size_kb: 100024
fill_policy: RING_BUFFER
}
data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched_switch"
ftrace_events: "sched_waking"
ftrace_events: "sched_wakeup_new"
ftrace_events: "sched_process_exec"
ftrace_events: "sched_process_exit"
ftrace_events: "sched_process_fork"
ftrace_events: "sched_process_free"
ftrace_events: "sched_process_hang"
ftrace_events: "sched_process_wait"
ftrace_events: "irq_handler_entry"
ftrace_events: "irq_handler_exit"
ftrace_events: "suspend_resume"
}
}
}
duration_ms: 20000
```
輸入以下指令就能開始測試:
```
sudo ./out/linux/tracebox -o trace.perfetto-trace --txt -c test/configs/suspend_resume.cfg
```
測試完後會產生 trace.perfetto-trace 檔案,隨後要執行這行程式碼:
```
sudo chown $USER:$USER trace.perfetto-trace
```
這條命令的作用是將 trace.perfetto-trace 文件的擁有者和群組更改為當前登入的用戶,這樣用戶便能對該文件擁有完全的控制權。如果沒有執行這步驟,生成的文件可能無法在 [ ui.perfetto](https://ui.perfetto.dev/) 中進行分析,因為文件的權限設定可能會限制對該文件的讀取或修改。
[ ui.perfetto](https://ui.perfetto.dev/) 這個網站可以將你執行的結果轉換為可視化的圖表和數據,幫助你更直觀地分析和理解追蹤數據。
此為第一次測試的結果:

可以看到,由於測量範圍過大,刻度的細節無法清晰呈現,因此無法進行更精確的觀察。不過,這個頁面允許使用 W, A, S, D 鍵進行縮放和移動,讓你可以更近距離地觀察數據。
下圖為放大後的結果:

由此圖可以得知當執行 `sudo systemctl suspend` 這行指令後系統會建立一個 `systemd-sleep thread` 並開始執行下列為此 `thread` 的資訊:

在影片中提到在 `suspend_enter` 階段,系統會進入休眠準備過程,這時可能會執行 `sync_filesystems()` 操作,即將文件系統中的修改(如尚未寫回磁碟的數據)同步到磁碟中,這樣可以確保在系統進入休眠狀態後不會遺失任何數據。
在這個階段,系統還會嘗試將 `dirty pages`寫回磁碟,以保證文件系統的一致性。這是一個非常關鍵的操作,因為如果系統在這個過程中被中斷,可能會導致數據丟失或文件系統損壞。
此時,userspace 無法中斷或干涉這個操作,因為它是由內核處理的,並且它需要保證在進入休眠前,所有關鍵數據都已經正確地寫入磁碟。因此,進程在這段時間內會處於 `Uninterruptible Sleep` 狀態也就是下圖中黃色區段,這意味著它們不會接受來自 userspace 的信號或中斷,直到文件系統同步完成,並且系統準備好進入休眠狀態。
因此我嘗試不去執行 `sync_filesystems()` 操作,而 `sync_on_suspend_enabled` 這個變數為會不會執行這 `sync_filesystems()` 的關鍵,由 [linux/kernel/power/suspend.c](https://github.com/torvalds/linux/blob/master/kernel/power/suspend.c#L364) 可以得知
```
static int enter_state(suspend_state_t state)
{
...
if (sync_on_suspend_enabled) {
trace_suspend_resume(TPS("sync_filesystems"), 0, true);
ksys_sync_helper();
trace_suspend_resume(TPS("sync_filesystems"), 0, false);
}
```
在系統中 `sync_on_suspend_enabled` 這個變數預設為 `true`,因此接下來嘗試將它關閉來觀察 `suspend_enter` 這個階段是否能加快,因此首先必須先找到`sync_on_suspend_enabled` 這個變數的位置,這個變數的位置為下列這個目錄:
```
/sys/power/sync_on_suspend
```
將其值改為 false 要輸入下列指令:
```
echo 0 | sudo tee /sys/power/sync_on_suspend
```
隨後開始觀察是否會加快 `suspend_enter` 這個階段的速度,下列為觀察結果:
* 輸入前:

* 輸入後:

可以發現 `suspend_enter` 的速度明顯提昇,因此改進`sync_filesystems()` 操作是非常關鍵的。


這兩張圖顯示了系統進入休眠與恢復的過程。首先,系統會分別執行 cpuhp 關閉 CPU,並同時執行 CPU 核心的遷移(migration)操作。當遷移過程完成後,接著會依次執行 cpuhp 來重新開啟 CPU 核心。
## 樹苺派測試
當下載好 [Raspberry Pi OS](https://zh.wikipedia.org/zh-tw/Raspberry_Pi_OS) 後便可以開始開發,首先一樣是先確認下列這些:
**Kernel 編譯選項**(所有核心都需要):
```shell
CONFIG_DEVMEM=y
CONFIG_PM_DEBUG=y
CONFIG_PM_SLEEP_DEBUG=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y
CONFIG_KPROBES=y
CONFIG_KPROBES_ON_FTRACE=y
```
### 重新編譯 kernel
確認完上述後由於我們的目標是去研究 suspend 跟 resume 的,因此還要去確認 `CONFIG_SUSPEND` 是否等於 y,而我檢查時發現到 `#CONFIG_SUSPEND is not set` ,因此此時需要重新編譯一個支援 CONFIG_SUSPEND 的 kernel。
一、首先要做的是先安裝必要的套件:
```shell
sudo apt update
sudo apt install git bc bison flex libssl-dev make libncurses-dev \
crossbuild-essential-arm64 libelf-dev libudev-dev libpci-dev \
libiberty-dev raspberrypi-kernel-headers
```
二、下載 Raspberry Pi 官方 kernel 原始碼
```shell
git clone --depth=1 https://github.com/raspberrypi/linux
cd linux
```
三、載入目前系統的 config 並修改
```
cp /boot/config-$(uname -r) .config
make menuconfig
```
隨後會有圖形界面並在這個圖形介面中打開以下項目:
```
[*] Suspend to RAM and standby
[*] Power Management support
```
四、編譯 kernel
```
make -j$(nproc) Image modules dtbs
```
五、安裝編譯後的 kernel
```
sudo make modules_install
sudo cp arch/arm64/boot/Image /boot/firmware/kernel8.img
```
當所有設定完成後,執行 `cat /sys/power/state` 時會看到 `freeze` 和 `mem` 兩個選項,其中 `mem` 支援 `i2idle`。然而,當我執行 `echo freeze | sudo tee /sys/power/state` 時,發現畫面在進入 `freeze` 模式後會完全靜止。
原本我想利用檢查 `journalctl` 日誌歷史來找尋線索,但是發現輸入`echo freeze | sudo tee /sys/power/state`後連日誌都無法寫入
後來我猜測是系統缺乏喚醒的手段,因此我下命令 `sudo find /sys -name wakeup` 用來在系統中查找所有名為 wakeup 的文件或目錄。這些文件通常與 硬體裝置 或 中斷 相關,並控制它們是否能夠喚醒系統。在輸入完上述指令後找到了下列這兩段:
```
/sys/devices/platform/soc/fe201000.serial/fe201000.serial:0/fe201000.serial:0.0/tty/ttyAMA0/power/wakeup
/sys/devices/platform/serial8250/serial8250:0/serial8250:0.0/tty/ttyS0/power/wakeup
```
樹莓派的串行端口(UART)中包含一個名為 wakeup 的文件,因此我想測試是否可以使用另一台電腦與樹莓派進行通信,來觸發並喚醒樹莓派裝置。
首先要做的就是開啟 UART 功能:確認 `/boot/firmware/config.txt` 中是否有 `enable_uart=1` 和 `dtoverlay=disable-bt` 如果沒有要找到 `/boot/firmware/config.txt` 並輸入 `sudo nano /boot/firmware/config.txt` 這個指令編輯這個檔案,在下面的 [all] 加入後重新開機,隨後找到你裝置的位置輸入下列指令:
```
echo enabled | sudo tee /sys/devices/platform/soc/fe201000.serial/fe201000.serial:0/fe201000.serial:0.0/tty/ttyAMA0/power/wakeup
echo enabled | sudo tee /sys/devices/platform/serial8250/serial8250:0/serial8250:0.0/tty/ttyS0/power/wakeup
```
將之 wakeup 功能打開並輸入 `ls -l /dev/serial0` 確認是否為下列這個:
```
/dev/serial0 -> /dev/ttyAMA0
```
當設置好後開始執行電腦間的通訊使用 usb to ttl 跟 [minicom](https://en.wikipedia.org/wiki/Minicom) 這個 Linux console 連接工具,使用步驟為下列:
1. 電腦端(有 USB to TTL)
插上 USB to TTL,找出 ttyUSB,例如:
```
ls /dev/ttyUSB*
```
啟動 minicom:
```
sudo minicom -b 9600 -D /dev/ttyUSB0
```
你打字會送出資料,經由 TXD → 傳到樹莓派
2. 樹莓派端
先接好 pin 腳([參考文章](https://ithelp.ithome.com.tw/articles/10215294))
執行:
```
sudo minicom -b 9600 -D /dev/serial0
```
執行完後你就能在樹苺派的畫面上看見輸入的文字,隨後輸入 `systemctl suspend` 後在另外一台電腦上敲擊任何的文字後,樹苺派就會從 suspend 中 resume ,但是我發現到樹苺派的網路模組會載入失敗,目前不清楚為什麼。
下列為 `dmesg` 中的一部分訊息:
```
[ 328.564397] PM: suspend exit
[ 328.596754] bcmgenet fd580000.ethernet: configuring instance for external RGMII (RX delay)
[ 328.600046] bcmgenet fd580000.ethernet eth0: Link is Down
[ 328.616230] brcmfmac: brcmf_sdio_txfail: sdio error, abort command and terminate frame
[ 328.620055] brcmfmac: brcmf_sdio_txfail: sdio error, abort command and terminate frame
[ 328.624007] brcmfmac: brcmf_sdio_txfail: sdio error, abort command and terminate frame
[ 328.625363] brcmfmac: brcmf_sdio_dpc: sdio ctrlframe tx failed err=-84
[ 328.625463] brcmfmac: brcmf_sdio_dpc: failed backplane access over SDIO, halting operation
[ 328.625578] ieee80211 phy0: brcmf_proto_bcdc_query_dcmd: brcmf_proto_bcdc_msg failed w/status -84
```
為了確保 resume 後網路模組仍能持續運作,需要手動在 Raspberry Pi 的 Device Tree 中,於 MMC 控制器(&mmcnr)節點加入「保留電源於休眠」屬性。修改示例如下:
```diff
&mmcnr {
pinctrl-names = "default";
pinctrl-0 = <&sdio_pins>;
bus-width = <4>;
+ keep-power-in-suspend;
status = "okay";
};
```
此為在樹苺派中測試 suspend 結果:


