TCM 使用者空間設計¶
設計¶
TCM 是 LIO 的另一個名稱,它是一個核心內的 iSCSI 目標(伺服器)。 現有的 TCM 目標在核心中執行。 TCMU(使用者空間中的 TCM)允許編寫使用者空間程式,這些程式充當 iSCSI 目標。 本文件描述了設計。
現有的核心為不同的 SCSI 傳輸協議提供了模組。 TCM 還模組化了資料儲存。 現有模組用於檔案、塊裝置、RAM 或使用另一個 SCSI 裝置作為儲存。 這些被稱為“後端儲存”或“儲存引擎”。 這些內建模組完全以核心程式碼實現。
背景¶
除了模組化用於攜帶 SCSI 命令的傳輸協議(“結構”)之外,Linux 核心目標 LIO 還模組化了實際的資料儲存。 這些被稱為“後端儲存”或“儲存引擎”。 該目標帶有後端儲存,允許將檔案、塊裝置、RAM 或另一個 SCSI 裝置用於匯出的 SCSI LUN 所需的本地儲存。 與 LIO 的其餘部分一樣,這些完全以核心程式碼實現。
這些後端儲存涵蓋了最常見的用例,但並非全部。 其他非核心目標解決方案(例如 tgt)能夠支援的一個新的用例是使用 Gluster 的 GLFS 或 Ceph 的 RBD 作為後端儲存。 然後,目標充當翻譯器,允許發起者將資料儲存在這些非傳統的聯網儲存系統中,同時仍然只使用標準協議本身。
如果目標是使用者空間程序,則支援這些非常容易。 例如,tgt 只需要為每個提供一個小型介面卡模組,因為這些模組僅使用 RBD 和 GLFS 的可用使用者空間庫。
在 LIO 中新增對這些後端儲存的支援要困難得多,因為 LIO 完全是核心程式碼。 與其進行大量工作將 GLFS 或 RBD API 和協議移植到核心,不如採用另一種方法,為 LIO 建立一個使用者空間直通後端儲存“TCMU”。
優勢¶
除了可以相對容易地支援 RBD 和 GLFS 之外,TCMU 還將允許更容易地開發新的後端儲存。 TCMU 與 LIO 環回結構結合,變得類似於 FUSE(使用者空間檔案系統),但在 SCSI 層而不是檔案系統層。 就像SUSE 一樣,如果你願意。
缺點是需要配置的元件更多,並且可能會出現故障。 這是不可避免的,但如果我們注意儘可能保持簡單,希望不會是致命的。
設計約束¶
良好的效能:高吞吐量,低延遲
乾淨地處理以下使用者空間情況
從未附加
掛起
死亡
行為不端
允許使用者和核心實現在未來具有靈活性
合理地節省記憶體
易於配置和執行
易於編寫使用者空間後端
實施概述¶
TCMU 介面的核心是核心和使用者空間之間共享的記憶體區域。 在此區域內:一個控制區域(郵箱); 一個無鎖的生產者/消費者迴圈緩衝區,用於傳遞命令並返回狀態; 以及一個輸入/輸出資料緩衝區區域。
TCMU 使用預先存在的 UIO 子系統。 UIO 允許在使用者空間中進行裝置驅動程式開發,這在概念上與 TCMU 用例非常接近,只是 TCMU 實現了一個專為 SCSI 命令設計的記憶體對映佈局,而不是物理裝置。 使用 UIO 還可以透過處理裝置自檢(例如,使用者空間確定共享區域大小的方法)和雙向信令機制來使 TCMU 受益。
記憶體區域中沒有嵌入的指標。 一切都表示為相對於區域起始地址的偏移量。 這允許在使用者程序死亡並在不同的虛擬地址對映該區域後,環仍然可以工作。
有關結構定義,請參見 target_core_user.h。
郵箱¶
郵箱始終位於共享記憶體區域的開頭,並且包含版本、有關命令環的起始偏移量和大小的詳細資訊,以及核心和使用者空間(分別)使用的頭尾指標,用於將命令放入環中,並指示命令何時完成。
版本 - 1(否則使用者空間應中止)
- 標誌
- TCMU_MAILBOX_FLAG_CAP_OOOC
指示支援亂序完成。 有關詳細資訊,請參見“命令環”。
- cmdr_off
命令環的起始位置相對於記憶體區域起始位置的偏移量,用於計算郵箱大小。
- cmdr_size
命令環的大小。 這不需要是 2 的冪。
- cmd_head
由核心修改,以指示何時將命令放置在環上。
- cmd_tail
由使用者空間修改,以指示何時已完成命令的處理。
命令環¶
核心透過將 mailbox.cmd_head 增加命令的大小(取模 cmdr_size)並將透過 uio_event_notify() 向用戶空間傳送訊號來將命令放置在環上。 命令完成後,使用者空間以相同的方式更新 mailbox.cmd_tail 並透過 4 位元組的 write() 向核心傳送訊號。 當 cmd_head 等於 cmd_tail 時,環為空 —— 當前沒有命令等待使用者空間處理。
TCMU 命令是 8 位元組對齊的。 它們以一個通用頭開始,該頭包含 “len_op”,這是一個 32 位值,用於儲存長度以及最低的未使用位中的操作碼。 它還包含 cmd_id 和標誌欄位,用於由核心 (kflags) 和使用者空間 (uflags) 設定。
當前僅定義了兩個操作碼 TCMU_OP_CMD 和 TCMU_OP_PAD。
當操作碼為 CMD 時,命令環中的條目是一個 struct tcmu_cmd_entry。 使用者空間透過 tcmu_cmd_entry.req.cdb_off 查詢 SCSI CDB(命令資料塊)。 這是相對於整個共享記憶體區域起始位置的偏移量,而不是條目。 可以透過 req.iov[] 陣列訪問輸入/輸出資料緩衝區。 iov_cnt 包含 iov[] 中描述資料輸入或資料輸出緩衝區所需的條目數。 對於雙向命令,iov_cnt 指定有多少 iovec 條目覆蓋資料輸出區域,iov_bidi_cnt 指定有多少 iovec 條目緊隨其後,在 iov[] 中覆蓋資料輸入區域。 與其他欄位一樣,iov.iov_base 是相對於區域起始位置的偏移量。
完成命令時,使用者空間設定 rsp.scsi_status,並在必要時設定 rsp.sense_buffer。 然後,使用者空間將 mailbox.cmd_tail 增加 entry.hdr.length(mod cmdr_size),並透過 UIO 方法(向檔案描述符寫入 4 位元組)向核心傳送訊號。
如果為 mailbox->flags 設定了 TCMU_MAILBOX_FLAG_CAP_OOOC,則核心能夠處理亂序完成。 在這種情況下,使用者空間可以按與原始順序不同的順序處理命令。 由於核心仍然會按照命令環中出現的相同順序處理命令,因此使用者空間需要在完成命令時更新 cmd->id(又名竊取原始命令的條目)。
當操作碼為 PAD 時,使用者空間僅更新 cmd_tail,如上所述 —— 這是一個空操作。(核心插入 PAD 條目以確保每個 CMD 條目在命令環中是連續的。)
將來可能會新增更多操作碼。 如果使用者空間遇到它不處理的操作碼,則必須在 hdr.uflags 中設定 UNKNOWN_OP 位(位 0),更新 cmd_tail,並繼續處理其他命令(如果有)。
資料區域¶
這是命令環之後的共享記憶體空間。 TCMU 介面中未定義此區域的組織,使用者空間應僅訪問待處理 iovs 引用的部分。
裝置發現¶
除了 TCMU 之外,其他裝置可能也在使用 UIO。 不相關的使用者程序也可能正在處理不同的 TCMU 裝置集。 TCMU 使用者空間程序必須透過掃描 sysfs class/uio/uio*/name 來查詢其裝置。 對於 TCMU 裝置,這些名稱的格式為
tcm-user/<hba_num>/<device_name>/<subtype>/<path>
其中 “tcm-user” 對於所有 TCMU 支援的 UIO 裝置都是通用的。 <hba_num> 和 <device_name> 允許使用者空間在核心目標的 configfs 樹中找到裝置的路徑。 假設通常的掛載點,可以在
/sys/kernel/config/target/core/user_<hba_num>/<device_name>
此位置包含使用者空間需要了解才能正確操作的屬性,例如 “hw_block_size”。
<subtype> 將是一個使用者空間程序唯一的字串,用於將 TCMU 裝置標識為期望由特定處理程式支援,<path> 將是一個額外的特定於處理程式的字串,用於使用者程序配置裝置(如果需要)。 由於 LIO 的限制,名稱不能包含 ‘:’。
對於所有如此發現的裝置,使用者處理程式開啟 /dev/uioX 並呼叫 mmap()
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
其中 size 必須等於從 /sys/class/uio/uioX/maps/map0/size 讀取的值。
裝置事件¶
如果新增或刪除了新裝置,則會透過 netlink 廣播通知,使用 “TCM-USER” 的通用 netlink 族名稱和名為 “config” 的多播組。 這將包括上一節中描述的 UIO 名稱以及 UIO 次要號碼。 這應該允許使用者空間標識 UIO 裝置和 LIO 裝置,以便在確定裝置受到支援(基於子型別)後,它可以採取適當的措施。
其他意外情況¶
使用者空間處理程式程序從未附加
TCMU 將釋出命令,然後在超時時間(30 秒)後中止它們。
使用者空間處理程式程序被終止
仍然可以重啟並重新連線到 TCMU 裝置。 命令環被保留。 但是,在超時時間後,核心將中止待處理的任務。
使用者空間處理程式程序掛起
核心將在超時時間後中止待處理的任務。
使用者空間處理程式程序是惡意的
該程序可以輕鬆地破壞其控制的裝置的處理,但不應能夠訪問其共享記憶體區域之外的核心記憶體。
編寫使用者直通處理程式(帶有示例程式碼)¶
處理 TCMU 裝置的使用者程序必須支援以下各項
發現和配置 TCMU uio 裝置
等待裝置上的事件
管理命令環:解析操作和命令,根據需要執行工作,設定響應欄位(scsi_status 和可能的 sense_buffer),更新 cmd_tail,並通知核心已完成工作
首先,考慮改為編寫一個 tcmu-runner 的外掛。 tcmu-runner 實現了所有這些,併為外掛作者提供了更高級別的 API。
TCMU 旨在允許多個不相關的程序分別管理 TCMU 裝置。 所有處理程式都應確保僅基於已知的子型別字串開啟其裝置。
發現和配置 TCMU UIO 裝置
/* error checking omitted for brevity */ int fd, dev_fd; char buf[256]; unsigned long long map_len; void *map; fd = open("/sys/class/uio/uio0/name", O_RDONLY); ret = read(fd, buf, sizeof(buf)); close(fd); buf[ret-1] = '\0'; /* null-terminate and chop off the \n */ /* we only want uio devices whose name is a format we expect */ if (strncmp(buf, "tcm-user", 8)) exit(-1); /* Further checking for subtype also needed here */ fd = open(/sys/class/uio/%s/maps/map0/size, O_RDONLY); ret = read(fd, buf, sizeof(buf)); close(fd); str_buf[ret-1] = '\0'; /* null-terminate and chop off the \n */ map_len = strtoull(buf, NULL, 0); dev_fd = open("/dev/uio0", O_RDWR); map = mmap(NULL, map_len, PROT_READ|PROT_WRITE, MAP_SHARED, dev_fd, 0); b) Waiting for events on the device(s) while (1) { char buf[4]; int ret = read(dev_fd, buf, 4); /* will block */ handle_device_events(dev_fd, map); }
管理命令環
#include <linux/target_core_user.h> int handle_device_events(int fd, void *map) { struct tcmu_mailbox *mb = map; struct tcmu_cmd_entry *ent = (void *) mb + mb->cmdr_off + mb->cmd_tail; int did_some_work = 0; /* Process events from cmd ring until we catch up with cmd_head */ while (ent != (void *)mb + mb->cmdr_off + mb->cmd_head) { if (tcmu_hdr_get_op(ent->hdr.len_op) == TCMU_OP_CMD) { uint8_t *cdb = (void *)mb + ent->req.cdb_off; bool success = true; /* Handle command here. */ printf("SCSI opcode: 0x%x\n", cdb[0]); /* Set response fields */ if (success) ent->rsp.scsi_status = SCSI_NO_SENSE; else { /* Also fill in rsp->sense_buffer here */ ent->rsp.scsi_status = SCSI_CHECK_CONDITION; } } else if (tcmu_hdr_get_op(ent->hdr.len_op) != TCMU_OP_PAD) { /* Tell the kernel we didn't handle unknown opcodes */ ent->hdr.uflags |= TCMU_UFLAG_UNKNOWN_OP; } else { /* Do nothing for PAD entries except update cmd_tail */ } /* update cmd_tail */ mb->cmd_tail = (mb->cmd_tail + tcmu_hdr_get_len(&ent->hdr)) % mb->cmdr_size; ent = (void *) mb + mb->cmdr_off + mb->cmd_tail; did_some_work = 1; } /* Notify the kernel that work has been finished */ if (did_some_work) { uint32_t buf = 0; write(fd, &buf, 4); } return 0; }
最後的說明¶
請注意按照 SCSI 規範定義的返回程式碼。 這些程式碼與 scsi/scsi.h 包含檔案中定義的一些值不同。 例如,CHECK CONDITION 的狀態程式碼為 2,而不是 1。