Linux 塊設備驅動 (2)

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 是如何的了,

  1. 首先調用 open 在 Ext4 上以讀寫方式打開 /mnt/test 文件,若不存在則創建一個。

    因爲 fio job file 指定了文件名,filename=/mnt/test

  2. 調用 fadvise64,使用 POSIX_FADV_DONTNEED 把 /mnt/test 在 page cache 裏的數據 flush 到磁盤。

    fio 做文件 IO 前,清除 /mnt/test 文件的 page cache,可以讓測試避免受到 page cache 影響。

  3. 調用 fadvise64,使用 POSIX_FADV_SEQUENTIAL 提示內核應用要對 /mnt/test 做順序 IO 操作。

    這是因爲 fio job file 定義了 rw=write,因此這是順序寫測試。

  4. 調用 write 對 /mnt/test 寫入 4K 大小的數據。一共 write 512 次,共 2M 數據。

    這是因爲 fio job file 定義了 ioengine=sync,bs=,4k,size=2M。

  5. 最後,調用 close 完成一次 /mnt/test 順序寫測試。重複上述過程,反覆迭代。

    fio job file 定義了 loops=1000000

另外,根據 strace 日誌的系統調用時間和調用次數的總結,我們可以得出如下結論,

  • 系統調用 openwriteclose 的開銷非常小,只有幾微秒。
  • 測試中 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 + readwrite 調用。這也從側面可以推測測試是順序寫 IO。此外,preadpwrite 系統調用提供了文件內的偏移量,有這個偏移量的數據,即可根據時間軸畫出 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 選項指定 perf997 次每秒的頻率對 CPU 上運行的用戶進程或者內核上下文進行採樣 (Sampling)。

    由於 Linux 內核的時鐘中斷是以 1000 次每秒的頻率週期觸發,所以按照 997 頻率採樣可以避免每次採樣都採樣到始終中斷相關的處理,減少干擾。

  • -a 選項指定採樣系統中所有的 CPU。

  • -g 選項指定記錄下用戶進程或者內核的調用棧。

    其中,--call-graph dwarf 指定調用棧收集的方式爲 dwarf,即 libdwarflibdunwind 的方式。Perf 還支持 fplbs 方式。

  • 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 時間。

    fiofio_syncio_queue 用掉了 48.53% 的 CPU,其中絕大部分時間在內核態,sys_write 系統調用就消耗了 45.78%。

    fiofile_invalidate_cache 函數佔用了 20.88% 的 CPU,其中大部分都在內核態,sys_fadvise64 系統調用消耗了 20.81%。

    在這裏我們注意到,sys_writesys_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 動態追蹤工具的更多信息,請參考延伸閱讀章節裏的鏈接。

5. 延伸閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章