1. 背景
在 Linux Block Driver - 1 中,我們實現了一個最簡塊設備驅動 Sampleblk。這個只有 200 多行源碼的塊設備驅動利用內存創建了標準的 Linux 磁盤。我們在基於 Linux 4.6.0 內核的環境下,加載該驅動,並在其上創建了 Ext4 文件系統。
本文將繼續之前的實驗,圍繞 Sampleblk 探究 Linux 塊設備驅動的運作機制。除非特別指明,本文中所有 Linux 內核源碼引用都基於 4.6.0。其它內核版本可能會有較大差異。
2. 準備
首先,在閱讀本文前,請按照 Linux Block Driver - 1中的步驟準備好實驗環境。確保可以做到如下步驟,
- 編譯和加載 Sampleblk Day1 驅動
- 用 Ext4 格式化 /dev/sampleblk1
- mount 文件系統到 /mnt
其次,爲了繼續後續實驗,還需做如下準備工作。
安裝 fio 測試軟件。
fio 是目前非常流行的 IO 子系統測試工具。作者 Jens Axboe 是 Linux IO 子系統的 maintainer,目前就職於 Facebook。互聯網上 FIO 安裝和使用的文章很多,這裏就不在贅述。不過最值得細讀的還是 fio HOWTO。
安裝 blktrace 工具。
也是 Jens Axboe 開發的 IO 子系統追蹤和性能調優工具。發行版有安裝包。關於該工具的使用可以參考 blktrace man page。
安裝 Linux Perf 工具。
Perf 是 Linux 源碼樹自帶工具,運行時動態追蹤,性能分析的利器。也可以從發行版找到安裝包。網上的 Perf 使用介紹很多。Perf Wiki 非常值得一看。下載 perf-tools 腳本。
perf-tools 腳本 是 Brendan Gregg 寫的基於 ftrace 和 perf 的工具腳本。全部由 bash 和 awk 寫成,無需安裝,非常簡單易用。Ftrace: The hidden light switch 這篇文章是 Brendan Gregg 給 LWN 的投稿,推薦閱讀。
3. 實驗與分析
3.1 文件順序寫測試
如一般 Linux 測試工具支持命令行參數外,fio 也支持 job file 的方式定義測試參數。本次實驗中使用的 fs_seq_write_sync_001 job file 內容如下,
; -- start job file --
[global] ; global shared parameters
filename=/mnt/test ; location of file in file system
rw=write ; sequential write only, no read
ioengine=sync ; synchronized, write(2) system call
bs=,4k ; fio iounit size, write=4k, read and trim are default(4k)
iodepth=1 ; how many in-flight io unit
size=2M ; total size of file io in one job
loops=1000000 ; number of iterations of one job
[job1] ; job1 specific parameters
[job2] ; job2 specific parameters
; -- end job file --
本次實驗將在 /dev/sampleblk1 上 mount 的 Ext4 文件系統上進行順序 IO 寫測試。其中 fio 將啓動兩個測試進程,同時對 /mnt/test 文件進行寫操作。
$ sudo fio ./fs_seq_write_sync_001
job1: (g=0): rw=write, bs=4K-4K/4K-4K/4K-4K, ioengine=sync, iodepth=1
job2: (g=0): rw=write, bs=4K-4K/4K-4K/4K-4K, ioengine=sync, iodepth=1
fio-2.1.10
Starting 2 processes
^Cbs: 2 (f=2): [WW] [58.1% done] [0KB/2208MB/0KB /s] [0/565K/0 iops] [eta 13m:27s]
...[snipped]...
fio: terminating on signal 2
job1: (groupid=0, jobs=1): err= 0: pid=22977: Thu Jul 21 22:10:28 2016
write: io=1134.8GB, bw=1038.2MB/s, iops=265983, runt=1118309msec
clat (usec): min=0, max=66777, avg= 1.63, stdev=21.57
lat (usec): min=0, max=66777, avg= 1.68, stdev=21.89
clat percentiles (usec):
| 1.00th=[ 0], 5.00th=[ 1], 10.00th=[ 1], 20.00th=[ 1],
| 30.00th=[ 1], 40.00th=[ 1], 50.00th=[ 2], 60.00th=[ 2],
| 70.00th=[ 2], 80.00th=[ 2], 90.00th=[ 2], 95.00th=[ 3],
| 99.00th=[ 4], 99.50th=[ 7], 99.90th=[ 18], 99.95th=[ 25],
| 99.99th=[ 111]
lat (usec) : 2=49.79%, 4=49.08%, 10=0.71%, 20=0.34%, 50=0.06%
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
lat (msec) : 100=0.01%
cpu : usr=8.44%, sys=69.65%, ctx=1935732, majf=0, minf=9
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued : total=r=0/w=297451591/d=0, short=r=0/w=0/d=0
latency : target=0, window=0, percentile=100.00%, depth=1
job2: (groupid=0, jobs=1): err= 0: pid=22978: Thu Jul 21 22:10:28 2016
write: io=1137.4GB, bw=1041.5MB/s, iops=266597, runt=1118309msec
clat (usec): min=0, max=62132, avg= 1.63, stdev=21.35
lat (usec): min=0, max=62132, avg= 1.68, stdev=21.82
...[snipped]...
Run status group 0 (all jobs):
WRITE: io=2271.2GB, aggrb=2080.5MB/s, minb=1038.2MB/s, maxb=1041.5MB/s, mint=1118309msec, maxt=1118309msec
Disk stats (read/write):
sda: ios=0/4243062, merge=0/88, ticks=0/1233576, in_queue=1232723, util=37.65%
從 fio 的輸出中可以看到 fio 啓動了兩個 job,並且按照 job file 規定的設置開始做文件系統寫測試。在測試進行到 58.1% 的時候,我們中斷程序,得到了上述的輸出。從輸出中我們得出如下結論,
- 兩個線程總共的寫的吞吐量爲 2080.5MB/s,在磁盤上的 IPOS 是 4243062。
- 每個線程的平均完成延遲 (clat) 爲 1.63us,方差是 21.57。
- 每個線程的平均總延遲 (lat) 爲 1.68us,方差是 21.89。
- 磁盤 IO merge 很少,磁盤的利用率也只有 37.65%。
- 線程所在處理器的時間大部分在內核態:69.65%,用戶態時間只有 8.44% 。
3.2 文件 IO Pattern 分析
3.2.1 使用 strace
首先,我們可以先了解一下 fio 測試在系統調用層面看的 IO pattern 是如何的。Linux 的 strace
工具是跟蹤應用使用系統調用的常用工具。
在 fio 運行過程中,我們獲得 fio 其中一個 job 的 pid 之後,運行了如下的 strace
命令,
$ sudo strace -ttt -T -e trace=desc -C -o ~/strace_fio_fs_seq_write_sync_001.log -p 94302
strace
man page 給出了命令的詳細用法,這裏只對本小節裏用到的各個選項做簡單的說明,
-ttt
打印出每個系統調用發生的起始時間戳。-T
則給出了每個系統調用的開銷。-e trace=desc
只記錄文件描述符相關係統調用。這樣可過濾掉無關信息,因爲本實驗是文件順序寫測試。-C
則在strace
退出前可以給出被跟蹤進程的系統調用在strace
運行期間使用比例和次數的總結。-o
則指定把strace
的跟蹤結果輸出到文件中去。
3.2.2 分析 strace 日誌
根據 strace 的跟蹤日誌,我們可對本次 fio 測試的 IO pattern 做一個簡單的分析。詳細日誌信息請訪問這裏,下面只給出其中的關鍵部分,
1466326568.892873 open("/mnt/test", O_RDWR|O_CREAT, 0600) = 3 <0.000013>
1466326568.892904 fadvise64(3, 0, 2097152, POSIX_FADV_DONTNEED) = 0 <0.000813>
1466326568.893731 fadvise64(3, 0, 2097152, POSIX_FADV_SEQUENTIAL) = 0 <0.000004>
1466326568.893744 write(3, "\0\260\35\0\0\0\0\0\0\320\37\0\0\0\0\0\0\300\35\0\0\0\0\0\0\340\37\0\0\0\0\0"..., 4096) = 4096 <0.000020>
[...snipped (512 write system calls)...]
1466326568.901551 write(3, "\0p\27\0\0\0\0\0\0\320\37\0\0\0\0\0\0\300\33\0\0\0\0\0\0\340\37\0\0\0\0\0"..., 4096) = 4096 <0.000006>
1466326568.901566 close(3) = 0 <0.000008>
[...snipped (many iterations of open, fadvise64, write, close)...]
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
72.55 0.192610 2 84992 write
27.04 0.071788 216 332 fadvise64
0.28 0.000732 4 166 open
0.13 0.000355 2 166 close
------ ----------- ----------- --------- --------- ----------------
100.00 0.265485 85656 total
根據 strace
日誌,我們就可以輕鬆分析這個 fio 測試的 IO Pattern 是如何的了,
首先調用
open
在 Ext4 上以讀寫方式打開 /mnt/test 文件,若不存在則創建一個。因爲 fio job file 指定了文件名,filename=/mnt/test
調用
fadvise64
,使用POSIX_FADV_DONTNEED
把 /mnt/test 在 page cache 裏的數據 flush 到磁盤。fio 做文件 IO 前,清除 /mnt/test 文件的 page cache,可以讓測試避免受到 page cache 影響。
調用
fadvise64
,使用POSIX_FADV_SEQUENTIAL
提示內核應用要對 /mnt/test 做順序 IO 操作。這是因爲 fio job file 定義了 rw=write,因此這是順序寫測試。
調用
write
對 /mnt/test 寫入 4K 大小的數據。一共 write 512 次,共 2M 數據。這是因爲 fio job file 定義了 ioengine=sync,bs=,4k,size=2M。
最後,調用
close
完成一次 /mnt/test 順序寫測試。重複上述過程,反覆迭代。fio job file 定義了 loops=1000000
另外,根據 strace
日誌的系統調用時間和調用次數的總結,我們可以得出如下結論,
- 系統調用
open
,write
和close
的開銷非常小,只有幾微秒。 - 測試中
write
調用次數最多,雖然單次write
只有幾微妙,但積累總時間最高。 - 測試中
fadvise64
調用次數比write
少,但POSIX_FADV_DONTNEED
帶來的 flush page cache 的操作可以達到幾百微秒。
3.2.3 使用 SystemTap
使用 strace
雖然可以拿到單次系統調用讀寫的字節數,但對大量的 IO 請求來說,不經過額外的腳本處理,很難得到一個總體的認識和分析。但是,我們可以通過編寫 SystemTap 腳本來對這個測試的 IO 請求大小做一個宏觀的統計,並且使用直方圖來直觀的呈現這個測試的文件 IO 尺寸分佈。
啓動 fio 測試後,只需要運行如下命令,即可收集到指定 PID 的文件 IO 的統計信息,
$ sudo ./fiohist.stp 94302
starting probe
^C
IO Summary:
read read write write
name open read KB tot B avg write KB tot B avg
fio 7917 0 0 0 3698312 14793248 4096
Write I/O size (bytes):
process name: fio
value |-------------------------------------------------- count
1024 | 0
2048 | 0
4096 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3698312
8192 | 0
16384 | 0
可以看到,直方圖和統計數據顯示,整個跟蹤數據收集期間都是 4K 字節的 write 寫操作,而沒有任何讀操作。而且,在此期間,沒有任何 read
IO 操作。同時,由於 write
系統調用參數並不提供文件內的偏移量,所以我們無法得知文件的寫操作是否是隨機還是順序的。
但是,如果是文件的隨機讀寫 IO,應該可以在 strace
時觀測到 lseek
+ read
或 write
調用。這也從側面可以推測測試是順序寫 IO。此外,pread
和 pwrite
系統調用提供了文件內的偏移量,有這個偏移量的數據,即可根據時間軸畫出 IO 文件內偏移的 Heatmap。
通過該圖,即可直觀地判斷是否是隨機還是順序 IO 了。
本例中的 SystemTap 腳本 fiohist.stp 是作者個人爲分析本測試所編寫。詳細代碼請參考文中給出的源碼鏈接。此外,在 Linux Perf Tools Tips 這篇文章裏收錄了關於在自編譯內核上運行 SystemTap 腳本的一些常見問題。
3.2.4 延遲統計
fio
的測試結果已經提供了詳盡的 IO 延遲數據。因爲 fs_seq_write_sync_001 文件定義的是文件 buffer IO,並且是同步寫模式。因此,fio
報告的延遲數據就是在文件 IO 層面上的,我們不需要使用其它的工具了。
查看 fio
源碼,可以發現,它記錄了一次 IO 流程的三個時間,
起始時間 (io_u->start_time) >>>>>> 觸發時間 (io_u->issue_time) >>>>>> 完成時間 (icd->time)
其具體含義分別如下,
起始時間
在文件打開的狀態下,是讀寫入文件的緩衝區準備好後的時間。源代碼定義:io_u->start_time。
觸發時間
同步 IO 時,是讀寫系統調用發起前的時間。異步 IO 時,是 IO 請求成功放入請求隊列後 (td->io_ops->queue) 返回的時間。源代碼定義:io_u->issue_time。
完成時間
是 IO 完成時的時間。源代碼定義:icd->time。
而在 fio
輸出裏,則存在三種類型的延遲數據,分別爲如下含義,
slat (submission latency)
即 IO 提交延遲。其確切含義是 IO 準備好到 IO 真正開始的時間 (即 io_u->issue_time - io_u->start_time)。需要注意的是,在同步 (SYNC) IO 模式下,slat 並不計算,這是因爲同步 IO 的這兩個時間非常接近,沒有計算意義。
clat (completion latency)
即 IO 完成延遲。其確切含義是 IO 真正開始到 IO 返回的時間 (即 icd->time - io_u->issue_time)。
lat (latency)
即 IO 總延遲,其確切含義是 IO 準備好到 IO 返回的總時間 (即 icd->time - io_u->issue_time)。
有了以上概念,再解讀下面的數據就很簡單了。
例如,本測試裏完成延遲 clat (completion latency) 的結果如下,其中包含了均值 (avg) 和方差 (stdev),
clat (usec): min=0, max=66777, avg= 1.63, stdev=21.57
而總延遲 lat (latency) 結果如下,
lat (usec): min=0, max=66777, avg= 1.68, stdev=21.89
其中,clat percentiles 給出了各種 IO 完成延遲的百分比分佈,
clat percentiles (usec):
| 1.00th=[ 0], 5.00th=[ 1], 10.00th=[ 1], 20.00th=[ 1],
| 30.00th=[ 1], 40.00th=[ 1], 50.00th=[ 2], 60.00th=[ 2],
| 70.00th=[ 2], 80.00th=[ 2], 90.00th=[ 2], 95.00th=[ 3],
| 99.00th=[ 4], 99.50th=[ 7], 99.90th=[ 18], 99.95th=[ 25],
| 99.99th=[ 111]
而總 IO 延遲的百分比分佈也包括在輸出了,
lat (usec) : 2=49.79%, 4=49.08%, 10=0.71%, 20=0.34%, 50=0.06%
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
lat (msec) : 100=0.01%
3.3 On CPU Time 分析
運行 fio 測試期間,我們可以利用 Linux perf
, 對系統做 ON CPU Time 分析。這樣可以進一步獲取如下信息,
在測試中,軟件棧的哪一部分消耗了主要的 CPU 資源。可以幫助我們確定 CPU 時間優化的主要方向。
通過查看消耗 CPU 資源的軟件調用棧,瞭解函數調用關係。
利用可視化工具,如 Flamgraph,對 Profiling 的大量數據做直觀的呈現。方便進一步分析和定位問題。
3.3.1 使用 perf
首先,當 fio 測試進入穩定狀態,運行 perf record
命令,
# perf record -a -g --call-graph dwarf -F 997 sleep 60
其中主要的命令行選項如下,
-F
選項指定perf
以997
次每秒的頻率對 CPU 上運行的用戶進程或者內核上下文進行採樣 (Sampling)。由於 Linux 內核的時鐘中斷是以
1000
次每秒的頻率週期觸發,所以按照997
頻率採樣可以避免每次採樣都採樣到始終中斷相關的處理,減少干擾。-a
選項指定採樣系統中所有的 CPU。-g
選項指定記錄下用戶進程或者內核的調用棧。其中,
--call-graph dwarf
指定調用棧收集的方式爲dwarf
,即libdwarf
和libdunwind
的方式。Perf 還支持fp
和lbs
方式。sleep 60
則是通過perf
指定運行的命令,這個命令起到了讓perf
運行 60 秒然後退出的效果。
在 perf record
之後,運行 perf report
查看採樣結果的彙總,
# sudo perf report --stdio
[...snipped...]
27.51% 0.10% fio [kernel.kallsyms] [k] __generic_file_write_iter
|
---__generic_file_write_iter
|
|--99.95%-- ext4_file_write_iter
| __vfs_write
| vfs_write
| sys_write
| do_syscall_64
| return_from_SYSCALL_64
| 0x7ff91cd381cd
| fio_syncio_queue
| td_io_queue
| thread_main
| run_threads
--0.05%-- [...]
[...snipped...]
3.3.2 使用 Flamegraph
使用 Flamegraph,可以把前面產生的 perf record
的結果可視化,生成火焰圖。
運行如下命令,
# perf script | stackcollapse-perf.pl > out.perf-folded
# cat out.perf-folded | flamegraph.pl > flamegraph_on_cpu_perf_fs_seq_write_sync_001.svg
然後,即可生成如下火焰圖,
該火焰圖是 SVG 格式的矢量圖,基於 XML 文件定義。在瀏覽器裏右擊在新窗口打開圖片,即可進入與火焰圖的交互模式。該模式下,統計數據信息和縮放功能都可以移動和點擊鼠標來完成交互。通過在交互模式下瀏覽和縮放火焰圖,我們可以得出如下結論,
perf record
共有 119644 個採樣數據,將此定義爲 100% CPU 時間。fio
進程共有 91079 個採樣數據,佔用 76.13% 的 CPU 時間。fio
的fio_syncio_queue
用掉了 48.53% 的 CPU,其中絕大部分時間在內核態,sys_write
系統調用就消耗了 45.78%。fio
的file_invalidate_cache
函數佔用了 20.88% 的 CPU,其中大部分都在內核態,sys_fadvise64
系統調用消耗了 20.81%。在這裏我們注意到,
sys_write
和sys_fadvise64
系統調用 CPU 佔用資源的比例是 2:1。而之前strace
得出的兩個系統調用消耗時間的比例是 3:1。這就意味着,sys_write
花費了很多時間在睡眠態。在 Ext4 文件系統的寫路徑,存在熱點鎖。
ext4_file_write_iter
函數裏的 inode mutex 的 mutex 自旋等待時間,佔用了 16.93% 的 CPU。與sys_write
系統調用相比,CPU 消耗佔比達到三分之一強。swapper
爲內核上下文,包含如下部分,native_safe_halt
代表 CPU 處於 IDEL 狀態,共有兩次,9.04% 和 9.18%。smp_reschedule_interrupt
代表 CPU 處理調度器的 IPI 中斷,用於處理器間調度的負載均衡。共有兩次,1.66% 和 1.61%。這部分需要方大矢量圖移動鼠標到相關函數才能看到。kblockd
工作隊列線程。由
block_run_queue_async
觸發,最終調用__blk_run_queue
把 IO 發送到下層的 sampleblk 塊驅動。共有兩部份,合計 0.88%。rcu_gp_kthread
處理 RCU 的內核線程,佔用 0.04 % 的 CPU 時間。
綜合以上分析,我們可以看到,火焰圖不但可以幫助我們理解 CPU 全局資源的佔用情況,而且還能進一步分析到微觀和細節。例如局部的熱鎖,父子函數的調用關係,和所佔 CPU 時間比例。
關於進一步的 Flamegraph 的介紹和資料,請參考 Brenden Gregg 的 Flamegraph 相關資源。
4. 小結
本文通過使用 Linux 下的各種追蹤工具 Strace,Systemtap,Perf,Ftrace,來分析 fio 測試的運行情況。實際上,利用 Linux 下的動態追蹤工具我們達到了以下目的,
- 掌握了本文中 fio 測試的主要特徵,文件 IO size,IO 時間分佈。這是性能分析裏 workload analysis 方法的一部分。
- 瞭解了 fio 測試 On CPU 時間的分析方法。這是性能分析裏 resource analysis 方法的一部分。
關於 Linux 動態追蹤工具的更多信息,請參考延伸閱讀章節裏的鏈接。