# 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` 狀態是耗時最久的裝置。 ![image](https://hackmd.io/_uploads/rJ0zJ9wllx.png) 根據下面這張圖可以發現裝置名稱:**nvidia @ 0000:01:00.0** `resume` 最久的: **Total Suspend 時間**:`211.969 ms` * 其中主體 `suspend` 操作耗時約 `199.800 ms` **Total Resume 時間**:`548.301 ms` * `Resume` 操作在黃色區段內明顯佔據了很大一段時間,長達 `535.737 ms` ![image](https://hackmd.io/_uploads/H1uOJqwegx.png) 從這兩張圖可以發現這兩個裝置是整體 **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` 狀態也是一樣是耗時最久的裝置。 ![image](https://hackmd.io/_uploads/SJoS9_wggg.png) 根據下面這張圖可以發現裝置名稱:**e1000e @ 0000:00:1f.6** 為 `resume` 最久的: * `Suspend` 耗時:`110.230 ms` * `Resume` 耗時:`977.777 ms` ![image](https://hackmd.io/_uploads/HkGP9dDelg.png) 經過多次測試後發現,在系統 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/) 這個網站可以將你執行的結果轉換為可視化的圖表和數據,幫助你更直觀地分析和理解追蹤數據。 此為第一次測試的結果: ![Screenshot from 2025-05-10 16-56-06](https://hackmd.io/_uploads/HJU3E5hxgx.png) 可以看到,由於測量範圍過大,刻度的細節無法清晰呈現,因此無法進行更精確的觀察。不過,這個頁面允許使用 W, A, S, D 鍵進行縮放和移動,讓你可以更近距離地觀察數據。 下圖為放大後的結果: ![Screenshot from 2025-05-10 17-16-47](https://hackmd.io/_uploads/B1TtKq2glx.png) 由此圖可以得知當執行 `sudo systemctl suspend` 這行指令後系統會建立一個 `systemd-sleep thread` 並開始執行下列為此 `thread` 的資訊: ![Screenshot from 2025-05-10 17-26-16](https://hackmd.io/_uploads/B1UTscnxee.png) 在影片中提到在 `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` 這個階段的速度,下列為觀察結果: * 輸入前: ![Screenshot from 2025-05-13 22-27-48](https://hackmd.io/_uploads/Sk7OcAxZle.png) * 輸入後: ![Screenshot from 2025-05-13 22-28-36](https://hackmd.io/_uploads/Sklpq0xWll.png) 可以發現 `suspend_enter` 的速度明顯提昇,因此改進`sync_filesystems()` 操作是非常關鍵的。 ![Screenshot from 2025-05-10 17-39-00](https://hackmd.io/_uploads/Sy46Cq2llx.png) ![Screenshot from 2025-05-10 17-52-29](https://hackmd.io/_uploads/rk31fihlex.png) 這兩張圖顯示了系統進入休眠與恢復的過程。首先,系統會分別執行 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 結果: ![Screenshot from 2025-05-13 14-59-24](https://hackmd.io/_uploads/rysRTwe-ee.png) ![Screenshot from 2025-05-13 14-58-19](https://hackmd.io/_uploads/HyOcaveZxg.png) ![Screenshot from 2025-05-13 14-58-56](https://hackmd.io/_uploads/SJa2pPxbll.png)