IO寫流程分析

IO寫流程分析

IO 寫流程圖示:[

系統調用

read系統調用的處理分爲用戶空間和內核空間處理兩部分。其中,用戶空間處理只是通過0x80中斷陷入內核,接着調用其中斷服務例程,即sys_read以進入內核處理流程。

對於read系統調用在內核的處理,如上圖所述,經過了VFS、具體文件系統,如ext2、頁高速緩衝存層、通用塊層、IO調度層、設備驅動層、和設備層。其中,VFS主要是用來屏蔽下層具體文件系統操作的差異,對上提供一個統一接口,正是因爲有了這個層次,所以可以把設備抽象成文件。具體文件系統,則定義了自己的塊大小、操作集合等。引入cache層的目的,是爲了提高IO效率。它緩存了磁盤上的部分數據,當請求到達時,如果在cache中存在該數據且是最新的,則直接將其傳遞給用戶程序,免除了對底層磁盤的操作。通用塊層的主要工作是,接收上層發出的磁盤請求,並最終發出IO請求(BIO)。IO調度層則試圖根據設置好的調度算法對通用塊層的bio請求合併和排序,回調驅動層提供的請求處理函數,以處理具體的IO請求。驅動層的驅動程序對應具體的物理設備,它從上層取出IO請求,並根據該IO請求中指定的信息,通過向具體塊設備的設備控制器發送命令的方式,來操縱設備傳輸數據。設備層都是具體的物理設備。

在這裏插入圖片描述

VFS層:

內核函數sys_read是read系統調用在該層的入口點。它根據文件fd指定的索引,從當前進程描述符中取出相應的file對象,並調用vfs_read執行文件讀取操作。vfs_read會調用與具體文件相關的read函數執行讀取操作,file->f_op.read。然後,VFS將控制權交給了ext2文件系統。(ext2在此作爲示例,進行解析)

pdflush

在linux操作系統中,寫操作是異步的,即寫操作返回的時候數據並沒有真正寫到磁盤上,而是先寫到了系統cache裏,隨後由pdflush內核線程將系統中的髒頁寫到磁盤上,在下面幾種情況下,
系統會喚醒pdflush回寫髒頁:

  1. 定時方式:
    定時機制定時喚醒pdflush內核線程,週期爲/proc/sys/vm/dirty_writeback_centisecs ,單位
    是(1/100)秒,每次週期性喚醒的pdflush線程並不是回寫所有的髒頁,而是隻回寫變髒時間超過
    /proc/sys/vm/dirty_expire_centisecs(單位也是1/100秒)。
    注意:變髒的時間是以文件的inode節點變髒的時間爲基準的,也就是說如果某個inode節點是10秒前變髒的,
    pdflush就認爲這個inode對應的所有髒頁的變髒時間都是10秒前,即使可能部分頁面真正變髒的時間不到10秒,
    細節可以查看內核函數wb_kupdate()。
  2. 內存不足的時候:
    這時並不將所有的dirty頁寫到磁盤,而是每次寫大概1024個頁面,直到空閒頁面滿足需求爲止。
  3. 寫操作時發現髒頁超過一定比例:
    當髒頁佔系統內存的比例超過 /proc/sys/vm/dirty_background_ratio 的時候,write系統調用會喚醒
    pdflush回寫dirty page,直到髒頁比例低於/proc/sys/vm/dirty_background_ratio,但write系統調
    用不會被阻塞,立即返回。當髒頁佔系統內存的比例超過/proc/sys/vm/dirty_ratio的時候, write系
    統調用會被被阻塞,主動回寫dirty page,直到髒頁比例低於/proc/sys/vm/dirty_ratio,這一點在
    2.4內核中是沒有的。
  4. 用戶調用sync系統調用:
    這是系統會喚醒pdflush直到所有的髒頁都已經寫到磁盤爲止。

IO Scheduler

每個塊設備或者塊設備的分區,都對應有自身的請求隊列(request_queue),而每個請求隊列都可以選擇一個I/O調度器來協調所遞交的request。I/O調度器的基本目的是將請求按照它們對應在塊設備上的扇區號進行排列,以減少磁頭的移動,提高效率。每個設備的請求隊列裏的請求將按順序被響應。實際上,除了這個隊列,每個調度器自身都維護有不同數量的隊列,用來對遞交上來的request進行處理,而排在隊列最前面的request將適時被移動到請求隊列中等待響應。

內核中實現的IO調度器主要有四種–Noop,Deadline,CFG, Anticipatory。

1. Noop算法

Noop調度算法是內核中最簡單的IO調度算法。Noop調度算法也叫作電梯調度算法,它將IO請求放入到一個FIFO隊列中,然後逐個執行這些IO請求,當然對於一些在磁盤上連續的IO請求,Noop算法會適當做一些合併。這個調度算法特別適合那些不希望調度器重新組織IO請求順序的應用。

​ 這種調度算法在以下場景中優勢比較明顯:

​ 1)在IO調度器下方有更加智能的IO調度設備。如果您的Block Device Drivers是Raid,或者SAN,NAS等存儲設備,這些設備會更好地組織IO請求,不用IO調度器去做額外的調度工作;

​ 2)上層的應用程序比IO調度器更懂底層設備。或者說上層應用程序到達IO調度器的IO請求已經是它經過精心優化的,那麼IO調度器就不需要畫蛇添足,只需要按序執行上層傳達下來的IO請求即可。

​ 3)對於一些非旋轉磁頭氏的存儲設備,使用Noop的效果更好。因爲對於旋轉磁頭式的磁盤來說,IO調度器的請求重組要花費一定的CPU時間,但是對於SSD磁盤來說,這些重組IO請求的CPU時間可以節省下來,因爲SSD提供了更智能的請求調度算法,不需要內核去畫蛇添足。這篇文章提及了SSD中使用Noop效果會更好。

2. Deadline算法

​ Deadline算法的核心在於保證每個IO請求在一定的時間內一定要被服務到,以此來避免某個請求飢餓。

​ Deadline算法中引入了四個隊列,這四個隊列可以分爲兩類,每一類都由讀和寫兩類隊列組成,一類隊列用來對請求按起始扇區序號進行排序,通過紅黑樹來組織,稱爲sort_list;另一類對請求按它們的生成時間進行排序,由鏈表來組織,稱爲fifo_list。每當確定了一個傳輸方向(讀或寫),那麼將會從相應的sort_list中將一批連續請求dispatch到requst_queue的請求隊列裏,具體的數目由fifo_batch來確定。只有下面三種情況纔會導致一次批量傳輸的結束:

​ 1)對應的sort_list中已經沒有請求了

​ 2)下一個請求的扇區不滿足遞增的要求

​ 3)上一個請求已經是批量傳輸的最後一個請求了。

​ 所有的請求在生成時都會被賦上一個期限值(根據jiffies),並按期限值排序在fifo_list中,讀請求的期限時長默認爲爲500ms,寫請求的期限時長默認爲5s,可以看出內核對讀請求是十分偏心的,其實不僅如此,在deadline調度器中,還定義了一個starved和writes_starved,writes_starved默認爲2,可以理解爲寫請求的飢餓線,內核總是優先處理讀請求,starved表明當前處理的讀請求批數,只有starved超過了writes_starved後,纔會去考慮寫請求。因此,假如一個寫請求的期限已經超過,該請求也不一定會被立刻響應,因爲讀請求的batch還沒處理完,即使處理完,也必須等到starved超過writes_starved纔有機會被響應。爲什麼內核會偏袒讀請求?這是從整體性能上進行考慮的。讀請求和應用程序的關係是同步的,因爲應用程序要等待讀取的內容完畢,才能進行下一步工作,因此讀請求會阻塞進程,而寫請求則不一樣,應用程序發出寫請求後,內存的內容何時寫入塊設備對程序的影響並不大,所以調度器會優先處理讀請求。

​ 默認情況下,讀請求的超時時間是500ms,寫請求的超時時間是5s。

這篇文章說在一些多線程應用下,Deadline算法比CFQ算法好。這篇文章說在一些數據庫應用下,Deadline算法比CFQ算法。

3. Anticipatory算法

​ Anticipatory算法的核心是局部性原理,它期望一個進程昨晚一次IO請求後還會繼續在此處做IO請求。在IO操作中,有一種現象叫“假空閒”(Deceptive idleness),它的意思是一個進程在剛剛做完一波讀操作後,看似是空閒了,不讀了,但是實際上它是在處理這些數據,處理完這些數據之後,它還會接着讀,這個時候如果IO調度器去處理另外一個進程的請求,那麼當原來的假空閒進程的下一個請求來的時候,磁頭又得seek到剛纔的位置,這樣大大增加了尋道時間和磁頭旋轉時間。所以,Anticipatory算法會在一個讀請求做完後,再等待一定時間t(通常是6ms),如果6ms內,這個進程上還有讀請求過來,那麼我繼續服務,否則,處理下一個進程的讀寫請求。

​ 在一些場景下,Antocipatory算法會有非常有效的性能提升。這篇文章有說,這篇文章也有一份評測。

​ 值得一提的是,Anticipatory算法從Linux 2.6.33版本後,就被移除了,因爲CFQ通過配置也能達到Anticipatory算的效果。

4. CFQ算法

​ CFQ(Completely Fair Queuing)算法,顧名思義,絕對公平算法。它試圖爲競爭塊設備使用權的所有進程分配一個請求隊列和一個時間片,在調度器分配給進程的時間片內,進程可以將其讀寫請求發送給底層塊設備,當進程的時間片消耗完,進程的請求隊列將被掛起,等待調度。 每個進程的時間片和每個進程的隊列長度取決於進程的IO優先級,每個進程都會有一個IO優先級,CFQ調度器將會將其作爲考慮的因素之一,來確定該進程的請求隊列何時可以獲取塊設備的使用權。IO優先級從高到低可以分爲三大類:RT(real time),BE(best try),IDLE(idle),其中RT和BE又可以再劃分爲8個子優先級。實際上,我們已經知道CFQ調度器的公平是針對於進程而言的,而只有同步請求(read或syn write)纔是針對進程而存在的,他們會放入進程自身的請求隊列,而所有同優先級的異步請求,無論來自於哪個進程,都會被放入公共的隊列,異步請求的隊列總共有8(RT)+8(BE)+1(IDLE)=17個。

​ 從Linux 2.6.18起,CFQ作爲默認的IO調度算法。

​ 對於通用的服務器來說,CFQ是較好的選擇。

對於使用哪種調度算法來說,還是要根據具體的業務場景去做足benchmark來選擇,不能僅靠別的文字來決定。

5. 更改IO調度算法

在RHEL5/OEL5以及之後的版本中(比如RHEL6和RHEL7),可以針對每塊磁盤制定I/O Scheduler,修改完畢立刻生效,比如:

$ cat /sys/block/sda1/queue/scheduler
[noop] anticipatory d#dline cfq

#修改爲cfq
$ echo 'cfq'>/sys/block/sda1/queue/scheduler

#立刻生效
$ cat /sys/block/sda1/queue/scheduler
noop anticipatory deadline [cfq]

6. 一些磁盤相關的內核參數

/sys/block/sda/queue/nr_requests 磁盤隊列長度。默認只有 128 個隊列,可以提高到 512 個.會更加佔用內存,但能更加多的合併讀寫操作,速度變慢,但能讀寫更加多的量

/sys/block/sda/queue/iosched/antic_expire 等待時間 。讀取附近產生的新請時等待多長時間

/sys/block/sda/queue/read_ahead_kb

​ 這個參數對順序讀非常有用,意思是,一次提前讀多少內容,無論實際需要多少.默認一次讀 128kb 遠小於要讀的,設置大些對讀大文件非常有用,可以有效的減少讀 seek 的次數,這個參數可以使用 blockdev –setra 來設置,setra 設置的是多少個扇區,所以實際的字節是除以2,比如設置 512 ,實際是讀 256 個字節.

/proc/sys/vm/dirty_ratio

這個參數控制文件系統的文件系統寫緩衝區的大小,單位是百分比,表示系統內存的百分比,表示當寫緩衝使用到系統內存多少的時候,開始向磁盤寫出數 據.增大之會使用更多系統內存用於磁盤寫緩衝,也可以極大提高系統的寫性能.但是,當你需要持續、恆定的寫入場合時,應該降低其數值,一般啓動上缺省是 10.下面是增大的方法: echo ’40’>

/proc/sys/vm/dirty_background_ratio

這個參數控制文件系統的pdflush進程,在何時刷新磁盤.單位是百分比,表示系統內存的百分比,意思是當寫緩衝使用到系統內存多少的時候, pdflush開始向磁盤寫出數據.增大之會使用更多系統內存用於磁盤寫緩衝,也可以極大提高系統的寫性能.但是,當你需要持續、恆定的寫入場合時,應該降低其數值,一般啓動上缺省是 5.下面是增大的方法: echo ’20’ >

/proc/sys/vm/dirty_writeback_centisecs

​ 這個參數控制內核的髒數據刷新進程pdflush的運行間隔.單位是 1/100 秒.缺省數值是500,也就是 5 秒.如果你的系統是持續地寫入動作,那麼實際上還是降低這個數值比較好,這樣可以把尖峯的寫操作削平成多次寫操作.設置方法如下: echo ‘200’ > /proc/sys/vm/dirty_writeback_centisecs 如果你的系統是短期地尖峯式的寫操作,並且寫入數據不大(幾十M/次)且內存有比較多富裕,那麼應該增大此數值: echo ‘1000’ > /proc/sys/vm/dirty_writeback_centisecs

/proc/sys/vm/dirty_expire_centisecs

這個參數聲明Linux內核寫緩衝區裏面的數據多“舊”了之後,pdflush進程就開始考慮寫到磁盤中去.單位是 1/100秒.缺省是 30000,也就是 30 秒的數據就算舊了,將會刷新磁盤.對於特別重載的寫操作來說,這個值適當縮小也是好的,但也不能縮小太多,因爲縮小太多也會導致IO提高太快.建議設置爲 1500,也就是15秒算舊. echo ‘1500’ > /proc/sys/vm/dirty_expire_centisecs 當然,如果你的系統內存比較大,並且寫入模式是間歇式的,並且每次寫入的數據不大(比如幾十M),那麼這個值還是大些的好.

IO性能分析命令

近期要在公司內部做個Linux IO方面的培訓, 整理下手頭的資料給大家分享下

img

​ 各種IO監視工具在Linux IO 體系結構中的位置

​ 源自 Linux Performance and Tuning Guidelines.pdf

1 系統級IO監控

iostat

iostat -xdm 1 # 個人習慣

img

%util 代表磁盤繁忙程度。100% 表示磁盤繁忙, 0%表示磁盤空閒。但是注意,磁盤繁忙不代表磁盤(帶寬)利用率高

argrq-sz 提交給驅動層的IO請求大小,一般不小於4K,不大於max(readahead_kb, max_sectors_kb)

​ 可用於判斷當前的IO模式,一般情況下,尤其是磁盤繁忙時, 越大代表順序,越小代表隨機

svctm 一次IO請求的服務時間,對於單塊盤,完全隨機讀時,基本在7ms左右,既尋道+旋轉延遲時間

注: 各統計量之間關係

=======================================

%util = ( r/s + w/s) * svctm / 1000 # 隊列長度 = 到達率 * 平均服務時間
avgrq-sz = ( rMB/s + wMB/s) * 2048 / (r/s + w/s) # 2048 爲 1M / 512

=======================================

總結:

iostat 統計的是通用塊層經過合併(rrqm/s, wrqm/s)後,直接向設備提交的IO數據,可以反映系統整體的IO狀況,但是有以下2個缺點:

1 距離業務層比較遙遠,跟代碼中的write,read不對應(由於系統預讀 + pagecache + IO調度算法等因素, 也很難對應)

2 是系統級,沒辦法精確到進程,比如只能告訴你現在磁盤很忙,但是沒辦法告訴你是誰在忙,在忙什麼?

2 進程級IO監控

iotop 和 pidstat (僅rhel6u系列)

iotop 顧名思義, io版的top

pidstat 顧名思義, 統計進程(pid)的stat,進程的stat自然包括進程的IO狀況

這兩個命令,都可以按進程統計IO狀況,因此可以回答你以下二個問題

    1. 當前系統哪些進程在佔用IO,百分比是多少?
    2. 佔用IO的進程是在讀?還是在寫?讀寫量是多少?

pidstat 參數很多,僅給出幾個個人習慣

​ pidstat -d 1 #只顯示IO

img

​ pidstat -u -r -d -t 1 # -d IO 信息,

​ # -r 缺頁及內存信息
​ # -u CPU使用率
​ # -t 以線程爲統計單位
​ # 1 1秒統計一次

iotop, 很簡單,直接敲命令

img

block_dump, iodump

iotop 和 pidstat 用着很爽,但兩者都依賴於/proc/pid/io文件導出的統計信息, 這個對於老一些的內核是沒有的,比如rhel5u2

因此只好用以上2個窮人版命令來替代:

echo 1 > /proc/sys/vm/block_dump # 開啓block_dump,此時會把io信息輸入到dmesg中

​ # 源碼: submit_bio@ll_rw_blk.c:3213

watch -n 1 “dmesg -c | grep -oP “\w+\d+\d+: (WRITE|READ)” | sort | uniq -c”

​ # 不停的dmesg -c

echo 0 > /proc/sys/vm/block_dump # 不用時關閉

也可以使用現成的腳本 iodump, 具體參見 http://code.google.com/p/maatkit/source/browse/trunk/util/iodump?r=5389

iotop.stp

systemtap腳本,一看就知道是iotop命令的窮人複製版,需要安裝Systemtap, 默認每隔5秒輸出一次信息

stap iotop.stp # examples/io/iotop.stp

總結

進程級IO監控 ,

  1. 可以回答系統級IO監控不能回答的2個問題
  2. 距離業務層相對較近(例如,可以統計進程的讀寫量)

但是也沒有辦法跟業務層的read,write聯繫在一起,同時顆粒度較粗,沒有辦法告訴你,當前進程讀寫了哪些文件? 耗時? 大小 ?

3 業務級IO監控

​ ioprofile

​ ioprofile 命令本質上是 lsof + strace, 具體下載可見 http://code.google.com/p/maatkit/

​ ioprofile 可以回答你以下三個問題:

​ 1 當前進程某時間內,在業務層面讀寫了哪些文件(read, write)?

​ 2 讀寫次數是多少?(read, write的調用次數)

​ 3 讀寫數據量多少?(read, write的byte數)

​ 假設某個行爲會觸發程序一次IO動作,例如: “一個頁面點擊,導致後臺讀取A,B,C文件”

============================================

​ ./io_event # 假設模擬一次IO行爲,讀取A文件一次, B文件500次, C文件500次

​ ioprofile -p pidof io_event -c count # 讀寫次數

img

​ ioprofile -p pidof io_event -c times # 讀寫耗時

img

ioprofile  -p  `pidof  io_event` -c sizes    # 讀寫大小

img

​ 注: ioprofile 僅支持多線程程序,對單線程程序不支持. 對於單線程程序的IO業務級分析,strace足以。

​ 總結:

​ ioprofile本質上是strace,因此可以看到read,write的調用軌跡,可以做業務層的io分析(mmap方式無能爲力)

4 文件級IO監控

​ 文件級IO監控可以配合/補充"業務級和進程級"IO分析

​ 文件級IO分析,主要針對單個文件, 回答當前哪些進程正在對某個文件進行讀寫操作.

​ 1 lsof 或者 ls /proc/pid/fd

​ 2 inodewatch.stp

lsof 告訴你 當前文件由哪些進程打開

lsof …/io # io目錄 當前由 bash 和 lsof 兩個進程打開

img

lsof 命令 只能回答靜態的信息, 並且"打開" 並不一定"讀取", 對於 cat ,echo這樣的命令, 打開和讀取都是瞬間的,lsof很難捕捉

可以用 inodewatch.stp 來彌補

stap inodewatch.stp major minor inode # 主設備號, 輔設備號, 文件inode節點號

stap inodewatch.stp 0xfd 0x00 523170 # 主設備號, 輔設備號, inode號,可以通過 stat 命令獲得

img

5 IO模擬器

iotest.py # 見附錄

開發人員可以 利用 ioprofile (或者 strace) 做詳細分析系統的IO路徑,然後在程序層面做相應的優化。

但是一般情況下調整程序,代價比較大,尤其是當不確定修改方案到底能不能有效時,最好有某種模擬途徑以快速驗證。

以爲我們的業務爲例,發現某次查詢時,系統的IO訪問模式如下:

訪問了A文件一次

訪問了B文件500次, 每次16字節, 平均間隔 502K

訪問了C文件500次, 每次200字節, 平均間隔 4M

這裏 B,C文件是交錯訪問的, 既

1 先訪問B,讀16字節,

2 再訪問C,讀200字節,

3 回到B,跳502K後再讀16字節,

4 回到C,跳4M後,再讀200字節

5 重複500次

strace 文件如下:

img

一個簡單樸素的想法, 將B,C交錯讀,改成先批量讀B , 再批量讀C,因此調整strace 文件如下:

img

將調整後的strace文件, 作爲輸入交給 iotest.py, iotest.py 按照 strace 文件中的訪問模式, 模擬相應的IO

iotest.py -s io.strace -f fmap

fmap 爲映射文件,將strace中的222,333等fd,映射到實際的文件中

===========================

111 = /opt/work/io/A.data
222 = /opt/work/io/B.data

333 = /opt/work/io/C.data

6 磁盤碎片整理

一句話: 只要磁盤容量不常年保持80%以上,基本上不用擔心碎片問題。

如果實在擔心,可以用 defrag 腳本

7 其他IO相關命令

blockdev 系列

=======================================

blockdev --getbsz /dev/sdc1 # 查看sdc1盤的塊大小

block blockdev --getra /dev/sdc1 # 查看sdc1盤的預讀(readahead_kb)大小

blockdev --setra 256 /dev/sdc1 # 設置sdc1盤的預讀(readahead_kb)大小,低版的內核通過/sys設置,有時會失敗,不如blockdev靠譜

=======================================

IO寫流程分析二

在Linux 開發中,有幾個關係到性能的東西,技術人員非常關注:進程,CPU,MEM,網絡IO,磁盤IO。本篇文件打算詳細全面,深入淺出。剖析文件IO的細節。從多個角度探索如何提高IO性能。本文儘量用通俗易懂的視角去闡述。不copy內核代碼。

​ 闡述之前,要先有個大視角,讓我們站在萬米高空,鳥瞰我們的文件IO,它們設計是分層的,分層有2個好處,一是架構清晰,二是解耦。讓我們看一下下面這張圖。

clip_image002

​ 圖一

穿越各層寫文件方式

程序的最終目的是要把數據寫到磁盤上, 但是系統從通用性和性能角度,儘量提供一個折中的方案來保證這些。讓我們來看一個最常用的寫文件典型example,也是路徑最長的IO。

{
    char *buf = malloc(MAX_BUF_SIZE);
    strncpy(buf, src, , MAX_BUF_SIZE);
    fwrite(buf, MAX_BUF_SIZE, 1, fp);
    fclose(fp);
}

這裏malloc的buf對於圖層中的application buffer,即應用程序的buffer;調用fwrite後,把數據從application buffer 拷貝到了 CLib buffer,即C庫標準IObuffer。fwrite返回後,數據還在CLib buffer,如果這時候進程core掉。這些數據會丟失。沒有寫到磁盤介質上。當調用fclose的時候,fclose調用會把這些數據刷新到磁盤介質上。除了fclose方法外,還有一個主動刷新操作fflush 函數,不過fflush函數只是把數據從CLib buffer 拷貝到page cache 中,並沒有刷新到磁盤上,從page cache刷新到磁盤上可以通過調用fsync函數完成。

從上面類子看到,一個常用的fwrite函數過程,基本上歷經千辛萬苦,數據經過多次copy,纔到達目的地。有人心生疑問,這樣會提高性能嗎,反而會降低性能吧。這個問題先放一放。

有人說,我不想通過fwrite+fflush這樣組合,我想直接寫到page cache。這就是我們常見的文件IO調用read/write函數。這些函數基本上是一個函數對應着一個系統調用,如sys_read/sys_write. 調用write函數,是直接通過系統調用把數據從應用層拷貝到內核層,從application buffer 拷貝到 page cache 中。

系統調用,write會觸發用戶態/內核態切換?是的。那有沒有辦法避免這些消耗。這時候該mmap出場了,mmap把page cache 地址空間映射到用戶空間,應用程序像操作應用層內存一樣,寫文件。省去了系統調用開銷。

那如果繼續刨根問底,如果想繞過page cache,直接把數據送到磁盤設備上怎麼辦。通過open文件帶上O_DIRECT參數,這是write該文件。就是直接寫到設備上。

如果繼續較勁,直接寫扇區有沒有辦法。這就是所謂的RAW設備寫,繞開了文件系統,直接寫扇區,想fdsik,dd,cpio之類的工具就是這一類操作。

IO調用鏈

列舉了上述各種穿透各種cache 層寫操作,可以看到系統提供的接口相當豐富,滿足你各種寫要求。下面通過講解圖一,瞭解一下文件IO的調用鏈。

fwrite是系統提供的最上層接口,也是最常用的接口。它在用戶進程空間開闢一個buffer,將多次小數據量相鄰寫操作先緩存起來,合併,最終調用write函數一次性寫入(或者將大塊數據分解多次write調用)。

Write函數通過調用系統調用接口,將數據從應用層copy到內核層,所以write 會觸發內核態 / 用戶態切換。當數據到達page cache後,內核並不會立即把數據往下傳遞。而是返回用戶空間。數據什麼時候寫入硬盤,有內核IO調度決定,所以write 是一個異步調用。這一點和read不同,read調用是先檢查page cache裏面是否有數據,如果有,就取出來返回用戶,如果沒有,就同步傳遞下去並等待有數據,再返回用戶,所以read是一個同步過程。當然你也可以把write的異步過程改成同步過程,就是在open文件的時候帶上O_SYNC標記。

數據到了page cache後,內核有pdflush線程在不停的檢測髒頁,判斷是否要寫回到磁盤中。把需要寫回的頁提交到IO隊列——即IO調度隊列。又IO調度隊列調度策略調度何時寫回。

提到IO調度隊列,不得不提一下磁盤結構。這裏要講一下,磁頭和電梯一樣,儘量走到頭再回來,避免來回搶佔是跑,磁盤也是單向旋轉,不會反覆逆時針順時針轉的。從網上copy一個圖下來,具體這裏就不介紹。

clip_image003

IO隊列有2個主要任務。一是合併相鄰扇區的,而是排序。合併相信很容易理解,排序就是儘量按照磁盤選擇方向和磁頭前進方向排序。因爲磁頭尋道時間是和昂貴的。

這裏IO隊列和我們常用的分析工具IOStat關係密切。IOStat中rrqm/s wrqm/s表示讀寫合併個數。avgqu-sz表示平均隊列長度。

內核中有多種IO調度算法。當硬盤是SSD時候,沒有什麼磁道磁頭,人家是隨機讀寫的,加上這些調度算法反而畫蛇添足。OK,剛好有個調度算法叫noop調度算法,就是什麼都不錯(合併是做了)。剛好可以用來配置SSD硬盤的系統。

從IO隊列出來後,就到了驅動層(當然內核中有更多的細分層,這裏忽略掉),驅動層通過DMA,將數據寫入磁盤cache。

至於磁盤cache時候寫入磁盤介質,那是磁盤控制器自己的事情。如果想要睡個安慰覺,確認要寫到磁盤介質上。就調用fsync函數吧。可以確定寫到磁盤上了。

一致性和安全性

談完調用細節,再將一下一致性問題和安全問題。既然數據沒有到到磁盤介質前,可能處在不同的物理內存cache中,那麼如果出現進程死機,內核死,掉電這樣事件發生。數據會丟失嗎。

當進程死機後:只有數據還處在application cache或CLib cache時候,數據會丟失。數據到了page cache。進程core掉,即使數據還沒有到硬盤。數據也不會丟失。

當內核core掉後,只要數據沒有到達disk cache,數據都會丟失。

掉電情況呢,哈哈,這時候神也救不了你,哭吧。

那麼一致性呢,如果兩個進程或線程同時寫,會寫亂嗎?或A進程寫,B進程讀,會寫髒嗎?

文章寫到這裏,寫得太長了,就舉出各種各樣的例子。講一下大概判斷原則吧。fwrite操作的buffer是在進程私有空間,兩個線程讀寫,肯定需要鎖保護的。如果進程,各有各的地址空間。是否要加鎖,看應用場景。

write操作如果寫大小小於PIPE_BUF(一般是4096),是原子操作,能保證兩個進程“AAA”,“BBB”寫操作,不會出現“ABAABB”這樣的數據交錯。O_APPEND 標誌能保證每次重新計算pos,寫到文件尾的原子性。

數據到了內核層後,內核會加鎖,會保證一致性的。

性能問題

性能從系統層面和設備層面去分析;磁盤的物理特性從根本上決定了性能。IO的調度策略,系統調用也是致命殺手。

磁盤的尋道時間是相當的慢,平均尋道時間大概是在10ms,也就是是每秒只能100-200次尋道。

磁盤轉速也是影響性能的關鍵,目前最快15000rpm,大概就每秒500轉,滿打滿算,就讓磁頭不尋道,設想所有的數據連續存放在一個柱面上。大家可以算一下每秒最多可以讀多少數據。當然這個是理論值。一般情況下,盤片轉太快,磁頭感應跟不上,所以需要轉幾圈才能完全讀出磁道內容。

另外設備接口總線傳輸率是實際速率的上限。

另外有些等密度磁盤,磁盤外圍磁道扇區多,線速度快,如果頻繁操作的數據放在外圍扇區,也能提高性能。

利用多磁盤併發操作,也不失爲提高性能的手段。

這裏給個業界經驗值:機械硬盤順序寫~30MB,順序讀取速率一般~50MB好的可以達到100多M, SSD讀達到~400MB,SSD寫性能和機械硬盤差不多。

Ps:

O_DIRECTRAW 設備最根本的區別是O_DIRECT是基於文件系統的,也就是在應用層來看,其操作對象是文件句柄,內核和文件層來看,其操作是基於 inode 和數據塊,這些概念都是和 **ext2/3 的文件系統相關,寫到磁盤上最終是 ext3 **文件。而 RAW 設備寫是沒有文件系統概念,操作的是扇區號,操作對象是扇區,寫出來的東西不一定是 ext3 文件(如果按照 ext3 規則寫就是 ext3 文件)。 一般基於 O_DIRECT 來設計優化自己的文件模塊,是不滿系統的 cache 和調度策略,自己在應用層實現這些,來制定自己特有的業務特色文件讀寫。但是寫出來的東西是ext3文件,該磁盤卸下來,mount 到其他任何 linux 系統上,都可以查看。 而基於 RAW 設備的設計系統,一般是不滿現有 ext3 的諸多缺陷,設計自己的文件系統。自己設計文件佈局和索引方式。舉個極端例子:把整個磁盤做一個文件來寫,不要索引。這樣沒有 inode 限制,沒有文件大小限制,磁盤有多大,文件就能多大。這樣的磁盤卸下來,mount 到其他 linux 系統上,是無法識別其數據的。 兩者都要通過驅動層讀寫;在系統引導啓動,還處於實模式的時候,可以通過 bios 接口讀寫 raw 設備。

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