如何使用 Linux vsock 实现快速虚拟机通信
How to use Linux vsock for fast VM communication

原始链接: https://popovicu.com/posts/how-to-use-linux-vsock-for-fast-vm-communication/

这个实验展示了Linux主机与虚拟机(VM)之间使用**vsock**进行通信,该技术专为高效的VM到主机(以及VM到VM)通信而设计,*无需*传统的TCP/IP协议栈或网络虚拟化。目标是在VM内建立一个gRPC服务,并使其可以从主机访问。 该项目使用**Bazel**构建,以保证可重复性,定义了一个简单的服务,能够将两个整数相加。Bazel用于生成C++ Protobuf和gRPC库。一个静态链接的服务器被构建用于在VM内运行,监听特定的vsock CID(上下文ID – 类似于IP地址)和端口。一个客户端,同样使用Bazel构建,连接到这个vsock地址来调用gRPC服务。 VM镜像使用`debootstrap`创建,并使用QEMU启动,配置为使用具有相应CID的vsock。在主机上运行客户端成功触发了VM内的加法服务,证明了功能性的RPC通信。这种方法能够以隔离的环境运行应用程序,可能结合不同的操作系统,并由于避免了网络虚拟化开销而提高了效率。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 如何使用 Linux vsock 实现快速 VM 通信 (popovicu.com) 5 分,由 mfrw 发表于 3 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

I’ve recently been experimenting with various ways to construct Linux VM images, but for these images to be practical, they need to interact with the outside world. At a minimum, they need to communicate with the host machine.

vsock is a technology specifically designed with VMs in mind. It eliminates the need for a TCP/IP stack or network virtualization to enable communication with or between VMs. At the API level, it behaves like a standard socket but utilizes a specialized addressing scheme.

In the experiment below, we’ll explore using vsock as the transport mechanism for a gRPC service running on a VM. We’ll build this project with Bazel for easy reproducibility. Check out this post if you need an intro to Bazel.

Table of contents

Open Table of contents

Motivation

There are many use cases for efficient communication between a VM and its host (or between multiple VMs). One simple reason is to create a hermetic environment within the VM and issue commands via RPC from the host. This is the primary driver for using gRPC in this example, but you can easily generalize the approach shown here to build far more complex systems.

GitHub repo

The complete repository is hosted here and serves as the source of truth for this experiment. While there may be minor inconsistencies between the code blocks below and the repository, please rely on GitHub as the definitive source.

Code breakdown

Let’s break down the code step by step:

External dependencies

Here are the external dependencies listed as Bazel modules:

bazel_dep(name = "rules_proto", version = "7.1.0")
bazel_dep(name = "rules_cc", version = "0.2.14")
bazel_dep(name = "protobuf", version = "33.1", repo_name = "com_google_protobuf")
bazel_dep(name = "grpc", version = "1.76.0.bcr.1")

This is largely self-explanatory. The protobuf repository is used for C++ proto-generation rules, and grpc provides the monorepo for Bazel rules to generate gRPC code for the C++ family of languages.

gRPC library generation

The following Bazel targets generate the necessary C++ Protobuf and gRPC libraries:

load("@rules_proto//proto:defs.bzl", "proto_library")
load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library")
load("@grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library")

proto_library(
    name = "vsock_service_proto",
    srcs = ["vsock_service.proto"],
)

cc_proto_library(
    name = "vsock_service_cc_proto",
    deps = [
        ":vsock_service_proto",
    ],
    visibility = [
        "//server:__subpackages__",
        "//client:__subpackages__",
    ],
)

cc_grpc_library(
    name = "vsock_service_cc_grpc",
    grpc_only = True,
    srcs = [
        ":vsock_service_proto",
    ],
    deps = [
        ":vsock_service_cc_proto",
    ],
    visibility = [
        "//server:__subpackages__",
        "//client:__subpackages__",
    ],
)

The protocol definition is straightforward:

syntax = "proto3";

package popovicu_vsock;

service VsockService {
  rpc Addition(AdditionRequest) returns (AdditionResponse) {}
}

message AdditionRequest {
  int32 a = 1;
  int32 b = 2;
}

message AdditionResponse {
  int32 c = 1;
}

It simply exposes a service capable of adding two integers.

Server implementation

The BUILD file is straightforward:

load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
    name = "server",
    srcs = [
        "server.cc",
    ],
    deps = [
        "@grpc//:grpc++",
        "//proto:vsock_service_cc_grpc",
        "//proto:vsock_service_cc_proto",
    ],
    linkstatic = True,
    linkopts = [
        "-static",
    ],
)

We want a statically linked binary to run on the VM. This choice simplifies deployment, allowing us to drop a single file onto the VM.

The code is largely self-explanatory:

#include <iostream>
#include <memory>
#include <string>

#include <grpc++/grpc++.h>
#include "proto/vsock_service.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using popovicu_vsock::VsockService;
using popovicu_vsock::AdditionRequest;
using popovicu_vsock::AdditionResponse;

// Service implementation
class VsockServiceImpl final : public VsockService::Service {
  Status Addition(ServerContext* context, const AdditionRequest* request,
                  AdditionResponse* response) override {
    int32_t result = request->a() + request->b();
    response->set_c(result);
    std::cout << "Addition: " << request->a() << " + " << request->b()
              << " = " << result << std::endl;
    return Status::OK;
  }
};

void RunServer() {
  // Server running on VM (guest)
  // vsock:-1:9999 means listen on port 9999, accept connections from any CID
  // CID -1 (VMADDR_CID_ANY) allows the host to connect to this VM server
  std::string server_address("vsock:3:9999");
  VsockServiceImpl service;

  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);

  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;

  server->Wait();
}

int main() {
  RunServer();
  return 0;
}

The only part requiring explanation is the server_address. The vsock: prefix indicates that we’re using vsock as the transport layer. gRPC supports various transports, including TCP/IP and Unix sockets.

The number 3 is the CID, or Context ID. This functions similarly to an IP address. Certain CIDs have special meanings. For instance, CID 2 represents the VM host itself; if the VM needs to connect to a vsock socket on the host, it targets CID 2. CID 1 is reserved for the loopback address. Generally, VMs are assigned CIDs starting from 3.

The 9999 is simply the port number, functioning just as it does in TCP/IP.

Client implementation

The BUILD file is, again, quite simple:

load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
    name = "client",
    srcs = [
        "client.cc",
    ],
    deps = [
        "@grpc//:grpc++",
        "//proto:vsock_service_cc_grpc",
        "//proto:vsock_service_cc_proto",
    ],
    linkstatic = True,
    linkopts = [
        "-static",
    ],
)

And the C++ code:

#include <iostream>
#include <memory>
#include <string>

#include <grpc++/grpc++.h>
#include "proto/vsock_service.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using popovicu_vsock::VsockService;
using popovicu_vsock::AdditionRequest;
using popovicu_vsock::AdditionResponse;

class VsockClient {
 public:
  VsockClient(std::shared_ptr<Channel> channel)
      : stub_(VsockService::NewStub(channel)) {}

  int32_t Add(int32_t a, int32_t b) {
    AdditionRequest request;
    request.set_a(a);
    request.set_b(b);

    AdditionResponse response;
    ClientContext context;

    Status status = stub_->Addition(&context, request, &response);

    if (status.ok()) {
      return response.c();
    } else {
      std::cout << "RPC failed: " << status.error_code() << ": "
                << status.error_message() << std::endl;
      return -1;
    }
  }

 private:
  std::unique_ptr<VsockService::Stub> stub_;
};

int main() {
  // Client running on host, connecting to VM server
  // vsock:3:9999 means connect to CID 3 (guest VM) on port 9999
  // CID 3 is an example - adjust based on your VM's actual CID
  std::string server_address("vsock:3:9999");

  VsockClient client(
      grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials()));

  int32_t a = 5;
  int32_t b = 7;
  int32_t result = client.Add(a, b);

  std::cout << "Addition result: " << a << " + " << b << " = " << result
            << std::endl;

  return 0;
}

Running it all together

Bazel shines here. You only need a working C++ compiler on your host system. Bazel automatically fetches and builds everything else on the fly, including the Protobuf compiler.

To get the statically linked server binary:

bazel build //server

Similarly, for the client:

bazel build //client

To create a VM image, I used debootstrap on an ext4 image, as described in this post on X:

This is a quick, albeit hacky, solution for creating a runnable Debian instance.

Next, I copied the newly built server binary to /opt within the image.

Now, the VM can be booted straight into the server binary as soon as the kernel runs:

 qemu-system-x86_64 -m 1G -kernel /tmp/linux/linux-6.17.2/arch/x86/boot/bzImage \
  -nographic \
  -append "console=ttyS0 init=/opt/server root=/dev/vda rw" \
  --enable-kvm \
  -smp 8 \
  -drive file=./debian.qcow2,format=qcow2,if=virtio -device vhost-vsock-pci,guest-cid=3

As shown in the last line, a virtual device is attached to the QEMU VM acting as vsock networking hardware, configured with CID 3.

The QEMU output shows:

[    1.581192] Run /opt/server as init process
[    1.889382] random: crng init done
Server listening on vsock:3:9999

To send an RPC to the server from the host, I ran the client binary:

bazel run //client

The output confirmed the result:

Addition result: 5 + 7 = 12

Correspondingly, the server output displayed:

Addition: 5 + 7 = 12

We have successfully invoked an RPC from the host to the VM!

Under the hood

I haven’t delved into the low-level system API for vsocks, as frameworks typically abstract this away. However, vsocks closely resemble TCP/IP sockets. Once created, they are used in the same way, though the creation API differs. Information on this is readily available online.

Conclusion

I believed it was more valuable to focus on a high-level RPC system over vsock rather than raw sockets. With gRPC, you can invoke a structured RPC on a server running inside the VM. This opens the door to running interesting applications in sealed, isolated environments, allowing you to easily combine different OSes (e.g., a Debian host and an Arch guest) or any platform supporting vsock. Additionally, gRPC allows you to write clients and servers in many different languages and technologies. This is achieved without network virtualization, resulting in increased efficiency.

I hope this was fun and useful to you as well.

Please consider following me on X and LinkedIn for further updates.

联系我们 contact @ memedata.com