(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 驅動程式需求:裝置狀態欄位
驅動程式必須(MUST)更新裝置狀態,依 3.1 所述的初始化流程,設定各位元以表示已完成的步驟。 驅動程式不能(MUST NOT)清除任何裝置狀態位元。 若驅動程式設定了 FAILED
位元,則在嘗試重新初始化之前,必須(MUST)先對裝置進行重置(reset)
當 DEVICE_NEEDS_RESET
被設為 1 時,驅動程式不應該(SHOULD NOT)依賴裝置操作的完成。 注意:例如,若 DEVICE_NEEDS_RESET
已設為 1,驅動程式既不能假設「進行中的請求會完成」,也不能假設「它們尚未完成」。 良好的實作應嘗試透過重置來復原
2.1.2 裝置需求:裝置狀態欄位
在 DRIVER_OK
之前,裝置不能(MUST NOT)取用(consume)任何緩衝區,亦不得向驅動程式送出任何「已用緩衝區(used buffer)」的通知
當裝置進入需要重置才能復原的錯誤狀態時,裝置應要(SHOULD)設定 DEVICE_NEEDS_RESET
。 若此時 DRIVER_OK
已被設為 1,則在設定 DEVICE_NEEDS_RESET
之後,裝置必須(MUST)向驅動程式送出裝置組態的變更通知
2.2 特徵位元(Feature Bits)
每個 virtio 裝置會提供它支援的所有特性。 在裝置初始化期間,驅動程式會讀取這些特性,並告訴裝置它所接受的子集合。 要重新協商,只能重置該裝置
這能讓系統具有向前與向後的相容性:如果裝置新增了一個特徵位元,較舊的驅動程式不會把那個特徵位元回寫給裝置。 同樣地,如果驅動程式新增了一個裝置不支援的功能,驅動程式也能知道該裝置沒有提供此新功能
特徵位元的配置如下:
- 0 到 23,以及 50 到 127:
用於特定裝置型別的特徵位元 - 24 到 41:
保留給 virtqueue 與功能協商機制的擴充用特徵位元 - 42 到 49,以及 128 與以上:
保留給未來擴充使用的特徵位元
注意:例如,對網路裝置(也就是 Device ID 1)而言,特徵位元 0 表示該裝置支援封包的 checksum。 而對於裝置組態空間(device configuration space),如果有了新欄位,則需要搭配一個新的特徵位元來表示
要讓功能協商機制保持可擴充的重點在於:裝置不應提供那些在驅動程式接受後,裝置本身無法處理的特徵位元(雖然依照本規格,驅動程式本就不該接受任何未定義、保留或不受支援的特徵)。 同理,驅動程式也不應接受自己不知道如何處理的特徵位元(雖然依照本規格,裝置本就不該主動提供未定義、保留或不受支援的特徵)。 對於保留或非預期的特徵,首選的處理方式是由驅動程式將其忽略
對僅限於特定匯流排的功能而言,上述原則更為重要,因為若未來規格將這些功能擴大到更多的匯流排上,很可能需要驅動程式與裝置改變既有行為。 支援多種匯流排的驅動程式與裝置,必須(MUST)謹慎維護每個匯流排各自允許的功能清單
2.2.1 驅動程式需求:特徵位元
驅動程式不能(MUST NOT)接受裝置未提供的功能,也不能(MUST NOT)接受一個需要其他尚未被接受的功能所依賴的功能
驅動程式必須(MUST)驗證裝置所提供的特徵位元。 對於下列任一情形的特徵位元,驅動程式必須(MUST)忽略且不能(MUST NOT)接受:
- 未在本規格中描述的
- 標記為保留(reserved)的
- 不適用於該特定匯流排的
- 未為該裝置型別所定義的
如果裝置沒有提供某個驅動程式所支援的功能,驅動程式應要(SHOULD)切換到向後相容模式,否則就必須(MUST)將裝置狀態的 FAILED
位元設為 1,並停止初始化
相對地,驅動程式不能(MUST NOT)僅僅因為裝置提供了一個驅動程式不支援的功能,就宣告失敗
2.2.2 裝置需求:特徵位元
裝置不能(MUST NOT)提供一個依賴於其他尚未被提供的功能的特徵。 裝置應該(SHOULD)接受驅動程式所接受的功能中的任何有效子集合,否則當驅動程式寫入時,裝置必須(MUST)讓裝置狀態欄位中的 FEATURES_OK
位元設定失敗
裝置不能(MUST NOT)提供那些即使驅動程式接受了,裝置本身也無法支援的特徵位元(即便本規格禁止驅動接受這些特徵位元)。 為求明確,這裡指的是:本規格未描述的特徵位元、被標記為保留(reserved)的特徵位元、對特定匯流排或特定裝置型別而言被保留或不受支援的特徵位元。 不過,若未來版本的本規格允許裝置支援相對應功能,則依該未來規格撰寫的裝置當然不在此限
若某裝置曾成功協商過一組功能(在裝置初始化期間接受了裝置狀態欄位中的 FEATURES_OK
位元),那麼在裝置或系統重置之後,對於相同的一組功能,它不應該(SHOULD NOT)在重新協商時失敗。 否則會影響從暫停(suspend)恢復以及錯誤復原的流程
2.2.3 Legacy 介面:特徵位元說明
Transitional 驅動程式必須(MUST)透過檢測特徵位元 VIRTIO_F_VERSION_1
有沒有被提供,來偵測 Legacy 裝置。 Transitional 裝置必須(MUST)透過檢測 VIRTIO_F_VERSION_1
有沒有被驅動程式確認,來偵測 Legacy 驅動程式。 在此情形下,將透過 Legacy 介面來使用該裝置
對 Legacy 介面的支援是選用的(OPTIONAL)。 因此,transitional 與 non-transitional 的裝置與驅動程式,皆符合本規格
當透過 Legacy 介面使用裝置時,transitional 裝置與 transitional 驅動程式必須(MUST)依照這些 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 裝置需求:裝置重置
裝置在收到重置之後,必須(MUST)將裝置狀態欄位重新初始化為 0
當裝置透過把裝置狀態欄位重新設為 0 來表示重置已完成之後,在驅動程式重新初始化裝置之前,裝置不能(MUST NOT)送出通知,亦不能(MUST NOT)與各個佇列(queues)互動
2.4.2 驅動程式需求:裝置重置
當驅動程式讀到裝置狀態欄位為 0 時,驅動程式應該(SHOULD)視其所發起的重置已經完成
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 驅動程式需求:裝置組態空間
驅動程式不能(MUST NOT)假設寬度大於 32 位元的欄位讀取是原子操作,也不能(MUST NOT)假設跨多個欄位的讀取是原子操作。 驅動程式應該(SHOULD)如下讀取裝置組態空間欄位:
u32 before, after;
do {
before = get_config_generation(device);
// 讀取一個或多個組態項目
after = get_config_generation(device);
} while (after != before);
Tips
大於 32-bit 欄位的讀取不保證原子性,跨欄位更不保證原子性。 建議的樣板是以 generation 計數循環讀取,確保前後一致
對於選用的組態空間欄位,驅動程式必須(MUST)先檢查對應的特徵是否已由裝置提供,然後才可存取該組態區段。 功能協商的細節請見第 3.1 節
驅動程式不能(MUST NOT)預先限制結構大小或裝置組態空間大小。 相反地,驅動程式應該(SHOULD)只檢查裝置組態空間是否足以包含裝置運作所需的欄位。 例如,若規格說明組態空間「包含一個 8 位元的欄位」,則應理解為組態空間的末端可能還有任意長度的尾端 padding,因此只要組態空間大於或等於該 8 位元的大小,都應接受
2.5.2 裝置需求:裝置組態空間
在驅動程式設定 FEATURES_OK
之前,裝置必須(MUST)允許驅動讀取任何裝置特定的組態欄位。 這也包含那些取決於特徵位元的欄位,只要這些特徵位元已由裝置提供即可
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 介面時,驅動程式應該(SHOULD)連續多次讀取,直到兩次的讀值一致為止
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 重置
在佇列被驅動程式重置之後,裝置不能(MUST NOT)再從該 virtqueue 執行任何請求,也不能(MUST NOT)就此 virtqueue 通知驅動程式。 裝置必須(MUST)把該 virtqueue 的所有狀態重置為預設值,包括 available 狀態與 used 狀態
Tips
- 靜默:不再消費、亦不再發送通知,避免與驅動在重建資料結構期間產生 race condition
- 清空狀態:可用/已用索引、wrap counter(於 packed)、暫存指標等回到初始值,確保後續 re-enable 從乾淨狀態開始
2.6.1.1.2 驅動程式需求:Virtqueue 重置
當驅動程式要求裝置重置某個佇列後,驅動程式必須(MUST)驗證該佇列是否已成功被重置。 在佇列成功重置之後,驅動程式可以(MAY)釋放與該 virtqueue 相關的任何資源
2.6.1.2 Virtqueue 重新啟用(Re-enable)
此流程與整體裝置初始化期間、針對單一佇列的初始化流程相同
2.6.1.2.1 裝置需求:Virtqueue 重新啟用
裝置必須(MUST)遵從任何可能已被驅動程式變更的佇列組態,例如最大佇列大小等
2.6.1.2.2 驅動程式需求:Virtqueue 重新啟用
在重新啟用佇列時,驅動程式必須(MUST)如同初次發現該 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
- 串鍊(chaining)讓多個不連續的記憶體碎片組成一個邏輯請求(例如 scatter-gather I/O),減少複製
- 投遞流程:填
virtq_desc
→ 把頭節點索引寫入avail.ring
→ 視策略通知裝置 - 完成流程:裝置把頭節點索引寫進
used.ring
,並可選擇通知驅動,驅動以索引回推原請求、釋放資源、喚醒上層
2.7.1 驅動程式需求:Virtqueues
驅動程式必須(MUST)確保每一個 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 驅動程式必須(MUST)從裝置讀取該 Queue Size,並且必須(MUST)依照下列公式配置整個 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 裝置與驅動程式必須(MUST)使用下列這個 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 裝置與驅動程式必須(MUST)以 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 裝置需求:訊息框架
裝置不能(MUST NOT)對描述子的具體排列方式做任何假設。 裝置可以(MAY)針對一串(chain)描述子的長度設定合理上限
2.7.4.2 驅動程式需求:訊息框架
驅動程式必須(MUST)把所有裝置可寫(device-writable)的描述子元素,放在所有裝置可讀(device-readable)的描述子元素之後
驅動程式不應該(SHOULD NOT)用過多的描述子來描述同一個緩衝區
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
裝置不能(MUST NOT)寫入裝置可讀的緩衝區,裝置也不應該(SHOULD NOT)讀取裝置可寫的緩衝區(除非為了除錯或診斷,裝置可以(MAY)這麼做)。 裝置不能(MUST NOT)寫入任何 Descriptor Table 的表項
Tips
- 方向約束保障資料與狀態的一致性與安全性:讀區不可被覆寫。 寫區若被讀,多半僅為偵錯
- 不可改寫表:Descriptor Table 由驅動擁有,裝置若改寫,會破壞鏈完整性與系統安全(可能被視為裝置故障)
2.7.5.2 驅動程式需求:Descriptor Table
驅動程式不能(MUST NOT)建立總長度超過 232 位元組的描述子鏈,這也意味著禁止環狀循環的描述子鏈
若已協商 VIRTIO_F_IN_ORDER
,當驅動在表格位移 x
的描述子上設置 VRING_DESC_F_NEXT
並將其提供給裝置時:對於表中最後一個描述子(x = queue_size − 1
),驅動必須(MUST)把 next
設為 0,對於其他描述子,必須(MUST)把 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
,否則驅動程式不能(MUST NOT)設置 VIRTQ_DESC_F_INDIRECT
旗標。 驅動程式不能(MUST NOT)在間接表內的描述子再次設置 VIRTQ_DESC_F_INDIRECT
(也就是說,每個描述子最多只指向一張間接表)。 驅動程式不能(MUST NOT)建立長度超過裝置 Queue Size 的描述子鏈
驅動程式不能(MUST NOT)同時在 flags 中設置 VIRTQ_DESC_F_INDIRECT
與 VIRTQ_DESC_F_NEXT
若已協商 VIRTIO_F_IN_ORDER
,間接描述子必須(MUST)以連續順序出現,對第一個描述子,next
取值為 1,第二個為 2,依此類推
Tips
- 禁止間接的間接:避免遞迴/多層 indirection 造成複雜度與攻擊面暴增
- 不能同時 INDIRECT + NEXT:一個主表節點要麼指向「資料/鏈的下一段」,要麼指向「一張間接表」,不能兩者兼具以免歧義
- 鏈長限制:即便使用間接表,總鏈長仍受 Queue Size 約束,避免無界遍歷
2.7.5.3.2 裝置需求:間接描述子
對於指向間接表的那個描述子,裝置必須(MUST)忽略其 flags & VIRTQ_DESC_F_WRITE
(write-only)旗標。 裝置必須(MUST)能處理「零個或多個一般鏈結的描述子,接著是一個 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
驅動程式不能(MUST NOT)讓某個 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
:
- 驅動程式必須(MUST)將
flags
設為 0 或 1 - 驅動程式可以(MAY)把
flags
設為 1,以告知裝置「不需要通知」
若已協商 VIRTIO_F_EVENT_IDX
:
- 驅動程式必須(MUST)將
flags
設為 0 - 驅動程式可以(MAY)使用
used_event
告知裝置:在裝置把索引等於used_event
的項目寫入 Used Ring 之前(等價地說,直到 Used Ring 的idx
達到used_event + 1
之前),都不必發送通知 - 驅動程式必須(MUST)能處理來自裝置的虛假(spurious)通知
2.7.7.2 裝置需求:已用緩衝區通知的抑制
若未協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須(MUST)忽略 used_event 的值
- 當裝置把描述子索引寫入 Used Ring 之後:
- 若 flags 為 1,裝置不應該(SHOULD NOT)發送通知
- 若 flags 為 0,裝置必須(MUST)發送通知
若已協商 VIRTIO_F_EVENT_IDX
:
- 裝置必須(MUST)忽略 flags 的最低位
- 當裝置把描述子索引寫入 Used Ring 之後:
- 若 Used Ring 的 idx(決定該索引寫入位置的那個值)等於 used_event,裝置必須(MUST)發送通知
- 否則裝置不應該(SHOULD NOT)發送通知
注意:例如,若 used_event
為 0,使用 VIRTIO_F_EVENT_IDX 的裝置會在第一個緩衝區被使用後向驅動發送通知(接著在第 65536 個之後再次通知,依此類推)