(WIP)Virtual I/O Device (VIRTIO) Version 1.3 翻譯
1 Introduction
本文件描述了「virtio」裝置家族的規格。 這些裝置雖然存在於虛擬環境中,但設計上它們在虛擬機內對 guest 來說,看起來就像實體裝置(同時本文件也直接將其視為實體裝置)。 這種相似性使得 guest 能夠使用標準的驅動程式與探索機制
virtio 及本規格的目的在於讓虛擬環境與 guest 能夠透過一個直接、有效率、標準且可擴充的機制來使用虛擬裝置,而不是依賴每個環境或作業系統特製化的機制
- 直接:
Virtio 裝置使用中斷與 DMA 等常見的匯流排機制,撰寫裝置驅動程式的人應該不陌生。 Virtio 裝置並沒有使用 page-flipping 或 COW(Copy-On-Write)等特殊的機制,它就是一個普通的裝置 - 高效:
Virtio 裝置由輸入與輸出的描述符的環(ring)組成,這些環被整齊地排列,以避免驅動程式與裝置同時寫入同一快取列時所產生的快取效應 - 標準:
除了需要支援裝置所連接的匯流排以外,Virtio 對其運作環境沒有其他要求。 本規格中,virtio 裝置透過 MMIO、Channel I/O 與 PCI 匯流排傳輸來實作,早期的草稿曾在其他匯流排上實作過,但未收錄於此 - 可擴充:
Virtio 裝置包含旗標,guest 作業系統在裝置初始化時會確認這些位元。 這機制允許向前與向後相容:裝置會提供所有它支援的功能,而驅動程式則會確認它支援且想要使用的部分
1.1 Normative References
- [RFC2119] Bradner S., “Key words for use in RFCs to Indicate Requirement Levels”, BCP 14, RFC 2119, March 1997. http://www.ietf.org/rfc/rfc2119.txt
- [RFC4122] Leach, P., Mealling, M., and R. Salz, “A Universally Unique IDentifier (UUID) URN Namespace”, RFC 4122, DOI 10.17487/RFC4122, July 2005.
http://www.ietf.org/rfc/rfc4122.txt - [S390 PoP] z/Architecture Principles of Operation, IBM Publication SA22-7832,
https://www.ibm.com/docs/en/SSQ2R2_15.0.0/com.ibm.tpf.toolkit.hlasm.doc/dz9zr006.pdf, and any future revisions - [S390 Common I/O] ESA/390 Common I/O-Device and Self-Description, IBM Publication SA22-7204,
https://www.ibm.com/resources/publications/OutputPubsDetails?PubID=SA22720401, and any future revisions - [PCI] Conventional PCI Specifications,
http://www.pcisig.com/specifications/conventional/, PCI-SIG - [PCIe] PCI Express Specifications
http://www.pcisig.com/specifications/pciexpress/, PCI-SIG - [IEEE 802] IEEE Standard for Local and Metropolitan Area Networks: Overview and Architecture,
http://www.ieee802.org/, IEEE - [SAM] SCSI Architectural Model,
http://www.t10.org/cgi-bin/ac.pl?t=f&f=sam4r05.pdf - [SCSI MMC] SCSI Multimedia Commands,
http://www.t10.org/cgi-bin/ac.pl?t=f&f=mmc6r00.pdf - [FUSE] Linux FUSE interface,
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/fuse.h - [errno] Linux error names and numbers,
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno-base.h - [eMMC] eMMC Electrical Standard (5.1), JESD84-B51,
http://www.jedec.org/sites/default/files/docs/JESD84-B51.pdf - [HDA] High Definition Audio Specification,
https://www.intel.com/content/dam/www/public/us/en/documents/product-specifications/high-definition-audio-specification.pdf - [I2C] I2C-bus specification and user manual,
https://www.nxp.com/docs/en/user-guide/UM10204.pdf - [SCMI] Arm System Control and Management Interface, DEN0056,
https://developer.arm.com/docs/den0056/c, version C and any future revisions - [RFC3447] J. Jonsson.,“Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography”, February 2003.
https://www.ietf.org/rfc/rfc3447.txt - [FIPS186-3] National Institute of Standards and Technology (NIST), FIPS Publication 180-3: Secure Hash Standard, October 2008.
https://csrc.nist.gov/csrc/media/publications/fips/186/3/archive/2009-06-25/documents/fips_186-3.pdf - [RFC5915] “Elliptic Curve Private Key Structure”, June 2010.
https://www.rfc-editor.org/rfc/rfc5915 - [RFC6025] C.Wallace., “ASN.1 Translation”, October 2010.
https://www.ietf.org/rfc/rfc6025.txt - [RFC3279] W.Polk., “Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile”, April 2002.
https://www.ietf.org/rfc/rfc3279.txt - [SEC1] Standards for Efficient Cryptography Group(SECG), “SEC1: Elliptic Cureve Cryptography”, Version 1.0, September 2000.
https://www.secg.org/sec1-v2.pdf - [RFC2784] Generic Routing Encapsulation. This protocol is only specified for IPv4 and used as either the payload or delivery protocol.
https://datatracker.ietf.org/doc/rfc2784/ - [RFC2890] Key and Sequence Number Extensions to GRE. This protocol describes extensions by which two fields, Key and Sequence Number, can be optionally carried in the GRE Header.
https://www.rfc-editor.org/rfc/rfc2890 - [RFC7676] IPv6 Support for Generic Routing Encapsulation (GRE). This protocol is specified for IPv6 and used as either the payload or delivery protocol. Note that this does not change the GRE header format or any behaviors specified by RFC 2784 or RFC 2890.
https://datatracker.ietf.org/doc/rfc7676/ - [GRE-in-UDP] GRE-in-UDP Encapsulation. This specifies a method of encapsulating network protocol packets within GRE and UDP headers. This protocol is specified for IPv4 and IPv6, and used as either the payload or delivery protocol.
https://www.rfc-editor.org/rfc/rfc8086 - [VXLAN] Virtual eXtensible Local Area Network.
https://datatracker.ietf.org/doc/rfc7348/ - [VXLAN-GPE] Generic Protocol Extension for VXLAN. This protocol describes extending Virtual eXtensible Local Area Network (VXLAN) via changes to the VXLAN header.
https://www.ietf.org/archive/id/draft-ietf-nvo3-vxlan-gpe-12.txt - [GENEVE] Generic Network Virtualization Encapsulation.
https://datatracker.ietf.org/doc/rfc8926/ - [IPIP] IP Encapsulation within IP.
https://www.rfc-editor.org/rfc/rfc2003 - [NVGRE] NVGRE: Network Virtualization Using Generic Routing Encapsulation
https://www.rfc-editor.org/rfc/rfc7637.html - [IP] INTERNET PROTOCOL
https://www.rfc-editor.org/rfc/rfc791 - [UDP] User Datagram Protocol
https://www.rfc-editor.org/rfc/rfc768 - [TCP] TRANSMISSION CONTROL PROTOCOL
https://www.rfc-editor.org/rfc/rfc793 - [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017
http://www.ietf.org/rfc/rfc8174.txt
1.2 Non-Normative References
- [Virtio PCI Draft] Virtio PCI Draft Specification. http://ozlabs.org/~rusty/virtio-spec/virtio-0.9.5.pdf
1.3 Terminology
本文件中的關鍵字「MUST」、「MUST NOT」、「REQUIRED」、「SHALL」、「SHALL NOT」、「SHOULD」、「SHOULD NOT」、「RECOMMENDED」、「NOT RECOMMENDED」、「MAY」、「OPTIONAL」在出現全大寫時,必須依 [RFC2119] 與 [RFC8174] 的定義來解釋
1.3.1 Legacy Interface: Terminology
在本規格 1.0 版本之前的規格草案(例如見 [Virtio PCI Draft])定義了一種與本規格相似但不同的驅動程式與裝置之間的介面。 由於這些已被廣泛部署,本規格納入了「選用(OPTIONAL)」功能,以簡化從這些早期草案介面過渡的工作
具體而言,裝置與驅動程式可能支援:
- Legacy 介面
指的是由本規格較早的草案(1.0 之前)所規定的介面 - Legacy 裝置
指在本規格發布之前實作,且在 host 端實作 Legacy 介面的裝置 - Legacy 驅動程式
指在本規格發布之前實作,且在 guest 端實作 Legacy 介面的驅動程式
Legacy 裝置與 Legacy 驅動程式不符合本規格。 為了簡化從這些早期草案介面的過渡,裝置可能會實作:
- Transitional 裝置
同時支援符合本規格的驅動程式,並允許 Legacy 驅動程式的裝置
同樣地,驅動程式可能會實作:
- Transitional 驅動程式
同時支援符合本規格的裝置,以及 Legacy 裝置的驅動程式
注意:Legacy 介面不是必需的,也就是說,除非你需要向後相容性,否則不要實作它們! 不具有任何 Legacy 相容性的裝置或驅動程式,分別稱為 non-transitional 裝置與 non-transitional 驅動程式
1.3.2 Transition from earlier specification drafts
對於已經實作 legacy 介面的裝置與驅動程式,為了支援本規格,可能需要進行一些修改。 在這種情況下,讀者可能會受益於章節標題中標示為「Legacy Interface」的部分。 這些段落凸顯了自早期草稿以來所做的更動
1.4 結構規格
許多裝置與驅動程式的「記憶體內」結構配置使用 C 的 struct
語法來描述。 所有結構預設都不包含額外的填充(padding)。 為了強調這點,在已知一般 C 編譯器可能在結構內插入額外填充的情況,會以 GNU C 的 __attribute__((packed))
語法加以標示
在結構定義中使用的整數資料型別,遵循以下慣例:
- u8、u16、u32、u64
指定長度(位元)的無號整數 - le16、le32、le64
指定長度(位元)、以小端序(little-endian)位元組順序儲存的無號整數 - be16、be32、be64
指定長度(位元)、以大端序(big-endian)位元組順序儲存的無號整數
本規格中有些欄位不會從位元組邊界開始,或不會在位元組邊界結束。 這類欄位被稱為 bit-fields。 一組 bit-fields 必定是某個整數型別欄位的 sub-division
列出整數欄位內的 bit-fields 時一律會從最低有效位(LSB)到最高有效位(MSB)依序列出。 bit-fields 被視為指定寬度的無號整數,並且保留位元由低到高的相鄰順序關係
例如:
struct S {
be16 {
A : 15;
B : 1;
} x;
be16 y;
};
這段描述表示:A
的值存放在 x
的低 15 位元,B
的值存放在 x
的最高 1 位元。 而 16 位元的 x
本身,以大端序儲存在結構 S
的起始位置,接著緊鄰其後的是無號整數 y
,同樣以大端序儲存,位於自結構起始位置算起的 2 位元組(16 位元)偏移處
請注意,此種記法與 C 的 bitfield 語法有些相似,但在可攜式(portable)程式碼中不應天真地轉換為 C 的 bitfield:它與 C 編譯器在小端架構上的 bitfield 打包方式相符,但不符合 C 編譯器在大端架構上的打包方式
Tips
不同端序與不同編譯器對 C bitfield 的配置(位序、填充)可能不一致。 規格採用自有的「描述性記法」,而非直接要求用 C bitfield,避免因編譯器實作差異導致跨平台不一致
假設 CPU_TO_BE16
會把本機端序的 16 位元整數轉換為大端序的位元組順序,那麼要產生要寫入 x
的值,其可攜式的 C 語言寫法如下:
CPU_TO_BE16(B << 15 | A)
1.5 常數規格
在許多情況下,裝置與驅動程式之間介面所使用的數值,會以 C 的 #define
搭配 /* ... */
註解語法來描述。 多個相關的數值會以共同名稱作為前綴,並以底線 _
作為分隔。 以 _XXX
作為後綴時,表示該群組中的全部數值。 例如:
/* Field Fld value A description */
#define VIRTIO_FLD_A (1 << 0)
/* Field Fld value B description */
#define VIRTIO_FLD_B (1 << 1)
這段為欄位 Fld
定義了兩個數值:Fld=1
代表 A
,Fld=2
代表 B
。 注意,<<
表示左移運算
此外,在此例中,VIRTIO_FLD_A
與 VIRTIO_FLD_B
分別對應 Fld
的數值 1 與 2。 進一步地,VIRTIO_FLD_XXX
代表該群組中的任一個(亦即 VIRTIO_FLD_A
或 VIRTIO_FLD_B
)
2 Virtio 裝置的基本機制
Virtio 裝置的枚舉/識別方式取決於其所連接的匯流排(見各匯流排專節:4.1 Virtio Over PCI Bus、4.2 Virtio Over MMIO 與 4.3 Virtio Over Channel I/O)
每個裝置都包含了下列幾個部分:
- 裝置狀態欄位(Device status field)
- 旗標(Feature bits)
- 通知(Notifications)
- 裝置組態空間(Device Configuration space)
- 一個或多個 virtqueue
2.1 裝置狀態欄位
當驅動程式初始化裝置時,會依 3.1 所指定的一連串步驟來進行
裝置狀態欄位提供了這個流程中已完成步驟的簡單低階指示。 可以把它想像成主控台上的號誌燈(紅綠燈),用來顯示每個裝置的狀態。 定義的位元如下(以下列出它們典型的設定順序):
ACKNOWLEDGE
(1)
表示 guest 作業系統已找到該裝置,並辨識其為有效的 virtio 裝置DRIVER
(2)
表示 guest 作業系統知道如何驅動此裝置。 注意:在設定此位元前,可能會有顯著(甚至無限期)的延遲,例如在 Linux 中,驅動程式可能是可載入模組FAILED
(128)
表示 guest 發生了問題,並且已放棄了這個裝置。 可能的原因包含內部錯誤、驅動程式因某種理由拒絕該裝置,或在裝置操作期間發生致命錯誤等FEATURES_OK
(8)
表示驅動程式已確認所有它支援的功能,功能協商已完成DRIVER_OK
(4)
表示驅動程式已設定完成,準備好驅動該裝置了DEVICE_NEEDS_RESET
(64)
表示裝置遭遇了無法自行復原的錯誤
裝置狀態欄位初始為 0,在重置期間,裝置也會把它重新設為 0
2.1.1 驅動程式需求:裝置狀態欄位
驅動程式必須更新裝置狀態,依 3.1 所述的初始化流程,設定各位元以表示已完成的步驟。 驅動程式不能清除任何裝置狀態位元。 若驅動程式設定了 FAILED
位元,則在嘗試重新初始化之前,必須先對裝置進行重置(reset)
當 DEVICE_NEEDS_RESET
被設為 1 時,驅動程式不應該依賴裝置操作的完成。 注意:例如,若 DEVICE_NEEDS_RESET
已設為 1,驅動程式既不能假設「進行中的請求會完成」,也不能假設「它們尚未完成」。 良好的實作應嘗試透過重置來復原
2.1.2 裝置需求:裝置狀態欄位
在 DRIVER_OK
之前,裝置不能取用(consume)任何緩衝區,亦不得向驅動程式送出任何「已用緩衝區(used buffer)」的通知
當裝置進入需要重置才能復原的錯誤狀態時,裝置應要設定 DEVICE_NEEDS_RESET
。 若此時 DRIVER_OK
已被設為 1,則在設定 DEVICE_NEEDS_RESET
之後,裝置必須向驅動程式送出裝置組態的變更通知
2.2 旗標(Feature Bits)
每個 virtio 裝置會提供它支援的所有特性。 在裝置初始化期間,驅動程式會讀取這些特性,並告訴裝置它所接受的子集合。 要重新協商,只能重置該裝置
這能讓系統具有向前與向後的相容性:如果裝置新增了一個旗標,較舊的驅動程式不會把那個旗標回寫給裝置。 同樣地,如果驅動程式新增了一個裝置不支援的功能,驅動程式也能知道該裝置沒有提供此新功能
旗標的配置如下:
- 0 到 23,以及 50 到 127:
用於特定裝置型別的旗標 - 24 到 41:
保留給 virtqueue 與功能協商機制的擴充用旗標 - 42 到 49,以及 128 與以上:
保留給未來擴充使用的旗標
注意:例如,對網路裝置(也就是 Device ID 1)而言,旗標 0 表示該裝置支援封包的 checksum。 而對於裝置組態空間(device configuration space),如果有了新欄位,則需要搭配一個新的旗標來表示
要讓功能協商機制保持可擴充的重點在於:裝置不應提供那些在驅動程式接受後,裝置本身無法處理的旗標(雖然依照本規格,驅動程式本就不該接受任何未定義、保留或不受支援的特徵)。 同理,驅動程式也不應接受自己不知道如何處理的旗標(雖然依照本規格,裝置本就不該主動提供未定義、保留或不受支援的特徵)。 對於保留或非預期的特徵,首選的處理方式是由驅動程式將其忽略
對僅限於特定匯流排的功能而言,上述原則更為重要,因為若未來規格將這些功能擴大到更多的匯流排上,很可能需要驅動程式與裝置改變既有行為。 支援多種匯流排的驅動程式與裝置,必須謹慎維護每個匯流排各自允許的功能清單
2.2.1 驅動程式需求:旗標
驅動程式不能接受裝置未提供的功能,也不能接受一個需要其他尚未被接受的功能所依賴的功能
驅動程式必須驗證裝置所提供的旗標。 對於下列任一情形的旗標,驅動程式必須忽略且不能接受:
- 未在本規格中描述的
- 標記為保留(reserved)的
- 不適用於該特定匯流排的
- 未為該裝置型別所定義的
如果裝置沒有提供某個驅動程式所支援的功能,驅動程式應要切換到向後相容模式,否則就必須將裝置狀態的 FAILED
位元設為 1,並停止初始化
相對地,驅動程式不能僅僅因為裝置提供了一個驅動程式不支援的功能,就宣告失敗
2.2.2 裝置需求:旗標
裝置不能提供一個依賴於其他尚未被提供的功能的特徵。 裝置應該接受驅動程式所接受的功能中的任何有效子集合,否則當驅動程式寫入時,裝置必須讓裝置狀態欄位中的 FEATURES_OK
位元設定失敗
裝置不能提供那些即使驅動程式接受了,裝置本身也無法支援的旗標(即便本規格禁止驅動接受這些旗標)。 為求明確,這裡指的是:本規格未描述的旗標、被標記為保留(reserved)的旗標、對特定匯流排或特定裝置型別而言被保留或不受支援的旗標。 不過,若未來版本的本規格允許裝置支援相對應功能,則依該未來規格撰寫的裝置當然不在此限
若某裝置曾成功協商過一組功能(在裝置初始化期間接受了裝置狀態欄位中的 FEATURES_OK
位元),那麼在裝置或系統重置之後,對於相同的一組功能,它不應該在重新協商時失敗。 否則會影響從暫停(suspend)恢復以及錯誤復原的流程
2.2.3 Legacy 介面:旗標說明
Transitional 驅動程式必須透過檢測旗標 VIRTIO_F_VERSION_1
有沒有被提供,來偵測 Legacy 裝置。 Transitional 裝置必須透過檢測 VIRTIO_F_VERSION_1
有沒有被驅動程式確認,來偵測 Legacy 驅動程式。 在此情形下,將透過 Legacy 介面來使用該裝置
對 Legacy 介面的支援是選用的(OPTIONAL)。 因此,transitional 與 non-transitional 的裝置與驅動程式,皆符合本規格
當透過 Legacy 介面使用裝置時,transitional 裝置與 transitional 驅動程式必須依照這些 Legacy 介面章節中所記載的需求來運作。 這些章節的規範文字,一般不適用於 non-transitional 裝置
2.3 通知(Notifications)
在本規格中,傳送通知(從驅動程式到裝置,或從裝置到驅動程式)的概念扮演重要角色。 通知的運作方式(modus operandi)取決於所使用的匯流排
通知有三種類型:
- 組態變更(configuration change notification)
- 可用緩衝區(available buffer notification)
- 已用緩衝區(used buffer notification)
configuration change 與 used buffer 這兩種通知由裝置送出,由驅動程式接收。 configuration change 通知表示裝置組態空間(device configuration space)發生了變更,used buffer 通知表示在該通知所指定的 virtqueue 上,可能有緩衝區被標記為已用的(used)
available buffer 通知由驅動程式送出,由裝置接收。 此類通知表示在該通知所指定的 virtqueue 上,可能有緩衝區被標記為可用的(available)
不同通知的語義、各匯流排的實作方式,以及其他重要面向,將在後續章節中詳細規範
多數匯流排會以中斷(interrupts)來實作由裝置送往驅動程式的通知。 因此,在先前版本的規格中,這類通知經常被稱為「中斷」。 本規格中有些名稱仍保留這種「中斷」的術語,偶爾也會以 event 一詞來指稱某個通知或其接收行為
2.4 裝置重置(Device Reset)
驅動程式可能會在不同時機主動發起裝置重置。 特別是在裝置初始化與裝置清理時,其必須執行重置。 驅動程式用來啟動重置的機制,取決於其使用的匯流排
2.4.1 裝置需求:裝置重置
裝置在收到重置之後,必須將裝置狀態欄位重新初始化為 0
當裝置透過把裝置狀態欄位重新設為 0 來表示重置已完成之後,在驅動程式重新初始化裝置之前,裝置不能送出通知,亦不能與各個佇列(queues)互動
2.4.2 驅動程式需求:裝置重置
當驅動程式讀到裝置狀態欄位為 0 時,驅動程式應該視其所發起的重置已經完成
2.5 裝置組態空間(Device Configuration Space)
裝置組態空間通常用於很少改變,或只在初始化時期使用的參數。 對於選用的組態欄位,會用旗標來指示其是否存在,未來版本的本規格很可能會在組態空間的尾端加入額外欄位以擴充。 注意:裝置組態空間中的多位元組欄位使用小端序(little-endian)格式
每種匯流排都會為裝置組態空間提供一個世代計數(generation count),只要存在兩次存取可能讀到不同版本內容的情況,這個值就會改變
Tips
從驅動程式的角度看,裝置組態空間(Device Configuration Space)就是一小段、可被程式讀/寫的暫存器/記憶體映射區,專門用來放「裝置的參數與狀態」,而且這些參數通常是初始化時要讀、或很少變動的那種。 它不是拿來搬大宗資料的(那是 virtqueue 的工作),而是控制面(control plane)的資訊面板
其內容是由 virtio 裝置暴露給驅動程式的一小段「欄位集合」,用來放各種裝置特定的設定/能力/狀態,驅動透過讀取它來決定要怎麼啟用裝置、配多少資源、是否開哪些功能,例如 virtio-net(網路)內存放了:
- MAC 位址(若裝置宣告
VIRTIO_NET_F_MAC
) - MTU(
VIRTIO_NET_F_MTU
) - 最大佇列對數(
VIRTIO_NET_F_MQ
,多佇列能力)
由於你可能一次要連續讀多個欄位(有的還是 64-bit),裝置可能在你讀到一半時被更新(例如容量剛被管理端調整)。 為了避免拼出的資訊是「前半新、後半舊」的混合體,匯流排會提供 generation count。 透過「先讀一次 generation → 讀欄位們 → 再讀一次 generation」,若前後讀到的 generation 相等,就代表這次讀到的是一致快照,反之就重讀
2.5.1 驅動程式需求:裝置組態空間
驅動程式不能假設寬度大於 32 位元的欄位讀取是原子操作,也不能假設跨多個欄位的讀取是原子操作。 驅動程式應該如下讀取裝置組態空間欄位:
u32 before, after;
do {
before = get_config_generation(device);
// 讀取一個或多個組態項目
after = get_config_generation(device);
} while (after != before);
Tips
大於 32-bit 欄位的讀取不保證原子性,跨欄位更不保證原子性。 建議的樣板是以 generation 計數循環讀取,確保前後一致
對於選用的組態空間欄位,驅動程式必須先檢查對應的特徵是否已由裝置提供,然後才可存取該組態區段。 功能協商的細節請見第 3.1 節
驅動程式不能預先限制結構大小或裝置組態空間大小。 相反地,驅動程式應該只檢查裝置組態空間是否足以包含裝置運作所需的欄位。 例如,若規格說明組態空間「包含一個 8 位元的欄位」,則應理解為組態空間的末端可能還有任意長度的尾端 padding,因此只要組態空間大於或等於該 8 位元的大小,都應接受
2.5.2 裝置需求:裝置組態空間
在驅動程式設定 FEATURES_OK
之前,裝置必須允許驅動讀取任何裝置特定的組態欄位。 這也包含那些取決於旗標的欄位,只要這些旗標已由裝置提供即可
2.5.3 Legacy 介面:裝置組態空間的端序說明
對於 legacy 介面,裝置組態空間通常採用 guest 的原生端序,而非 PCI 的小端序。 各裝置正確的端序在其對應的章節中另有記載
2.5.4 Legacy 介面:裝置組態空間
Legacy 裝置沒有「組態世代(generation)」的欄位,因此在組態被更新時容易遭遇競態(race conditions)。 這會影響到像是區塊容量(block capacity,見 5.2.4)與網路 MAC(見 5.1.4)等欄位。 在使用 legacy 介面時,驅動程式應該連續多次讀取,直到兩次的讀值一致為止
2.6 Virtqueues
在 virtio 裝置上用於大量資料傳輸的機制被稱為 virtqueue。 每個裝置可以有 0 至 65536 個 virtqueue,並以索引值來識別,索引值範圍為 0 到 65535。 例如,最簡單的網路設備會有一個用於傳送的 virtqueue 和一個用於接收的 virtqueue
驅動程式會透過把可用緩衝區(available buffer)加入佇列(把描述該請求的緩衝區加入某個 virtqueue),讓裝置取得請求。 必要時還可以觸發驅動端事件(driver event),亦即向裝置送出可用緩衝區的通知
裝置負責執行請求,並在完成時把已用緩衝區(used buffer)加入佇列(將緩衝區標記為 used),以通知驅動。 之後裝置可以觸發裝置端事件(device event),亦即向驅動送出已用緩衝區的通知
裝置會回報它實際寫入至每個被使用的緩衝區的記憶體的位元組數,稱為「used length」。 一般來說,裝置不需要按照驅動程式提供緩衝區的順序來使用它們
Tips
virtqueue 是進行大量(bulk)I/O 的核心抽象。 它把「要處理的資料緩衝」排成可供裝置消費的佇列,由驅動與裝置協同操作。 設計上允許一個裝置擁有多個佇列,以支援並行處理
「將請求加入佇列」意旨把一段描述 I/O 的記憶體區塊(buffer/descriptor)掛到 virtqueue。 是否會同時「通知」裝置,依策略決定:
- 高吞吐時可能抑制(coalesce)通知,一次加入多筆後再通知,以減少中斷/MMIO 次數
- 低延遲時則每筆都通知,加速裝置開工。 這與 2.3 的通知機制呼應:driver → device 的是「available buffer」通知
裝置完成處理後會回填狀態/長度等資訊,並更新「已用」結構。 同樣可以彈性地選擇是否要立刻通知驅動,以在延遲與中斷負載間取捨(例如批次完成、多筆合併一次中斷)
used length 是由裝置回報的實際產出長度,與原先驅動提供的 buffer 容量不同。 驅動需據此判斷:
- 資料是否截斷(如通常不允許 used length > 預期容量,可能需丟棄)
- 收到的有效資料大小(例如網路 RX 長度、區塊 I/O 回傳的 bytes)。 這也常被用來驅動「分段處理」或「下一步解析」
這個流程預設允許亂序完成:裝置可依內部排程(大小排序、併發執行、硬體管線)選取可用的 buffer,因此驅動不可假設 FIFO。 這能提升效能與資源利用,但驅動的完成處理必須能對應「任意順序」的回報(例如根據 cookie/descriptor id 對應請求)
有些裝置總是按照緩衝區被提供的順序來使用描述符(descriptor)。 這類裝置可以提供 VIRTIO_F_IN_ORDER
特徵,若成功協商,這項資訊可能允許進一步最佳化,或簡化驅動與/或裝置端的程式碼
Tips
VIRTIO_F_IN_ORDER
是「保證順序」的合約:
- 對驅動,省去額外的亂序對應結構(如 hash table)與紀錄
- 對裝置,則須放棄某些重排最佳化。 是否啟用取決於場景(低複雜度 vs. 高吞吐需求)。 若未協商此特徵,驅動必須按亂序路徑實作
每個 virtqueue 最多可以由 3 個部分組成:
- Descriptor Area:用於描述緩衝區
- Driver Area:由驅動程式提供給裝置的額外資料
- Device Area:由裝置提供給驅動程式的額外資料
Tips
- Descriptor Area:載明每個 buffer 的位址、長度、方向等(例如 readable/writable)
- Driver Area:驅動傳遞控制資訊給裝置(如「哪些描述符現已可用」、available 索引等)
- Device Area:裝置把完成資訊傳回驅動(如 used 索引、used length、狀態碼)
實際的具體結構與佈局,取決於 Split 或 Packed 兩種格式
先前版本(見 2.7 之後)對這些部分使用了不同名稱:
- Descriptor Table(對應 Descriptor Area)
- Available Ring(對應 Driver Area)
- Used Ring(對應 Device Area)
virtqueue 支援兩種格式:Split Virtqueues(見 2.7)與 Packed Virtqueues(見 2.8)。 每個驅動與裝置至少會支援其中一種(Packed 或 Split),也可能兩者都支援
2.6.1 Virtqueue 重置(Virtqueue Reset)
當 VIRTIO_F_RING_RESET
成功協商後,驅動程式可以個別重置某個 virtqueue。 如何重置 virtqueue 取決於各匯流排的規定。 virtqueue 的重置分成兩個步驟:驅動程式先重置該佇列,之後可以選擇性地將其重新啟用(re-enable)
Tips
Re-enable ≈ 重新初始化單一佇列:
- 驅動端:重新配置描述符區、Driver/Device Area、更新可用索引並(視需求)開啟通知,還可以調整參數(如 queue size,用於降級或升級吞吐)
- 裝置端:必須按照驅動最新設定運作,避免使用舊的佇列長度或過期的 DMA 位址。 這一來可在不中斷整個裝置的情況下,針對單佇列完成錯誤復原或容量調整
2.6.1.1 Virtqueue 重置
2.6.1.1.1 裝置需求:Virtqueue 重置
在佇列被驅動程式重置之後,裝置不能再從該 virtqueue 執行任何請求,也不能就此 virtqueue 通知驅動程式。 裝置必須把該 virtqueue 的所有狀態重置為預設值,包括 available 狀態與 used 狀態
Tips
- 靜默:不再消費、亦不再發送通知,避免與驅動在重建資料結構期間產生 race condition
- 清空狀態:可用/已用索引、wrap counter(於 packed)、暫存指標等回到初始值,確保後續 re-enable 從乾淨狀態開始
2.6.1.1.2 驅動程式需求:Virtqueue 重置
當驅動程式要求裝置重置某個佇列後,驅動程式必須驗證該佇列是否已成功被重置。 在佇列成功重置之後,驅動程式可以(MAY)釋放與該 virtqueue 相關的任何資源
2.6.1.2 Virtqueue 重新啟用(Re-enable)
此流程與整體裝置初始化期間、針對單一佇列的初始化流程相同
2.6.1.2.1 裝置需求:Virtqueue 重新啟用
裝置必須遵從任何可能已被驅動程式變更的佇列組態,例如最大佇列大小等
2.6.1.2.2 驅動程式需求:Virtqueue 重新啟用
在重新啟用佇列時,驅動程式必須如同初次發現該 virtqueue 時那樣配置佇列資源,但也可以(MAY)採用與先前不同的參數
2.7 Split Virtqueues
在本標準 1.0(含更早)的版本中只支援 Split 形式的 virtqueue。 Split 形式會把一個 virtqueue 拆成多個部分,其中每一部分只允許驅動程式或裝置其中之一寫入,而不會同時由雙方寫入。 當把緩衝區標記為可用、或把它標記為已用時,必須更新多個部分,以及(或)同一部分中的多個位置
Tips
Split 的核心理念是「所有權分離」來降低 cache line 爭用與同步成本:
- Driver Area 只由驅動寫(提供可用描述符)
- Device Area 只由裝置寫(回報完成)
- Descriptor Area 雙方都能讀,但不能同時寫同一格
因此在「投遞」與「完成」時,需要跨多個區塊更新(例如把索引寫進 Available Ring,完成時把索引寫進 Used Ring),這就是 Split 的操作模式
每個佇列都有一個 16 位元的 queue size 參數,用來設定表列項目的數量,並由此決定整個佇列的總大小
virtqueue 由三個部分組成:
- Descriptor Table(佔用 Descriptor Area)
- Available Ring(佔用 Driver Area)
- Used Ring(佔用 Device Area)
每一部分在 guest 記憶體中都必須是實體連續(physically-contiguous)的,並且各自有不同的對齊要求。 各部分的記憶體對齊與大小如下:
Virtqueue 部分 | 對齊(bytes) | 大小(bytes) |
---|---|---|
Descriptor Table | 16 | 16 × (Queue Size) |
Available Ring | 2 | 6 + 2 × (Queue Size) |
Used Ring | 4 | 6 + 8 × (Queue Size) |
其中對齊欄給出每個部分所需的最小對齊,大小欄給出該部分的總位元組數
Queue Size 對應於一個 virtqueue 中所能容納的最大緩衝區數量,例如,如果佇列大小為 4,則最多只能有 4 個緩衝區被排入佇列。 Queue Size 的取值一律為 2 的冪次,其最大值為 32768。 這個數值的提供方式由各匯流排特定的規範決定
當驅動程式要把緩衝區送交裝置時,會在 Descriptor Table 中填入一個項目(或把多個描述符串鍊起來),接著把描述符索引寫入 Available Ring,然後通知裝置。 當裝置處理完該緩衝區後,會把描述符索引寫入 Used Ring,並送出 used buffer 通知
Tips
所謂的描述符就是 Descriptor Table 裡面的元素,它是 metadata,負責描述「一段實體連續的記憶體」。 實際存放資料的位置被稱為緩衝區(buffer),在 2.7.5 的示意中描述符會以 addr
紀錄緩衝區的位址
一個請求可能會包含多個緩衝區,也因此要有多個描述符來記錄這些緩衝區,他是以 list 的方式來記錄的,被稱為描述符鏈。 鏈上的每一個描述符一樣都存在 Descriptor table 中,但在 Available ring 與 Used ring 內只會紀錄 list head,用的時候要從頭一個一個找到目標
個人覺得規格這部分寫得很亂,有時候還會將「緩衝區」與「請求」混用。 可以先看看以下材料幫助理解,再回來看規格:
2.7.1 驅動程式需求:Virtqueues
驅動程式必須確保每一個 virtqueue 部分(Descriptor/Available/Used)的首個位元組的實體位址都是上表內的對齊值的整數倍
2.7.2 Legacy 介面:Virtqueue 佈局說明
在 Legacy 介面下,virtqueue 的佈局有額外限制。 每個 virtqueue 會佔用兩個或更多個實體連續的頁面(通常一頁是 4096 bytes,但會依匯流排而異,以下稱作 Queue Align),並由三個部分構成:
Descriptor Table | Available Ring(...padding...) | Used Ring |
---|
Tips
Legacy 要求「以頁對齊的大區塊」配置,三者按順序排在同一片連續區域,中間以 padding 補到 Queue Align 邊界,接著才放 Used Ring,以便舊硬體/舊驅動一次性地映射整個 virtqueue 區塊
控制整個 virtqueue 總位元組數的是匯流排特定的 Queue Size 欄位。 在使用 Legacy 介面時,transitional 驅動程式必須從裝置讀取該 Queue Size,並且必須依照下列公式配置整個 virtqueue 的總大小(其中 Queue Align 記作 qalign
,Queue Size 記作 qsz
):
#define ALIGN(x) (((x) + qalign) & qalign)
static inline unsigned virtq_size(unsigned int qsz)
{
return ALIGN(sizeof(struct virtq_desc)*qsz + sizeof(u16)*(3 + qsz))
+ ALIGN(sizeof(u16)*3 + sizeof(struct virtq_used_elem)*qsz);
}
Tips
這段把 Descriptor Table + Available Ring 先對齊到 qalign
邊界,再加上 Used Ring,並再對齊到 qalign
,但這邊少了典型的「-1 / ~(qalign-1)
」遮罩動作)
舉個例子:若 qalign = 4096
、qsz = 256
,你先算出前兩塊大小,向上取 4096 邊界,再加上 Used Ring 的大小並向上取 4096 邊界,得到最終配置大小
此作法在 Legacy 下保證 Used Ring 必定從新的頁對齊邊界開始,符合當年的硬體/驅動假設
這會因 padding 而浪費一些空間。 使用 Legacy 介面時,transitional 裝置與驅動程式必須使用下列這個 virtqueue 佈局結構,來定位 virtqueue 中的各元素:
struct virtq {
// 每個描述符 16 bytes
struct virtq_desc desc[ Queue Size ];
// 可用描述符頭的環(遞增索引)
struct virtq_avail avail;
// 補到下一個 Queue Align 邊界
u8 pad[ Padding ];
// 已用描述符頭的環(遞增索引)
struct virtq_used used;
};
Tips
desc[]
、avail
、used
的排列與前述「Descriptor Table → Available Ring →(padding)→ Used Ring」對應avail
/used
的「遞增索引(free-running index)」表示索引會不斷地向前遞增(16-bit wrap),配合qsz
的 2 的冪性質,只需遮罩即可計算環上的位置
2.7.3 Legacy 介面:Virtqueue 的端序說明
請注意,使用 Legacy 介面時,transitional 裝置與驅動程式必須以 guest 的原生端序作為欄位與 virtqueue 的端序,這與本標準對非 Legacy 介面規定的「小端序」不同。 另外需假定 host 端已知 guest 的端序
Tips
- 非 Legacy(現行)統一採 little-endian,便於跨平台互通。 Legacy 保留了「照 guest 架構來」的慣例
- 若同時支援 Legacy 與非 Legacy,驅動需針對資料結構的讀寫做對應轉換,以免索引、長度等欄位解讀錯誤
2.7.4 訊息框架(Message Framing)
用描述符(descriptor)對訊息進行框架化(framing)的方法,與緩衝區內容本身無關。 例如,一個網路傳送緩衝區由 12 位元組的表頭(header)加上後面的網路資料包組成。 最簡單的做法,是在 Descriptor Table 中放一個 12 位元組的輸出(output)描述符,接著再放一個 1514 位元組的輸出描述符
但若 header 與資料包在記憶體中相鄰,也可以用一個 1526 位元組的單一輸出描述符來表示,甚至也可以用三個或更多描述符(只是那樣可能會降低效率)
Tips
這邊在說如何把一筆 I/O 請求「切成」一個或多個描述符,這完全取決於驅動的 framing 策略,裝置不應對「切法」做假設
以網路的例子來說,常見的做法是把可由裝置讀取的 header 與 payload 分別以一或多個「device-readable」描述符表達。 若兩者記憶體相鄰,可合併為一個更大的描述符,以降低 ring 更新與 DMA 映射開銷
請注意,有些裝置實作對「描述符總數/總長度」設有寬鬆但合理的限制(例如基於 host 作業系統中的 IOV_MAX
)。 實務上這不是問題:如果驅動把一個網路封包切成 1500 個、每個 1 位元組的描述符這種不合理的大小,實作有權拒絕它
Tips
IOV_MAX
是許多作業系統對單次 scatter-gather I/O 的向量數量上限。 裝置端(或 vhost/host)通常也會沿用或設置類似限制
規格雖允許彈性 framing,但驅動不應濫用,否則實作有權拒絕或退化處理。 良好的實作會在「延遲 vs. 吞吐 vs. CPU 負載」間取得平衡,例如為網卡選用數個描述符以涵蓋 header + payload + optional metadata
2.7.4.1 裝置需求:訊息框架
裝置不能對描述符的具體排列方式做任何假設。 裝置可以(MAY)針對一串(chain)描述符的長度設定合理上限
2.7.4.2 驅動程式需求:訊息框架
驅動程式必須把所有裝置可寫(device-writable)的描述符元素,放在所有裝置可讀(device-readable)的描述符元素之後
驅動程式不應該用過多的描述符來描述同一個緩衝區
Tips
確保裝置先讀完它需要的指令/表頭資料,再在鏈尾寫入回應/狀態,以避免讀寫區塊交錯引發的同步與安全風險(例如裝置尚未讀 header 就先寫 status)
2.7.4.3 Legacy 介面:訊息框架
遺憾的是,早期驅動實作採用了過於簡單的版型,儘管本規格已經文字上避免此事,但裝置端逐漸依賴那些簡化的版型。 此外,virtio_blk
的 SCSI 命令規格還要求根據框架邊界(frame boundaries)推斷欄位長度(見 5.2.6.3 Legacy Interface: Device Operation)
因此,在使用 Legacy 介面時,VIRTIO_F_ANY_LAYOUT
特徵用來告知裝置與驅動:雙方對 framing 不做任何假設。 若未協商到此特徵,各裝置章節會列出 transitional 驅動程式需要遵守的額外要求
2.7.5 Virtqueue 的 Descriptor Table
Descriptor Table 指向驅動程式要提供給裝置使用的各個緩衝區。 addr
是實體位址,各緩衝區可透過 next
串鍊起來。 單個描述符能夠描述一塊對裝置而言唯讀(device-readable)或唯寫(device-writable)的緩衝區,但一串描述符可以同時包含可讀與可寫的緩衝區
提供給裝置的記憶體內容,取決於裝置型別。 最常見的形式是:資料以一個表頭(header)開頭(其中欄位採小端序),供裝置讀取,並在尾端加上一個狀態尾部(status trailer),供裝置寫入:
struct virtq_desc {
/* Address (guest-physical). */
le64 addr;
/* Length. */
le32 len;
/* This marks a buffer as continuing via the next field. */
#define VIRTQ_DESC_F_NEXT 1
/* This marks a buffer as device write-only (otherwise device read-only). */
#define VIRTQ_DESC_F_WRITE 2
/* This means the buffer contains a list of buffer descriptors. */
#define VIRTQ_DESC_F_INDIRECT 4
/* The flags as indicated above. */
le16 flags;
/* Next field if flags & NEXT */
le16 next;
};
表中描述符的數量由此 virtqueue 的 queue size 決定:它也代表描述符鏈可能的最大長度
若已協商 VIRTIO_F_IN_ORDER
,驅動程式會依「環狀順序」使用描述符:從表格的偏移量 0 開始,走到表尾再繞回表頭。 注意:舊版 [Virtio PCI Draft] 把本結構稱為 vring_desc
,並把常數命名為 VRING_DESC_F_NEXT
等,但布局與數值相同
2.7.5.1 裝置需求:Descriptor Table
裝置不能寫入裝置可讀的緩衝區,裝置也不應該讀取裝置可寫的緩衝區(除非為了除錯或診斷,裝置可以(MAY)這麼做)。 裝置不能寫入任何 Descriptor Table 的表項
Tips
- 方向約束保障資料與狀態的一致性與安全性:讀區不可被覆寫。 寫區若被讀,多半僅為偵錯
- 不可改寫表:Descriptor Table 由驅動擁有,裝置若改寫,會破壞鏈完整性與系統安全(可能被視為裝置故障)
2.7.5.2 驅動程式需求:Descriptor Table
驅動程式不能建立總長度超過 232 位元組的描述符鏈,這也意味著禁止環狀循環的描述符鏈
若已協商 VIRTIO_F_IN_ORDER
,當驅動在表格偏移量 x
的描述符上設置 VRING_DESC_F_NEXT
並將其提供給裝置時:對於表中最後一個描述符(x = queue_size − 1
),驅動必須把 next
設為 0,對於其他描述符,必須把 next
設為 x + 1
2.7.5.3 間接描述符(Indirect Descriptors)
有些裝置能從同時派發大量且巨大的請求中受益。 VIRTIO_F_INDIRECT_DESC
特徵允許這樣的作法(見附錄 A:virtio_queue.h)。 為了提升 ring 的承載量,驅動程式可以把一張間接描述符表放在記憶體的任意處,並在主 virtqueue 中插入一個描述符(其 flags & VIRTQ_DESC_F_INDIRECT
為 1)指向承載該「間接描述符表」的緩衝區,此時 addr
與 len
分別是該間接表的位址與位元組長度
Tips
主 ring 的 qsz
有上限,當單筆請求需要非常多 SG 片段時,把「描述符們」搬到外部表,可以大幅減少主 ring 佔用,提升並行度。 主 ring 上只會放 1 個 INDIRECT
節點,裝置循著它到外部表,遍歷裡面的 virtq_desc[]
以取得整筆請求的所有片段
間接表的結構如下(len
是指向本表之那個描述符的長度。 這是變數,因此下列程式碼不能直接編譯):
struct indirect_descriptor_table {
/* 每個描述符 16 位元組 */
struct virtq_desc desc[len / 16];
};
第一個間接描述符位於間接表的起始處(索引 0),剩下的間接描述符會透過 next
串接。 當某個間接描述符的 flags & VIRTQ_DESC_F_NEXT
為 0 時,表示鏈結在此終止。 單一間接描述符表中,可以同時包含裝置可讀與裝置可寫的描述符
若已協商 VIRTIO_F_IN_ORDER
,間接描述符必須依序使用連續索引:0、1、2、...,依此類推
2.7.5.3.1 驅動程式需求:間接描述符
除非已協商 VIRTIO_F_INDIRECT_DESC
,否則驅動程式不能設置 VIRTQ_DESC_F_INDIRECT
旗標。 驅動程式不能在間接表內的描述符再次設置 VIRTQ_DESC_F_INDIRECT
(也就是說,每個描述符最多只指向一張間接表)。 驅動程式不能建立長度超過裝置 Queue Size 的描述符鏈
驅動程式不能同時在 flags 中設置 VIRTQ_DESC_F_INDIRECT
與 VIRTQ_DESC_F_NEXT
若已協商 VIRTIO_F_IN_ORDER
,間接描述符必須以連續順序出現,對第一個描述符,next
取值為 1,第二個為 2,依此類推
Tips
- 禁止間接的間接:避免遞迴/多層 indirection 造成複雜度與攻擊面暴增
- 不能同時 INDIRECT + NEXT:一個主表節點要麼指向「資料/鏈的下一段」,要麼指向「一張間接表」,不能兩者兼具以免歧義
- 鏈長限制:即便使用間接表,總鏈長仍受 Queue Size 約束,避免無界遍歷
2.7.5.3.2 裝置需求:間接描述符
對於指向間接表的那個描述符,裝置必須忽略其 flags & VIRTQ_DESC_F_WRITE
(write-only)旗標。 裝置必須能處理「零個或多個一般鏈結的描述符,接著是一個 flags & VIRTQ_DESC_F_INDIRECT
的描述符」這種情況。 注意:雖然少見(多數實作要麼全用一般描述符,要麼只用單一間接元素),但這種版型是有效的
Tips
- 忽略 WRITE 的原因:指向間接表的主表節點只是個指標,本身不承載資料讀/寫語義,因此 WRITE 在此無意義
- 混合版型:允許「先來幾段普通描述符(例如固定 header),最後再用一張間接表(包含大量 payload 片段)」的組合,裝置需能正確遍歷兩段式結構,提升彈性與擴充性
2.7.6 Virtqueue 的 Available Ring
Available Ring 的佈局如下:
struct virtq_avail {
#define VIRTQ_AVAIL_F_NO_INTERRUPT 1
le16 flags;
le16 idx;
le16 ring[ /* Queue Size */ ];
le16 used_event; /* 只有在 VIRTIO_F_EVENT_IDX 協商成功時才存在 */
};
驅動程式利用 Available Ring 來把緩衝區提供給裝置:環中的每個項目都指向一條描述符鏈(descriptor chain)的頭節點。 這個結構只會由驅動程式寫入,並由裝置讀取
idx
欄位表示驅動程式下一個要寫入環的位置(以 queue size 取模),它從 0 起算並遞增。 注意:舊版 [Virtio PCI Draft] 將此結構稱為 vring_avail
,並將常數命名為 VRING_AVAIL_F_NO_INTERRUPT
,但其布局與數值皆相同
Tips
Available Ring 是「投遞佇列」。 驅動在 Descriptor Table 填好描述符(或串鍊)後,把頭節點索引寫入 ring[idx & (qsz-1)]
,再遞增 idx
。 因 queue size 為 2 的冪,可用位元遮罩快速取模。 flags
可用來提示裝置「是否希望被通知」(未協商 EVENT_IDX
的情況下),若協商了 EVENT_IDX
,used_event
會出現,用更精細的門檻值控制「用到哪一格再通知」。 這個結構的單向寫入權(driver-only write)降低了 cache line 爭用與同步需求
2.7.6.1 驅動程式需求:Available Ring
驅動程式不能讓某個 virtqueue 的 available idx
倒退(i.e. 無法「收回」已曝光的緩衝區)
2.7.7 已用緩衝區通知的抑制(Used Buffer Notification Suppression)
若未協商 VIRTIO_F_EVENT_IDX
,驅動程式可透過 Available Ring 的 flags 提供一種較「陽春」的機制,告知裝置在緩衝區被標記為已用時不必通知。 若已協商 VIRTIO_F_EVENT_IDX
,則可改用效能更好的 used_event
,由驅動指定裝置可前進到哪個進度才需要通知
這兩種抑制通知的方法都不可靠,因為它們並未與裝置同步,但作為最佳化仍然有用
Tips
通知抑制是提示而非強制承諾,目標是減少中斷或 MMIO 次數,換取更高吞吐。 flags
僅能「要或不要」通知,used_event
則能設定「水位」,例如告知「用到索引 N 才發一次通知」
由於裝置與驅動可能在不同 CPU/時間線上並行,抑制條件與實際完成時序可能錯開,所以規格要求雙方都要容忍「多發」或「漏發」而不致錯亂
2.7.7.1 驅動程式需求:已用緩衝區通知的抑制
若未協商 VIRTIO_F_EVENT_IDX
:
- 驅動程式必須將
flags
設為 0 或 1 - 驅動程式可以(MAY)把
flags
設為 1,以告知裝置「不需要通知」
若已協商 VIRTIO_F_EVENT_IDX
:
- 驅動程式必須將
flags
設為 0 - 驅動程式可以(MAY)使用
used_event
告知裝置:在裝置把索引等於used_event
的項目寫入 Used Ring 之前(等價地說,直到 Used Ring 的idx
達到used_event + 1
之前),都不必發送通知 - 驅動程式必須能處理來自裝置的虛假(spurious)通知
2.7.7.2 裝置需求:已用緩衝區通知的抑制
若未協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須忽略
used_event
的值 - 當裝置把描述符索引寫入 Used Ring 之後:
- 若 flags 為 1,裝置不應該發送通知
- 若 flags 為 0,裝置必須發送通知
若已協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須忽略 flags 的最低位
- 當裝置把描述符索引寫入 Used Ring 之後:
- 若 Used Ring 的
idx
(決定該索引寫入位置的那個值)等於used_event
,裝置必須發送通知 - 否則裝置不應該發送通知
- 若 Used Ring 的
注意:例如,若 used_event
為 0,使用 VIRTIO_F_EVENT_IDX
的裝置會在第一個緩衝區被使用後向驅動發送通知(接著在第 65536 個之後再次通知,依此類推)
2.7.8 Virtqueue 的 Used Ring
Used Ring 的佈局如下:
struct virtq_used {
#define VIRTQ_USED_F_NO_NOTIFY 1
le16 flags;
le16 idx;
struct virtq_used_elem ring[ /* Queue Size */];
le16 avail_event; /* 只有在 VIRTIO_F_EVENT_IDX 協商成功時才存在 */
};
/* 這裡 id 使用 le32 是為了填補(padding)的考量。 */
struct virtq_used_elem {
/* 已用描述符鏈的頭節點索引 */
le32 id;
/* 裝置對該描述符鏈中屬於「裝置可寫」部分實際寫入的位元組數 */
le32 len;
};
Used Ring 是裝置在完成後歸還緩衝區的位置:它只會由裝置寫入,並由驅動程式讀取
環中的項目都是一組的:id
指出描述該緩衝區的描述符鏈之頭節點索引(對應先前 guest 放進 Available Ring 的那一筆),len
是實際寫入緩衝區的位元組總數。 注意:對於使用不可信的緩衝區的驅動而言,len
特別有用:如果驅動不知道裝置實際寫入了多少,就必須事先把整個緩衝區清零,才能避免資料外洩
例如,網路驅動可能把接收的緩衝區直接交給無特權的使用者空間應用程式。 若網路裝置沒有覆寫該緩衝區中原本的所有位元組,就可能把其他行程釋放的記憶體內容洩露給應用程式
idx
欄位表示裝置下一個要寫入環的位置(以 queue size 取模),它從 0 開始並逐漸遞增。 注意:舊版 [Virtio PCI Draft] 將這兩個結構稱為 vring_used
與 vring_used_elem
,並將常數命名為 VRING_USED_F_NO_NOTIFY
,但其布局與數值皆相同
2.7.8.1 Legacy 介面:Used Ring
從歷史上看,以前許多驅動都忽略了 len
的值,結果許多裝置也都沒有正確地設定 len
值。 因此,在使用 Legacy 介面時,若情況允許,一般建議忽略 Used Ring 項目中的 len
。 各裝置型別的特定已知問題,會在各自章節列出
2.7.8.2 裝置需求:Used Ring
裝置在更新 Used idx
之前,必須先設定 len
裝置在更新 Used idx
之前,必須從第一個裝置可寫的緩衝區開始,至少寫入 len
個位元組到描述符
裝置可以(MAY)寫入多於 len
個位元組到描述符。 注意:在某些錯誤情況下,裝置可能無法準確得知緩衝區的哪些部分已被寫入。 這也是為何要允許寫入多於 len
個的位元組:這比「讓驅動誤以為有未初始化記憶體被覆寫了」還好
Tips
先填 len
再遞增 idx
可確保驅動在看到完成項目時,len
已就緒。 「至少 len
個位元組」是對資料最小成功量的承諾,允許「多於 len
個位元組」是為了讓裝置在不確定的情況下保守回報,避免有資料外洩的假象。 對於含多段可寫區塊的鏈,寫入的起點與長度計算需遵循鏈內部順序與邊界
2.7.8.3 驅動程式需求:Used Ring
驅動程式不能對裝置可寫緩衝區中超過前 len
位元組的資料作出任何假設,並且應該忽略該部分的資料
Tips
由於裝置保證「至少寫了 len
個有效位元組」,但不保證 len
之外還有多少是真的寫過的,驅動只能信前 len
個位元組,把後面的當成垃圾資料直接忽略
2.7.9 描述符的 in-order 使用
有些裝置總是按照描述符被提供的相同順序來使用它們。 此類裝置可以提供 VIRTIO_F_IN_ORDER
特徵,若成功協商,裝置可藉由只寫入單一個 used ring 項目,來通知驅動程式有一個批次(batch)緩衝區已被使用,該 used ring 項目的 id
會對應此批次中最後一個緩衝區之描述符鏈的頭節點索引
Tips
VIRTIO_F_IN_ORDER
成立時,裝置保證「投遞順序 = 使用順序」。 因此裝置無需對每一筆完成都各寫一個 used 項目,可以把連續的多筆完成合併成一次回報,僅寫回此批次最後一筆的 id
。 驅動知道 in-order,便能從「上一批末端」一路推定到此 id
之間的所有請求都已完成,達成批次完成通知
接著,裝置會依批次大小在環上往前跳過相應的距離。 相對地,它也會把 used 的 idx
依該批次大小遞增。 驅動程式需要查出該 used 的 id
,並計算此批次的大小,才能把自身的進度推進到裝置下一次將寫入 used ring 項目的位置
如此一來,used ring 上所寫入的那個項目,其偏移會對應到該批次的第一個 available ring 項目的偏移,而下一個批次的 used 項目,也會對應到下一批次的第一個 available 項目的偏移,依此類推
被跳過的那些緩衝區(亦即沒有對應的 used ring 項目者),會被視為裝置已完整使用(讀取或寫入)完畢
2.7.10 可用緩衝區通知的抑制
裝置可以用與 2.7.7 中驅動抑制「已用緩衝區」通知類似的方式,來抑制「可用緩衝區」通知。 也就是說,裝置在 used ring 中操作 flags
或 avail_event
,其方式等同於驅動在 available ring 中操作 flags
或 used_event
2.7.10.1 驅動程式需求:可用緩衝區通知的抑制
驅動程式在配置 used ring 時,必須把其中的 flags
初始化為 0
若未協商 VIRTIO_F_EVENT_IDX
:
- 驅動程式必須忽略
avail_event
的值 - 在驅動把描述符索引寫入 available ring 之後:
- 若
flags
為 1,驅動程式不應該送出通知 - 若
flags
為 0,驅動程式必須送出通知
- 若
若已協商 VIRTIO_F_EVENT_IDX
:
- 驅動程式必須忽略
flags
的最低位 - 在驅動把描述符索引寫入 available ring 之後:
- 若 available ring 的
idx
(決定該索引放置位置的那個值)等於avail_event
,驅動程式必須送出通知 - 否則,驅動程式不應該送出通知
- 若 available ring 的
2.7.10.2 裝置需求:可用緩衝區通知的抑制
若未協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須把
flags
設為 0 或 1 - 裝置可以(MAY)把
flags
設為 1,告知驅動「不需要通知」
若已協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須把
flags
設為 0 - 裝置可以(MAY)使用
avail_event
告知驅動:在驅動把索引等於avail_event
的項目寫入 available ring 之前(等價地說,直到 available ring 的idx
達到avail_event + 1
之前),都不需要通知 - 裝置必須能處理來自驅動的虛假(spurious)通知
2.7.11 操作 virtqueue 的輔助工具
Linux 核心原始碼在 include/uapi/linux/virtio_ring.h
中,提供了上述定義與更易用的輔助 routine。 該檔案由 IBM 與 Red Hat 以 BSD 三條款授權(3-clause BSD)明確釋出,因而可自由供其他專案使用,其內容也(略有差異地)重製於附錄 A 的 virtio_queue.h 中
2.7.12 Virtqueue 的操作
Virtqueue 的操作分成兩個部分:向裝置供給新的可用緩衝區,以及處理來自裝置的已用緩衝區。 注意:例如,最簡單的 virtio 網路裝置擁有兩個 virtqueue:傳送(transmit)與接收(receive)。 驅動把要送出的封包(裝置可讀)加入傳送 virtqueue,並在它們被使用後釋放。 同樣地,把接收用的緩衝區(裝置可寫)加入接收 virtqueue,並在它們被使用後加以處理
下面將在 Split 形式的 virtqueue 下,分別詳述這兩部分的需求
2.7.13 將緩衝區提供給裝置(Supplying Buffers to The Device)
驅動程式把緩衝區提供給裝置的某個 virtqueue,流程如下:
- 驅動程式把緩衝區放入 Descriptor Table 中的可用描述符裡,必要時進行串鍊(見 2.7.5)
- 驅動程式把該描述符鏈頭節點的索引,寫入 Available Ring 的下一個項目(entry)
- 若可進行批次(batch),則可以(MAY)重複步驟 1 與 2
- 驅動程式執行合適的記憶體欄柵(memory barrier),以確保裝置在進入下一步之前,能看到更新過的 Descriptor Table 與 Available Ring
- 把 Available 的
idx
依「本次加入 Available Ring 的頭節點數」來遞增 - 驅動程式再次執行合適的記憶體欄柵,確保它先完成對
idx
欄位的更新,再去檢查通知抑制條件 - 若通知沒有被抑制,驅動程式就向裝置送出「available buffer」通知
請注意,以上流程沒有額外處理 Available Ring 緩衝的「繞回」:因為 Ring 與 Descriptor Table 大小相同,步驟(1)會阻止這種情況發生
此外,Queue Size 的最大值為 32768(16 位元內可表示的最高 2 的冪),因此 16 位元的 idx
值永遠能區分環是滿的還是空的。 接下來將更詳細說明每一個階段的需求
2.7.13.1 把緩衝區放入 Descriptor Table
一個緩衝區由 0 個或多個裝置可讀且實體連續的元素,接在 0 個或多個裝置可寫且實體連續的元素之後所構成(兩側各自至少有一個元素)。 以下演算法會把它映射到 Descriptor Table,形成一條描述符鏈:
對每個緩衝區元素 b
:
- 取得下一個可用的描述符表項
d
- 把
d.addr
設為b
的起始實體位址 - 把
d.len
設為b
的長度 - 若
b
是裝置可寫,則把d.flags
設為VIRTQ_DESC_F_WRITE
,否則設為 0 - 若後面還有下一個元素:
- (a) 把
d.next
設為下一個可用描述符的索引 - (b) 在
d.flags
中設置VIRTQ_DESC_F_NEXT
位元
- (a) 把
實務上,d.next
平時也會拿來把空閒描述符串成 free list。 開始映射前,通常會先用一個「剩餘可用數量」的計數來檢查是否足夠
2.7.13.2 更新 Available Ring
描述符鏈的頭就是前述演算法中的第一個 d
,也就是指向此緩衝區第一段的描述符索引。 最樸素的作法可以(MAY)這樣寫(假設已做正確的小端序轉換):
avail->ring[avail->idx % qsz] = head;
但一般而言,驅動可能會在更新 idx
之前,先加入多條描述符鏈(在更新 idx
的那一刻才對裝置可見),因此常見的寫法是維持一個「已加入計數」:
avail->ring[(avail->idx + added++) % qsz] = head;
2.7.13.3 更新 idx
idx
會遞增,並在 65536 處自然繞回:
avail->idx += added;
只要驅動更新了 Available 的 idx
,就等於曝光了對應的描述符與其內容。 裝置可以(MAY)立刻取用這些描述符鏈與其所參照的記憶體
2.7.13.3.1 驅動程式需求:更新 idx
在更新 idx
之前,驅動程式必須執行合適的記憶體欄柵,以確保裝置看到的是最新的內容
2.7.13.4 通知裝置
通知裝置的方法是匯流排特定的,但一般成本都不低。 因此若不需要,裝置可以(MAY)依 2.7.10 的機制抑制這類通知。 驅動必須要注意:要先讓新的 idx
值對外可見,再去檢查通知是否被抑制
2.7.13.4.1 驅動程式需求:通知裝置
在讀取 flags
或 avail_event
之前,驅動程式必須執行合適的記憶體欄柵,以避免錯失通知
2.7.14 從裝置接收已用緩衝區
當裝置操作完描述符所指涉的緩衝區時(依 virtqueue 與裝置型別,可能是讀、寫或兩者的部分組合),它會依 2.7.7 的說明向驅動程式送出「used buffer」通知
為了最佳化效能,驅動程式在處理 Used Ring 時可以(MAY)暫時關閉「used buffer」通知,但要小心:在「清空環」與「重新開啟通知」之間,可能會遺漏通知。 通常可在重新啟用通知後,再次檢查是否還有新的已用緩衝區來處理此問題,例如:
virtq_disable_used_buffer_notifications(vq);
for (;;) {
if (vq->last_seen_used != le16_to_cpu(virtq->used.idx)) {
virtq_enable_used_buffer_notifications(vq);
mb();
if (vq->last_seen_used != le16_to_cpu(virtq->used.idx))
break;
virtq_disable_used_buffer_notifications(vq);
}
struct virtq_used_elem *e = virtq.used->ring[vq->last_seen_used%vsz];
process_buffer(e);
vq->last_seen_used++;
}
2.8 Packed Virtqueues
Packed virtqueue 是一種替代性的、較為緊湊的 virtqueue 佈局,它使用可讀可寫的記憶體,該記憶體由 host 與 guest 兩邊共用,且雙方都可做讀寫。 是否使用 packed virtqueue 由 VIRTIO_F_RING_PACKED
旗標(feature bit)協商決定,每個 packed virtqueue 最多支援 215 個項目(entries)
在目前的各種 transport 中,virtqueue 位於驅動程式配置的 guest 記憶體內。 每個 packed virtqueue 由三個部分構成:
- Descriptor Ring —— 佔用 Descriptor Area
- Driver Event Suppression —— 佔用 Driver Area
- Device Event Suppression —— 佔用 Device Area
其中,Descriptor Ring 由多個描述符組成,每個描述符可以包含下列部分:
- Buffer ID
- Element Address
- Element Length
- Flags
一個緩衝區由零個或多個「裝置可讀、且物理上連續」的元素,接在零個或多個「裝置可寫、且物理上連續」的元素所構成(每個緩衝區至少要有一個元素)
Tips
記得 virtio 的「readable → writable」分段慣例:先列出裝置可讀的區塊,再列出可寫的區塊,兩類各自可以是多段且各自要物理連續,這樣裝置處理時可以先消費輸入,再產生輸出
當驅動程式要把這個緩衝區送給裝置時,它會將至少一個「可用」的描述符寫入 Descriptor Ring,用以描述該緩衝區的元素。 描述符是透過其中存放的 Buffer ID 與緩衝區建立關係的
接著,驅動程式會通知裝置。 當裝置處理完該緩衝區後,它會把包含該 Buffer ID 的「已用」裝置描述符寫回 Descriptor Ring(覆寫先前驅動程式所提供的描述符),並送出「已用」事件的通知
Descriptor Ring 以環狀的方式被使用:驅動程式依序將描述符寫入環中。 到達環的末端後,下一個描述符會放回環的開頭。 一旦環中被驅動程式的描述符填滿,驅動程式就會停止送出新請求,等待裝置處理並寫回一些「已用」描述符之後,才會再提供新的驅動程式描述符
同樣地,裝置會按順序從環中讀取描述符,並偵測到已由驅動程式標記為可用的描述符。 當完成對描述符的處理後,裝置會把「已用」描述符寫回環中
注意:裝置雖然會按順序讀取驅動程式的描述符並依順序做處理,但實際完成的順序可能不同步(非按順序完成)。 因此,裝置會依「實際完成」的順序來寫入「已用」描述符
Device Event Suppression 資料結構對裝置來說是唯寫的,它包含用來降低「裝置事件」數量的資訊,也就是讓「送往裝置的可用緩衝區通知」變得更少。 Driver Event Suppression 資料結構對裝置來說是唯讀的,它包含用來降低「驅動程式事件」數量的資訊,也就是讓「送往驅動程式的已用緩衝區通知」變得更少
Tips
- Device Event Suppression(位在 Device Area):由 device 寫、driver 讀,用來減少 driver → device 的 kick(也就是「available/kick 通知」)
- Driver Event Suppression(位在 Driver Area):由 driver 寫、device 讀,用來減少 device → driver 的中斷(也就是「used 通知」)
2.8.1 驅動程式與裝置的環繞回計數器(Ring Wrap Counter)
驅動程式與裝置各自都需在內部維護一個單一位元的環繞回計數器,初始值為 1
由驅動程式維護的計數器稱為 Driver Ring Wrap Counter,每當驅動程式把環中的「最後一個」描述符標記為可用之後,它就會切換(翻轉)此計數器的值。 由裝置維護的計數器稱為 Device Ring Wrap Counter,每當裝置把環中的「最後一個」描述符標記為已用之後,它就會切換(翻轉)此計數器的值
可以很容易地看出:當驅動程式與裝置在處理同一個描述符,或當所有可用描述符都已被使用時,兩者的環繞回計數器會相互一致
Tips
同一圈、同一格時,兩端的 counter 理當一致。 當沒有尚未處理的可用描述符時,雙方應該都處於相同的 wrap 狀態
要把描述符標記為可用或已用,驅動程式與裝置都會使用下列兩個旗標:
#define VIRTQ_DESC_F_AVAIL (1 << 7)
#define VIRTQ_DESC_F_USED (1 << 15)
要把描述符標記為「可用」時,驅動程式會把 Flags 中的 VIRTQ_DESC_F_AVAIL
設成與內部 Driver Ring Wrap Counter 相同的值,同時把 VIRTQ_DESC_F_USED
設成相反的值(與內部的 Driver Ring Wrap Counter 不同的值)
當要把描述符標記為「已用」時,裝置會把 Flags 中的 VIRTQ_DESC_F_USED
設成與內部 Device Ring Wrap Counter 相同的值,同時把 VIRTQ_DESC_F_AVAIL
也設成相同的值
因此,對「可用」的描述符而言,VIRTQ_DESC_F_AVAIL
與 VIRTQ_DESC_F_USED
兩個位元會是不同的值。 對「已用」的描述符而言,兩個位元會是相同的值
注意:上述觀察主要用於健全性檢查(sanity-checking),因為那只是必要條件而非充分條件,例如,所有描述符在初始階段的值都會是零。 為了正確偵測已用與可用,驅動程式與裝置可以記錄上一次觀察到的 VIRTQ_DESC_F_USED
/VIRTQ_DESC_F_AVAIL
的值。 其他用來偵測 VIRTQ_DESC_F_AVAIL
/VIRTQ_DESC_F_USED
位元變化的技術也可能可行
2.8.2 對可用與已用描述符的輪詢
裝置與驅動程式對描述符的寫入可以做重排序(reorder),但雙方只需輪詢(或測試)記憶體中的單一位置:也就是在環狀順序裡,位於先前處理過的描述符之後的下一個裝置描述符
Tips
「可以重排序」提醒你要配合記憶體屏障確保可見性。 不過為了效率,雙方不需要掃描整個環,只要看「下一格」就好。 對驅動而言,這是在觀察裝置是否把下一格改成已用的了; 對裝置而言,這是在觀察驅動是否把下一格改成可用的了
有時候,裝置在處理了一批多個可用描述符之後,只需要寫出單一個「已用」描述符。 更詳細的情況會在後文解釋,例如使用描述符鏈(descriptor chaining)或按順序(in-order)使用時會發生此情形。 此時,裝置會寫出一個已用描述符,其 Buffer ID 為這一組裡最後一個描述符的 ID。 處理完這個已用描述符之後,裝置與驅動程式都會在環上向前跳過該群組內其餘描述符的數量,直到處理(對驅動是讀取、對裝置是寫入)到下一個已用描述符為止
2.8.3 Write 旗標
在可用描述符中,Flags 內的 VIRTQ_DESC_F_WRITE
位元用來標記該描述符所對應的緩衝區是唯寫(write-only)的還是唯讀(read-only)的
/* 這會把描述符標成「裝置唯寫」的(否則為「裝置唯讀」的)。 */
#define VIRTQ_DESC_F_WRITE 2
在已用描述符中,這個位元則用來表示「裝置是否已經對該緩衝區的任一部分寫入過資料了」
2.8.4 元素位址與長度
在可用描述符中,Element Address 對應到該緩衝元素的實體位址,元素的長度則儲存在 Element Length 中。 緩衝元素應為物理連續的
Tips
如同前面 2.7 一開始所述,規格混用了名詞,這邊的緩衝元素就是指單個 buffer,一個 descriptor 對應到一個 buffer。 後面看到「緩衝」這個詞的時候要自己判斷一下他是指單個 buffer,還是指整個請求的 buffer
在已用描述符中,Element Address 不被使用。 Element Length 則用來表示裝置已經初始化(寫入)的緩衝區長度。 對於未設 VIRTQ_DESC_F_WRITE
旗標的已用描述符,Element Length 不被使用,驅動程式應忽略它
2.8.5 Scatter-Gather 支援
有些驅動需要在一個請求中提供多個緩衝元素的清單(亦稱 scatter/gather list)。 有兩種功能支援這點:描述符鏈(descriptor chaining)與間接描述符(indirect descriptors)
若驅動未使用這兩種功能,則每個緩衝區都是物理上連續的,且為唯讀或唯寫的,並可由單一描述符完整描述
Tips
未使用這兩種功能就代表緩衝區內只有一個元素,因此會有後續的那些特徵
雖然少見(多數實作要麼只使用非間接的描述符來建立所有清單,要麼只使用單一間接元素),但若兩種功能皆已協商,則在同一個 ring 中混用「間接」與「非間接」描述符是合法的,只要每一份清單本身只含單一類型的描述符即可
Scatter/gather 清單只可用於可用描述符,對應的已用描述符則以單一項目代表整份清單
裝置會透過與 transport 或裝置本身相關的限制值來限制清單中的描述符數量。 若未加限制,清單可包含的最大描述符數量等於 virtqueue 的大小
2.8.6 Next 旗標:描述符鏈接
在 packed ring 格式中,驅動可以用多個描述符來提供一份 scatter/gather 清單,這會為除了最後一個可用描述符之外,其餘的每個可用描述符的 Flags 欄位設定 VIRTQ_DESC_F_NEXT
/* 這表示該緩衝會「接續於下一個」。 */
#define VIRTQ_DESC_F_NEXT 1
Buffer ID 會放在清單中的最後一個描述符裡
驅動一定要在把清單的其餘描述符都寫入 ring 之後,才能將這份清單的第一個描述符標為可用的。 這能保證裝置在 ring 上永遠不會看到「部分寫入」的 scatter/gather 清單
注意:清單中的所有描述符都必須正確設置/清除所有旗標,包括 VIRTQ_DESC_F_AVAIL
、VIRTQ_DESC_F_USED
、VIRTQ_DESC_F_WRITE
,而不是只有第一個要設定
裝置只會為整份清單寫出單一個已用描述符,之後它會依清單中描述符的數量在 ring 上向前跳過。 為了能跳到裝置寫入下一個已用描述符的位置,驅動需要記住每個 Buffer ID 所對應清單的大小
例如,若描述符的使用順序和它被標記為可用的順序相同,則結果會是:已用描述符會覆寫該清單中的「第一個」可用描述符,下一份清單的已用描述符會覆寫下一份清單的第一個可用描述符,如此類推
VIRTQ_DESC_F_NEXT
在已用描述符中不被使用,驅動應忽略它
2.8.7 Indirect 旗標:Scatter-Gather 支援
有些裝置可以透過同時派發大量且巨大的請求來獲益,而 VIRTIO_F_INDIRECT_DESC
旗標允許了這種作法。 為了提升 ring 的容量,驅動可以在記憶體中的任意位置儲存一張「間接描述符表」(對裝置為唯讀的),並在主 virtqueue 中插入一個描述符(其 Flags 設 VIRTQ_DESC_F_INDIRECT
)去引用一個緩衝元素,而該元素中包含這張間接描述符表,addr
與 len
分別指向間接表的位址與其位元組長度
/* 表示該元素中裝的是一張描述符表。 */
#define VIRTQ_DESC_F_INDIRECT 4
間接表的佈局如下(len
為引用這張表的描述符的 Buffer Length,屬可變長度):
struct pvirtq_indirect_descriptor_table {
/* 真正的描述符結構(每個為 struct pvirtq_desc) */
struct pvirtq_desc desc[len / sizeof(struct pvirtq_desc)];
};
第一個描述符位於間接描述符表的起始位置,後續的間接描述符緊接其後。 對於間接表中的描述符,旗標位中只有 VIRTQ_DESC_F_WRITE
是有效的,其餘皆不被使用,Buffer ID 也不被使用,裝置應忽略它們
而對於在主 virtqueue 上帶有 VIRTQ_DESC_F_INDIRECT
的那個描述符而言,其 VIRTQ_DESC_F_WRITE
也不被使用,裝置應忽略它
2.8.8 按序(In-order)使用描述符
有些裝置總是以與描述符被標記為可用的相同順序來使用它們,這些裝置可以提供 VIRTIO_F_IN_ORDER
特徵。 若已協商,裝置便可只寫出一個已用描述符(其 Buffer ID 對應到該批次中最後一個描述符),來通知驅動該批次緩衝已被使用
Tips
VIRTIO_F_IN_ORDER
讓裝置承諾「完成順序 = 提交順序」,因此允許用「最後一個的 Buffer ID」代表整批完成,降低寫回與通知次數
之後,裝置會依該批次的大小在 ring 上向前跳過,驅動需要查出該已使用的 Buffer ID,計算這一批的大小,才能推進到裝置將寫入下一個已用描述符的位置
這將導致該批次的已用描述符會覆寫該批次中第一個可用描述符,下一批的已用描述符則覆寫下一批的第一個可用描述符,依此類推
那些被跳過(未寫出已用描述符)的緩衝,會被當作已被裝置完整使用(讀或寫)了
Tips
驅動端的工作是:看見代表性的已用項目後,能復原該批的大小(例如以事先記錄的鏈長或批次資訊),並一次性把 ring 的消費指標跳到下一個代表項目的位置
這種批次覆寫規則與 2.8.6 的「清單首項被覆寫」相呼應,只是這裡的「批」未必是透過 NEXT 串接,而是由按序特性與驅動的分組策略來界定
2.8.9 多緩衝請求
有些裝置會把多個緩衝合併起來,一起處理為單一個請求。 這些裝置會在將請求中的其他描述符(對應其餘緩衝)都被標記為已用並寫回環之後,才把對應到第一個緩衝的描述符標記為已用。 這能保證驅動程式在環上不會看到「部分完成」的請求
2.8.10 驅動與裝置的事件抑制
在許多系統中,對「已用/可用」緩衝的通知會帶來可觀的額外開銷。 為了降低這些開銷,每個 virtqueue 都包含兩個相同的結構,用來控制裝置與驅動之間的通知
Driver Event Suppression 結構對裝置而言是唯讀的,用來控制「裝置送往驅動」的已用緩衝通知
Device Event Suppression 結構對驅動而言是唯讀的,用來控制「驅動送往裝置」的可用緩衝通知
這兩個事件抑制結構都包含下列欄位:
- Descriptor Ring Change Event Flags
其取值如下:/* Enable events */ #define RING_EVENT_FLAGS_ENABLE 0x0 /* Disable events */ #define RING_EVENT_FLAGS_DISABLE 0x1 /* * Enable events for a specific descriptor * (as specified by Descriptor Ring Change Event Offset/Wrap Counter). * Only valid if VIRTIO_F_EVENT_IDX has been negotiated. */ #define RING_EVENT_FLAGS_DESC 0x2 /* The value 0x3 is reserved */
- Descriptor Ring Change Event Offset
當事件旗標設定為「特定描述符事件」時:此欄位為環內的偏移量(以描述符大小為單位)。 只有當該描述符分別被標記為可用/已用時,事件才會觸發 - Descriptor Ring Change Event Wrap Counter
當事件旗標設定為「特定描述符事件」時:此欄位用於匹配環繞回計數器的值。 只有在計數器匹配且描述符被標記為可用/已用時,事件才會觸發
在寫出一些描述符後,裝置與驅動都應該要查閱對應的事件抑制結構,以判斷是否需要送出相應的已用或可用緩衝通知
2.8.10.1 結構大小與對齊
virtqueue 的各部分在 guest 記憶體中都必須是物理連續的,且有不同的對齊需求。 各部分的記憶體對齊與大小需求(單位為位元組)如下表:
Virtqueue Part | Alignment | Size |
---|---|---|
Descriptor Ring | 16 | 16∗(Queue Size) |
Device Event Suppression | 4 | 4 |
Driver Event Suppression | 4 | 4 |
「Alignment」欄顯示了 virtqueue 每個部分所需的最小對齊需求。 「Size」欄顯示了每個部分的總位元組數。 Queue Size 對應於該 virtqueue 中描述符的最大數量,例如,若佇列大小為 4,則在任何特定時間最多可將 4 個緩衝區佇列。 Queue Size 不一定要是 2 的冪次
2.8.11 驅動需求:Virtqueue
驅動程式必須確保 virtqueue 每個部分的「第一個位元組」的實體位址,是上述表格內所列的對齊值的整數倍
2.8.12 裝置需求:Virtqueue
裝置必須「依環上出現的順序」處理驅動的描述符。 裝置必須「依實際完成的順序」把裝置端的描述符寫回環中。 裝置在「開始寫入描述符之後」可以重新排序描述符寫入的順序
2.8.13 Virtqueue 描述符格式
可用描述符指向驅動要交給裝置的緩衝,addr
為實體位址,而描述符透過 id
欄位與緩衝建立關係:
struct pvirtq_desc {
/* 緩衝位址 */
le64 addr;
/* 緩衝長度 */
le32 len;
/* 緩衝 ID */
le16 id;
/* 視描述符類型而定的旗標 */
le16 flags;
};
描述符環以全零初始化
2.8.14 事件抑制結構格式
以下這個結構用於減少驅動與裝置之間的通知數量:
struct pvirtq_event_suppress {
le16 {
desc_event_off : 15; /* 描述符環變更事件偏移量 */
desc_event_wrap : 1; /* 描述符環變更事件的環繞回計數器 */
} desc; /* 當 desc_event_flags 設為 RING_EVENT_FLAGS_DESC 時生效 */
le16 {
desc_event_flags : 2, /* 描述符環變更事件旗標 */
reserved : 14; /* 保留,設為 0 */
} flags;
};
2.8.15 裝置需求:Virtqueue 描述符表
裝置不得對「裝置可讀」的緩衝進行寫入,且裝置不應讀取「裝置可寫」的緩衝。 除非觀察到旗標中的 VIRTQ_DESC_F_AVAIL
位元發生了變化(例如相較於初始的零值),否則裝置不得使用該描述符。 改變了旗標中的 VIRTQ_DESC_F_USED
位元之後,裝置不得再更動該描述符
2.8.16 驅動需求:Virtqueue 描述符表
除非觀察到旗標中的 VIRTQ_DESC_F_USED
位元發生了變化,否則驅動不得更動描述符。 驅動在改變旗標中的 VIRTQ_DESC_F_AVAIL
位元之後,不得再更動該描述符。 在通知裝置時,驅動必須把 next_off
與 next_wrap
以匹配尚未提供給裝置的下一個描述符。 驅動可以在未提供任何新描述符的情況下,傳送多次可用緩衝區的通知
2.8.17 驅動需求:Scatter-Gather 支援
驅動不得建立超過裝置允許長度的描述符清單。 驅動不得建立長度超過 Queue Size 的描述符清單,這意味著描述符清單中禁止存在環狀迴路
驅動必須把所有「裝置可寫」的描述符元素放在所有「裝置可讀」元素之後。 驅動不得指望裝置會多用描述符來寫出整份清單,在把清單的第一個描述符標記為可用之前,驅動必須確保環中有足夠空間容納整份清單。 驅動不得在清單中所有後續描述符都被標記為可用之前,就先把清單的第一個描述符設為可用的
2.8.18 裝置需求:Scatter-Gather 支援
對於以 VIRTQ_DESC_F_NEXT
旗標鏈接的清單,裝置必須依照驅動標記為可用的順序來使用其中的描述符。
裝置可以限制一份清單所允許包含的緩衝數量
2.8.19 驅動需求:間接描述符
若為協商 VIRTIO_F_INDIRECT_DESC
特徵,驅動不得自行設定 VIRTQ_DESC_F_INDIRECT
旗標。 對於間接描述符表中的描述符,驅動除了 VIRTQ_DESC_F_WRITE
外,不得設定任何其他旗標
驅動不得建立長度超過裝置允許的描述符鏈。 在由 VIRTQ_DESC_F_NEXT
鏈接的 scatter-gather 清單中,驅動不得在「直接描述符」上設置 VIRTQ_DESC_F_INDIRECT
旗標
2.8.20 Virtqueue 運作
virtqueue 的運作分成兩部分:向裝置提供新的可用緩衝,以及處理來自裝置的已用緩衝。 以下將在 packed virtqueue 格式下,分別詳述這兩部分的需求
2.8.21 向裝置提供緩衝
驅動向裝置的某個 virtqueue 提供緩衝的流程如下:
- 驅動把緩衝放入 Descriptor Ring 中的可用空描述符
- 驅動執行適當的記憶體屏障,確保在檢查通知抑制之前,已經完成對描述符的更新
- 若通知未被抑制,驅動就通知裝置有新的可用緩衝
接下來會更詳細說明上述每個階段的需求
2.8.21.1 將可用緩衝放入描述符環
對於每個緩衝元素 b
:
- 取得下一個描述符表項目
d
- 取得下一個可用的 buffer id 值
- 將
d.addr
設為b
開頭的實體位址 - 將
d.len
設為b
的長度 - 將
d.id
設為該 buffer id - 依下列方式計算
flags
:- (a) 若
b
為裝置可寫,將VIRTQ_DESC_F_WRITE
位設為 1,否則設為 0 - (b) 將
VIRTQ_DESC_F_AVAIL
設為當前 Driver Ring Wrap Counter 的值 - (c) 將
VIRTQ_DESC_F_USED
設為與當前 Driver Ring Wrap Counter 相反的值
- (a) 若
- 執行記憶體屏障,以確保描述符已完成初始化
- 將
d.flags
設為計算出的flags
值 - 若
d
是環中的最後一個描述符,則切換 Driver Ring Wrap Counter - 否則,將
d
前進到下一個描述符
上述流程會讓「單一描述符的緩衝」變成可用的。 然而一般來說,驅動程式可能會將一批描述符作為單一請求的一部分。 在那種情況下,驅動程式會延後更新第一個描述子的 flags
(以及先前的記憶體屏障),直至其餘描述符都完成了初始化之後
一旦驅動更新了描述符的 flags
欄位,就等於公開了該描述符及其內容。 裝置可以立即存取該描述符、驅動所建立的任何後續描述符,以及它們所參考的記憶體
2.8.21.1.1 驅動需求:更新 flags
在更新 flags
之前,驅動必須執行適當的記憶體屏障,以確保裝置能看到最新版本的資料
2.8.21.2 傳送可用緩衝通知
實際的裝置通知方法依匯流排而定,但通常成本不低。 因此,若裝置不需要通知,可以使用第 2.8.14 節所述、位於 Device Area 的事件抑制結構,來抑制這類通知
驅動在檢查通知是否被抑制之前,必須先公開(寫出)新的 flags
值
2.8.21.3 實作範例
以下是驅動程式碼的範例。 它沒有嘗試降低可用緩衝通知的次數,也不支援 VIRTIO_F_EVENT_IDX
特徵:
/* 注意:vq->avail_wrap_count 的初始值為 1 */
/* 注意:vq->sgs 是與 ring 同大小的陣列 */
d = alloc_id(vq);
first = vq->next_avail;
sgs = 0;
for (each buffer element b) {
sgs++;
vq->ids[vq->next_avail] = -1;
vq->desc[vq->next_avail].address = get_addr(b);
vq->desc[vq->next_avail].len = get_len(b);
avail = vq->avail_wrap_count ? VIRTQ_DESC_F_AVAIL : 0;
used = !vq->avail_wrap_count ? VIRTQ_DESC_F_USED : 0;
f = get_flags(b) | avail | used;
if (b is not the last buffer element) {
f |= VIRTQ_DESC_F_NEXT;
}
/* 在全部就緒前,不要把第一個描述符標記為可用 */
if (vq->next_avail == first) {
flags = f;
} else {
vq->desc[vq->next_avail].flags = f;
}
last = vq->next_avail;
vq->next_avail++;
if (vq->next_avail >= vq->size) {
vq->next_avail = 0;
vq->avail_wrap_count ^= 1;
}
}
vq->sgs[id] = sgs;
/* 在清單中的最後一個描述符寫入 ID */
vq->desc[last].id = id;
write_memory_barrier();
vq->desc[first].flags = flags;
memory_barrier();
if (vq->device_event.flags != RING_EVENT_FLAGS_DISABLE) {
notify_device(vq);
}
2.8.21.3.1 驅動需求:傳送可用緩衝通知
在讀取位於 Device Area 的事件抑制結構之前,驅動必須執行適當的記憶體屏障。 若未這麼做,可能導致本應送出的可用緩衝通知未被送出
2.8.22 從裝置接收已用緩衝
一旦裝置使用了描述符所參考的緩衝(依 virtqueue 與裝置性質,可能是讀、寫,或兩者的部分),它會依第 2.8.14 節所述向驅動送出「已用緩衝通知」
注意:為了最佳效能,驅動在處理已用緩衝時可以暫時停用已用通知,但要小心在清空環與重新啟用通知之間遺漏通知的問題。 一般做法是在重新啟用通知之後,再次檢查是否還有更多已用緩衝:
/* 注意:vq->used_wrap_count 的初值為 1 */
vq->driver_event.flags = RING_EVENT_FLAGS_DISABLE;
for (;;) {
struct pvirtq_desc *d = vq->desc[vq->next_used];
/*
* 檢查:
* 1. 該描述符已被標記為可用。 若驅動在另一執行緒並行提供新描述符,
* 這個檢查就有必要。 也可用其他方式檢查,例如追蹤未處理的可用
* 描述符/緩衝數是否非 0
* 2. 該描述符已被裝置使用
*/
flags = d->flags;
bool avail = flags & VIRTQ_DESC_F_AVAIL;
bool used = flags & VIRTQ_DESC_F_USED;
if (avail != vq->used_wrap_count || used != vq->used_wrap_count) {
vq->driver_event.flags = RING_EVENT_FLAGS_ENABLE;
memory_barrier();
/*
* 重新測試:以防驅動在並行路徑又提供了新的描述符,
* 或裝置在驅動啟用事件之前又使用了更多描述符
*/
flags = d->flags;
bool avail = flags & VIRTQ_DESC_F_AVAIL;
bool used = flags & VIRTQ_DESC_F_USED;
if (avail != vq->used_wrap_count || used != vq->used_wrap_count) {
break;
}
vq->driver_event.flags = RING_EVENT_FLAGS_DISABLE;
}
read_memory_barrier();
/* 依下一個緩衝,跳過其餘描述符 */
id = d->id;
assert(id < vq->size);
sgs = vq->sgs[id];
vq->next_used += sgs;
if (vq->next_used >= vq->size) {
vq->next_used -= vq->size;
vq->used_wrap_count ^= 1;
}
free_id(vq, id);
process_buffer(d);
}
2.9 驅動端通知
驅動有時需要向裝置送出「可用緩衝通知」。 在未協商 VIRTIO_F_NOTIFICATION_DATA
的情況下,如果也沒有協商 VIRTIO_F_NOTIF_CONFIG_DATA
,則通知內容為 virtqueue 索引,若有協商 VIRTIO_F_NOTIF_CONFIG_DATA
,則為裝置提供的 virtqueue 通知組態資料。 通知的方法與提供該類通知組態資料的方式,依 transport 而定
然而,如果裝置能在「不讀取記憶體中的 virtqueue」的情況下得知佇列的可用資料量,就會更有效率,或有助於除錯。 為了協助這些最佳化,當協商了 VIRTIO_F_NOTIFICATION_DATA
時,驅動送給裝置的通知會包含下列資訊:
vq_index
或vq_notif_config_data
對應到某個 virtqueue 的「virtqueue 索引」或「裝置提供的佇列通知組態資料」next_off
環中「下一個可用描述符將被寫入的位置(偏移量)」。 未協商VIRTIO_F_RING_PACKED
時,指可用索引(available index)的低 15 位; 協商了VIRTIO_F_RING_PACKED
時,指「描述符環內」下一個可用描述符的位置偏移量(以描述符項目為單位)next_wrap
環繞回計數器(Wrap Counter)。 協商了VIRTIO_F_RING_PACKED
時,這是下一個可用描述符所對應的 wrap counter; 未協商時,指可用索引的最高位(bit 15)
注意:即使沒有提供更多緩衝區,驅動仍可發送多次通知。 在協商了 VIRTIO_F_NOTIFICATION_DATA
的情況下,這些通知的 next_off
與 next_wrap
值會相同
Tips
這裡區分了有/沒有 VIRTIO_F_NOTIFICATION_DATA
的兩種承載格式。 未協商時,只能用傳統的 queue 索引或裝置提供的固定組態塊。 實際怎麼寫入寄存器或 MMIO 都由各 transport(PCI、MMIO 等)定義
在有協商 VIRTIO_F_NOTIFICATION_DATA
的情況下,就可以做上述的最佳化,裝置可少一次讀記憶體,其中
next_off
/next_wrap
讓裝置不用碰記憶體就能知道「驅動下一步要寫哪裡」- 非 packed 模式用 avail index 的低 15 位與最高位分別扮演
off
/wrap
的角色,packed 模式則直接用偏移量與獨立的 wrap bit 來表示
2.10 共享記憶體區域
共享記憶體區域是供裝置使用的額外功能,能建立一塊在裝置與驅動之間「持續共享」的記憶體,而不用像 virtqueue 元素那樣在雙方之間來回傳遞。 範例用法包括共享快取,以及針對具版本的資料結構而設的版本池
該記憶體區域由裝置配置,並提供給驅動使用。 當裝置以軟體方式在 host 上實作時,這種配置允許該記憶體區域由 host 上的函式庫配置,不過該裝置可能無法完全控制該函式庫
一個裝置可能有多個與之關聯的共享記憶體區域。 每個區域都有一個 shmid 用以識別其身分,其意義由裝置自行定義
共享記憶體區域的列舉與定位,依 transport 的規定進行。 記憶體一致性的規則會依區域與裝置而異,這由各裝置在需要時另行指定
2.10.1 區域內的定址
對共享記憶體區域的引用以「自區域起點算起的偏移量」表示,而不是絕對記憶體位址。 這些偏移量可用於「共享記憶體中的結構之間的引用」,也可以用於「virtqueue 中引用共享記憶體的請求」。 shmid 可以顯式給出,也可以由引用的語境推斷出來
2.10.2 裝置需求:共享記憶體區域
共用記憶體區域不得暴露用於控制裝置操作或用於串流資料的共用記憶體區域
2.11 匯出物件
當一個 virtio 裝置建立的物件需要跟另一個獨立的 virtio 裝置共享時,前者可以產生一個 UUID 來「匯出」它,然後把這個 UUID 交給後者用來識別該物件
至於「什麼叫物件、如何匯出、如何匯入」,則由各裝置型別自己定義。 建議依 [RFC4122] 產生第 4 版 UUID
2.12 裝置群組
有時候,讓一個裝置去控制另一組裝置會很有用。 以下是在這種情況下所用到的術語:
Device group(裝置群組,或簡稱 group)
包含零個或多個裝置Owner device(擁有者裝置,或簡稱 owner)
控制該群組的裝置Member device(成員裝置)
群組內的裝置。 擁有者裝置本身並不是該群組的成員Member identifier(成員識別碼)
每個成員都具有此識別碼,它在群組內唯一,並用於透過擁有者裝置來定址該成員Group type identifier(群組型別識別碼)
用來指定群組中有哪些型別的成員裝置、如何解讀成員識別碼,以及擁有者擁有哪些控制權。 一個特定的擁有者可以控制多個不同型別的群組,但對於同一型別只能控制一個群組,因此「型別 + 擁有者」共同決定(唯一識別)此群組即便某些群組類型僅支援特定 transport,群組型別識別碼仍為全域性的,而非 transport 特定的,不應出現大量的新群組類型
Tips
一個 owner(擁有者裝置)負責管理同群組內的多個成員裝置,每個成員以群組內唯一的成員識別碼被定址。 為了區分不同的群組種類,還有「群組型別識別碼」。 同一個 owner 對「同一型別」的群組只能有一個,因此(owner, type)這對組合就能唯一決定那個群組
註:每個裝置只有單一個驅動程式,因此在本節語境中,未特別說明時 “the driver” 一般指的是擁有者裝置的驅動程式。 若有歧義,則以 “owner driver” 指稱擁有者裝置的驅動程式,“member driver” 指稱成員裝置的驅動程式
目前規範的群組型別及其識別碼如下:
SR-IOV 群組型別(0x1)
此裝置群組以 PCI Single Root I/O Virtualization (SR-IOV) 的實體功能(PF)裝置作為擁有者,並把其所有 SR-IOV 虛擬功能(VF)作為成員(參見 [PCIe])
這個群組的群組型別識別碼為 0x1,PF 裝置本身不是該群組的成員
此群組的成員識別碼可具有從 0x1 到 NumVFs 的值(由擁有者裝置的 SR-IOV 擴充功能指定),其數值等於該成員裝置的 SR-IOV VF 編號。 僅當擁有者裝置的 SR-IOV 擴充能力之 SR-IOV 控制暫存器中的 VF Enable 位元被設為 1 時,此群組才存在(參見 [PCIe])
此群組型別中的擁有者與成員裝置都使用 Virtio PCI 傳輸(見 4.1)
Tips
SR-IOV 會將一個 PCIe PF(實體功能)切分出多個 VF(虛擬功能)。 host OS 的 PF 驅動把 SR-IOV 的 VF Enable 打開並設定 NumVFs 後,這些 VF 會被枚舉出來。 hypervisor 可用 IOMMU/VFIO 把某些 VF 直通給 VM,VM 內的 OS 會把該 VF 視為一張獨立裝置
這裡把 PF 當成 owner,所有 VF 都是同一個群組的成員。 為了避免自我管理循環,PF 不算作成員。 成員識別碼直接對應 VF 號碼(1..NumVFs),而且群組存在的前提是 PF 的 SR-IOV 功能已啟動(VF Enable)。 由於是 Virtio 規範中的案例,PF 與 VF 之間透過 Virtio PCI transport 溝通與管理
2.12.1 群組管理命令
驅動程式會把群組管理命令送到某個群組的擁有者裝置,以控制該群組內的成員裝置。 舉例來說,在某成員裝置被其驅動程式初始化之前,就可以透過這種機制先進行組態設定
所有群組管理命令都採用下列形式:
struct virtio_admin_cmd {
/* Device-readable part */
le16 opcode;
/*
* 1 - SR-IOV
* 2-65535 - reserved
*/
le16 group_type;
/* unused, reserved for future extensions */
u8 reserved1[12];
le64 group_member_id;
le64 command_specific_data[];
/* Device-writable part */
le16 status;
le16 status_qualifier;
/* unused, reserved for future extensions */
u8 reserved2[4];
u8 command_specific_result[];
};
對所有命令而言,opcode
、group_type
,以及(若需要)group_member_id
與 command_specific_data
由驅動程式填入。 擁有者裝置則會回填 status
,並在需要時填入 status_qualifier
與 command_specific_result
一般來說,任何未使用的「裝置可讀」欄位都由驅動程式設為 0,裝置會忽略它們。 任何未使用的「裝置可寫」欄位都由裝置設為 0,驅動程式會忽略它們
opcode
指定命令種類。 opcode
的有效數值如下表所示:
opcode | 名稱 | 命令描述 |
---|---|---|
0x0000 | VIRTIO_ADMIN_CMD_LIST_QUERY | 向驅動程式提供此群組型別所支援的命令清單 |
0x0001 | VIRTIO_ADMIN_CMD_LIST_USE | 向裝置提供此群組型別實際要使用的命令清單 |
0x0002 | VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE | 寫入傳統(legacy)common 組態結構 |
0x0003 | VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ | 從傳統(legacy)common 組態結構讀取 |
0x0004 | VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE | 寫入傳統(legacy)device 組態結構 |
0x0005 | VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ | 從傳統(legacy)device 組態結構讀取 |
0x0006 | VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO | 查詢通知(notification)區域資訊 |
0x0007 - 0x7FFF | - | 使用 struct virtio_admin_cmd 的命令 |
0x8000 - 0xFFFF | - | 保留給未來命令(可能使用不同結構) |
group_type
用來指定群組型別識別碼,group_member_id
則用來指定群組內的成員識別碼。 關於群組型別識別碼與成員識別碼的定義,請參見 2.12 節
status
以較抽象的形式來描述命令結果與(可能的)失敗原因,適合轉發給應用程式。 status_qualifier
則以較低階、特定於 virtio 的形式描述失敗情形,方便除錯。 下表列出了可能的 status
值,為了簡化常見實作,這些值刻意與常見的 Linux 錯誤名稱與代碼相對應:
狀態(十進位) | 名稱 | 描述 |
---|---|---|
00 | VIRTIO_ADMIN_STATUS_OK | 成功完成 |
11 | VIRTIO_ADMIN_STATUS_EAGAIN | 請重試 |
12 | VIRTIO_ADMIN_STATUS_ENOMEM | 資源不足 |
22 | VIRTIO_ADMIN_STATUS_EINVAL | 無效的命令 |
other | - | 群組管理命令錯誤 |
當 status
為 VIRTIO_ADMIN_STATUS_OK
時,status_qualifier
為保留位,裝置會把它設為 0。 下表列出了可能的 status_qualifier
值:
狀態 | 名稱 | 描述 |
---|---|---|
0x00 | VIRTIO_ADMIN_STATUS_Q_OK | 與 VIRTIO_ADMIN_STATUS_OK 搭配使用 |
0x01 | VIRTIO_ADMIN_STATUS_Q_INVALID_COMMAND | 命令錯誤:沒有額外資訊 |
0x02 | VIRTIO_ADMIN_STATUS_Q_INVALID_OPCODE | 不支援或無效的 opcode |
0x03 | VIRTIO_ADMIN_STATUS_Q_INVALID_FIELD | command_specific_data 中有不支援或無效的欄位 |
0x04 | VIRTIO_ADMIN_STATUS_Q_INVALID_GROUP | 不支援或無效的 group_type |
0x05 | VIRTIO_ADMIN_STATUS_Q_INVALID_MEMBER | 不支援或無效的 group_member_id |
0x06 | VIRTIO_ADMIN_STATUS_Q_NORESOURCE | 裝置內部資源不足:可嘗試重試 |
0x07 | VIRTIO_ADMIN_STATUS_Q_TRYAGAIN | 命令阻塞時間過長:應該重試 |
0x08–0xFFFF | - | 保留作未來使用 |
每種命令都使用不同的 command_specific_data
與 command_specific_result
結構,兩者的長度取決於各自的結構,會另行描述,或由該結構的說明隱含而得
在驅動程式向裝置發送任何群組管理命令之前,必須先告訴裝置「打算使用哪些命令」。 在初始狀態(重設之後),預設只會使用兩個命令:VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
在針對某一群組的任何成員發送其他命令之前,驅動程式需先透過 VIRTIO_ADMIN_CMD_LIST_QUERY
查詢所支援的命令,並以 VIRTIO_ADMIN_CMD_LIST_USE
告知裝置其能夠使用的命令
VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
均使用下列結構來描述命令的操作碼集合:
struct virtio_admin_cmd_list {
/* Indicates which of the below fields were returned */
le64 device_admin_cmd_opcodes[];
};
此結構是以小端序儲存的 64 位元整數陣列,被支援的 opcode
命令,對應的位元會被設為 1。 也就是說,device_admin_cmd_opcodes[0]
(第一個 64-bit 元素)對應 opcode 0–63,device_admin_cmd_opcodes[1]
對應 64–127,依此類推
例如,若陣列長度為 2,且 device_admin_cmd_opcodes[0] = 0x3
、device_admin_cmd_opcodes[1] = 0x1
,則表示僅支援 opcode 0、1 與 64
陣列長度取決於支援的 opcode 範圍,其必須足以涵蓋所有被設為 1 的位元,換句話說陣列長度可由「最大支援的 opcode
+ 1,除以 64,向上取整」計算求得
對 VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
而言,command_specific_result
與 command_specific_data
的長度分別為 DIV_ROUND_UP(max_cmd,64) * 8
,其中 DIV_ROUND_UP
表示向上取整的整數除法,而 max_cmd
是最大的可用 opcode
命令
該陣列允許比實際所需更大,並可在尾端附加任意數量的全零元素。 因此,由 VIRTIO_ADMIN_CMD_LIST_QUERY
回傳的 device_admin_cmd_opcodes[0]
中,對應 opcode
0(VIRTIO_ADMIN_CMD_LIST_QUERY
)與 1(VIRTIO_ADMIN_CMD_LIST_USE
)的位元 0 與 1 必定會被設為 1
Tips
因為預設只會使用兩個命令:VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
對於 VIRTIO_ADMIN_CMD_LIST_QUERY
命令,opcode
會被設為 0x0,group_member_id
不被使用(由驅動設為 0)。 此命令無命令特定資料,若成功,裝置會在 command_specific_result
中,以 struct virtio_admin_cmd_list
的格式回傳結果,描述由 group_type
指定之群組型別所支援的群組管理命令清單
對於 VIRTIO_ADMIN_CMD_LIST_USE
命令,opcode
會被設為 0x1,group_member_id
不使用(由驅動設為 0)。 其 command_specific_data
以 struct virtio_admin_cmd_list
的格式提供資料,描述驅動程式針對由 group_type
指定之群組型別「要使用」的群組管理命令清單。 本命令沒有命令特定的回傳結果
Tips
外層:
virtio_admin_cmd
所有「群組管理命令」都用同一個外層結構送來送去。 它用opcode
(外層的 admin cmd opcode)表示「我要執行哪一種管理命令」(例如LIST_QUERY
=0x0、LIST_USE
=0x1...),並帶著兩個可變長度的欄位:command_specific_data
(driver → device 的命令參數)command_specific_result
(device → driver 的命令結果)
內層:各命令專屬的 payload 版型
每一種管理命令,都自己定義「command_specific_data/result
的格式」。 對LIST_QUERY
和LIST_USE
這兩個命令而言,內層格式就是:struct virtio_admin_cmd_list { /* Indicates which of the below fields were returned */ le64 device_admin_cmd_opcodes[]; // 位圖(bitset)陣列 };
也就是說 virtio_admin_cmd_list
是這兩個命令的 payload 結構。 規範用一個 struct
來描述其版面與對齊,而裡面實質上就是一個 le64
位圖陣列
其中位圖的一個 bit 就對應到一個命令 opcode
:
- 外層
virtio_admin_cmd.opcode
:指出正在執行哪一種管理命令(例如現在是LIST_QUERY
或LIST_USE
) - 內層
device_admin_cmd_opcodes[]
位圖:用 bit 來列出哪些「管理命令 opcode」被支援/要使用- bit 的索引用來管理命令的
opcode
值(例如 bit 0 代表LIST_QUERY
、bit 1 代表LIST_USE
、bit 2 代表LEGACY_COMMON_CFG_WRITE
...)
- bit 的索引用來管理命令的
而 LIST_QUERY
/ LIST_USE
的資料流如下:
LIST_QUERY
(外層opcode=0x0
)- driver → device:
command_specific_data
為空(長度 0) - device → driver:在
command_specific_result
回傳一個virtio_admin_cmd_list
,其中位圖(bitset)指出「這個群組型別支援哪些管理命令」 - 規範要求回來的位圖中,bit0/bit1(對應
LIST_QUERY
/LIST_USE
)一定要被設為 1
- driver → device:
LIST_USE
(外層opcode=0x1
)- driver → device:在
command_specific_data
送一個virtio_admin_cmd_list
,位圖列出「driver 打算用哪些命令」(必須是剛查到清單的子集) - device → driver:沒有
command_specific_result
(空),只用status
告訴它 OK 或INVALID_FIELD
等
- driver → device:在
因為 device_admin_cmd_opcodes[]
是 le64 位圖陣列,要能覆蓋到「最大的那個被提到的 opcode」,就需要:
長度(bytes) = ceil((max_cmd + 1) / 64) × 8
這份「位圖陣列」就是 LIST_QUERY
回來的 command_specific_result
,以及 LIST_USE
送出的 command_specific_data
的實際長度
[driver] [device(owner)]
┌───────────────────────────────────┐
│ virtio_admin_cmd (envelope) │
│ opcode = 0x0 (LIST_QUERY) │
│ command_specific_data = empty │
└───────────────────────────────────┘
─────────►(裝置回覆)
┌──────────────────────────────────┐
│ virtio_admin_cmd (envelope) │
│ status = OK │
│ command_specific_result = │
│ virtio_admin_cmd_list { │
│ le64 opcodes[] // 位圖 │
│ } │
└──────────────────────────────────┘
接著 driver 選擇子集:
┌───────────────────────────────────┐
│ virtio_admin_cmd (envelope) │
│ opcode = 0x1 (LIST_USE) │
│ command_specific_data = │
│ virtio_admin_cmd_list { │
│ le64 opcodes[] // 位圖子集 │
│ } │
└───────────────────────────────────┘
─────────►(裝置只回 status,無 result)
驅動程式會先發出 VIRTIO_ADMIN_CMD_LIST_QUERY
,以查詢對該群組有效的命令清單,然後才會對群組中的成員發送其他命令。 接著,驅動程式會從 VIRTIO_ADMIN_CMD_LIST_QUERY
回傳的清單中擇一子集合,亦即「驅動理解且會使用」的那部分,以 VIRTIO_ADMIN_CMD_LIST_USE
命令送交裝置啟用
若裝置支援驅動程式所宣告要使用的命令清單,則裝置會以 VIRTIO_ADMIN_STATUS_OK
完成該命令。 若裝置不支援該清單(例如驅動無法使用某些必要命令),則裝置會以 VIRTIO_ADMIN_STATUS_INVALID_FIELD
完成該命令
注意:若驅動程式不熟悉某命令 opcode
的使用方式,則應預設它不會在 device_admin_cmd_opcodes
中把對應位元設為 1,因為裝置可能在不同命令 opcode
之間存在相依性
應預設同一個群組中的所有成員會支援並使用相同的命令清單。 不過,若某擁有者裝置同時支援多種群組型別,則不同群組型別之間,支援的命令清單可能會不同
2.12.1.1 傳統(legacy)介面
在某些系統裡,即使某個裝置不直接支援傳統(legacy)介面,系統仍需要搭配 legacy driver 使用。 遇到這種情況時,群組的擁有者裝置可以替群組內的成員裝置提供 legacy 介面的功能。 這樣擁有者裝置的驅動程式便能代表成員裝置的 legacy 驅動程式,去存取該成員裝置的 legacy 介面
Tips
這段定義了「代管」的概念:member 沒有 legacy 介面,但 owner 代為出面,讓外界仍以 legacy 方式操作。 從軟體路徑看,真正下命令的是 owner 的驅動,它接受來自 legacy 驅動的意圖,再轉譯成群組管理命令去觸達 member。 這讓沒有 legacy 介面的硬體,也能在受限環境(如舊軟體或 VM)裡被使用。 重點是「介面轉接」而非改變成員本體
例如,在 SR-IOV 的群組型別中,群組成員(VF)無法如傳統 PCI 驅動程式所期待的那樣,於 BAR0 的 I/O BAR 呈現 legacy 介面。 若在虛擬機中執行 legacy driver,負責執行該虛擬機的 hypervisor 可以提供一個在 BAR0 具備 I/O BAR 的虛擬裝置。 hypervisor 會攔截 legacy driver 對這個 I/O BAR 的存取,並以群組管理命令把它們轉送到群組的擁有者裝置(PF)
下列命令用來支援上述的 legacy 介面功能:
- 傳統(legacy)Common 組態寫入命令
- 傳統(legacy)Common 組態讀取命令
- 傳統(legacy)Device 組態寫入命令
- 傳統(legacy)Device 組態讀取命令
目前這些命令只有針對 SR-IOV 群組型別來定義。 整體而言,它們帶來的效果與第 4.1.4.10 節所列、透過傳統介面對成員裝置進行存取的效果相同,只是這裡一律假定採用小端序(little-endian)格式
2.12.1.1.1 傳統(legacy)Common 組態寫入命令
此命令的效果,等同於透過傳統介面對 virtio 的 common 組態結構進行寫入。 command_specific_data
採用 struct virtio_admin_cmd_legacy_common_cfg_wr_data
的格式,描述要執行的存取
struct virtio_admin_cmd_legacy_common_cfg_wr_data {
u8 offset; /* Starting byte offset within the common configuration structure to write */
u8 reserved[7];
u8 data[];
};
對於 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
,opcode
會被設為 0x2
。 group_member_id
指向要存取的成員裝置。 offset
指在 virtio common 組態結構(不含裝置特定組態)內要寫入的位置偏移量。 要寫入的資料長度就是 data
的長度
對 offset
與 data
的長度,規格沒有對長度或對齊施加任何限制,但其結果必須指向單一欄位,且完全落在 virtio 的 common 組態結構之內(不含裝置特定組態)
此命令沒有命令特定的回傳結果
Tips
寫入通常只回 status
/status_qualifier
,不回資料 payload。 驅動若要確認值,應再發讀取命令,這也是多數控制暫存器寫入的常見設計
2.12.1.1.2 傳統(legacy)Common 組態讀取命令
此命令的效果,等同於透過傳統介面從 virtio 的 common 組態結構讀取。 command_specific_data
採用 struct virtio_admin_cmd_legacy_common_cfg_rd_data
的格式,描述要執行的存取
struct virtio_admin_cmd_legacy_common_cfg_rd_data {
u8 offset; /* Starting byte offset within the common configuration structure to read */
};
對於 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
,opcode
設為 0x3
。 group_member_id
指向要存取的成員裝置。 offset
代表在 virtio common 組態結構(不含裝置特定組態)內要讀取的位置偏移量
struct virtio_admin_cmd_legacy_common_cfg_rd_result {
u8 data[];
};
對 offset
與 data
的長度,規格沒有對長度或對齊施加任何限制,但其結果必須指向單一欄位,且完全落在 virtio 的 common 組態結構之內(不含裝置特定組態)
當命令成功完成時,裝置會以 struct virtio_admin_cmd_legacy_common_cfg_rd_result
的格式回傳 command_specific_result
。 讀到的資料長度,就是 data
的長度
2.12.1.1.3 傳統(legacy)Device 組態寫入命令
此命令的效果,等同於透過傳統介面對 virtio 的裝置特定(device-specific)組態進行寫入。 command_specific_data
採用 struct virtio_admin_cmd_legacy_dev_reg_wr_data
的格式,描述要執行的存取
struct virtio_admin_cmd_legacy_dev_reg_wr_data {
u8 offset; /* Starting byte offset within the device-specific configuration to write */
u8 reserved[7];
u8 data[];
};
對於 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
,opcode
設為 0x4。 group_member_id
指向要存取的成員裝置。 offset
代表在 virtio 的裝置特定組態內要寫入的位置偏移量。 要寫入的資料長度,就是 data
的長度
對 offset
與 data
的長度,規格沒有對長度或對齊施加任何限制,但其結果必須指向單一欄位,且完全落在裝置特定(device-specific)組態之內
此命令沒有命令特定的回傳結果
2.12.1.1.4 傳統(legacy)Device 組態讀取命令
此命令的效果,等同於透過傳統介面從 virtio 的裝置特定(device-specific)組態讀取。 command_specific_data
採用 struct virtio_admin_cmd_legacy_common_cfg_rd_data
的格式,描述要執行的存取
struct virtio_admin_cmd_legacy_dev_cfg_rd_data {
u8 offset; /* 要讀取的 device-specific 組態內之起始位元組偏移量 */
};
對於 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
,opcode
設為 0x5。 group_member_id
指向要存取的成員裝置。 offset
代表在 virtio 的裝置特定組態中要讀取的位置偏移量
struct virtio_admin_cmd_legacy_dev_reg_rd_result {
u8 data[];
};
對 offset
與 data
的長度,規格沒有對長度或對齊施加任何限制,但其結果必須指向單一欄位,且完全落在裝置特定(device-specific)組態之內
當命令成功完成時,裝置會以 struct virtio_admin_cmd_legacy_dev_reg_rd_result
的格式回傳 command_specific_result
。 讀到的資料長度,就是 data
的長度
2.12.1.1.5 傳統(legacy)驅動程式通知
擁有者裝置的驅動程式,能透過執行 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
,以 offset
對應 Queue Notify,並在 data
放入要通知的 16 位元 virtqueue 索引,來向使用 legacy 介面運作的成員裝置發送驅動程式通知
然而,由於 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
也會用在較慢的路徑(slow path)的組態上,擁有者裝置可以另行提供一個專用機制,專門用於把這類驅動程式通知送給成員裝置。 對於 SR-IOV 群組型別,可選的 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
命令可滿足此需求:它會回傳一個或多個可用於發送這類通知的地址。 回傳的通知地址可以位於裝置的記憶體空間(PCI BAR 或 VF BAR)中
在此替代做法中,驅動程式通知的發送方式是:以小端序(little-endian)將要通知的 16 位元 virtqueue 索引,寫到 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
回傳的通知位址
透過該通知位址送出的任何驅動程式通知,其效果與使用 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、並以 offset
對應 Queue Notify 的作法相同
此命令僅針對 SR-IOV 群組型別有定義
對於 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
,opcode
設為 0x6。 group_member_id
指向要存取的成員裝置。 本命令不使用 command_specific_data
當裝置支援 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
命令時,群組擁有者裝置會在 SR-IOV 擴充能力(Extended Capability)中,將 VF 的 BAR0 固定為 0
struct virtio_pci_legacy_notify_info {
u8 flags; /* 0 = end of list, 1 = owner device, 2 = member device */
u8 bar; /* BAR of the member or the owner device */
u8 padding[6];
le64 offset; /* Offset within bar. */
};
struct virtio_admin_cmd_legacy_notify_info_result {
struct virtio_pci_legacy_notify_info entries[4];
};
flags
為 0x1 表示該通知位址屬於擁有者裝置;為 0x2 表示該通知位址屬於成員裝置,為 0x0 則表示自該項起之後的所有項目,皆為 entries
陣列中的無效項目。 flags
的其他數值保留不用
bar
的數值 0x1 至 0x5 分別指定 BAR1 至 BAR5:當 flags
為 0x1 時,所指為裝置 PCI 標頭(PCI header)中的 Base Address Registers。 當 flags 為 0x2 時,所指為裝置 SR-IOV 擴充能力中的 VF BARn
offset
表示相對於 bar 指定之 BAR 的通知位址偏移量,此值需要以 2-byte 對齊
當命令成功完成時,command_specific_result
會採用 struct virtio_admin_cmd_legacy_notify_info_result
的格式。 裝置最多可提供 4 筆各異的通知位址。 在此情況下,驅動程式可使用任一筆項目。 項目的順序對驅動程式而言具有偏好提示的意義,期望驅動程式優先使用陣列中較前面的項目。 驅動程式也應忽略任何無效的項目,包含(若存在)清單結尾項目及其後所有項目
2.12.1.1.6 裝置需求:Legacy 介面
裝置對下列命令必須採「全有或全無」的支援:VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
。 對上述四個命令,裝置在解碼與編碼 data
的值時,必須使用小端序(little-endian)格式
對 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
,若 offset
與 data
長度所對應的存取不是單一欄位,或未完全落在 virtio 的 common 組態(且不含裝置特定組態)之內,裝置必須讓該命令失敗
對 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
,若 offset
與 data
長度所對應的存取不是單一欄位,或未完全落在 virtio 的裝置特定組態之內,裝置必須讓該命令失敗
VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
命令的效果,必須等同於透過傳統介面寫入 virtio 的 common 組態結構
VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
命令的效果,必須等同於透過傳統介面讀取 virtio 的 common 組態結構
VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
命令的效果,必須等同於透過傳統介面寫入 virtio 的裝置特定組態
VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
命令的效果,必須等同於透過傳統介面讀取 virtio 的裝置特定組態
對於 SR-IOV 群組型別,當擁有者裝置支援 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
、VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
等命令時:
- 擁有者裝置與群組成員裝置應遵循第 4.1.2 節所記載、非轉換式(non-transitional)裝置的 PCI Revision ID 與 Subsystem Device ID 規則
- 擁有者裝置應遵循第 4.1.2 節所記載、非轉換式裝置的 PCI Device ID 規則
- 裝置在任何由
VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
命令回傳之通知位址收到的任何驅動程式通知,必須等同於該通知係透過VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、且以offset
對應 Queue Notify 所送達的效果
若裝置支援 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
命令,則:
- 裝置必須同時支援
VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
等所有命令 - 在
VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
的命令結果中,最後一個struct virtio_pci_legacy_notify_info
條目必須令 flags 為 0 - 在該命令結果中,任何有效條目必須具有未被硬接為 0 的 bar
- 在該命令結果中,任何有效條目必須具有 2 位元組對齊的 offset
- 裝置可以(MAY)在結果中提供擁有者裝置的條目、成員裝置的條目,或兩者皆有
- 針對 SR-IOV 群組型別,群組擁有者裝置必須在 SR-IOV 擴充能力中,將 VF 的 BAR0 硬接為 0
2.12.1.1.7 驅動程式需求:Legacy 介面
對於 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
、VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
、VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
這些命令,驅動程式在編碼與解碼 data
的值時,必須使用小端序(little-endian)格式
對 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_COMMON_CFG_READ
而言,驅動程式應當設定 offset
與 data
長度,使其只對應到 virtio common 組態結構中的單一欄位,且不包含裝置特定(device-specific)組態
對 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_WRITE
與 VIRTIO_ADMIN_CMD_LEGACY_DEV_CFG_READ
而言,驅動程式應當設定 offset
與 data
長度,使其只對應到裝置特定(device-specific)組態中的單一欄位
若支援 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
命令,驅動程式應當使用該命令提供的通知位址,將所有驅動程式通知送達裝置
若 VIRTIO_ADMIN_CMD_LEGACY_NOTIFY_INFO
回傳的 struct virtio_admin_cmd_legacy_notify_info_result
中,某一個 struct virtio_pci_legacy_notify_info
條目的 flags
值為 0x0,則驅動程式必須忽略該條目以及其後所有條目。 另針對其餘條目,驅動程式必須驗證下列事項:
flags
的值為 0x1 或 0x2bar
依flags
指示,對應到擁有者或成員裝置的一個有效 BARoffset
具備 2 位元組對齊,且對應到 bar 所指定的 BAR 範圍內的一個位址
驅動程式必須忽略它不符合上述條件的條目
2.12.1.2 裝置需求:群組管理命令
裝置必須檢核 opcode
、group_type
與 group_member_id
。 若其中任一為無效或不支援,則將 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,並相應設定 status_qualifier
:
- 若
group_type
無效,status_qualifier
必須設為VIRTIO_ADMIN_STATUS_Q_INVALID_GROUP
; - 否則,若
opcode
無效,status_qualifier
必須設為VIRTIO_ADMIN_STATUS_Q_INVALID_OPCODE
; - 否則,若特定命令會用到
group_member_id
而其值無效,status_qualifier
必須設為VIRTIO_ADMIN_STATUS_Q_INVALID_MEMBER
若命令成功完成,裝置必須將 status
設為 VIRTIO_ADMIN_STATUS_OK
。 若命令失敗,裝置必須將 status
設為不同於 VIRTIO_ADMIN_STATUS_OK
的值
若 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,裝置狀態不可改變。 也就是說,該命令不得對裝置產生任何副作用,特別是裝置不得因此命令而進入錯誤狀態。 命令失敗時,裝置狀態一般而言不應改變,能不改變就盡量不改變
裝置可以對驅動欲使用的 opcode
強制額外限制與相依關係,若驅動所宣告的命令清單違反了裝置內部的相依性,則可以讓 VIRTIO_ADMIN_CMD_LIST_USE
命令失敗,接著將 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,將 status_qualifier
設為 VIRTIO_ADMIN_STATUS_Q_INVALID_FIELD
並回報
若裝置支援多種群組型別,則各群組型別的命令必須彼此獨立運作。 特別是,裝置可以針對不同群組型別,回傳不同的 VIRTIO_ADMIN_CMD_LIST_QUERY
結果
在重設之後,若裝置支援某一群組型別,且尚未收到該群組型別的 VIRTIO_ADMIN_CMD_LIST_USE
,裝置必須假定驅動可使用的合法命令清單僅包含 VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
這兩個命令
成功完成 VIRTIO_ADMIN_CMD_LIST_USE
之後,裝置必須將驅動可使用的合法命令清單,更新為該命令之 command_specific_data
所提供的清單
裝置必須依據驅動所用的清單來檢核命令;對於不在清單中的任何命令,必須讓其失敗,接著將 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,將 status_qualifier
設為 VIRTIO_ADMIN_STATUS_Q_INVALID_OPCODE
並回報
裝置回報的「支援命令清單」不得縮減(但可以擴大):一旦透過 VIRTIO_ADMIN_CMD_LIST_QUERY
回報某命令為「支援」,之後不可再回報為「不支援」。 另外,當某一組命令已被使用(曾成功執行 VIRTIO_ADMIN_CMD_LIST_USE
),在裝置或系統重設後,裝置應當對於隨後以相同清單進行的 VIRTIO_ADMIN_CMD_LIST_USE
呼叫予以成功完成
若重設後該呼叫失敗,裝置不得僅僅因為「所用的命令清單」而讓其失敗。 否則會干擾系統從暫停恢復與錯誤復原。 若系統組態能保證驅動不快取先前 VIRTIO_ADMIN_CMD_LIST_USE
的值(如韌體升降級情境),則可以有例外
在處理 SR-IOV 群組型別的命令時,若裝置沒有 SR-IOV 擴充能力(Extended Capability),或 VF Enable 為清除狀態,則裝置必須讓所有命令失敗,接著將 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,將 status_qualifier
設為 VIRTIO_ADMIN_STATUS_Q_INVALID_GROUP
並回報
否則,若 group_member_id
不在 1 到 NumVFs(含)之間,裝置必須讓所有命令失敗,接著將 status
設為 VIRTIO_ADMIN_STATUS_EINVAL
,將 status_qualifier
設為 VIRTIO_ADMIN_STATUS_Q_INVALID_MEMBER
並回報。 NumVFs、VF Migration Capable 與 VF Enable 皆指 [PCIe] 所規定之 SR-IOV 擴充能力中的暫存器
2.12.1.3 驅動程式需求:群組管理命令
驅動程式可以透過送出 VIRTIO_ADMIN_CMD_LIST_QUERY
(帶入相符的 group_type
),來確認裝置是否支援某個特定的群組型別
在發出任何其他命令之前(初始的 VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
除外),驅動程式必須先送出 VIRTIO_ADMIN_CMD_LIST_USE
,並等待其以 VIRTIO_ADMIN_STATUS_OK
完成
驅動程式可以多次送出 VIRTIO_ADMIN_CMD_LIST_USE
,但若裝置上還有其他命令已提交且尚未處理完成,則不得送出 VIRTIO_ADMIN_CMD_LIST_USE
若驅動程式不熟悉某個命令 opcode
的使用方式,則在 device_admin_cmd_opcodes
中不應設定其位元,因為命令之間可能存在相依關係
驅動程式不得要求(透過 VIRTIO_ADMIN_CMD_LIST_USE
)使用任何尚未在相同 group_type
下、由 VIRTIO_ADMIN_CMD_LIST_QUERY
回報為支援的命令
在送出包含正確命令位元清單與群組型別的 VIRTIO_ADMIN_CMD_LIST_USE
之前,驅動程式不得對該群組型別使用任何命令
驅動程式可以透過送出 VIRTIO_ADMIN_CMD_LIST_USE
,並在其 command_specific_data
中清除相應位元,來「封鎖」VIRTIO_ADMIN_CMD_LIST_QUERY
與 VIRTIO_ADMIN_CMD_LIST_USE
的使用
對於具有保留(reserved)status
值的命令錯誤,驅動程式必須比照 VIRTIO_ADMIN_STATUS_EINVAL
的方式處理(但可在錯誤回報/診斷訊息上有所區別)
對於具有保留(reserved)status_qualifier
值的命令錯誤,驅動程式必須比照 VIRTIO_ADMIN_STATUS_Q_INVALID_COMMAND
的方式處理(但可在錯誤回報/診斷訊息上有所區別)
當以 SR-IOV 群組型別送出命令時,驅動程式須為 group_member_id
指定介於 1 與 NumVFs(含)之間的值。 另外,驅動程式必須確保:只要任何此類命令仍未完成,VF Migration Capable 維持為清除(clear),且 VF Enable 維持為設置(set)。 NumVFs、VF Migration Capable 與 VF Enable 皆指 [PCIe] 所規定之 SR-IOV 擴充能力中的暫存器
2.13 管理(Administration)Virtqueue
擁有者裝置的「管理 virtqueue」用來提交群組管理命令。 單一擁有者裝置可以擁有一個以上的管理 virtqueue
如果已協商出 VIRTIO_F_ADMIN_VQ
功能,擁有者裝置會公開一個或多個管理 virtqueue。 管理 virtqueue 的數量與位置,會由擁有者裝置依各傳輸層的特定方式來公開
驅動程式把請求排入任一條管理 virtqueue,而裝置就從同一條 virtqueue 取用這些請求。 由於裝置取用時沒有順序上的約束,必須由驅動程式負責確保命令之間的嚴格順序。 例如,如果需要一致性,驅動可以在第一個命令處理完成之前,等待其結果,再送出依賴於第一個命令的後續命令
管理 virtqueue 的使用方式如下:
- 驅動程式以
struct virtio_admin_cmd
結構提交命令,並使用由兩部分組成的緩衝區:前段為裝置可讀,後段為裝置可寫 - 裝置可讀的部分包含從
opcode
到command_specific_data
的欄位 - 裝置可寫的緩衝區則包含從
status
到command_specific_result
(含)的欄位
對每一種命令,本規範都會描述 command_specific_data
與 command_specific_result
的專屬格式;這兩個欄位的長度視命令而定
然而,為了確保前向相容:
- 允許驅動程式提交比裝置預期更長的緩衝區(也就是,長於
opcode
到command_specific_data
的長度)。 這讓驅動即便有些結構欄位未被裝置使用,仍可維持單一格式結構 - 允許驅動程式提交比裝置預期更短的緩衝區(也就是,短於
status
到command_specific_result
的長度)。 這讓裝置即便有些結構欄位未被驅動使用,仍可維持單一格式結構
裝置會把驅動提交的兩個部分(裝置可讀與裝置可寫)的長度,與自身預期相比較,然後默默地截斷成「驅動提交的長度」與「本規範描述的長度」兩者較短者。 落在較短長度之外的資料,裝置一律無聲忽略。 任何缺漏的欄位,一律視為值為 0
同理,驅動會把裝置回報的「已使用緩衝區長度」與自身預期相比較,然後默默地截斷結構到該已使用長度。 落在裝置回報之已使用長度之外的資料,驅動一律無聲忽略。 任何缺漏的欄位,一律視為值為 0
這樣簡化了驅動與裝置的實作:雙方只要為某個命令及其結果維持一份「較大的單一結構」(例如一個 C 結構)即可。 當新版本規範出現時,可以把新欄位加在結構的尾端,而驅動與裝置可直接使用完整結構,而不必過度在意版本差異
2.13.1 裝置需求:群組管理命令
裝置必須支援比本規範所述更短的「裝置可讀/裝置可寫」緩衝區,其行為為:
- 對於落在裝置可讀緩衝區之外、原本會被讀取的資料,一律視為值為 0
- 對於落在指定的裝置可寫緩衝區之外的資料,一律丟棄
裝置必須支援比本規範所述更長的「裝置可讀/裝置可寫」緩衝區,其行為為:
- 對裝置可讀緩衝區中、超出預期長度的資料,一律忽略
- 在裝置可寫緩衝區中,僅寫入規範所預期的結構內容,忽略多出的緩衝區,並以位元組為單位回報實際寫入的長度作為「已使用長度」
裝置應當把裝置可寫緩衝區初始化到「本規範所描述之結構長度」與「驅動所提供之緩衝區長度」兩者中的較短者(即使該緩衝區內容全為 0 也一樣)
僅因為提供的緩衝區比本規範所述更短或更長,裝置不得讓命令失敗。 裝置必須初始化 struct virtio_admin_cmd
中裝置可寫的那一部分,且其大小需為 64 位元的整數倍。 裝置必須在 struct virtio_admin_cmd
中初始化 status
與 status_qualifier
在同一條管理 virtqueue 上,裝置必須按照命令被排入佇列的順序來處理。 如果已配置多條管理 virtqueue,裝置可以在不同的 virtqueue 之間處理命令而不施加任何順序約束
如果裝置把 status
設為 VIRTIO_ADMIN_STATUS_EAGAIN
或 VIRTIO_ADMIN_STATUS_ENOMEM
,則該命令不得產生任何副作用,使其可安全地重試
2.13.2 驅動程式需求:群組管理命令
驅動程式可以提供比本規範所述更長的 struct virtio_admin_cmd
裡的「裝置可讀/裝置可寫」部分。 驅動程式應當提供的「裝置可讀」部分,其大小至少要等於本規範所描述的結構大小(即使整段內容全為 0 也可以)
驅動程式必須讓 struct virtio_admin_cmd
的「裝置可讀」與「裝置可寫」兩部分,其長度皆為 64 位元的整數倍
(語境疑似應為 driver)struct virtio_admin_cmd
的「裝置可讀」與「裝置可寫」兩部分,其長度必須皆大於 0。 不過,command_specific_data
與 command_specific_result
可以為 0 長度,除非該命令另有規定
驅動程式不得假設裝置會依規範把整個「裝置可寫」部分完全初始化,相反地,驅動必須把落在裝置實際使用範圍之外的結構,視為其值為 0
如果配置了多條管理 virtqueue,驅動程式必須為放在不同管理 virtqueue 上的命令自行確保順序
對於以 VIRTIO_ADMIN_STATUS_EAGAIN
完成的命令,驅動程式應當重試
3 一般初始化與裝置操作
本節先總覽裝置的初始化流程,接著再展開裝置細節,以及各步驟如何執行。 閱讀本節時,最好同時參考各「匯流排特定」章節,以了解與特定裝置通訊的方式
3.1 裝置初始化
3.1.1 驅動程式需求:裝置初始化
驅動程式必須依照下列順序初始化裝置:
- 重設(reset)裝置
- 設定
ACKNOWLEDGE
狀態位元:表示客體作業系統已注意到裝置 - 設定
DRIVER
狀態位元:表示客體作業系統知道如何驅動該裝置 - 讀取裝置的功能位元(feature bits),並把作業系統與驅動程式可理解的功能位元子集寫回裝置。 在此步驟中,驅動可以讀取(但不得寫入)裝置特定(device-specific)的組態欄位,以在接受裝置之前檢查是否能支援該裝置
- 設定
FEATURES_OK
狀態位元。 驅動在此步驟之後不得再接受新的功能位元 - 重新讀取裝置狀態,確認
FEATURES_OK
仍為設置狀態:否則表示裝置不支援我們所選的功能子集,該裝置不可用 - 執行裝置特定的設定,包括為裝置探測 virtqueue、進行可選的匯流排層設定、讀取與可能寫入裝置的 virtio 組態空間,以及填入(populate)各 virtqueue
- 設定
DRIVER_OK
狀態位元。 此時裝置視為「已上線(live)」
若上述任何步驟發生無法復原的錯誤,驅動程式應當設定 FAILED
狀態位元以表示已放棄該裝置(必要時可稍後重設裝置以重新啟動流程)。 在此情況下,驅動不得繼續初始化
在設定 DRIVER_OK
之前,驅動程式不得向裝置送出「緩衝區可用」通知
3.1.2 傳統(Legacy)介面:裝置初始化
傳統裝置不支援 FEATURES_OK
狀態位元,因此沒有優雅的方法讓裝置表明「功能組合不被支援」。 它們也沒有明確的機制來結束功能協商,導致裝置往往在首次使用時才最終確定功能,而且無法引入會徹底改變裝置初始行為的新功能
傳統驅動實作常在設定 DRIVER_OK
之前就開始使用裝置,有時甚至在把功能位元寫入裝置之前就使用
結果是第 5 與第 6 步被省略,而第 4、7、8 步被合併混在一起
因此,當使用傳統介面時:
- 過渡型(transitional)驅動必須依 3.1 的初始化順序執行,但省略第 5 與第 6 步
- 過渡型裝置必須支援驅動在第 4 步之前就寫入裝置的組態欄位
- 過渡型裝置必須支援驅動在第 8 步之前就使用裝置
3.2 裝置操作
在裝置運作期間,裝置組態空間中的各欄位,可能由驅動或由裝置本身改變
只要有由裝置觸發的組態變更,驅動便會被通知。 這使驅動可以快取裝置組態,除非收到通知,否則無需進行昂貴的組態讀取
3.2.1 裝置組態變更的通知
對於那些「裝置特定(device-specific)」組態資訊可能變動的裝置,當發生此類變更時,會送出「組態變更通知」
此外,當裝置把 DEVICE_NEEDS_RESET
設為設置狀態(見 2.1.2)時,也會觸發此通知
3.3 裝置清理
一旦驅動程式設定了 DRIVER_OK
狀態位元,裝置上所有已配置的 virtqueue 都被視為「已上線(live)」。 而只要裝置被重設,該裝置的所有 virtqueue 就都不再是 live
3.3.1 驅動程式需求:裝置清理由
在 live 的 virtqueue 上,對於已暴露(exposed)的緩衝區,也就是那些已被提供給裝置、但尚未被裝置使用的緩衝區——驅動程式不得修改其 virtqueue 項目
因此,在移除這些已暴露的緩衝區之前,驅動程式必須先確保該 virtqueue 不再是 live(例如透過裝置重設)