LINUX 內(nèi)核中 SCSI 子系統(tǒng)由 SCSI 上層,中間層和底層驅(qū)動(dòng)模塊 [1] 三部分組成,主要負(fù)責(zé)管理 SCSI 資源和處理其他子系統(tǒng),如文件系統(tǒng),提交到 SCSI 子系統(tǒng)中的 IO 請(qǐng)求。因此,理解 SCSI 子系統(tǒng)的 IO 處理機(jī)制對(duì)理解整個(gè) SCSI 子系統(tǒng)就顯的十分重要,同時(shí)也有助于理解整個(gè) LINUX 內(nèi)核的 IO 處理機(jī)制。本文從 SCSI 設(shè)備訪問請(qǐng)求的提交,SCSI 子系統(tǒng)對(duì)訪問請(qǐng)求的處理和 SCSI 子系統(tǒng)錯(cuò)誤處理三個(gè)方面,闡述了 SCSI 子系統(tǒng)的 IO 處理機(jī)制。
SCSI 設(shè)備訪問請(qǐng)求的提交分為兩個(gè)步驟:用戶空間提交訪問請(qǐng)求到通用塊層以及通用塊層提交塊訪問請(qǐng)求到 SCSI 子系統(tǒng)。
在 LINUX 用戶空間,有三種方式提交對(duì) SCSI 設(shè)備的訪問請(qǐng)求到通用塊層:
- 通過文件系統(tǒng)提供的文件訪問接口進(jìn)行訪問。對(duì)建立在 SCSI 設(shè)備上的 LINUX 文件系統(tǒng)中的文件讀寫操作,就屬于這種訪問方式;
- RAW 設(shè)備訪問方式。這種訪問方式比較常見的應(yīng)用就是
dd
命令。 RAW 設(shè)備訪問方式和通過文件系統(tǒng)提供的文件訪問接口進(jìn)行訪問的最大區(qū)別在于前者對(duì) SCSI 設(shè)備直接進(jìn)行線性地址訪問,不需要由文件系統(tǒng)進(jìn)行地址映射; - SCSI PASSTHROUGH 方式。通過 LINUX 提供的 SG 進(jìn)行訪問,就屬于這種方式,用戶可以直接發(fā) CDB[2] 命令給 SCSI 設(shè)備。所以,通過該接口,用戶可以做一些 SCSI 管理操作,如 SES 管理等。
圖 1 顯示了 LINUX 內(nèi)核對(duì)于三種請(qǐng)求提交方式的處理過程。
圖 1. LINUX 內(nèi)核處理三種訪問請(qǐng)求的方式

經(jīng)由文件系統(tǒng)或 RAW 設(shè)備方式提交的請(qǐng)求,會(huì)通過底層塊設(shè)備訪問層(ll_rw_block()),由其生成塊 IO 請(qǐng)求(BIO),并提交給通用塊層 [3] ;而通過 SG 接口提交的訪問請(qǐng)求,會(huì)調(diào)用 SCSI 中間層提供的接口,將請(qǐng)求直接交由通用塊層進(jìn)行處理。
通用塊層提交塊訪問請(qǐng)求到 SCSI 子系統(tǒng)
為什么要通過通用塊層呢?這是因?yàn)槭紫韧ㄓ脡K層會(huì)根據(jù)磁盤訪問的特性對(duì)請(qǐng)求進(jìn)行優(yōu)化操作;其次,通用塊層提供了調(diào)度功能,能夠?qū)φ?qǐng)求進(jìn)行調(diào)度;再次,通用塊層可擴(kuò)展的結(jié)構(gòu),使各種設(shè)備的塊驅(qū)動(dòng)都能比較容易的和其集成。
當(dāng)請(qǐng)求提交到通用塊層后,通用塊層需要完成準(zhǔn)備,調(diào)度并交付塊訪問請(qǐng)求給 SCSI 中間層的操作。塊訪問請(qǐng)求可以理解為描述了塊訪問區(qū)域,訪問方式和關(guān)聯(lián)的 BIO 的請(qǐng)求,在內(nèi)核中用 'struct request'
結(jié)構(gòu)表示。塊設(shè)備會(huì)有對(duì)應(yīng)的塊訪問請(qǐng)求設(shè)備隊(duì)列,用于記錄需要該設(shè)備處理的訪問請(qǐng)求,新生成的塊訪問請(qǐng)求會(huì)被加入到對(duì)應(yīng)設(shè)備的塊訪問請(qǐng)求隊(duì)列中。 SCSI 子系統(tǒng)對(duì) IO 的處理,實(shí)際上是處理塊訪問請(qǐng)求隊(duì)列上的塊訪問請(qǐng)求。
通用塊層提供了兩種方式調(diào)度處理塊訪問請(qǐng)求隊(duì)列:直接調(diào)度和通過 LINUX 內(nèi)核工作隊(duì)列機(jī)制調(diào)度執(zhí)行。兩種方式,最后都會(huì)調(diào)用塊訪問請(qǐng)求隊(duì)列處理函數(shù)進(jìn)行處理,而 SCSI 設(shè)備在初始化時(shí)會(huì)向通用塊層注冊(cè) SCSI 子系統(tǒng)定義的塊訪問請(qǐng)求隊(duì)列處理函數(shù)。清單 1[4] 顯示了這個(gè)過程。這樣當(dāng)通用塊層處理 SCSI 設(shè)備的塊訪問請(qǐng)求隊(duì)列時(shí),調(diào)用的就是 SCSI 中間層定義的這些處理函數(shù)。通過這種方式,通用塊層就將塊訪問請(qǐng)求的處理交給了 SCSI 子系統(tǒng)。
清單 1. 處理函數(shù)
struct request_queue *scsi_alloc_queue(struct scsi_device *sdev) { …… q = blk_init_queue(scsi_request_fn, NULL); //request generate block layer allocate a request queue …… blk_queue_prep_rq(q, scsi_prep_fn); //Prepare a scsi request blk_queue_max_hw_segments(q, shost->sg_tablesize); //define sg table size …… blk_queue_softirq_done(q, scsi_softirq_done); } |
當(dāng) SCSI 子系統(tǒng)的請(qǐng)求隊(duì)列處理函數(shù)被通用塊層調(diào)用后,SCSI 中間層會(huì)根據(jù)塊訪問請(qǐng)求的內(nèi)容,生成、初始并提交 SCSI 命令 (struct scsi_cmd
) 到 SCSI TARGET 端。
SCSI 命令記錄了命令描述塊 (CDB),感測(cè)數(shù)據(jù)緩存 (SENSE BUFFER),IO 超時(shí)時(shí)間等 SCSI 相關(guān)的信息和 SCSI 子系統(tǒng)處理命令需要的一些其他信息,如回調(diào)函數(shù)等。清單 2 顯示了這個(gè)命令的主要結(jié)構(gòu)。
清單 2. 主要結(jié)構(gòu)
struct scsi_cmnd { …… void (*done) (struct scsi_cmnd *); /* Mid-level done function */ …… int retries; /*retried time*/ int timeout_per_command; /*timeout define*/ …… enum dma_data_direction sc_data_direction; /*data transfer direction*/ …… unsigned char cmnd[MAX_COMMAND_SIZE]; /*cdb*/ void *request_buffer; /* Actual requested buffer */ struct request *request; /* The command we are working on */ …… unsigned char sense_buffer[SCSI_SENSE_BUFFERSIZE]; /* obtained by REQUEST SENSE when * CHECK CONDITION is received on original * command (auto-sense) */ /* Low-level done function - can be used by */ /*low-level driver to point to completion function. */ void (*scsi_done) (struct scsi_cmnd *); …… }; |
初始化的過程首先按照電梯調(diào)度算法,從塊設(shè)備的請(qǐng)求隊(duì)列上取出一個(gè)塊訪問請(qǐng)求,根據(jù)塊訪問請(qǐng)求的信息,定義 SCSI 命令中數(shù)據(jù)傳輸?shù)姆较?,長(zhǎng)度和地址。其次,定義 CDB,SCSI 中間層的回調(diào)函數(shù)等。
在完成初始化后,SCSI 中間層通過調(diào)用scsi_host_template
[5]結(jié)構(gòu)中定義
的queuecommand
函數(shù)將 SCSI 命令提交給 SCSI 底層驅(qū)動(dòng)部分。queuecommand
函數(shù),是一個(gè) SCSI 命令隊(duì)列處理函數(shù),在 SCSI 底層驅(qū)動(dòng)中,定義了queuecommand
函數(shù)的具體實(shí)現(xiàn)。因此,SCSI 中間層,調(diào)用queuecommand
函數(shù)實(shí)際上就是調(diào)用了底層驅(qū)動(dòng)定義的queuecommand
函數(shù)的處理實(shí)體,將 SCSI 命令提交給了各個(gè)廠家定義的 SCSI 底層驅(qū)動(dòng)進(jìn)行處理。這個(gè)過程和通用塊設(shè)備層調(diào)用 SCSI 中間層的處理函數(shù)進(jìn)行塊請(qǐng)求處理的機(jī)制很相似,這也體現(xiàn)了 LINUX 內(nèi)核代碼具有很好的擴(kuò)展性。底層驅(qū)動(dòng)接受到請(qǐng)求后,就要開始處理 SCSI 命令了,這一層和硬件關(guān)系緊密,所以這塊代碼一般都是由各個(gè)廠家自己實(shí)現(xiàn)?;玖鞒炭筛爬椋簭牡讓域?qū)動(dòng)維護(hù)的隊(duì)列中,取出一個(gè) SCSI 命令,封裝成廠家自定義的請(qǐng)求格式,然后采用 DMA 或者其他方式,將請(qǐng)求提交給 SCSI TARGET 端,由 SCSI TARGET 端對(duì)請(qǐng)求處理,并返回執(zhí)行結(jié)果給 SCSI 底層驅(qū)動(dòng)層。
當(dāng) SCSI 底層驅(qū)動(dòng)接受到 SCSI TARGET 端返回的命令執(zhí)行結(jié)果后,SCSI 子系統(tǒng)主要通過兩次回調(diào)過程完成對(duì)命令執(zhí)行結(jié)果的處理。 SCSI 底層驅(qū)動(dòng)在接受到 SCSI TARGET 端返回的命令執(zhí)行結(jié)果后,會(huì)調(diào)用 SCSI 中間層定義的回調(diào)函數(shù),將處理結(jié)果交付給 SCSI 中間層進(jìn)行處理,這是第一次回調(diào)過程。 SCSI 中間層處理完成后,將調(diào)用 SCSI 上層定義的回調(diào)函數(shù),結(jié)束 IO 在整個(gè) SCSI 子系統(tǒng)中的處理,這為第二次回調(diào)過程。
第一次回調(diào):
SCSI 中間層在調(diào)用queuecommand
函數(shù)將 SCSI 命令提交給 SCSI 底層驅(qū)動(dòng)的同時(shí),也將回調(diào)函數(shù)指針傳給了 SCSI 底層驅(qū)動(dòng)。底層驅(qū)動(dòng)接受到 SCSI TARGET 端返回的命令執(zhí)行結(jié)果后,會(huì)調(diào)用該回調(diào)函數(shù),產(chǎn)生一個(gè)中斷號(hào)為 BLOCK_SOFTIRQ 的軟中斷進(jìn)行第一次回調(diào)處理。在這次回調(diào)處理過程中,SCSI 中間層首先會(huì)根據(jù) SCSI 底層驅(qū)動(dòng)處理的結(jié)果判斷請(qǐng)求處理是否成功。處理成功,并不意味著處理沒有錯(cuò)誤,而是返回的信息,能夠讓 SCSI 中間層很明確的知道,對(duì)于這個(gè)命令,中間層已經(jīng)沒有必要繼續(xù)進(jìn)行處理了。所以,對(duì)于處理成功的 SCSI 命令,SCSI 中間層會(huì)調(diào)用第二次回調(diào)函數(shù)進(jìn)入到第二次回調(diào)過程。清單 3 顯示了 SCSI 中間層定義的該軟中斷的處理函數(shù)。
清單 3. 該軟中斷的處理函數(shù)
static void scsi_softirq_done(struct request *rq) { …… disposition = scsi_decide_disposition(cmd); …… switch (disposition) { case SUCCESS: scsi_finish_command(cmd); //enter to second callback process break; case NEEDS_RETRY: scsi_retry_command(cmd); break; case ADD_TO_MLQUEUE: scsi_queue_insert(cmd, SCSI_MLQUEUE_DEVICE_BUSY); break; default: if (!scsi_eh_scmd_add(cmd, 0)) scsi_finish_command(cmd); } } |
第二次回調(diào):
不同的 SCSI 上層模塊會(huì)定義自己不同的第二次回調(diào)函數(shù),如 SD 模塊,會(huì)在sd_init_command
函數(shù)中,定義自己的第二次回調(diào)函數(shù)sd_rw_intr
,這個(gè)回調(diào)函數(shù)會(huì)根據(jù) SD 模塊的需要,對(duì) SCSI 命令執(zhí)行的結(jié)果做進(jìn)一步的處理。清單 4 顯示了 SD 模塊注冊(cè)第二次回調(diào)的代碼。雖然各個(gè) SCSI 上層模塊可以定義自己的第二次回調(diào)函數(shù),但是這些回調(diào)函數(shù)最終都會(huì)結(jié)束 SCSI 子系統(tǒng)對(duì)這個(gè)塊訪問請(qǐng)求的處理。
清單 4. SD 模塊注冊(cè)第二次回調(diào)的代碼
static int sd_init_command(struct scsi_cmnd * SCpnt) { …… SCpnt->done = sd_rw_intr; return 1; } |
由于 SCSI 底層驅(qū)動(dòng)是由廠商自己實(shí)現(xiàn)的,在此就不予討論。除此之外,SCSI 子系統(tǒng)的出錯(cuò)處理,主要是由 SCSI 中間層完成。在第一次回調(diào)過程中,SCSI 底層驅(qū)動(dòng)將 SCSI 命令的處理結(jié)果以及獲取的 SCSI 狀態(tài)信息返回給 SCSI 中間層,SCSI 中間層先對(duì) SCSI 底層驅(qū)動(dòng)返回的 SCSI 命令執(zhí)行的結(jié)果進(jìn)行判斷,若無法得到明確的結(jié)論,則對(duì) SCSI 底層驅(qū)動(dòng)返回的 SCSI 狀態(tài)、感測(cè)數(shù)據(jù)等進(jìn)行判斷。對(duì)于判斷結(jié)論為處理成功的 SCSI 命令,SCSI 中間層會(huì)直接進(jìn)行第二次回調(diào);對(duì)于判斷結(jié)論為需要重試的命令,則會(huì)被加入塊設(shè)備請(qǐng)求對(duì)列,重新被處理。這個(gè)過程可稱為 SCSI 中間層對(duì) SCSI 命令執(zhí)行結(jié)果的基本判斷方法。
一切看起來似乎是這么簡(jiǎn)單,但是實(shí)際上并非如此,有些錯(cuò)誤是沒有明確的判斷依據(jù)的,如感測(cè)數(shù)據(jù)錯(cuò)誤或 TIMEOUT 錯(cuò)誤。為了解決這個(gè)問題,LINUX 內(nèi)核中 SCSI 子系統(tǒng)引入了一個(gè)專門進(jìn)行錯(cuò)誤處理的線程,對(duì)于無法判斷錯(cuò)誤原因的 SCSI 命令,都會(huì)交由該線程進(jìn)行處理。線程處理過程和兩個(gè)隊(duì)列密切相關(guān),一個(gè)是錯(cuò)誤處理隊(duì)列(eh_work_q
),一個(gè)是錯(cuò)誤處理完成隊(duì)列 (done_q
) 。錯(cuò)誤處理隊(duì)列記錄了需要進(jìn)行錯(cuò)誤處理的 SCSI 命令,錯(cuò)誤處理完成隊(duì)列記錄了在錯(cuò)誤處理過程中被處理完成的 SCSI 命令。清單 5 顯示了線程對(duì)錯(cuò)誤處理隊(duì)列上記錄的命令進(jìn)行錯(cuò)誤處理的過程。
清單 5. 錯(cuò)誤處理的過程
scsi_unjam_host{ …… if (!scsi_eh_get_sense(&eh_work_q, &eh_done_q)) //get sense data if (!scsi_eh_abort_cmds(&eh_work_q, &eh_done_q)) //abort command scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q); //reset scsi_eh_flush_done_q(&eh_done_q); //complete error io on done_q …… } |
整個(gè)處理過程可歸納為四個(gè)階段:
- 感測(cè)數(shù)據(jù)查詢階段
通過查詢感測(cè)數(shù)據(jù),為處理 SCSI 命令重新提供判斷依據(jù),并按照前述基本判斷方法進(jìn)行判斷。如果判斷結(jié)果為成功或者重試,則可將該命令從錯(cuò)誤處理隊(duì)列移到錯(cuò)誤處理完成隊(duì)列。若判斷失敗,則命令將會(huì)繼續(xù)保留在 SCSI 錯(cuò)誤處理隊(duì)列中,錯(cuò)誤處理進(jìn)入到 ABORT 階段。
- ABORT階段
在這個(gè)階段中,錯(cuò)誤處理隊(duì)列上的 SCSI 命令會(huì)被主動(dòng) ABORT 掉。被 ABORT 的命令,會(huì)被加入到錯(cuò)誤處理完成隊(duì)列。若 ABORT 過程結(jié)束,錯(cuò)誤處理隊(duì)列上還存在未能被處理的命令,則需進(jìn)入 START STOP UNIT 階段進(jìn)行處理。
- START STOP UNIT階段
在這個(gè)階段,START STOP UNIT[6] 命令會(huì)被發(fā)送到與錯(cuò)誤處理隊(duì)列上的命令相關(guān)的 SCSI DEVICE 上,去試圖恢復(fù) SCSI DEVICE,如果在 START STOP UNIT 階段結(jié)束后,依舊有命令在錯(cuò)誤處理隊(duì)列上,則需要進(jìn)入 RESET 階段進(jìn)行處理。
- RESET階段
RESET 階段的處理過程分三個(gè)層次:DEVICE RESET,BUS RESET 和 HOST RESET 。首先對(duì)與錯(cuò)誤隊(duì)列上的命令相關(guān)的 SCSI DEVICE,進(jìn)行 RESET 操作,如果 DEVICE RESET 后,SCSI 設(shè)備能處于正常狀態(tài),則和該設(shè)備相關(guān)的錯(cuò)誤處理隊(duì)列上的錯(cuò)誤命令,會(huì)被加入到錯(cuò)誤處理完成隊(duì)列中。若通過 DEVICE RESET 不能處理所有的錯(cuò)誤命令,則需進(jìn)入到 BUS RESET 階段,BUS RESET 會(huì)對(duì)與錯(cuò)誤處理隊(duì)列上的命令相關(guān)的 BUS,進(jìn)行 RESET 操作。若 BUS RESET 還不能成功處理所有錯(cuò)誤處理隊(duì)列上的 SCSI 命令,則會(huì)進(jìn)入到 HOST RESET 階段,HOST RESET 會(huì)對(duì)與錯(cuò)誤處理隊(duì)列上的命令相關(guān)的 HOST 進(jìn)行 RESET 操作。當(dāng)然,很有可能 HOST RESET 也不能成功處理所有錯(cuò)誤命令,則只能認(rèn)為錯(cuò)誤處理隊(duì)列上錯(cuò)誤命令相關(guān)的 SCSI 設(shè)備不能被使用了。這些不能被使用的設(shè)備會(huì)被標(biāo)記為不能使用狀態(tài),同時(shí)相關(guān)的錯(cuò)誤命令都會(huì)被加入到錯(cuò)誤處理完成隊(duì)列中。
對(duì)于被加入到錯(cuò)誤處理完成隊(duì)列上的請(qǐng)求,若是在設(shè)備狀態(tài)正確,命令重試次數(shù)小于允許次數(shù)的情況下,這些命令將被重新加入到塊訪問請(qǐng)求隊(duì)列中,進(jìn)行重新處理;否則,直接進(jìn)行第二次回調(diào)處理,完成 SCSI 子系統(tǒng)對(duì)塊訪問請(qǐng)求的處理。這樣,SCSI 子系統(tǒng)就完成了 SCSI 命令錯(cuò)誤處理的整個(gè)過程。
本文淺析了 SCSI 子系統(tǒng)中的 IO 處理機(jī)制,希望對(duì)大家理解 SCSI 子系統(tǒng)和塊設(shè)備驅(qū)動(dòng)能有所幫助。
- 【 1 】 Linux SCSI 子系統(tǒng)剖析 -http://www.ibm.com/developerworks/cn/linux/l-scsi-subsystem/。
- 【 2 】 請(qǐng)參考 SCSI Primary Commands-4(SPC4)。
- 【 3 】《 Linux 內(nèi)核分析及編程》,倪繼利,電子工業(yè)出版社。
- 【 4 】http://www./pub/linux/kernel/v2.6/linux-2.6.18.8.tar.gz(GPL LICENCE Version 2)。
- 【 5 】《 Linux 核心》 ,David A Rusling,http://man./os/linux_kern/drivers.htm。
- 【 6 】參考:“ SCSI-3 BLOCK Commands (SBC) ”,Information Technology Industry Council。
- 在 developerWorks Linux 專區(qū) 尋找為 Linux 開發(fā)人員(包括 Linux 新手入門)準(zhǔn)備的更多參考資料,查閱我們 最受歡迎的文章和教程。
- 在 developerWorks 上查閱所有 Linux 技巧 和 Linux 教程。