spice-gtk源码分析(九):USB设备重定向
spice-gtk支持将本地USB设备重定向到远程虚拟机,实现USB设备的透明使用。本文分析USB重定向的实现机制。
背景与目标
USB重定向允许用户将客户端的USB设备(如U盘、打印机、扫描仪等)连接到远程虚拟机,就像设备直接插在虚拟机上一样。spice-gtk通过以下组件实现:
- SpiceUsbDeviceManager:管理USB设备列表和自动连接
- SpiceUsbredirChannel:USB数据传输通道
- USB后端抽象:封装libusb操作
- ACL助手:处理Linux权限问题

SpiceUsbDeviceManager:设备管理
核心数据结构
// usb-device-manager.c
struct _SpiceUsbDeviceManagerPrivate {
SpiceSession *session; // 关联的会话
gboolean auto_connect; // 是否自动连接
gchar *auto_connect_filter; // 自动连接过滤规则
gchar *redirect_on_connect; // 连接时重定向规则
#ifdef USE_USBREDIR
SpiceUsbBackend *context; // USB后端上下文
struct usbredirfilter_rule *auto_conn_filter_rules; // 解析后的过滤规则
struct usbredirfilter_rule *redirect_on_connect_rules;
int auto_conn_filter_rules_count;
int redirect_on_connect_rules_count;
gboolean redirecting; // 是否正在重定向
#ifdef G_OS_WIN32
usbdk_api_wrapper *usbdk_api; // UsbDk API包装器
HANDLE usbdk_hider_handle; // UsbDk隐藏句柄
#endif
GPtrArray *devices; // USB设备列表
GPtrArray *channels; // USB重定向通道列表
#endif
};
设备热插拔处理
SpiceUsbDeviceManager监听USB设备的插拔事件:
// usb-device-manager.c
static void spice_usb_device_manager_hotplug_cb(void *user_data,
SpiceUsbDevice *dev,
gboolean added)
{
SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(user_data);
struct hotplug_idle_cb_args *args = g_malloc0(sizeof(*args));
// 通过idle回调切换到主线程处理
args->manager = g_object_ref(manager);
args->device = spice_usb_backend_device_ref(dev);
args->added = added;
g_idle_add(spice_usb_device_manager_hotplug_idle_cb, args);
}
static gboolean spice_usb_device_manager_hotplug_idle_cb(gpointer user_data)
{
struct hotplug_idle_cb_args *args = user_data;
SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(args->manager);
if (args->added) {
spice_usb_device_manager_add_dev(manager, args->device);
} else {
spice_usb_device_manager_remove_dev(manager, args->device);
}
spice_usb_backend_device_unref(args->device);
g_object_unref(manager);
g_free(args);
return FALSE;
}
static void spice_usb_device_manager_add_dev(SpiceUsbDeviceManager *manager,
SpiceUsbDevice *bdev)
{
SpiceUsbDeviceManagerPrivate *priv = manager->priv;
const UsbDeviceInformation *b_info = spice_usb_backend_device_get_info(bdev);
SpiceUsbDevice *device;
// 检查设备是否已存在
if (spice_usb_device_manager_find_device(manager,
b_info->bus,
b_info->address)) {
SPICE_DEBUG("device not added %d:%d %04x:%04x",
b_info->bus, b_info->address, b_info->vid, b_info->pid);
return;
}
device = spice_usb_device_new(bdev);
if (!device) {
return;
}
g_ptr_array_add(priv->devices, device);
// 如果启用自动连接,检查过滤规则并连接
if (priv->auto_connect) {
gboolean can_redirect, auto_ok;
can_redirect = spice_usb_device_manager_can_redirect_device(manager, device, NULL);
auto_ok = spice_usb_backend_device_check_filter(bdev,
priv->auto_conn_filter_rules,
priv->auto_conn_filter_rules_count) == 0;
if (can_redirect && auto_ok) {
spice_usb_device_manager_connect_device_async(manager, device, NULL,
spice_usb_device_manager_auto_connect_cb,
spice_usb_device_ref(device));
}
}
SPICE_DEBUG("device added %04x:%04x (%p)",
spice_usb_device_get_vid(device),
spice_usb_device_get_pid(device),
device);
g_signal_emit(manager, signals[DEVICE_ADDED], 0, device);
}
spice_usb_device_manager_add_dev()函数设计分析:
- 线程切换机制:热插拔回调通过
g_idle_add切换到主线程处理,这是因为libusb的回调函数在其自己的线程中执行。GLib的对象系统要求在主线程中操作,因此需要通过idle回调将操作延迟到主线程,确保线程安全。 - 自动连接的二重检查:函数先检查
can_redirect(检查是否有可用通道、设备是否已被占用等),再检查filter_check(检查过滤规则)。这种二重检查确保了只有在满足所有条件时才会自动连接设备,避免了不必要的连接尝试和错误处理。
过滤规则
spice-gtk使用过滤规则控制哪些设备可以自动连接:
// usb-device-manager.c
// 过滤规则格式:class,vendor,product,version,allow
// 多个规则用|分隔,规则内用,分隔
// 例如:"0x03,-1,-1,-1,0|-1,-1,-1,-1,1"
// 含义:拒绝HID类设备(0x03),允许其他所有设备
static void spice_usb_device_manager_set_property(GObject *gobject,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
// ...
case PROP_AUTO_CONNECT_FILTER: {
const gchar *filter = g_value_get_string(value);
#ifdef USE_USBREDIR
struct usbredirfilter_rule *rules;
int r, count;
// 解析过滤规则字符串
r = usbredirfilter_string_to_rules(filter, ",", "|", &rules, &count);
if (r) {
if (r == -ENOMEM) {
g_error("Failed to allocate memory for auto-connect-filter");
}
g_warning("Error parsing auto-connect-filter string, keeping old filter");
break;
}
SPICE_DEBUG("auto-connect filter set to %s", filter);
free(priv->auto_conn_filter_rules);
priv->auto_conn_filter_rules = rules;
priv->auto_conn_filter_rules_count = count;
#endif
g_free(priv->auto_connect_filter);
priv->auto_connect_filter = g_strdup(filter);
break;
}
// ...
}
过滤规则示例:
| 规则字符串 | 说明 |
|---|---|
"0x03,-1,-1,-1,0" | 拒绝所有HID类设备(键盘、鼠标) |
"-1,-1,-1,-1,1" | 允许所有设备 |
"0x03,-1,-1,-1,0|-1,-1,-1,-1,1" | 拒绝HID类,允许其他 |
"0x08,0x1234,0x5678,-1,1" | 允许特定VID/PID的存储设备 |
异步连接设备
spice-gtk使用异步API连接USB设备:
// usb-device-manager.c
void spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *manager,
SpiceUsbDevice *device,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(manager));
#ifdef USE_USBREDIR
GTask *task = g_task_new(G_OBJECT(manager), cancellable, callback, user_data);
g_task_set_task_data(task, device, NULL);
_set_redirecting(manager, TRUE);
_spice_usb_device_manager_connect_device_async(manager, device,
cancellable,
_connect_device_async_cb,
task);
#endif
}
static void
_spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *manager,
SpiceUsbDevice *device,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
SpiceUsbDeviceManagerPrivate *priv = manager->priv;
guint i;
task = g_task_new(manager, cancellable, callback, user_data);
// 检查设备是否已连接
if (spice_usb_device_manager_is_device_connected(manager, device)) {
g_task_return_new_error(task,
SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
"Cannot connect an already connected usb device");
goto done;
}
// 查找空闲的通道
for (i = 0; i < priv->channels->len; i++) {
SpiceUsbredirChannel *channel = g_ptr_array_index(priv->channels, i);
if (spice_usbredir_channel_get_device(channel)) {
continue; /* Skip already used channels */
}
// 异步连接到通道
spice_usbredir_channel_connect_device_async(channel,
device,
cancellable,
spice_usb_device_manager_channel_connect_cb,
task);
return;
}
// 没有空闲通道
g_task_return_new_error(task,
SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
_("No free USB channel"));
done:
g_object_unref(task);
}
SpiceUsbredirChannel:数据传输通道
通道状态管理
// channel-usbredir.c
enum SpiceUsbredirChannelState {
STATE_DISCONNECTED,
#ifdef USE_POLKIT
STATE_WAITING_FOR_ACL_HELPER, // 等待ACL助手完成权限设置
#endif
STATE_CONNECTED,
STATE_DISCONNECTING,
};
struct _SpiceUsbredirChannelPrivate {
SpiceUsbDevice *device; // 关联的USB设备
SpiceUsbBackend *context; // USB后端上下文
SpiceUsbBackendChannel *host; // USB后端通道
enum SpiceUsbredirChannelState state; // 当前状态
#ifdef USE_POLKIT
GTask *task; // 异步任务
SpiceUsbAclHelper *acl_helper; // ACL助手
#endif
GMutex device_connect_mutex; // 设备连接互斥锁
};
设备连接流程
连接USB设备需要经过多个步骤:
// channel-usbredir.c
void spice_usbredir_channel_connect_device_async(SpiceUsbredirChannel *channel,
SpiceUsbDevice *device,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
SpiceUsbredirChannelPrivate *priv = channel->priv;
#ifdef USE_POLKIT
const UsbDeviceInformation *info = spice_usb_backend_device_get_info(device);
#endif
GTask *task;
g_return_if_fail(SPICE_IS_USBREDIR_CHANNEL(channel));
g_return_if_fail(device != NULL);
CHANNEL_DEBUG(channel, "connecting device %04x:%04x (%p) to channel %p",
spice_usb_device_get_vid(device),
spice_usb_device_get_pid(device),
device, channel);
task = g_task_new(channel, cancellable, callback, user_data);
if (!priv->host) {
g_task_return_new_error(task,
SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
"Error libusb context not set");
goto done;
}
if (priv->state != STATE_DISCONNECTED) {
g_task_return_new_error(task,
SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
"Error channel is busy");
goto done;
}
priv->device = spice_usb_backend_device_ref(device);
#ifdef USE_POLKIT
// 如果不是模拟设备,需要设置ACL权限
if (info->bus != BUS_NUMBER_FOR_EMULATED_USB) {
priv->task = task;
priv->state = STATE_WAITING_FOR_ACL_HELPER;
priv->acl_helper = spice_usb_acl_helper_new();
// 禁止键盘抓取(避免权限对话框被拦截)
g_object_set(spice_channel_get_session(SPICE_CHANNEL(channel)),
"inhibit-keyboard-grab", TRUE, NULL);
// 异步打开ACL
spice_usb_acl_helper_open_acl_async(priv->acl_helper,
info->bus,
info->address,
cancellable,
spice_usbredir_channel_open_acl_cb,
channel);
return;
}
#endif
// 模拟设备直接连接
g_task_run_in_thread(task, _open_device_async_cb);
done:
g_object_unref(task);
}
spice_usbredir_channel_connect_device_async()函数设计分析:
- ACL权限处理流程:对于非模拟设备,函数首先进入
STATE_WAITING_FOR_ACL_HELPER状态,然后启动ACL助手进程。ACL助手通过polkit请求权限,可能需要用户交互(如输入密码)。只有在权限获取成功后,才会继续执行设备连接。 - inhibit-keyboard-grab的作用:在启动ACL助手之前,函数设置
inhibit-keyboard-grab为TRUE。这是因为如果SPICE客户端正在抓取键盘输入,polkit的权限对话框可能无法接收键盘输入,导致用户无法完成权限授权。禁止键盘抓取确保了权限对话框的正常工作。 - 模拟设备优化:模拟设备(如CD共享创建的虚拟USB设备)使用特殊的bus号
BUS_NUMBER_FOR_EMULATED_USB,可以直接跳过ACL检查,因为这些设备不需要系统权限。这种优化提高了虚拟设备的连接速度。
static void
_open_device_async_cb(GTask *task,
gpointer object,
gpointer task_data,
GCancellable *cancellable)
{
GError *err = NULL;
SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(object);
SpiceUsbredirChannelPrivate *priv = channel->priv;
spice_usbredir_channel_lock(channel);
if (!spice_usbredir_channel_open_device(channel, &err)) {
g_clear_pointer(&priv->device, spice_usb_backend_device_unref);
}
spice_usbredir_channel_unlock(channel);
if (err) {
g_task_return_error(task, err);
} else {
g_task_return_boolean(task, TRUE);
}
}
static gboolean spice_usbredir_channel_open_device(
SpiceUsbredirChannel *channel, GError **err)
{
SpiceUsbredirChannelPrivate *priv = channel->priv;
g_return_val_if_fail(priv->state == STATE_DISCONNECTED
#ifdef USE_POLKIT
|| priv->state == STATE_WAITING_FOR_ACL_HELPER
#endif
, FALSE);
// 将设备附加到USB后端通道
if (!spice_usb_backend_channel_attach(priv->host, priv->device, err)) {
if (*err == NULL) {
g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
"Error attaching device: (no error information)");
}
return FALSE;
}
priv->state = STATE_CONNECTED;
return TRUE;
}
### 数据收发处理
USB重定向通道处理双向数据传输:
```c
// channel-usbredir.c
/* coroutine context */
static void usbredir_handle_msg(SpiceChannel *c, SpiceMsgIn *in)
{
SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(c);
SpiceUsbredirChannelPrivate *priv = channel->priv;
int r = 0, size;
uint8_t *buf;
g_return_if_fail(priv->host != NULL);
// 处理压缩数据
if (spice_msg_in_type(in) == SPICE_MSG_SPICEVMC_COMPRESSED_DATA) {
SpiceMsgCompressedData *compressed_data_msg = spice_msg_in_parsed(in);
if (try_handle_compressed_msg(compressed_data_msg, &buf, &size)) {
/* uncompressed ok*/
} else {
buf = NULL;
r = USB_REDIR_ERROR_READ_PARSE;
}
} else { /* Regular SPICE_MSG_SPICEVMC_DATA msg */
buf = spice_msg_in_raw(in, &size);
}
spice_usbredir_channel_lock(channel);
if (r == 0)
// 读取来自虚拟机的USB数据
r = spice_usb_backend_read_guest_data(priv->host, buf, size);
if (r != 0 && priv->device != NULL) {
// 处理错误
SpiceUsbDevice *device = priv->device;
device_error_data err_data;
gchar *desc;
GError *err;
desc = spice_usb_device_get_description(device, NULL);
err = spice_usb_backend_get_error_details(r, desc);
g_free(desc);
CHANNEL_DEBUG(c, "%s", err->message);
err_data.channel = channel;
err_data.caller = coroutine_self();
err_data.device = spice_usb_backend_device_ref(device);
err_data.error = err;
spice_usbredir_channel_unlock(channel);
// 切换到主线程处理错误
g_idle_add(device_error, &err_data);
coroutine_yield(NULL);
spice_usb_backend_device_unref(err_data.device);
g_error_free(err);
} else {
spice_usbredir_channel_unlock(channel);
}
if (spice_msg_in_type(in) == SPICE_MSG_SPICEVMC_COMPRESSED_DATA) {
g_free(buf);
}
}
usbredir_handle_msg()函数设计分析:
- 压缩数据处理:函数首先检查消息类型是否为
SPICE_MSG_SPICEVMC_COMPRESSED_DATA,如果是压缩数据,则调用try_handle_compressed_msg进行解压。这种设计支持LZ4压缩,可以减少网络传输量,提高USB重定向的性能。 - 错误处理的异步性:当发生错误时,函数通过
g_idle_add将错误处理切换到主线程。这是因为错误处理可能涉及UI更新(如显示错误对话框),而协程上下文不适合直接操作UI。idle回调确保了错误处理在主线程中安全执行。 - 互斥锁保护:函数使用
spice_usbredir_channel_lock/unlock保护设备操作,防止多个协程同时访问USB设备。这种保护机制确保了USB操作的原子性,避免了竞态条件。
// 写入数据到虚拟机
G_GNUC_INTERNAL
int spice_usbredir_write(SpiceUsbredirChannel *channel, uint8_t *data, int count)
{
SpiceMsgOut *msg_out;
#ifdef USE_LZ4
// 尝试压缩(如果支持)
if (try_write_compress_LZ4(channel, data, count)) {
spice_usb_backend_return_write_data(channel->priv->host, data);
return count;
}
#endif
// 发送未压缩数据
msg_out = spice_msg_out_new(SPICE_CHANNEL(channel),
SPICE_MSGC_SPICEVMC_DATA);
spice_marshaller_add_by_ref_full(msg_out->marshaller, data, count,
usbredir_free_write_cb_data, channel);
spice_msg_out_send(msg_out);
return count;
}
## USB后端抽象
spice-gtk通过`SpiceUsbBackend`抽象层封装libusb操作:
```c
// usb-backend.h
typedef struct _SpiceUsbBackend SpiceUsbBackend;
typedef struct _SpiceUsbBackendChannel SpiceUsbBackendChannel;
// USB后端API
SpiceUsbBackend *spice_usb_backend_new(GError **error);
void spice_usb_backend_delete(SpiceUsbBackend *context);
// 热插拔注册
gboolean spice_usb_backend_register_hotplug(SpiceUsbBackend *be,
void *user_data,
usb_hot_plug_callback proc,
GError **error);
void spice_usb_backend_deregister_hotplug(SpiceUsbBackend *be);
// 设备操作
SpiceUsbDevice *spice_usb_backend_device_ref(SpiceUsbDevice *dev);
void spice_usb_backend_device_unref(SpiceUsbDevice *dev);
gconstpointer spice_usb_backend_device_get_libdev(const SpiceUsbDevice *dev);
const UsbDeviceInformation* spice_usb_backend_device_get_info(const SpiceUsbDevice *dev);
gboolean spice_usb_backend_device_isoch(SpiceUsbDevice *dev);
// 通道操作
SpiceUsbBackendChannel *spice_usb_backend_channel_new(SpiceUsbBackend *context,
SpiceUsbredirChannel *usbredir_channel);
void spice_usb_backend_channel_delete(SpiceUsbBackendChannel *ch);
gboolean spice_usb_backend_channel_attach(SpiceUsbBackendChannel *ch,
SpiceUsbDevice *dev,
GError **error);
void spice_usb_backend_channel_detach(SpiceUsbBackendChannel *ch);
int spice_usb_backend_read_guest_data(SpiceUsbBackendChannel *ch, uint8_t *data, int count);
void spice_usb_backend_return_write_data(SpiceUsbBackendChannel *ch, void *data);
后端抽象的优势:
- 平台无关:隐藏libusb的平台特定实现细节
- 易于测试:可以替换为模拟后端进行测试
- 扩展性:支持Windows的UsbDk等特殊实现
ACL助手:权限管理
在Linux系统上,访问USB设备需要适当的权限。spice-gtk使用polkit通过ACL助手获取权限:
// usb-acl-helper.c
struct _SpiceUsbAclHelperPrivate {
GTask *task;
GIOChannel *in_ch; // 标准输入通道
GIOChannel *out_ch; // 标准输出通道
GCancellable *cancellable;
gulong cancellable_id;
};
void spice_usb_acl_helper_open_acl_async(SpiceUsbAclHelper *self,
guint16 bus,
guint8 address,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
SpiceUsbAclHelperPrivate *priv = self->priv;
gchar *argv[4];
GError *error = NULL;
GPid pid;
gint stdin_fd, stdout_fd;
// 启动polkit助手进程
argv[0] = SPICE_CLIENT_GLIB_USB_ACL_HELPER;
argv[1] = g_strdup_printf("%u", bus);
argv[2] = g_strdup_printf("%u", address);
argv[3] = NULL;
// 使用g_spawn_async_with_pipes创建进程
if (!g_spawn_async_with_pipes(NULL, argv, NULL,
G_SPAWN_DO_NOT_REAP_CHILD |
G_SPAWN_STDERR_TO_DEV_NULL,
NULL, NULL, &pid,
&stdin_fd, &stdout_fd, NULL, &error)) {
g_task_return_error(priv->task, error);
return;
}
// 设置IO通道监视输出
priv->out_ch = g_io_channel_unix_new(stdout_fd);
g_io_channel_set_encoding(priv->out_ch, NULL, NULL);
g_io_channel_set_buffered(priv->out_ch, FALSE);
g_io_add_watch(priv->out_ch, G_IO_IN | G_IO_HUP, cb_out_watch, self);
// 等待polkit对话框完成
// ...
}
ACL助手流程:
- 启动
spice-client-glib-usb-acl-helper进程 - 进程通过polkit请求权限
- 显示权限对话框(如果需要)
- 设置USB设备节点的ACL权限
- 返回结果给spice-gtk
CD共享:虚拟光驱
spice-gtk支持将ISO镜像或物理CD作为USB设备共享:
// usb-device-manager.c
gboolean
spice_usb_device_manager_create_shared_cd_device(SpiceUsbDeviceManager *manager,
gchar *filename,
GError **err)
{
#ifdef USE_USBREDIR
SpiceUsbDeviceManagerPrivate *priv = manager->priv;
CdEmulationParams cd_params = {
.filename = filename,
.delete_on_eject = 1,
};
// 创建模拟的USB CD设备
return create_emulated_cd(priv->context, &cd_params, err);
#else
g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
_("USB redirection support not compiled in"));
return FALSE;
#endif
}
CD共享实现:
- 使用USB Mass Storage Device (MSD)协议模拟USB CD设备
- 支持ISO镜像文件和物理CD设备
- 通过SCSI命令处理CD读取操作
- 设备VID/PID使用Red Hat的特殊值(0x2b23:0xCDCD)
信号与事件
SpiceUsbDeviceManager信号
| 信号名 | 参数 | 说明 |
|---|---|---|
device-added | device | USB设备插入 |
device-removed | device | USB设备拔出 |
auto-connect-failed | device, error | 自动连接失败 |
device-error | device, error | 设备错误 |
属性
| 属性名 | 类型 | 说明 |
|---|---|---|
auto-connect | gboolean | 是否自动连接新设备 |
auto-connect-filter | gchar* | 自动连接过滤规则 |
redirect-on-connect | gchar* | 连接时重定向规则 |
free-channels | gint | 可用通道数量 |
总结
spice-gtk的USB重定向实现具有以下特点:
- 设备管理:通过SpiceUsbDeviceManager统一管理USB设备列表和连接状态
- 过滤机制:支持灵活的过滤规则控制自动连接行为
- 异步操作:所有设备连接操作都是异步的,避免阻塞UI
- 权限处理:通过polkit和ACL助手处理Linux权限问题
- 数据压缩:支持LZ4压缩减少网络传输量
- CD共享:支持将ISO镜像作为USB设备共享
- 错误处理:完善的错误处理和恢复机制
这种设计使得spice-gtk能够提供可靠的USB重定向功能,同时保持良好的用户体验。
更多推荐



所有评论(0)