Skip to content

Commit 9ac9512

Browse files
committed
update ch8/sec2.2
1 parent 37e437e commit 9ac9512

File tree

1 file changed

+97
-35
lines changed

1 file changed

+97
-35
lines changed

source/chapter8/2device-driver-2.rst

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ virtio设备的基本组成要素如下:
7676

7777
**描述符表**
7878

79-
描述符表用来存放I/O传输请求信息,即是设备驱动程序与设备进行交互的缓冲区,由 ``Queue Size`` 个Descriptor(描述符)组成。描述符中包括buffer的物理地址 -- addr字段,数据buffer的长度 -- len字段,可以链接到 ``next Descriptor`` 的next指针并形成描述符链。buffer所在物理地址空间需要操作系统或运行时在初始化时分配好,并在后续由设备驱动程序在其中填写IO传输相关的命令/数据,或者是设备返回I/O操作的结果。多个描述符(I/O操作命令,I/O操作数据块,I/O操作的返回结果)形成的描述符链可以表示一个完整的I/O操作请求。
79+
描述符表用来指向I/O传输请求的信息,即是设备驱动程序与设备进行交互的缓冲区(buffer),由 ``Queue Size`` 个Descriptor(描述符)组成。描述符中包括buffer的物理地址 -- addr字段,buffer的长度 -- len字段,可以链接到 ``next Descriptor`` 的next指针(用于把多个描述符链接成描述符链)。buffer所在物理地址空间需要操作系统或运行时在初始化时分配好,并在后续由设备驱动程序在其中填写IO传输相关的命令/数据,或者是设备返回I/O操作的结果。多个描述符(I/O操作命令,I/O操作数据块,I/O操作的返回结果)形成的描述符链可以表示一个完整的I/O操作请求。
8080

8181
**可用环**
8282

@@ -93,73 +93,135 @@ virtio设备的基本组成要素如下:
9393

9494
.. https://rootw.github.io/2019/09/firecracker-virtio/
9595
96-
对于设备驱动和外设之间采用virtio协议进行交互的原理如下图所示
96+
对于设备驱动和外设之间采用virtio机制(也可称为协议)进行交互的原理如下图所示
9797

9898

9999
.. image:: virtio-cpu-device-io2.png
100100
:align: center
101101
:name: virtio-cpu-device-io2
102102

103103

104-
设备驱动与外设可以共同访问内存,内存中存在一个称为环形队列的数据结构,该队列可分成由I/O请求组成的请求队列(Available Ring)和由I/O响应组成的响应队列(Used Ring)。一个IO的处理过程可以分成如下四步:
104+
设备驱动与外设可以共同访问约定的物理内存。这些物理内存将保存具体的I/O请求和I/O响应。当设备驱动程序想要向设备发送命令/数据时,它会在约定的物理内存中填充命令/数据,各个物理内存块所在的起始地址和大小信息放在描述符表的描述符中,再把这些描述符链接在一起,形成描述符链。
105105

106-
1. 用户进程发出I/O请求时,设备驱动将I/O请求放入请求队列(Available Ring)中并通知设备;
107-
2. 设备收到通知后从请求队列中取出I/O请求并在内部进行实际处理;
108-
3. 设备将IO处理完成后,将结果作为I/O响应放入响应队列(Used Ring)并以中断方式通知CPU;
109-
4. 设备驱动从响应队列中取出I/O处理结果并最终返回给用户进程。
106+
而描述符链的起始描述符的索引信息会放入一个称为环形队列的数据结构,该队列可分为包含I/O请求的起始描述符的项组成的请求队列(可用环 Available Ring)和由包含I/O响应的描述符的项组成的响应队列(已用环 Used Ring)。
107+
108+
一个用户进程发起的I/O操作的处理过程大致可以分成如下四步:
109+
110+
1. 用户进程发出I/O请求,经过层层下传给到设备驱动程序,设备驱动程序将I/O请求的信息位置放入请求队列中并通过某种通知机制(如写某个设备寄存器)通知设备;
111+
2. 设备收到通知后,从请求队列中的位置描述取出I/O请求并在内部进行实际I/O处理;
112+
3. 设备完成I/O处理或出错后,将结果作为I/O响应的位置放入响应队列(已用环 Used Ring)并以某种通知机制(如外部中断)通知CPU;
113+
4. 设备驱动根据响应队列(已用环 Used Ring)中的位置描述取出I/O处理结果并最终返回给用户进程。
110114

111115

112116
.. image:: vring.png
113117
:align: center
114118
:name: vring
115119

116120

117-
当virtio设备驱动程序想要向virtio设备发送数据时,它会填充Descriptor Table中的一项或几项链接在一起,形成描述符链,并将描述符索引写入Available Ring中,然后它通知virtio设备(向queue notify寄存器写入队列index)。当virtio设备收到通知,并完成I/O操作后,virtio设备将描述符索引写入Used Ring中并发送中断,让操作系统进行进一步处理并回收描述符。
118-
119121
virtio设备驱动的执行流程
120122
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121123

122124
**设备的初始化**
123125

124-
1. 重启设备状态,状态位写入 0
125-
2. 设置状态为 ACKNOWLEDGE,guest(driver)端当前已经识别到了设备
126-
3. 设置状态为 Driver,guest 知道如何驱动当前设备
127-
4. 设备特定的安装和配置:特征位的协商,virtqueue 的安装,读写设备专属的配置空间等
128-
5. 设置状态为 Driver_OK 或者 Failed(如果中途出现错误)
129-
6. 当前设备初始化完毕,可以进行配置和使用
126+
操作系统通过某种方式(设备发现,基于设备树的查找等)找到virtio设备后,设备驱动程序进行设备初始化的常规步骤如下所示:
127+
128+
1. 重启设备状态,设置设备状态域为0
129+
2. 设置设备状态域为 ``ACKNOWLEDGE`` ,表明当前已经识别到了设备
130+
3. 设置设备状态域为 ``DRIVER`` ,表明驱动程序知道如何驱动当前设备
131+
4. 进行设备特定的安装和配置,包括协商特征位,建立virtqueue,访问设备配置空间等, 设置设备状态域为 ``FEATURES_OK``
132+
5. 设置设备状态域为 ``DRIVER_OK`` 或者 ``FAILED``(如果中途出现错误)
133+
134+
注意,上述的步骤不是必须都要做到的,但最终需要设置设备状态域为 ``DRIVER_OK`` ,这样才能通过设备驱动程序正常访问设备。
135+
136+
137+
**虚拟队列的相关操作**
138+
139+
虚拟队列的相关操作包括两个部分:向设备提供新的可用缓冲区(可用环),以及处理设备使用的已用缓冲区(已用环)。 比如,最简单的virtio网络设备具有两个虚拟队列:发送虚拟队列和接收虚拟队列。驱动程序将发出(设备可读)的数据包添加到传输虚拟队列中,然后在数据包被设备使用后将其释放。接收(设备可写)缓冲区被添加到接收虚拟队列中,缓冲区中的数据包会被设备驱动程序处理。
140+
141+
这两部分的具体操作如下:
130142

131-
**设备的安装和配置**
143+
**向设备提供缓冲区**
132144

133-
设备操作包括两个部分:driver提供 buffers 给设备,处理 device使用过的 buffers。
145+
驱动程序给设备的虚拟队列提供缓冲区,具体步骤如下所示:
134146

135-
**初始化 virtqueue**
136147

137-
该部分代码的实现具体为:
148+
1. 驱动程序将缓冲区放入描述符表中的空闲描述符中,并根据需要把多个描述符进行链接,形成一个描述符链;
149+
2. 驱动程序将描述符链头的索引放入可用环的下一个环条目中;
150+
3. 如果可以进行批处理(batching),则可以重复执行步骤1和2;
151+
4. 驱动程序执行适当的内存屏障操作,以确保设备能看到更新的描述符表和可用环;
152+
5. 根据添加到可用环中的描述符链头的数量,增加available idx;
153+
6. 驱动程序执行适当的内存屏障操作,以确保在检查通知前更新available idx;
154+
7. 驱动程序会将"有可用的缓冲区"的通知发送给设备。
138155

139-
1. 选择 virtqueue 的索引,写入 Queue Select 寄存器
140-
2. 读取 queue size 寄存器获得 virtqueue 的可用数目
141-
3. 分配并清零连续物理内存用于存放 virtqueue。把内存地址除以 4096 写入 Queue Address 寄存器
142156

143-
**Guest 向设备提供 buffer**
157+
**将缓冲区放入描述符表**
144158

145-
1. 把 buffer 添加到 description table 中,填充 addr,len,flags
146-
2. 更新 available ring head
147-
3. 更新 available ring 中的 index
148-
4. 通知 device,通过写入 virtqueue index 到 Queue Notify 寄存器
159+
缓冲区用于表示一个I/O请求的具体内容,由零个或多个设备可读/可写的物理地址连续的内存块组成(一般前面是可读的内存块,后续跟着可写的内存块)。我们把构成缓冲区的内存块称为缓冲区元素,把缓冲区映射到描述符表中以形成描述符链的具体步骤:
149160

150-
**Device 使用 buffer 并填充 used ring**
161+
对于每个缓冲区元素b:
151162

152-
device 端使用 buffer 后填充 used ring 的过程如下:
163+
1. 获取下一个空闲描述符表条目d
164+
2. 将d.addr设置为b的的起始物理地址
165+
3. 将d.len设置为b的长度
166+
4. 如果b是设备可写的,则将d.flags设置为 ``VIRTQ_DESC_F_WRITE`` ,否则设置为0
167+
5. 如果b之后还有一个缓冲元素c:
168+
5.1 将d.next设置为下一个空闲描述符元素的索引
169+
5.2 将d.flags中的VIRTQ_DESC_F_NEXT位置1
153170

154-
1. 从描述符表格(descriptor table)中找到 available ring 中添加的 buffers,映射内存
155-
2. 从分散-聚集的 buffer 读取数据
156-
3. 取消内存映射,更新 ring[idx]中的 id 和 len 字段
157-
4. 更新响应队列 vring_used 中的 idx
158-
5. 如果设置了使能中断,产生中断并通知操作系统描述符已经使用
159-
6. 设备驱动从响应队列 vring_used 中取出IO处理结果并返回给应用程序
171+
**更新可用环**
160172

173+
描述符链头是上述步骤中的第一个d,即。描述符表条目的索引,指向缓冲区的第一部分。一个驱动程序实现可以执行以下的伪码操作(假定在与小端字节序之间进行适当的转换)来更新可用环:
174+
175+
.. code-block:: Rust
176+
177+
avail.ring[avail.idx % qsz] = head;
161178
162179
180+
但是,通常驱动程序可以在更新idx之前添加许多描述符链 (这时它们对于设备是可见的),因此通常要对驱动程序已添加的数目进行计数:
181+
182+
.. code-block:: Rust
183+
184+
avail.ring[(avail.idx + added++) % qsz] = head;
185+
186+
idx总是递增,并在到达65536后又回到0:
187+
188+
.. code-block:: Rust
189+
190+
avail.idx += added;
191+
192+
一旦驱动程序更新了 ``avail.idx`` ,这表示描述符及其它指向的缓冲区能够被设备看到。这样设备就可以访问驱动程序创建的描述符链和它们指向的内存。驱动程序必须在idx更新之前执行合适的内存屏障操作,以确保设备看到最新的buffer内容。
193+
194+
**通知设备**
195+
196+
设备一般都是挂接在总线上,所以通知设备的实际方法是特定于总线的,且通常开销比较大。但在virtio的虚拟场景下,我们不用太担心性能问题。驱动程序必须在设备读取标志或 avail_event之前执行适当的内存屏障,以避免丢失通知。
197+
198+
**从设备接收使用的缓冲区**
199+
200+
一旦设备使用(可以是读或写,取决于设备和虚拟队列的属性)了描述符所指向的缓冲区,设备便会向驱动程序发送 ``已用缓冲区通知(used buffer notification)`` 。
201+
202+
为了优化性能,驱动程序可以在处理已用环(used ring)时禁用 ``已用缓冲区通知`` ,但是要注意在清空环和重新启用通知之间丢失通知的问题。这通常可以通过在重新启用通知后重新检查更多的 ``已用缓冲区`` 的方法来解决,相关的伪代码如下所示:
203+
204+
.. code-block:: Rust
205+
206+
virtq_disable_used_buffer_notifications(vq);
207+
208+
for (;;) {
209+
if (vq.last_seen_used != le16_to_cpu(virtq.used.idx)) {
210+
virtq_enable_used_buffer_notifications(vq);
211+
mb();
212+
213+
if (vq.last_seen_used != le16_to_cpu(virtq.used.idx))
214+
break;
215+
216+
virtq_disable_used_buffer_notifications(vq);
217+
}
218+
219+
struct virtq_used_elem *e = virtq.used.ring[vq.last_seen_used%vsz];
220+
process_buffer(e);
221+
vq.last_seen_used++;
222+
}
223+
224+
163225
基于MMIO方式的virtio设备
164226
-----------------------------------------
165227

0 commit comments

Comments
 (0)