iscsi:IO操作流程(二)
上次我們討論了iscsi initiator IO操作需要經過的各個層次,以及每層所涉及的IO資料結構的變化。今天主要討論IO如何形成SCSI指令並下發的。
我們知道在通用塊層,IO最終放在request_queue中暫時儲存。為了減少尋道時間,通用塊層採用“蓄流”的方式,將IO蓄到佇列裡一段時間,使接下來的IO有機會與已經存在的IO進行合併,即所謂的電梯演算法。
當在一定合適的時機,“蓄流”開啟時,IO繼續進行向下層傳遞,進而呼叫塊裝置驅動為其請求佇列提供的request_fn
進行處理。SCSI磁碟驅動在掃描發現併為磁碟分配請求佇列時將期例項化。具體指向為scsi_request_fn
函式。
下圖展現了scsi_request_fn
該函式呼叫blk_peek_request
中逐個取出請求,並進行處理。每次呼叫它就從可處理的請求佇列中獲到一個請求。請求有可能是新的,有可能是由於SCSI子系統的底層相關裝置不能處理請求再次加入到請求佇列的。如果已經處理過意味著scsi cmd
已經申請;如果是新的,呼叫prep_rq_fn
進行一些請求處理前的準備工作。scsi磁碟驅動(以下簡稱sd)在這個時間進行SCSI命令的構建(SCSI CDB)。這個預期可能有三種返回值:
- BLKPREP_OK:表示命令初期準備成功,則繼續往下執行
- BLKPREP_DEFER:暫時不能夠進行處理,則將請求再次加入到佇列
- BLKPREP_KILL:請求沒辦法繼續進行下去。這時從佇列中請下請求,向上層報告IO失敗。
接下來做三件事:
首先,進行裝置的檢查工作。
當前的計算機體系結構中,裝置的操作明顯慢於CPU的計算能力,是現代計算機的關鍵瓶頸之一。因而,在真正需要裝置進行處理之前,儘可能的完善檢查IO處理的條件是否已經滿足。總之,針對IO層面的錯誤處理,要麼儘可能多的恢復現場使業務能夠正常的執行下去,要麼儘可能的早的失敗,給予應用進行錯誤處理的機會。此次的檢查主要針對後者,主要是硬體層面的檢查,具體點是對裝置狀態、目標端、主機介面卡等。
裝置不能接收IO的情況分為三種分別為裝置離線、裝置正忙於處理其他事務、裝置被阻塞住:
- 裝置離線時,當裝置再次上線時,需要重新初始化、重新建立請求佇列,當前的狀態和資料不需要處理,因而IO請求不需要進行處理了,直接放棄掉並向上層返回錯誤資訊。
- 裝置忙和裝置阻塞住意味著裝置臨時不能處理IO請求,一旦狀態移除,IO將會被繼續處理,此時系統將IO請求放置到延遲佇列中,擇機進行處理。
- 如果裝置的狀態沒有問題,而裝置所屬的目標端(target)、介面卡(host)存在問題時,可以確保是臨時不能處理IO,此時將IO再次加入到裝置的請求佇列中,下次再次進行重試。
其次,初始化錯誤處理。
scsi_init_cmd_errh
用於命令執行出錯處理的初始化,設定超時定時器(新核心中的出理只進行了一些初始化的工作,超時沒有進行設定)。
最後,將scsi命令轉交到底層裝置(LLD Low Level Device)進行處理。
scsi_dispatch_cmd
用於將命令傳遞到底層裝置驅動進行處理,一旦這個操作完成,意味著IO請求已經穿過SCSI中間層,傳遞到SCSI transport layer層。如果該返回0表示分發成功,否則表示出現錯誤,不再繼續處理請求佇列中的其它請求。
這個函式的主要工作如下:
- 檢查裝置是否已經被刪除。如果被刪除,不需要下發了,直接返回錯即可。
- 檢查裝置是否被阻塞住。
- 檢查命令的長度是否已經超過裝置可以接受的長度。
- 呼叫主要介面卡的queuecommand
命令將資料進行分發
如果SCSI指令沒有被分發,最終可能因為:
- SCSI_MLQUEUE_DEVICE_BUSY:裝置臨時被阻塞。
- SCSI_MLQUEUE_TARGET_BUSY:目標被阻塞
- SCSI_MLQUEUE_HOST_BUSY:主機介面卡被阻塞
在上述三種情況下,將IO請求再次放入佇列,以便下次重試。
到此為止,SCSI層面的處理完成。
總結一下,IO請求將視系統當前的情況進行處理,最終都會執行結果分為三種情況:
當裝置因故障移除時,直接向上層報告IO錯誤;當SCSI子系統的相關裝置(介面卡、目標節點、裝置)暫時不能處理IO,加入到佇列後期處理;當裝置正常時,回調適配器的queuecommand
進一步進行處理。
返回來,我們再討論一下SCSI命令初始化的問題。
上面說明,系統根據請求資訊準備scsi指令在prep_rq_fn
中進行。sd初始化時,將其初始化為scsi_prep_fn
。scsi_prep_fn
呼叫scsi_setup_cmnd
進行scsi命令的初始化工作。scsi子系統的請求兩個層次(1)上層用請求(核心中稱做來源於檔案系統的請求,這種說法不是特別的準確);(2)來源於scsi中間層的請求。前者需要構建SCSI命令,後者SCSI命令已經構建完成。
來自上層應用的請求需要構建成哪些指令,如何構建指令與裝置型別有很大關係:來源於磁碟的IO請求需要構建的命令與塊操作相關,遵循SCSI SPC標準,而來源於磁帶的IO請求需要遵循相關的標準。下圖表達了SCSI協議族各協議之間的關係。一般地,裝置型別特定的命令集由具體裝置驅動來實現。此時,將呼叫scsi上層驅動的init_cmd
實現。對於磁碟裝置,裝置載入時,將其指向函式:sd_init_command
。
綜上sd裝置來源於上層應用請求的scsi命令構建呼叫關係為:
prep_rq_fn->scsi_prep_fn->scsi_setup_cmnd->scsi_setup_fs_cmnd->sd_init_command
個人感覺上層應用請求命令構建的時機找的有點彆扭。道理上,由sd來構建磁碟讀寫相關的命令是符合常理設計的。sd作為SCSI服務的上層驅動,主要通過裝置型別與其他相同層次的上層驅動在功能進行區分。必然的,磁碟IO相關的SCSI指令的構建由sd來完成;磁碟帶IO相關的SCSI指令由st來完成…………但是時機選擇回撥機制實現,而不是在請求下發之前就準備好,不確定是什麼原因。
sd_init_command
函式實現如下,
static int sd_init_command(struct scsi_cmnd *cmd)
{
struct request *rq = cmd->request;
switch (req_op(rq)) {
case REQ_OP_DISCARD:
switch (scsi_disk(rq->rq_disk)->provisioning_mode) {
case SD_LBP_UNMAP:
return sd_setup_unmap_cmnd(cmd);
case SD_LBP_WS16:
return sd_setup_write_same16_cmnd(cmd, true);
case SD_LBP_WS10:
return sd_setup_write_same10_cmnd(cmd, true);
case SD_LBP_ZERO:
return sd_setup_write_same10_cmnd(cmd, false);
default:
return BLKPREP_INVALID;
}
case REQ_OP_WRITE_ZEROES:
return sd_setup_write_zeroes_cmnd(cmd);
case REQ_OP_WRITE_SAME:
return sd_setup_write_same_cmnd(cmd);
case REQ_OP_FLUSH:
return sd_setup_flush_cmnd(cmd);
case REQ_OP_READ:
case REQ_OP_WRITE:
return sd_setup_read_write_cmnd(cmd);
case REQ_OP_ZONE_REPORT:
return sd_zbc_setup_report_cmnd(cmd);
case REQ_OP_ZONE_RESET:
return sd_zbc_setup_reset_cmnd(cmd);
default:
BUG();
}
}
它根據請求的操作的型別構建不同的scsi指令。
- REQ_OP_DISCARD:告訴塊裝置放棄使用某指定的塊。這個請求是配合自動精簡配置使用。這個功能對應SCSI指令的UNMAP命令。當此操作沒呼叫時,target不再為相應 LBA對映儲存空間;
- REQ_OP_WRITE_ZEROES:對指定的1個或者多個扇區寫0
- REQ_OP_WRITE_SAME: 將1個或者多個扇區寫成相同的資料。
- REQ_OP_FLUSH:通知target同步快取資料
- REQ_OP_READ:讀操作
- REQ_OP_WRITE:寫操作
- REQ_OP_ZONE_REPORT REQ_OP_ZONE_RESET:應用於瓦式儲存的特殊操作
請寫請求將sd_setup_read_write_cmnd
函式構建相應的scsi命令結構。由於協議不斷的發展,SCSI標準中有4種不同規格的寫讀操作稱作:read(10)、read(12)、read(16)、read(32)和write(10)、write(12)、write(16)、write(32)。括號中的數字與命令描述塊的大小,如read(10)的命令描述符為10個位元組。10、12、16這三種的命令的區別是表求LBA所佔用的空間不同,分別為4、6、8個位元組。read(32)與write(32)主要是用於端到端T10 type2資料驗證時使用。
理解了上述區別就比較好理解操作的構建過程了。
- 如果需要支援T10 type2資料驗證,使用read(32)、write(32)
- 否則根據硬碟的最大扇區數分析,儘可能使用最小的命令描述塊(CDB)