Dragonball VMM 侧 Vsock 设计与实现

一种源自于 FireCrack 的 Hypervsock 实现方案

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_REQUESTVsockMuxer::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 队列中。rxqpending_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;
}

VsockChannelVsockMuxerVsockConnection 结构体实现。还记得之前我们介绍过的 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_cidpeer_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

  1. https://github.com/torvalds/linux/commit/d021c344051af91f42c5ba9fdedc176740cbd238
  2. https://nxw.name/2023/virtqueue-code
All rights reserved
Except where otherwise noted, content on this page is copyrighted.