Virtio-vsock 是一种轻量 guest 和 host 通信机制,vsock 最先被 vmware 公司提出,最初向内核提出的 commit 见 [1]。与 QEMU 基于 vhost 协议不同,Dragonball(与 Firecracker 的思路一致)实现了一种 hypervsock 技术,可以让 VMM 复用一个 UDS 与多个 vsock 连接通讯。
什么是 vsock?
Vsock 是一个 host 和 guest 的高性能数据交换机制,底层都是基于 virtio 实现的。相比于基于 virtio 的网络设备,vsock 的优势是不走网络协议栈,不需要多层帧封装,这是 vsock 高性能的原因。
Vsock 的实现方式和使用方式完全可以类比 TCP/IP。使用 Context ID(CID)作为虚拟机地址,可以类比 TCP,每一个虚拟机会被分配一个唯一的 CID,同一个虚拟机内部的不同 app 又通过端口(port)作为区分。
下图展示的是 vhost 协议的 vsock,kernel 负责数据的转发和路由。Vsock 支持双向数据传输:
- Host -> guest: HostApp0 连接到 CID = 3 和 port = 1025 的 app0,使用方式与使用其他 socket 一样,在 host 上启动一个 vsock server,参见这里。
- Guest -> host: App0 想要获取来自 HostApp0 的数据,则需要监听 CID = 2(
VMADDR_CID_HOST
)和 port = 1025,在 guest 中启动一个 vsock client 的代码,参见这里。
Vsock 有几个保留的 CID,如下表所示。
名称 | 值 | 描述 |
---|---|---|
VMADDR_CID_ANY |
-1 | 监听来自任何虚拟机的数据,一般来说是用在 guest 中,类比 TCP,可以理解为监听 0.0.0.0 。 |
VMADDR_CID_HYPERVISOR |
0 | Hypervisor 的 CID。 |
VMADDR_CID_LOCAL |
1 | 本虚拟机,可以理解为 127.0.0.1 。 |
VMADDR_CID_HOST |
2 | 表示 host 地址,用于 guest 监听来自/发送给 host 数据。 |
强烈建议看下 vsock notes,这里有 vsock 维护者给出的一些例子以及如何使用 socket 接口实现 vsock 通信。
Vsock on Dragonball 使用姿势
Vsock 在 QEMU 中都是借助 vhost 协议实现的。上面介绍的是一种类似于 vhost-net 的方式,把 virtqueue offload 到内核中。Vsock 还支持将数据 offload 到其他用户 app 中(使用 vhost-user 协议)。
Firecracker 则是在 VMM 中实现一个复用器(VsockMuxer),host app 通过 Unix Domain Socket(UDS) 建立与 muxer 的连接,由 muxer 将数据转发到 guest 中。Dragonball 也采用了 Firecracker 一样的方案向用户暴露 vsock 交互接口。
Host app 主动建立一个 vsock 连接的流程是:
如果启用了 vsock 设备,Dragonball 会对外暴露一个名为
kata.hvsock
的 socket。Host app 用标准 socket API 的
connect()
接口,建立与kata.hvsock
的连接。Host app 向 socket 连接发送
connect 1025\n
,表明想要与监听 CID 为 3,端口为 1025 的 guest app 建立 vsock 连接。- Dragonball 通过 virtqueue 发送一个 REQUEST 请求,表明想要建立连接。
- Guest 接收请求后,如果同意建立连接则会发送一个 RESPONSE 请求,此时一个 vsock 连接就建立好了。
Host app 与 guest app 交互数据。
数据发送的链路变成了,host app 通过 UDS 把数据发送给 Dragonball,Dragonball 从 stream 中读取数据,通过 vsock 连接转发给 guest app。数据的接收链路就是发送链路的反过程,这里就不再赘述了。
CID 的本意是在内核中区分 vsock 来自于/发送给哪个 VMM,在这种使用方式下,vsock 请求定点转发到 guest 中,因而 CID 可以被固定为 3(Tips: 3 以前的 CID 都已经被预定了)。
实现细节
了解了怎么用之后,在这个小节中我们将详细介绍 Dragonball 的 vsock 是如何工作的。事实上这部分的逻辑还是比较复杂的,本文也尽量在不贴代码的基础上把整个流程讲明白。
与 Vsock 相关的核心结构体,建议配合代码一起食用(commit 为 0046461
):
VsockDeviceMgr
负责 vsock 设备的管理工作,对外提供了诸如attach_devices()
插入 vsock 设备等方法。Vsock
是一个标准的 virtio 设备,它实现了VirtioDevice
trait,同时它实现了 virtio-vsock 的全部功能,持有 virtqueue、epoll handler、muxer 等组件。
VsockEpollHandler
是事件驱动的核心,当 TX queue 以及 host stream 的数据准备完毕时,都需要通过 epoll 通知 VMM 处理(BTW,这里说的 host stream 是指 UDS 建立的连接)。VsockMuxer
实现了VsockGenericMuxer
trait(通用复用器),一个 muxer 即可在 host 和 guest 之间建立起多个 vsock 连接,来自 host 的数据会转发到 muxer 中,muxer 会查询对应的连接并进一步通过 vsock 协议将数据转发到 guest 中。- 后端(backend)是指 host 层面上的后端,提供了一个类 socket 的接口,允许 host app 以更多花样与 muxer 建立连接,已支持的 backends 包括:
VsockUnixStreamBackend
允许通过 UDS 的方式与 muxer 建立连接(我们上面的例子都默认使用了这个 backend)。VsockTcpBackend
允许通过 TCP socket 的方式与 muxer 建立连接。VsockInnerBackend
通过 channel 实现了 in-process 的方式与 muxer 建立连接。HybridStream
暂时没研究,以后再说吧。
VsockConnection
是在 host app 与 muxer 建立连接后被生成的,host -> guest 发送数据的时候负责从 host stream 中读取数据并放到 vsock 包中,guest -> host 则负责将 vsock 包中的数据写入到 host stream 中。
设备插入
Kata Containers 的 runtime 负责调用 Dragonball API 接口插入 vsock 设备,VsockDeviceMgr
负责 vsock 设备管理,比如插入或者拔出。VsockDeviceMgr::attach_devices()
是插入 vsock 设备的核心函数,创建了一个 Vsock
结构体,并根据配置选择 backend。介绍这部分内容时,将以 VsockUnixStreamBackend
为蓝本介绍,因为 runtime-rs 默认会使用这种方式并在 /run/kata/{sid}/root/kata.hvsock
创建一个 socket 文件,其中 sid 表示 pod id。
Vsock
结构体实现了 vsock 设备,负责将 host stream 的数据通过 vsock 连接传递给 driver,将 driver 数据回传给 host stream 等。
pub struct Vsock<AS: GuestAddressSpace, M: VsockGenericMuxer = VsockMuxer> {
cid: u64,
queue_sizes: Arc<Vec<u16>>,
device_info: VirtioDeviceInfo,
subscriber_id: Option<SubscriberId>,
muxer: Option<M>,
phantom: PhantomData<AS>,
}
一些重要的字段:
muxer
是一个复用器,建立了 host app 与 guest app 的数据通路。device_info.epoll_manager
是一个 epoll 事件处理器,监控 vsock 设备相关的事件。
设备激活与事件监控
Vsock 设备就是通知机制和数据通路相互配合实现的数据传输。
Vsock
结构体实现了 VirtioDevice
trait 能够以 MMIO 的方式注册在 guest 中。VirtioDevice::activate()
是在 virtio 设备被激活的时候被调用,比如在 VMM 启动的时候。该方法初始化了 VsockEpollHandler
结构体,监控了 4 种事件(events),分别是:
RXQ_EVENT
: RX queue 事件。TXQ_EVENT
: TX queue 事件。EVQ_EVENT
: Event queue 事件。BACKEND_EVENT
: Backend 事件,最终调用了VsockMuxer::notify()
方法根据不同的 listeners 执行不同的流程:EpollListener::Connection
:表明 vsock 连接建立后,host app 和 guest app 交换数据。EpollListener::Backend
:表明 backend fd 有新数据到来。EpollListener::LocalStream
:表明准备建立 vsock 连接。EpollListener::PassFdStream
:暂时没研究,以后再说吧。
建立连接与数据传输
绑定 vsock 设备时,vsock device manager 会检查配置中 uds 路径是否为空,如果不为空则创建并添加 VsockUnixStreamBackend
到复用器中。
impl VsockDeviceMgr {
// ...
pub fn attach_devices(
&mut self,
ctx: &mut DeviceOpContext,
) -> std::result::Result<(), StartMicroVmError> {
// ...
for info in self.info_list.iter_mut() {
// ...
if let Some(uds_path) = info.config.uds_path.as_ref() {
// (1)
let unix_backend = VsockUnixStreamBackend::new(uds_path.clone())
.map_err(VirtioError::VirtioVsockError)
.map_err(StartMicroVmError::CreateVsockDevice)?;
// (2)
device
.add_backend(Box::new(unix_backend), true)
.map_err(VirtioError::VirtioVsockError)
.map_err(StartMicroVmError::CreateVsockDevice)?;
}
// ...
}
(1) 调用了 VsockUnixStreamBackend::new()
创建一个 unix stream backend,绑定了 host_sock
地址并返回了 VsockUnixStreamBackend
实例。
impl VsockUnixStreamBackend {
pub fn new(host_sock_path: String) -> Result<Self> {
// ...
let host_sock = UnixListener::bind(&host_sock_path)
.and_then(|sock| sock.set_nonblocking(true).map(|_| sock))
.map_err(VsockError::Backend)?;
// ...
Ok(VsockUnixStreamBackend {
host_sock,
host_sock_path,
})
}
}
(2) 调用 VsockMuxer::add_backend()
主要做了两件事情:(2.1) 将新创建的 host_sock
的 fd 添加到 epoll 监控中,有新连接进入的时候,VsockEpollHandler
的 backend 的EpollListener::Backend
listener 会被触发,在前面已经介绍过了。(2.2) 设置 backend type 到 backend 实例的映射。
impl VsockGenericMuxer for VsockMuxer {
fn add_backend(&mut self, backend: Box<dyn VsockBackend>, is_peer_backend: bool) -> Result<()> {
// backend_type 是 `VsockBackendType::UnixStream`
let backend_type = backend.r#type();
if self.backend_map.contains_key(&backend_type) {
return Err(Error::BackendRegistered(backend_type));
}
// (2.1)
self.add_listener(
backend.as_raw_fd(),
EpollListener::Backend(backend_type.clone()),
)?;
// (2.2)
self.backend_map.insert(backend_type.clone(), backend);
if is_peer_backend {
self.peer_backend = Some(backend_type);
}
Ok(())
}
}
此时,uds 已经创建完毕并被 muxer 监控,等待 host app 连接。
一个 host app 想要与 1024
端口的 peer 建立连接,第一件事情是连接 host_sock
(图中 (3)),host_sock
的 fd 的监控 epoll 会被激活。VsockMuxer::handle_event()
impl VsockMuxer {
// ...
fn handle_event(&mut self, fd: RawFd, event_set: epoll::Events) {
// ...
match self.listener_map.get_mut(&fd) {
// ...
Some(EpollListener::Backend(backend_type)) => {
if let Some(backend) = self.backend_map.get_mut(backend_type) {
// ...
backend
// (4)
.accept()
.map_err(Error::BackendAccept)
.and_then(|stream| {
// (5)
self.add_listener(
stream.as_raw_fd(),
EpollListener::LocalStream(stream),
)
})
.unwrap_or_else(|err| {
warn!("vsock: unable to accept local connection: {:?}", err);
});
} else {
error!("vsock: unsable to find specific backend {:?}", backend_type)
}
}
// ...
}
}
(4) 每种 backend 都会实现 VsockBackend
trait,它提供了一种类 socket 接口。我们重点关注的是 accept()
,它的功能是接受一个 socket 连接并返回一个 unix stream,这样 host app 和 vsock device 的数据通路就建立好了,当 unix stream 的 fd 有 EPOLLIN
事件触发,说明 host app 向 stream 写入了新数据。
pub trait VsockBackend: AsRawFd + Send {
fn accept(&mut self) -> std::io::Result<Box<dyn VsockStream>>;
fn connect(&self, dst_port: u32) -> std::io::Result<Box<dyn VsockStream>>;
fn r#type(&self) -> VsockBackendType;
fn as_any(&self) -> &dyn Any;
}
impl VsockBackend for VsockUnixStreamBackend {
fn accept(&mut self) -> std::io::Result<Box<dyn VsockStream>> {
// (4.1)
let (stream, _) = self.host_sock.accept()?;
stream.set_nonblocking(true)?;
Ok(Box::new(stream))
}
// ...
}
(4.1) VsockUnixStreamBackend::accept()
就是调用的 socket 的 accept()
,因为已经有连接进入了,这次调用不会被阻塞,调用的结果是产生了 stream,被 (5) 所使用。
(5) 将新创建好的 stream 的 fd 加入到监控中,事件类型是 EpollListener::LocalStream
。
此时,host app 与 muxer 之间的数据流已经建立好了,但是 vmm peer 与 guest peer 的 vsock 连接还没有建立,原因是 host app 还没有告知 vsock 设备要连接哪个端口。
接下来介绍的是 vsock 连接建立的过程,这部分主要的逻辑在 handle_event()
中。
fn handle_event(&mut self, fd: RawFd, event_set: epoll::Events) {
// ...
match self.listener_map.get_mut(&fd) {
// (6)
Some(EpollListener::LocalStream(_)) => {
// (7)
if let Some(EpollListener::LocalStream(mut stream)) = self.remove_listener(fd) {
// (8)
Self::read_local_stream_port(&mut stream)
.and_then(|read_port_result| match read_port_result {
ReadPortResult::Connect(peer_port) => {
// (9)
let local_port = self.allocate_local_port();
// (10)
self.add_connection(
ConnMapKey {
local_port,
peer_port,
},
VsockConnection::new_local_init(
stream,
uapi::VSOCK_HOST_CID,
self.cid,
local_port,
peer_port,
),
)
}
// ...
})
.unwrap_or_else(|err| {
info!("vsock: error adding local-init connection: {:?}", err);
})
}
}
// ...
}
}
(6) 表示 host app 向 stream 写入 CONNECT 1024\n
,表明它想要连接监听 1024 端口的 vsock peer。Muxer 监控将会捕捉 stream 的 fd 的事件并进入 handle_event()
的 LocalStream
arm 处理该事件。
(7) 表示将 stream 的 fd 从监控中移除,原因是 stream fd 对应的 listener 是 EpollListener::LocalStream
,这表示等待 stream 提供 vsock 端口信息,而现在 host app 已经提供了端口,该 listener 就可以被移除了。在添加后面的 vsock 连接时,这个 stream fd 将会被重新监控。
(8) 调用了 VsockMuxer::read_local_stream_port()
,它会从 stream 中读取并解析 CONNECT 1024\n
字符串,返回端口号。
(9) 调用 VsockMuxer::read_local_stream_port()
从本地找到一个随机的、没有使用的端口。
(10) 虽然只有一个标识,但是有三个部分:构建了一个 ConnMapKey
结构体,这个结构体包含本地端口和对端端口,唯一标识一个 vsock 连接。调用 VsockConnection::new_local_init()
创建了一个 VsockConnection
实例,代表一个 vsock 连接。
pub fn new_local_init(
stream: Box<dyn VsockStream>,
local_cid: u64,
peer_cid: u64,
local_port: u32,
peer_port: u32,
) -> Self {
Self {
local_cid,
peer_cid,
local_port,
peer_port,
stream,
state: ConnState::LocalInit,
tx_buf: TxBuf::default(),
fwd_cnt: Wrapping(0),
peer_buf_alloc: 0,
peer_fwd_cnt: Wrapping(0),
rx_cnt: Wrapping(0),
last_fwd_cnt_to_peer: Wrapping(0),
// (10.1)
pending_rx: PendingRxSet::from(PendingRx::Request),
expiry: None,
}
}
(10.1) new_local_init()
大部分都是填充 VsockConnection
结构体字段的,pending_rx
是需要特别关注的一个字段,存放了准备发向 peer 的数据。它在初始化的时候填入了一个初始值 PendingRx::Request
,对应的数据包是 VSOCK_OP_REQUEST
。VsockMuxer::recv_pkt()
检查 vsock connection 是否有 PendingRx::Request
请求(这部分将在稍后章节详细介绍),发送 vsock request 数据包并建立 vsock 连接,类似于 TCP 的三次握手建立连接(但是过程相对来说更简单)。
fn add_connection(&mut self, key: ConnMapKey, conn: VsockConnection) -> Result<()> {
// ...
// (10.2)
self.add_listener(
conn.as_raw_fd(),
EpollListener::Connection {
key,
evset: conn.get_polled_evset(),
backend: conn.stream.backend_type(),
},
)
.map(|_| {
// (10.3)
if conn.has_pending_rx() {
self.rxq.push(MuxerRx::ConnRx(key));
}
// (10.4)
self.conn_map.insert(key, conn);
})
}
(10.2) 这个 stream fd 之前被移除了 (7),现在 vsock 连接已经建立,stream 有新数据进入表明开始传输真正的数据了,所以 listener 变为了 EpollListener::Connection
。
(10.3) 由于在创建 VsockConnection
连接时加入了一个等待发送的请求 (10.1),所以这个连接是有等待发送数据的,这个连接就会被添加到 VsockMuxer::rxq
队列中。rxq
和 pending_rx
的关系如图所示。
(10.4) 之前提到了:ConnMapKey
可以唯一索引出一个 VsockConnection
实例。映射关系保存在 VsockMuxer::conn_map
。
小小总结一下,目前(1)stream fd 的 listener 已经被修改为 EpollListener::Connection
。(2)Vsock 连接已经创建完毕,request 数据包准备发往对端。
(11) Vsock 的数据包(packet)在什么情况下会被发送呢?有以下三种情况:
- Backend events: 后端事件,比如 host app 向 stream 写入数据的时候会触发这类事件。
- Rxqueue events: rxqueue 事件(rxqueue 事件是谁触发的?)。
- Txqueue events: txqueue 事件,原因是可能在接收数据的时候 backend 囤积了一些新数据。
以上三种方式最终都会调用 VsockEpollHandler::process_rx()
,该方法找到有数据等待发送的 connection,然后将数据包通过 virtqueue 发送给 peer。
fn process_rx(&mut self, mem: &AS::M) {
trace!("{}: epoll_handler::process_rx()", self.id);
let mut raise_irq = false;
{
// (11.1)
let rxvq = &mut self.config.queues[QUEUE_RX].queue_mut().lock();
loop {
// (11.2)
let mut iter = match rxvq.iter(mem) {
// ...
};
// (11.3)
if let Some(mut desc_chain) = iter.next() {
// (12)
let used_len = match VsockPacket::from_rx_virtq_head(&mut desc_chain) {
Ok(mut pkt) => {
// (13)
if self.muxer.recv_pkt(&mut pkt).is_ok() {
pkt.hdr().len() as u32 + pkt.len()
}
// ...
}
// ...
};
raise_irq = true;
// (11.4)
let _ = rxvq.add_used(mem, desc_chain.head_index(), used_len);
} else {
break;
}
}
}
if raise_irq {
// (11.5)
if let Err(e) = self.signal_used_queue(QUEUE_RX) {
error!("{}: failed to notify guest for RX queue, {:?}", self.id, e);
}
}
}
(11.1) Vsock 的 queues 不支持多队列,目前只支持以下三种:
QUEUE_RX (0x0)
: 类 TCP 接收队列(接收是相对于 driver 说的);QUEUE_TX (0x1)
: 类 TCP 发送队列;QUEUE_CFG (0x2)
: 控制队列。
这段代码从 config
中找到了 rxqueue(图上深绿色部分)。
(11.2) 创建了一个 avail ring 的迭代器,其类型是 AvailIter
,关于它是如何运作参考 [2] I/O 虚拟化(二):Virtio 协议代码走读。
(11.3) 遍历 avail ring 的 desc table 中的 descriptors,同样的细节参考 [2]。
(12) 从 descriptor(A.K.A DescriptorChain
)中取出一个 vsock 数据包(类型是 VsockPacket
)。
(13) 是 device 与 virtqueue 交互的关键步骤。在进入 muxer 的 recv_pkt()
介绍之前,先介绍一下 VsockChannel
trait。它提供了三个方法,recv_pkt()
表示数据是从 host 发向 guest,send_pkt()
表示数据从 guest 发向 host,has_pending_rx()
表示 channel 中是否有等待发送的数据。
pub trait VsockChannel {
fn recv_pkt(&mut self, pkt: &mut VsockPacket) -> Result<()>;
fn send_pkt(&mut self, pkt: &VsockPacket) -> Result<()>;
fn has_pending_rx(&self) -> bool;
}
VsockChannel
被 VsockMuxer
和 VsockConnection
结构体实现。还记得之前我们介绍过的 muxer 和 connection 之间的层次关系吗?VsockMuxer::has_pending_rx()
当其包含的任何一个 connection 有待发送数据的时候返回 true
,而 VsockConnection::has_pending_rx()
指一个 connection 有待发送的数据包时返回 true
。其他的操作类似,它们的差异在于操作层次。
impl VsockChannel for VsockMuxer {
// ...
}
impl VsockChannel for VsockConnection {
// ...
}
铺垫了这么多,现在可以仔细看看 VsockMuxer::recv_pkt()
到底做了什么。
fn recv_pkt(&mut self, pkt: &mut VsockPacket) -> VsockResult<()> {
// (13.1)
while let Some(rx) = self.rxq.peek() {
let res = match rx {
// ...
MuxerRx::ConnRx(key) => {
let mut conn_res = Err(VsockError::NoData);
let mut do_pop = true;
// (13.3)
self.apply_conn_mutation(key, |conn| {
// (14)
conn_res = conn.recv_pkt(pkt);
// (13.2)
do_pop = !conn.has_pending_rx();
});
if do_pop {
self.rxq.pop().unwrap();
}
conn_res
}
};
// (13.4)
if res.is_ok() {
// ...
return Ok(());
}
}
Err(VsockError::NoData)
}
(13.1) 从 rxq
中获得队首的 vsock connection,注意这里用的是 peek()
不是 pop()
,也就是 connection 不会从队列中移除。
(14) 调用了 VsockConnection::recv_pkt()
方法构造数据包。
fn recv_pkt(&mut self, pkt: &mut VsockPacket) -> VsockResult<()> {
// (14.1)
self.init_pkt(pkt);
// (14.2)
if self.pending_rx.remove(PendingRx::Rst) {
pkt.set_op(uapi::VSOCK_OP_RST);
return Ok(());
}
// (14.3)
if self.pending_rx.remove(PendingRx::Response) {
self.state = ConnState::Established;
pkt.set_op(uapi::VSOCK_OP_RESPONSE);
return Ok(());
}
// (14.4)
if self.pending_rx.remove(PendingRx::Request) {
self.expiry =
Some(Instant::now() + Duration::from_millis(defs::CONN_REQUEST_TIMEOUT_MS));
pkt.set_op(uapi::VSOCK_OP_REQUEST);
return Ok(());
}
// (14.5)
if self.pending_rx.remove(PendingRx::Rw) {
// ...
let buf = pkt.buf_mut().ok_or(VsockError::PktBufMissing)?;
// ...
match self.stream.read(&mut buf[..max_len]) {
Ok(read_cnt) => {
if read_cnt == 0 {
// ...
} else {
pkt.set_op(uapi::VSOCK_OP_RW).set_len(read_cnt as u32);
}
// ...
return Ok(());
}
// ...
};
}
// ...
Err(VsockError::NoData)
}
(14.1) 数据包 pkt
是在 process_rx()
中被创建的一个未被初始化的数据包。VsockConnection::init_pkt()
的作用是把 header 用 0 填充后,填入一些基本的控制信息,比如 local_cid
、peer_cid
等。
fn init_pkt<'a>(&self, pkt: &'a mut VsockPacket) -> &'a mut VsockPacket {
for b in pkt.hdr_mut() {
*b = 0;
}
pkt.set_src_cid(self.local_cid)
.set_dst_cid(self.peer_cid)
.set_src_port(self.local_port)
.set_dst_port(self.peer_port)
.set_type(uapi::VSOCK_TYPE_STREAM)
.set_buf_alloc(defs::CONN_TX_BUF_SIZE)
.set_fwd_cnt(self.fwd_cnt.0)
}
(14.2)/(14.3)/(14.4)/(14.5) 表示 vsock 连接的不同状态,分别是:
- 重置连接(
Rst
)。 - 响应连接(
Response
):peer 主动建连,local 同意建连。 - 请求建立连接(
Request
):local 主动建连,VsockConnection::new_local_init()
中默认填充的请求。 - 读写请求(
Rw
):连接建立后传输数据的请求。
(14.2)-(14.4) 都是控制类数据包,自身不携带任何数据,仅修改数据包的 op 字段,不再赘述。
(14.5) 从 stream 中读取 host app 传递过来的数据,如上图 (11) 所示。
总结,(14) 构造了一个完整的数据包,这个数据包将会被放置在 rxqueue 中传递给 peer。
(13.2) 如果一个连接已经没有要发送的数据的话,则从队列中移除。
(13.3) VsockMuxer::apply_conn_mutation()
在执行了 (13.2) 和 (14) 之后完成一些后置处理,比如连接状态从 ConnState::LocalInit
变为 ConnState::Established
,要向 stream 写入 OK 1024\n
字符串,告知 host app 连接到 1024 端口的 vsock 连接已经建立完毕。
(13.4) 只要有一个数据包构建好了,VsockMuxer::recv_pkt()
就会返回,执行 process_rx()
的接下来的流程。
(11.4) 步骤 (13) 把等待发送的数据都放到了 rxqueue 中,这里需要将使用完毕的 descriptor 添加到 used ring 中,细节参考 [2]。
(11.5) 向 guest 发送中断,告知 rxqueue 有新数据到来。
References
- https://github.com/torvalds/linux/commit/d021c344051af91f42c5ba9fdedc176740cbd238
- https://nxw.name/2023/virtqueue-code