歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux服務器

Linux 內核 SCSI IO 子系統分析

概述

    LINUX 內核中 SCSI 子系統由 SCSI 上層,中間層和底層驅動模塊 [1] 三部分組成,主要負責管理 SCSI 資源和處理其他子系統,如文件系統,提交到 SCSI 子系統中的 IO 請求。因此,理解 SCSI 子系統的 IO 處理機制對理解整個 SCSI 子系統就顯的十分重要,同時也有助於理解整個 LINUX 內核的 IO 處理機制。本文從 SCSI 設備訪問請求的提交,SCSI 子系統對訪問請求的處理和 SCSI 子系統錯誤處理三個方面,闡述了 SCSI 子系統的 IO 處理機制。

    SCSI設備訪問請求的提交

    SCSI 設備訪問請求的提交分為兩個步驟:用戶空間提交訪問請求到通用塊層以及通用塊層提交塊訪問請求到 SCSI 子系統。

    用戶空間提交訪問請求到通用塊層

    在 LINUX 用戶空間,有三種方式提交對 SCSI 設備的訪問請求到通用塊層:

    通過文件系統提供的文件訪問接口進行訪問。對建立在 SCSI 設備上的 LINUX 文件系統中的文件讀寫操作,就屬於這種訪問方式;RAW 設備訪問方式。這種訪問方式比較常見的應用就是dd命令。 RAW 設備訪問方式和通過文件系統提供的文件訪問接口進行訪問的最大區別在於前者對 SCSI 設備直接進行線性地址訪問,不需要由文件系統進行地址映射;SCSI PASSTHROUGH 方式。通過 LINUX 提供的 SG 進行訪問,就屬於這種方式,用戶可以直接發 CDB[2] 命令給 SCSI 設備。所以,通過該接口,用戶可以做一些 SCSI 管理操作,如 SES 管理等。

    圖 1 顯示了 LINUX 內核對於三種請求提交方式的處理過程。

Linux 內核 SCSI IO 子系統分析
圖 1. LINUX 內核處理三種訪問請求的方式

    經由文件系統或 RAW 設備方式提交的請求,會通過底層塊設備訪問層(ll_rw_block()),由其生成塊 IO 請求(BIO),並提交給通用塊層 [3] ;而通過 SG 接口提交的訪問請求,會調用 SCSI 中間層提供的接口,將請求直接交由通用塊層進行處理。

    通用塊層提交塊訪問請求到SCSI子系統

    為什麼要通過通用塊層呢?這是因為首先通用塊層會根據磁盤訪問的特性對請求進行優化操作;其次,通用塊層提供了調度功能,能夠對請求進行調度;再次,通用塊層可擴展的結構,使各種設備的塊驅動都能比較容易的和其集成。

    當請求提交到通用塊層後,通用塊層需要完成准備,調度並交付塊訪問請求給 SCSI 中間層的操作。塊訪問請求可以理解為描述了塊訪問區域,訪問方式和關聯的 BIO 的請求,在內核中用 'struct request'結構表示。塊設備會有對應的塊訪問請求設備隊列,用於記錄需要該設備處理的訪問請求,新生成的塊訪問請求會被加入到對應設備的塊訪問請求隊列中。 SCSI 子系統對 IO 的處理,實際上是處理塊訪問請求隊列上的塊訪問請求。

    通用塊層提供了兩種方式調度處理塊訪問請求隊列:直接調度和通過 LINUX 內核工作隊列機制調度執行。兩種方式,最後都會調用塊訪問請求隊列處理函數進行處理,而 SCSI 設備在初始化時會向通用塊層注冊 SCSI 子系統定義的塊訪問請求隊列處理函數。清單 1[4] 顯示了這個過程。這樣當通用塊層處理 SCSI 設備的塊訪問請求隊列時,調用的就是 SCSI 中間層定義的這些處理函數。通過這種方式,通用塊層就將塊訪問請求的處理交給了 SCSI 子系統。

    清單 1. 處理函數

    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);

    }

    SCSI子系統處理塊訪問請求

    當 SCSI 子系統的請求隊列處理函數被通用塊層調用後,SCSI 中間層會根據塊訪問請求的內容,生成、初始並提交 SCSI 命令 (struct scsi_cmd) 到 SCSI TARGET 端。

    SCSI命令初始化和提交

    SCSI 命令記錄了命令描述塊 (CDB),感測數據緩存 (SENSE BUFFER),IO 超時時間等 SCSI 相關的信息和 SCSI

子系統處理命令需要的一些其他信息,如回調函數等。清單 2 顯示了這個命令的主要結構。

    清單 2. 主要結構

    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 *);

    ……

    };

    初始化的過程首先按照電梯調度算法,從塊設備的請求隊列上取出一個塊訪問請求,根據塊訪問請求的信息,定義 SCSI 命令中數據傳輸的方向,長度和地址。其次,定義 CDB,SCSI 中間層的回調函數等。

    在完成初始化後,SCSI 中間層通過調用scsi_host_template[5]結構中定義的queuecommand函數將 SCSI 命令提交給 SCSI 底層驅動部分。queuecommand函數,是一個 SCSI 命令隊列處理函數,在 SCSI 底層驅動中,定義了queuecommand函數的具體實現。因此,SCSI 中間層,調用queuecommand函數實際上就是調用了底層驅動定義的queuecommand函數的處理實體,將 SCSI 命令提交給了各個廠家定義的 SCSI 底層驅動進行處理。這個過程和通用塊設備層調用 SCSI 中間層的處理函數進行塊請求處理的機制很相似,這也體現了 LINUX 內核代碼具有很好的擴展性。底層驅動接受到請求後,就要開始處理 SCSI 命令了,這一層和硬件關系緊密,所以這塊代碼一般都是由各個廠家自己實現。基本流程可概括為:從底層驅動維護的隊列中,取出一個 SCSI 命令,封裝成廠家自定義的請求格式,然後采用 DMA 或者其他方式,將請求提交給 SCSI TARGET 端,由 SCSI TARGET 端對請求處理,並返回執行結果給 SCSI 底層驅動層。

    SCSI命令執行結果的處理

    當 SCSI 底層驅動接受到 SCSI TARGET 端返回的命令執行結果後,SCSI 子系統主要通過兩次回調過程完成對命令執行結果的處理。 SCSI 底層驅動在接受到 SCSI TARGET 端返回的命令執行結果後,會調用 SCSI 中間層定義的回調函數,將處理結果交付給 SCSI 中間層進行處理,這是第一次回調過程。 SCSI 中間層處理完成後,將調用 SCSI 上層定義的回調函數,結束 IO 在整個 SCSI 子系統中的處理,這為第二次回調過程。

    第一次回調:

    SCSI 中間層在調用queuecommand函數將 SCSI 命令提交給 SCSI 底層驅動的同時,也將回調函數指針傳給了 SCSI 底層驅動。底層驅動接受到 SCSI TARGET 端返回的命令執行結果後,會調用該回調函數,產生一個中斷號為 BLOCK_SOFTIRQ 的軟中斷進行第一次回調處理。在這次回調處理過程中,SCSI 中間層首先會根據 SCSI 底層驅動處理的結果判斷請求處理是否成功。處理成功,並不意味著處理沒有錯誤,而是返回的信息,能夠讓 SCSI 中間層很明確的知道,對於這個命令,中間層已經沒有必要繼續進行處理了。所以,對於處理成功的 SCSI 命令,SCSI 中間層會調用第二次回調函數進入到第二次回調過程。清單 3 顯示了 SCSI 中間層定義的該軟中斷的處理函數。

    清單 3. 該軟中斷的處理函數

    static void scsi_softirq_done(struct request *rq)

    {

    ……

    disposition = scsi_decide_di

sposition(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);

    }

    }

     第二次回調:

        不同的 SCSI 上層模塊會定義自己不同的第二次回調函數,如 SD 模塊,會在sd_init_command函數中,定義自己的第二次回調函數sd_rw_intr,這個回調函數會根據 SD 模塊的需要,對 SCSI 命令執行的結果做進一步的處理。清單 4 顯示了 SD 模塊注冊第二次回調的代碼。雖然各個 SCSI 上層模塊可以定義自己的第二次回調函數,但是這些回調函數最終都會結束 SCSI 子系統對這個塊訪問請求的處理。

        清單 4. SD 模塊注冊第二次回調的代碼

        static int sd_init_command(struct scsi_cmnd * SCpnt)

        {

        ……

        SCpnt->done = sd_rw_intr;

        return 1;

        }

        SCSI子系統的錯誤處理

        由於 SCSI 底層驅動是由廠商自己實現的,在此就不予討論。除此之外,SCSI 子系統的出錯處理,主要是由 SCSI 中間層完成。在第一次回調過程中,SCSI 底層驅動將 SCSI 命令的處理結果以及獲取的 SCSI 狀態信息返回給 SCSI 中間層,SCSI 中間層先對 SCSI 底層驅動返回的 SCSI 命令執行的結果進行判斷,若無法得到明確的結論,則對 SCSI 底層驅動返回的 SCSI 狀態、感測數據等進行判斷。對於判斷結論為處理成功的 SCSI 命令,SCSI 中間層會直接進行第二次回調;對於判斷結論為需要重試的命令,則會被加入塊設備請求對列,重新被處理。這個過程可稱為 SCSI 中間層對 SCSI 命令執行結果的基本判斷方法。

        一切看起來似乎是這麼簡單,但是實際上並非如此,有些錯誤是沒有明確的判斷依據的,如感測數據錯誤或 TIMEOUT 錯誤。為了解決這個問題,LINUX 內核中 SCSI 子系統引入了一個專門進行錯誤處理的線程,對於無法判斷錯誤原因的 SCSI 命令,都會交由該線程進行處理。線程處理過程和兩個隊列密切相關,一個是錯誤處理隊列(eh_work_q),一個是錯誤處理完成隊列 (done_q) 。錯誤處理隊列記錄了需要進行錯誤處理的 SCSI 命令,錯誤處理完成隊列記錄了在錯誤處理過程中被處理完成的 SCSI 命令。清單 5 顯示了線程對錯誤處理隊列上記錄的命令進行錯誤處理的過程。

        清單 5. 錯誤處理的過程

        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

        ……

        }

        整個處理過程可歸納為四個階段:

        感測數據查詢階段

        通過查詢感測數據,為處理 SCSI 命令重新提供判斷依據,並按照前述基本判斷方法進行判斷。如果判斷結果為成功或者重試,則可將該命令從錯誤處理隊列移到錯誤處理完成隊列。若判斷失敗,則命令將會繼續保留在 SCSI 錯誤處理隊列中,錯誤處理進入到 ABORT 階段。

    &nb

    sp;   ABORT階段

        在這個階段中,錯誤處理隊列上的 SCSI 命令會被主動 ABORT 掉。被 ABORT 的命令,會被加入到錯誤處理完成隊列。若 ABORT 過程結束,錯誤處理隊列上還存在未能被處理的命令,則需進入 START STOP UNIT 階段進行處理。

        START STOP UNIT階段

        在這個階段,START STOP UNIT[6] 命令會被發送到與錯誤處理隊列上的命令相關的 SCSI DEVICE 上,去試圖恢復 SCSI DEVICE,如果在 START STOP UNIT 階段結束後,依舊有命令在錯誤處理隊列上,則需要進入 RESET 階段進行處理。

        RESET階段

        RESET 階段的處理過程分三個層次:DEVICE RESET,BUS RESET 和 HOST RESET 。首先對與錯誤隊列上的命令相關的 SCSI DEVICE,進行 RESET 操作,如果 DEVICE RESET 後,SCSI 設備能處於正常狀態,則和該設備相關的錯誤處理隊列上的錯誤命令,會被加入到錯誤處理完成隊列中。若通過 DEVICE RESET 不能處理所有的錯誤命令,則需進入到 BUS RESET 階段,BUS RESET 會對與錯誤處理隊列上的命令相關的 BUS,進行 RESET 操作。若 BUS RESET 還不能成功處理所有錯誤處理隊列上的 SCSI 命令,則會進入到 HOST RESET 階段,HOST RESET 會對與錯誤處理隊列上的命令相關的 HOST 進行 RESET 操作。當然,很有可能 HOST RESET 也不能成功處理所有錯誤命令,則只能認為錯誤處理隊列上錯誤命令相關的 SCSI 設備不能被使用了。這些不能被使用的設備會被標記為不能使用狀態,同時相關的錯誤命令都會被加入到錯誤處理完成隊列中。

        對於被加入到錯誤處理完成隊列上的請求,若是在設備狀態正確,命令重試次數小於允許次數的情況下,這些命令將被重新加入到塊訪問請求隊列中,進行重新處理;否則,直接進行第二次回調處理,完成 SCSI 子系統對塊訪問請求的處理。這樣,SCSI 子系統就完成了 SCSI 命令錯誤處理的整個過程。

        結束語

        本文淺析了 SCSI 子系統中的 IO 處理機制,希望對大家理解 SCSI 子系統和塊設備驅動能有所幫助

    Copyright © Linux教程網 All Rights Reserved