QEMU 启动配置和spice新增通道

spice新增继承port的通道

spice新增通道关联的相关模块有4部分

  1. spice portcal 协议

在enmus.h文件增加通道的枚举

enum {
    SPICE_CHANNEL_MAIN = 1,
    SPICE_CHANNEL_DISPLAY,
    SPICE_CHANNEL_INPUTS,
    SPICE_CHANNEL_CURSOR,
    SPICE_CHANNEL_PLAYBACK,
    SPICE_CHANNEL_RECORD,
    SPICE_CHANNEL_TUNNEL,
    SPICE_CHANNEL_SMARTCARD,
    SPICE_CHANNEL_USBREDIR,
    SPICE_CHANNEL_PORT,
    SPICE_CHANNEL_WEBDAV,
 // 这里新增
    SPICE_END_CHANNEL
};

  1. spice-common 基础

spice-common有自动代码 生成器生成对应通道的解析函数,在spice.proto文件新增通道定义:

channel WebDAVChannel : PortChannel {
};

// 这里新增
//channel ??Channel : PortChannel {
//};

protocol Spice {
    MainChannel main = 1;
    DisplayChannel display;
    InputsChannel inputs;
    CursorChannel cursor;
    PlaybackChannel playback;
    RecordChannel record;
    TunnelChannel tunnel;
    SmartcardChannel smartcard;
    UsbredirChannel usbredir;
    PortChannel port;
    WebDAVChannel webdav;
    // 这里新增
};

  1. spice-server端

spice-server需要新增对于qemu启动时的设备名字对比,在向客户端启动main_handle_channel_list()的的通道type和id。

static red::shared_ptr<RedVmcChannel> red_vmc_channel_new(RedsState *reds, uint8_t channel_type)
{
    switch (channel_type) {
        case SPICE_CHANNEL_USBREDIR:
        case SPICE_CHANNEL_WEBDAV:
        case SPICE_CHANNEL_PORT:
        // 这里新增对应的新通道枚举 
            break;
        default:
            g_error("Unsupported channel_type for red_vmc_channel_new(): %u", channel_type);
            return red::shared_ptr<RedVmcChannel>();
    }

    int id = reds_get_free_channel_id(reds, channel_type);
    if (id < 0) {
        g_warning("Free ID not found creating new VMC channel");
        return red::shared_ptr<RedVmcChannel>();
    }

    return red::make_shared<RedVmcChannel>(reds, channel_type, id);
}

static const char *const channel_names[] = {
    [ SPICE_CHANNEL_MAIN     ] = "main",
    [ SPICE_CHANNEL_DISPLAY  ] = "display",
    [ SPICE_CHANNEL_INPUTS   ] = "inputs",
    [ SPICE_CHANNEL_CURSOR   ] = "cursor",
    [ SPICE_CHANNEL_PLAYBACK ] = "playback",
    [ SPICE_CHANNEL_RECORD   ] = "record",
    [ SPICE_CHANNEL_TUNNEL   ] = "tunnel",
    [ SPICE_CHANNEL_SMARTCARD] = "smartcard",
    [ SPICE_CHANNEL_USBREDIR ] = "usbredir",
    [ SPICE_CHANNEL_PORT     ] = "port",
    [ SPICE_CHANNEL_WEBDAV   ] = "webdav",
    // 这里新增对应的新通道枚举和名字
};

static int
spice_server_char_device_add_interface(SpiceServer *reds, SpiceBaseInstance *sin)
{
 //............................
    else if (strcmp(char_device->subtype, SUBTYPE_PORT) == 0) {
        if (strcmp(char_device->portname, "org.spice-space.webdav.0") == 0) {
            dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_WEBDAV);
        } else if (strcmp(char_device->portname, "org.spice-space.stream.0") == 0) {
            dev_state = stream_device_connect(reds, char_device);
        } // 这里新增对应的新通的注册设备名字对比,进行对应的连接
        else {
            dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_PORT);
        }
    }
 //............................
}
  1. spice-gtk端
// 首先类似webdavchannel继承portchannel

// 在通道创建新增对应的
SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
{
    SpiceChannel *channel;
    GType gtype = 0;

    g_return_val_if_fail(s != NULL, NULL);

    switch (type) {
    //.................................
    // 省略
    //.................................        
    case SPICE_CHANNEL_PORT:
        gtype = SPICE_TYPE_PORT_CHANNEL;
        break;
    // 对新增通道枚举判断
    case ??:
        gtype = ??;
        break;
    default:
        SPICE_DEBUG("unsupported channel kind: %s: %d",
                spice_channel_type_to_string(type), type);
        return NULL;
    }
    channel = SPICE_CHANNEL(g_object_new(gtype,
                                         "spice-session", s,
                                         "channel-type", type,
                                         "channel-id", id,
                                         NULL));
    // 该通道没有manager,手动对新增通道枚举判断进行连接
    if(?? == type){
        spice_channel_connect(channel);
    }                                   
    return channel;
}

Qmue 启动配置

在编译好的qemu build路径下执行:

sudo ./qemu-system-x86_64 -machine pc,accel=kvm -cpu host,vme=on,ss=on,vmx=on,pdcm=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,stibp=on,arch-capabilities=on,xsaveopt=on,pdpe1gb=on,abm=on,ibpb=on,ibrs=on,amd-stibp=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,hv-time=on,hv-relaxed=on,hv-vapic=on,hv-spinlocks=0x1fff,hv-vpindex=on -smp 4,sockets=1,cores=2,threads=2 -m 4G -device qemu-xhci -device virtio-serial-pci -netdev user,id=net0 -device virtio-net-pci,netdev=net0 -drive file=/home/w/share/win10.qcow2,if=virtio,format=qcow2 -boot c -display gtk -spice port=5901,disable-ticketing=on -device virtio-serial-pci,id=virtio-serial0,bus=pci.0 -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=fs_channnel,id=channel1,name=org.spice-space.fs.0 -chardev spiceport,name=org.spice-space.fs.0,id=fs_channnel -device ich9-usb-ehci1,id=usb -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,multifunction=on -chardev spicevmc,name=usbredir,id=usbredirchardev1 -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1

其中这段比较重要,是开启一些hyper加速的,未开启会导致大量kvm_mmio操作导致运行异常卡顿

-cpu host,vme=on,ss=on,vmx=on,pdcm=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,stibp=on,arch-capabilities=on,xsaveopt=on,pdpe1gb=on,abm=on,ibpb=on,ibrs=on,amd-stibp=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,hv-time=on,hv-relaxed=on,hv-vapic=on,hv-spinlocks=0x1fff,hv-vpindex=on

上面的Qemu虚拟的网络是一个虚拟内网地址,只可以向外访问无法访问反向访问,下面是开启桥接的方法:

nmcli con add type bridge con-name br0 ifname br0
nmcli con add type bridge-slave ifname enp1s0 master br0

nmcli con modify br0 ipv4.addresses 172.19.*.*/26
nmcli con modify br0 ipv4.gateway 172.19.0.254
nmcli con modify br0 ipv4.method manual

# 之后手动禁用之前使用enp1s0进行的连接,之后shh这台机器直接ssh到br0分配的ip

上面的命令在linux上建立桥接和转发。

sudo qemu-system-x86_64 -machine pc,accel=kvm -cpu host,vme=on,ss=on,vmx=on,pdcm=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,stibp=on,arch-capabilities=on,xsaveopt=on,pdpe1gb=on,abm=on,ibpb=on,ibrs=on,amd-stibp=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,hv-time=on,hv-relaxed=on,hv-vapic=on,hv-spinlocks=0x1fff,hv-vpindex=on -smp 4,sockets=1,cores=2,threads=2 -m 4G -device qemu-xhci -device virtio-serial-pci -device virtio-net-pci,netdev=user0,mac=fa:16:3e:82:49:95 -netdev bridge,id=user0,br=br0 -drive file=/home/w/code/qemu-src/win10.qcow2,if=virtio,format=qcow2 -boot c -display gtk -spice port=5901,disable-ticketing=on -device virtio-serial-pci,id=virtio-serial0,bus=pci.0 -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=fs_channnel,id=channel1,name=org.spice-space.fs.0 -chardev spiceport,name=org.spice-space.fs.0,id=fs_channnel

桥接启动(需要sudo)

Win7启动

使用win7的iso安装的系统因该缺少virtio的相关驱动,使用其他启动

sudo qemu-system-x86_64 -machine pc,accel=kvm -cpu host,vme=on,ss=on,vmx=on,pdcm=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,stibp=on,arch-capabilities=on,xsaveopt=on,pdpe1gb=on,abm=on,ibpb=on,ibrs=on,amd-stibp=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,hv-time=on,hv-relaxed=on,hv-vapic=on,hv-spinlocks=0x1fff,hv-vpindex=on -smp 4,sockets=1,cores=2,threads=2 -m 4G -device qemu-xhci -device virtio-serial-pci -netdev user,id=net0 -device virtio-net-pci,netdev=net0 -drive file=/home/w/code/qemu-src/win7.qcow2,format=qcow2 -spice port=5901,disable-ticketing=on -device virtio-serial-pci,id=virtio-serial0,bus=pci.0 -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=fs_channnel,id=channel1,name=org.spice-space.fs.0 -chardev spiceport,name=org.spice-space.fs.0,id=fs_channnel -cameradev proxy,id=camera0 -device usb-video,cameradev=camera0,terminal=camera  -chardev spicevmc,name=usbredir,id=usbredirchardev1 -device usb-redir,chardev=usbredirchardev1,cameradev=camera0,id=usbredirdev1

Client SPICE CHANNEL DISCONNECT PROCESS

First call spice_session_disconnect disconnect SpiceSession.

void spice_session_disconnect(SpiceSession *session) {
    SpiceSessionPrivate *s;

    g_return_if_fail(SPICE_IS_SESSION(session));

    s = session->priv;

    SPICE_DEBUG("session: disconnecting %u", s->disconnecting);
    if (s->disconnecting != 0)
        return;

    g_object_ref(session);
    s->disconnecting = spice_g_idle_add((GSourceFunc)session_disconnect_idle, session);
}

then call real disconnect in another thread:

static gboolean session_disconnect_idle(SpiceSession *self) {
    SpiceSessionPrivate *s = self->priv;

    session_disconnect(self, FALSE);
    s->disconnecting = 0;

    g_object_unref(self);

    return FALSE;
}

then disconnect all channel in this session, if main channel will call spice_channel_disconnect otherwise will call spice_session_channel_destroy:

static void session_disconnect(SpiceSession *self, gboolean keep_main) {
    SpiceSessionPrivate *s;

    s = self->priv;

    for (GList *l = s->channels; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        if (keep_main && channel == s->cmain) {
            spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);
        } else {
            spice_session_channel_destroy(self, channel);
        }
    }

    s->connection_id = 0;

    g_clear_pointer(&s->name, g_free);
    memset(s->uuid, 0, sizeof(s->uuid));

    spice_session_abort_migration(self);
}

in function spice_session_channel_destroy will first emit signals[SPICE_SESSION_CHANNEL_DESTROY] notice Qt then call spice_channel_disconnect

static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel)
{
    g_return_if_fail(SPICE_IS_SESSION(session));
    g_return_if_fail(SPICE_IS_CHANNEL(channel));

    SpiceSessionPrivate *s = session->priv;
    GList *l;

    if (s->migration_left)
        s->migration_left = g_list_remove(s->migration_left, channel);

    for (l = s->channels; l != NULL; l = l->next) {
        if (l->data == channel)
            break;
    }

    g_return_if_fail(l != NULL);

    if (channel == s->cmain) {
        CHANNEL_DEBUG(channel, "the session lost the main channel");
        s->cmain = NULL;
    }

    s->channels = g_list_delete_link(s->channels, l);

    g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_DESTROY], 0, channel);

    g_clear_object(&channel->priv->session);
    spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);

    /* Wait until the channel is properly freed so that we can emit a
     * 'disconnected' signal */
    s->channels_destroying++;
    g_object_weak_ref(G_OBJECT(channel), channel_finally_destroyed, g_object_ref(session));

    g_object_unref(channel);
}

in function spice_channel_disconnect will set c->has_error to true, spice_channel_coroutine loop will exit:

void spice_channel_disconnect(SpiceChannel *channel, SpiceChannelEvent reason)
{
    SpiceChannelPrivate *c;

    CHANNEL_DEBUG(channel, "channel disconnect %u", reason);

    g_return_if_fail(SPICE_IS_CHANNEL(channel));
    g_return_if_fail(channel->priv != NULL);

    c = channel->priv;

    if (c->state == SPICE_CHANNEL_STATE_UNCONNECTED)
        return;

    if (reason == SPICE_CHANNEL_SWITCHING)
        c->state = SPICE_CHANNEL_STATE_SWITCHING;

    c->has_error = TRUE; /* break the loop */

    if (c->state == SPICE_CHANNEL_STATE_MIGRATING) {
        c->state = SPICE_CHANNEL_STATE_READY;
    } else {
        SPICE_CHANNEL_GET_CLASS(channel)->channel_ready_close(channel);
        spice_channel_wakeup(channel, TRUE);
    }

    if (reason != SPICE_CHANNEL_NONE)
        g_signal_emit(G_OBJECT(channel), signals[SPICE_CHANNEL_EVENT], 0, reason);
}

in function spice_channel_coroutine will quit while loop spice_channel_iterate. then call spice_channel_reset reset channel and call spice_channel_delayed_unref free channel.

static void *spice_channel_coroutine(void *data)
{
    // .....
    while (spice_channel_iterate(channel))
        ;

cleanup:
    CHANNEL_DEBUG(channel, "Coroutine exit %s", c->name);

    spice_channel_reset(channel, FALSE);

    if (c->state == SPICE_CHANNEL_STATE_RECONNECTING ||
        c->state == SPICE_CHANNEL_STATE_SWITCHING) {
        g_warn_if_fail(c->event == SPICE_CHANNEL_NONE);
        if (channel_connect(channel, c->tls)) {
            g_object_unref(channel);
            return NULL;
        }

        c->event = SPICE_CHANNEL_ERROR_CONNECT;
    }

    spice_g_idle_add(spice_channel_delayed_unref, channel);
    /* Co-routine exits now - the SpiceChannel object may no longer exist,
       so don't do anything else now unless you like SEGVs */
    return NULL;
}

Then automatically dispose of unreferenced channels.

static gboolean spice_channel_delayed_unref(gpointer data)
{
    SpiceChannel *channel = SPICE_CHANNEL(data);
    SpiceChannelPrivate *c = channel->priv;
    gboolean was_ready = c->state == SPICE_CHANNEL_STATE_READY;

    CHANNEL_DEBUG(channel, "Delayed unref channel %p", channel);

    g_return_val_if_fail(c->coroutine.coroutine.exited == TRUE, FALSE);

    c->state = SPICE_CHANNEL_STATE_UNCONNECTED;

    if (c->event != SPICE_CHANNEL_NONE) {
        g_coroutine_signal_emit(channel, signals[SPICE_CHANNEL_EVENT], 0, c->event);
        c->event = SPICE_CHANNEL_NONE;
        g_clear_error(&c->error);
    }

    if (was_ready)
        g_coroutine_signal_emit(channel, signals[SPICE_CHANNEL_EVENT], 0, SPICE_CHANNEL_CLOSED);

    g_object_unref(channel);

    return FALSE;
}

USB Video device descriptor in QEMU

// top
static const USBDesc desc_video = {
    .id = {
        .idVendor          = USBVIDEO_VENDOR_NUM,
        .idProduct         = USBVIDEO_PRODUCT_NUM,
        .bcdDevice         = 0x0019,//0,
        .iManufacturer     = 0x00, //STRING_MANUFACTURER,
        .iProduct          = 0x02, //STRING_PRODUCT,
        .iSerialNumber     = 0x01, //STRING_SERIALNUMBER,
    },
    .full = &desc_device_full,
    .high = &desc_device_high,
    .str  = usb_video_stringtable,
};

struct USBDescDevice {
    uint16_t                  bcdUSB;
    uint8_t                   bDeviceClass;
    uint8_t                   bDeviceSubClass;
    uint8_t                   bDeviceProtocol;
    uint8_t                   bMaxPacketSize0;
    uint8_t                   bNumConfigurations;

    const USBDescConfig       *confs;
};

static const USBDescDevice desc_device_full = {
    .bcdUSB                        = 0x0100,
    .bDeviceClass                  = USB_CLASS_MISCELLANEOUS,
    .bDeviceSubClass               = 2,
    .bDeviceProtocol               = 1, /* Interface Association */
    .bMaxPacketSize0               = 8,
    .bNumConfigurations            = 1,
    .confs = (USBDescConfig[]) {
        {
            .bNumInterfaces        = 2,
            .bConfigurationValue   = 1,
            .iConfiguration        = 0, //STRING_CONFIG,
            .bmAttributes          = USB_CFG_ATT_ONE | USB_CFG_ATT_SELFPOWER,
            .bMaxPower             = 0x32,
            .nif_groups            = ARRAY_SIZE(desc_if_groups),
            .if_groups             = desc_if_groups,
            .nif                   = ARRAY_SIZE(desc_ifaces),
            .ifs                   = desc_ifaces,
        },
    },
};

typedef struct vcam_dev_descriptor
{
    SF_PACK_DEF_VAR(uint8_t, bLength);
    SF_PACK_DEF_VAR(uint8_t, bDescriptorType);
    SF_PACK_DEF_VAR(uint16_t, bcdUSB);
    SF_PACK_DEF_VAR(uint8_t, bDeviceClass);
    SF_PACK_DEF_VAR(uint8_t, bDeviceSubClass);
    SF_PACK_DEF_VAR(uint8_t, bDeviceProtocol);
    SF_PACK_DEF_VAR(uint8_t, bMaxPacketSize0);
    SF_PACK_DEF_VAR(uint16_t, idVendor);
    SF_PACK_DEF_VAR(uint16_t, idProduct);
    SF_PACK_DEF_VAR(uint16_t, bcdDevice);
    SF_PACK_DEF_VAR(uint8_t, iManufacturer);
    SF_PACK_DEF_VAR(uint8_t, iProduct);
    SF_PACK_DEF_VAR(uint8_t, iSerialNumber);
    SF_PACK_DEF_VAR(uint8_t, bNumConfigurations);
} vcam_dev_descriptor;

// level 1
static const USBDescDevice desc_device_full = {
    .bcdUSB                        = 0x0100,
    .bDeviceClass                  = USB_CLASS_MISCELLANEOUS,
    .bDeviceSubClass               = 2,
    .bDeviceProtocol               = 1, /* Interface Association */
    .bMaxPacketSize0               = 8,
    .bNumConfigurations            = 1,
    .confs = (USBDescConfig[]) {
        {
            .bNumInterfaces        = 2,
            .bConfigurationValue   = 1,
            .iConfiguration        = 0, //STRING_CONFIG,
            .bmAttributes          = USB_CFG_ATT_ONE | USB_CFG_ATT_SELFPOWER,
            .bMaxPower             = 0x32,
            .nif_groups            = ARRAY_SIZE(desc_if_groups),
            .if_groups             = desc_if_groups,
            .nif                   = ARRAY_SIZE(desc_ifaces),
            .ifs                   = desc_ifaces,
        },
    },
};

// USBDescConfig
struct USBDescConfig {
    uint8_t                   bNumInterfaces;
    uint8_t                   bConfigurationValue;
    uint8_t                   iConfiguration;
    uint8_t                   bmAttributes;
    uint8_t                   bMaxPower;

    /* grouped interfaces */
    uint8_t                   nif_groups;
    const USBDescIfaceAssoc   *if_groups;

    /* "normal" interfaces */
    uint8_t                   nif;
    const USBDescIface        *ifs;
};

typedef struct vcam_config_descriptor
{
    SF_PACK_DEF_VAR(uint8_t, bLength);
    SF_PACK_DEF_VAR(uint8_t, bDescriptorType);
    SF_PACK_DEF_VAR(uint16_t, wTotalLength);
    SF_PACK_DEF_VAR(uint8_t, bConfigurationValue);
    SF_PACK_DEF_VAR(uint8_t, iConfiguration);
    SF_PACK_DEF_VAR(uint8_t, bmAttributes);
    SF_PACK_DEF_VAR(uint8_t, MaxPower);
    SF_PACK_DEF_OBJS(vcam_other_descriptor, arrOtherDescs);
    SF_PACK_DEF_OBJS(vcam_interface, arrIFDescs);
} vcam_config_descriptor;