【VIRT.0x01】Qemu - II:VNC 模块源码分析

本文最后更新于:2023年10月27日 晚上

vnc,🐕都不用

0x00.一切开始之前

VNC 即 Virtual Network Computing,是基于RFB(Remote Frame Buffer)协议进行通信的远程桌面协议,与 telnet、ssh 等相比,VNC 最大的特点便是支持图形化了,用户可以看到远程机器的图形化界面,且能使用键盘与鼠标进行输入

Qemu 虚拟机同样支持通过 VNC只需要指定 -vnc 参数便能够建立 VNC Server,从而使得远程用户可以通过 VNC 连接到 Qemu 虚拟机上

本篇主要是对 Qemu 中 VNC 实现的源码分析,同时也会夹杂着部分 Qemu 显示相关的分析,源码版本 Qemu 7.0.0

0x01. qemu_init_displays() - 显示设备初始化

我们都知道 Qemu 的入口函数是 softmmu/main.c 中的 qemu_main(),在其中会调用到 sotftmmuvl.c 中的 qemu_init() 函数进行 Qemu 的初始化工作,包括一系列的参数解析、设备初始化等

1
2
3
4
5
6
7
8
9
10
11
12
13
#undef main
#define main qemu_main

//...

int main(int argc, char **argv, char **envp)
{
qemu_init(argc, argv, envp);
qemu_main_loop();
qemu_cleanup();

return 0;
}

本篇我们主要关注与 VNC 相关的内容,注意到在 qemu_init() 的末尾会调用到 qemu_init_displays() 进行显示初始化的工作

1
2
3
4
5
6
7
8
void qemu_init(int argc, char **argv, char **envp)
{
//...

qemu_init_displays();

//...
}

这个函数比较简短,首先是调用 init_displaystate()qemu_display_init() 对虚拟机本地的显示设备进行初始化,之后才是使用 qemu_opts_foreach 宏来遍历参数中与 vnc 相关的配置,最后调用到的是 vnc_init_func()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void qemu_init_displays(void)
{
DisplayState *ds;

/* 初始化本地显示 */
ds = init_displaystate();
qemu_display_init(ds, &dpy);

/* 必须在终端初始化后, 由 SDL 库更改信号的 handlers */
os_setup_signal_handling();

/* 初始化远程显示 */
#ifdef CONFIG_VNC
qemu_opts_foreach(qemu_find_opts("vnc"),
vnc_init_func, NULL, &error_fatal);
#endif

if (using_spice) {
qemu_spice.display_init();
}
}

〇、显示设备相关结构体

在 Qemu 当中有着多个与显示设备相关的结构,他们之间的关系如下图所示:

image.png

本图来自于这个ppt

I、QemuConsole - 单个控制台实例

在开始分析之前我们先介绍一个新的结构体:QemuConsole,一个该结构体实例在 Qemu 中表示一个控制台(console)实例,对应着一个特定的 VGA 设备与一组特定的输入设备。在 Qemu 当中主要有两类控制台:图形化控制台与字符型控制台。

该结构体定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct QemuConsole {
Object parent;

int index;
console_type_t console_type; // 控制台类型
DisplayState *ds; // 对应的显示设备
DisplaySurface *surface;
DisplayScanout scanout;
int dcls;
DisplayGLCtx *gl;
int gl_block;
QEMUTimer *gl_unblock_timer;
int window_id;

/* 图形化控制台状态. */
Object *device; // 对应的图形化设备
uint32_t head;
QemuUIInfo ui_info;
QEMUTimer *ui_timer; // 对应的 Timer
const GraphicHwOps *hw_ops; // 硬件操作函数
void *hw;

/* 字符控制台状态 */
int width;
int height;
int total_height;
int backscroll_height;
int x, y;
int x_saved, y_saved;
int y_displayed;
int y_base;
TextAttributes t_attrib_default; /* 默认字符属性 */
TextAttributes t_attrib; /* 当前活动字符属性 */
TextCell *cells;
int text_x[2], text_y[2], cursor_invalidate;
int echo;

int update_x0;
int update_y0;
int update_x1;
int update_y1;

enum TTYState state;
int esc_params[MAX_ESC_PARAMS];
int nb_esc_params;

Chardev *chr;
/* 先进先出的按键输入 */
Fifo8 out_fifo;
CoQueue dump_queue;

QTAILQ_ENTRY(QemuConsole) next;
};

对于图形化设备相关的操作,主要通过函数表 hw_ops 来完成,对于纯字符型显示设备而言,该函数表为 text_console_ops,对于普通的 VGA 设备而言则为 vga_ops,我们在后面会看到这一点。

QemuConsole 的创建主要通过 new_console() 来完成,在 Qemu 中有一个全局的 QemuConsole 变量 consoles,每当创建一个新的 QemuConsole 后就会通过尾插法插入到这个全局的 QemuConsole 链表中

1
2
static QTAILQ_HEAD(, QemuConsole) consoles =
QTAILQ_HEAD_INITIALIZER(consoles);

什么是 console?狭义地说,console 最初指的就是一个设备的控制台,包含了显示设备与基本输入设备 ,与 terminal 不同,console 通常是机器自带的,而 terminal 则往往指的是需要我们通过串口进行连接的外部设备
不过随着时代的发展,现在对于 console 与 terminal 之间概念的定义区别也逐渐趋于模糊,现在的机器大都不再有实体的 console,而采用软件模拟的方式,例如在 Linux 中定义了 6 个 virtual terminal(可以使用 ctrl + f1 ~ f6 进行切换),而当我们向 /dev/console 输入时,则会输出到当前的 virtual terminal 上;而在一些其他的类 UNIX 系统中,console 则往往被固定为第一个 virtual terminal

II、DisplayState - 显示设备总状态

在 Qemu 当中使用一个 DisplayState 表示显示设备的总状态,该结构体定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct DisplayState {
QEMUTimer *gui_timer; // 对应更新的 timer
uint64_t last_update; // 上次更新时间
uint64_t update_interval;
bool refreshing;
bool have_gfx; // 是否有图形化显示
bool have_text; // 是否有纯文本显示

QLIST_HEAD(, DisplayChangeListener) listeners; // 各个 Display 的 Listener 的链表
};

static DisplayState *display_state; // 全局的 DisplayState

通常而言,一个 DisplayState 可以对应着多个 QemuConsole,因为其可以对应着多个 VGA 设备,因此相应地其可以有着多个 DisplayChangeListener 来监视多个 Display 设备的更改,多个 DisplayChangeListener 之间连成一个链表

III、DisplayChangeListener - 监视单个显示设备的更改

DisplayChangeListener 结构体用于监视单个显示设备的更改并进行相关操作,因此一个 DisplayChangeListener 应当与一个特定的 QemuConsole 相关联

通常而言一个 QemuConsole 可以有着多个 DisplayChangeListener(例如一个 QemuConsole 可以对应有着一个 VNC 的 DCL + 一个本地虚拟终端的 DCL)

该结构体定义于 include/ui/console.h 中,如下:

1
2
3
4
5
6
7
8
struct DisplayChangeListener {
uint64_t update_interval;
const DisplayChangeListenerOps *ops;
DisplayState *ds;
QemuConsole *con;

QLIST_ENTRY(DisplayChangeListener) next;
};

其成员 ops 为一个 DisplayChangeListenerOps 函数表,该函数表中包含大量的函数指针,用以进行显示相关的操作(例如 dpy_refresh 指针用于刷新显示, gfx_hw_update 指针用于进行显卡硬件相关更新,我们在后面会看到这一点)

一、init_displaystate() - 遍历所有的 QemuConsole 并加入 qom tree,初始化字符型 console

init_displaystate() 主要用于对 QemuConsole 进行初始化的工作,该函数定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* 由 main() 调用, 在创建 QemuConsoles 之后
* 在初始化 ui (sdl/vnc/...) 之前.
*/
DisplayState *init_displaystate(void)
{
gchar *name;
QemuConsole *con;

get_alloc_displaystate();
QTAILQ_FOREACH(con, &consoles, next) {
if (con->console_type != GRAPHIC_CONSOLE &&
con->ds == NULL) {
text_console_do_init(con->chr, display_state);
}

/* 在这里连接上 qom tree (而不在 new_console()),
* 在所有的 QemuConsoles 都被创建后,其顺序与序号
* 都将不再更改 */
name = g_strdup_printf("console[%d]", con->index);
object_property_add_child(container_get(object_get_root(), "/backend"),
name, OBJECT(con));
g_free(name);
}

return display_state;
}

该函数主要是遍历全局的 QemuConsole 链表并将其加入到 qom tree 中,若非图形化的 console 则还会调用 text_console_do_init() 进行初始化工作,主要是将其设为标准的 80*24 的字符显示设备,并将其 hw_ops 设为 text_console_ops,其 hw 则指向 QemuConsole 自身

二、qemu_display_init() - 调用对应类型 QemuDisplay 的 init 函数进行初始化

该函数同样定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
void qemu_display_init(DisplayState *ds, DisplayOptions *opts)
{
assert(opts->type < DISPLAY_TYPE__MAX);
if (opts->type == DISPLAY_TYPE_NONE) {
return;
}
assert(dpys[opts->type] != NULL);
dpys[opts->type]->init(ds, opts);
}

这里的 dpys 是一个 QemuDisplay 类型的全局数组,定义于 ui/console.c 中,表示 Qemu 所支持的所有显示类型,我们可以通过 qemu_display_register() 将显示类型注册到该数组中:

1
2
3
4
5
6
7
static QemuDisplay *dpys[DISPLAY_TYPE__MAX];

void qemu_display_register(QemuDisplay *ui)
{
assert(ui->type < DISPLAY_TYPE__MAX);
dpys[ui->type] = ui;
}

QemuDisplay 结构体定义于 ui/console.h 中,表示 Qemu 所支持的单个显示类型,主要就是类型 + 初始化的函数指针,如下:

1
2
3
4
5
struct QemuDisplay {
DisplayType type;
void (*early_init)(DisplayOptions *opts);
void (*init)(DisplayState *ds, DisplayOptions *opts);
};

不过经笔者调试通常情况下不特定指定的话都会是 DISPLAY_TYPE_NONE

0x02. vnc_init_func() - 初始化单个 VNC Server

在一个 Qemu 实例当中,我们通常只会使用到一个 VGA 设备(也可以多个),不过我们可以同时启动多个 VNC Server,例如我们可以附加启动参数 -vnc yourip:0 -vnc yourip:1,此时 Qemu 就会在 5700 与 5701 端口上启动两个 VNC 服务器,而每个 VNC Server 的启动都是通过 vnc_init_func() 来完成的

当我们只有一个 VGA 设备输出时,其输出会被同时更新给多个 VNC Client,此时多个 VNC Client 所获取到的画面是相同的

vnc_init_func() 定义于 ui/vnc.c 中,比较简短,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int vnc_init_func(void *opaque, QemuOpts *opts, Error **errp)
{
Error *local_err = NULL;
char *id = (char *)qemu_opts_id(opts);

assert(id);
vnc_display_init(id, &local_err);
if (local_err) {
error_propagate(errp, local_err);
return -1;
}
vnc_display_open(id, &local_err);
if (local_err != NULL) {
error_propagate(errp, local_err);
return -1;
}
return 0;
}

主要做了两件事:

  • vnc_display_init():初始化一个 VncDisplay 实例
  • vnc_display_open():启动一个 VNC Server

vnc_display_open() 主要就是单纯的创建一个普通的 server,以及一些和 VNC 协议具体细节相关的部分,故我们接下来主要分析 vnc_display_init()

〇、VNC 相关结构体

I、VncDisplay - 单个 VNC Server 实例

该结构体定义于 ui/vnc.h 中,表示单个 VNC Server 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct VncDisplay
{
QTAILQ_HEAD(, VncState) clients;
int num_connecting;
int num_shared;
int num_exclusive;
int connections_limit;
VncSharePolicy share_policy;
QIONetListener *listener;
QIONetListener *wslistener;
DisplaySurface *ds;
DisplayChangeListener dcl;
kbd_layout_t *kbd_layout;
int lock_key_sync;
QEMUPutLEDEntry *led;
int ledstate;
QKbdState *kbd;
QemuMutex mutex;

QEMUCursor *cursor;
int cursor_msize;
uint8_t *cursor_mask;

struct VncSurface guest; /* guest visible surface (aka ds->surface) */
pixman_image_t *server; /* vnc server surface */
int true_width; /* server surface width before rounding up */

const char *id;
QTAILQ_ENTRY(VncDisplay) next;
bool is_unix;
char *password;
time_t expires;
int auth;
int subauth; /* Used by VeNCrypt */
int ws_auth; /* Used by websockets */
int ws_subauth; /* Used by websockets */
bool lossy;
bool non_adaptive;
bool power_control;
QCryptoTLSCreds *tlscreds;
QAuthZ *tlsauthz;
char *tlsauthzid;
#ifdef CONFIG_VNC_SASL
VncDisplaySASL sasl;
#endif

AudioState *audio_state;
};

我们主要关注这几个成员变量:

  • clients:连接到该服务器上的所有客户端,每个客户端为一个 VncClient 实例,多个 VncClient 实例间形成一个链表
  • ds
  • dcl:该 VNC Server 所监听的 QemuConsole 的监听器,当对应的 QemuConsole 发生更改时便会调用 dcl->ops 中对应函数指针
  • next:多个 VncDisplay 之间互相连接形成一个链表

同时存在着一个全局变量 vnc_displays,Qemu 中所有的 VncDisplay 都挂载在该链表上:

1
2
static QTAILQ_HEAD(, VncDisplay) vnc_displays =
QTAILQ_HEAD_INITIALIZER(vnc_displays);

II、VncState - 单个 VNC 连接(VNC Client)

该结构体定义于 ui/vnc.h 中,表示连接到特定 VNC Server 上的单个 VNC Client 实例,其中包含客户端的各种信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
struct VncState
{
uint64_t magic;
QIOChannelSocket *sioc; /* The underlying socket */
QIOChannel *ioc; /* The channel currently used for I/O */
guint ioc_tag;
gboolean disconnecting;

DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT], VNC_DIRTY_BITS);
uint8_t **lossy_rect; /* Not an Array to avoid costly memcpy in
* vnc-jobs-async.c */

VncDisplay *vd;
VncStateUpdate update; /* Most recent pending request from client */
VncStateUpdate job_update; /* Currently processed by job thread */
int has_dirty;
uint32_t features;
int absolute;
int last_x;
int last_y;
uint32_t last_bmask;
size_t client_width; /* limited to u16 by RFB proto */
size_t client_height; /* limited to u16 by RFB proto */
VncShareMode share_mode;

uint32_t vnc_encoding;

int major;
int minor;

int auth;
int subauth; /* Used by VeNCrypt */
char challenge[VNC_AUTH_CHALLENGE_SIZE];
QCryptoTLSSession *tls; /* Borrowed pointer from channel, don't free */
#ifdef CONFIG_VNC_SASL
VncStateSASL sasl;
#endif
bool encode_ws;
bool websocket;

#ifdef CONFIG_VNC
VncClientInfo *info;
#endif

/* Job thread bottom half has put data for a forced update
* into the output buffer. This offset points to the end of
* the update data in the output buffer. This lets us determine
* when a force update is fully sent to the client, allowing
* us to process further forced updates. */
size_t force_update_offset;
/* We allow multiple incremental updates or audio capture
* samples to be queued in output buffer, provided the
* buffer size doesn't exceed this threshold. The value
* is calculating dynamically based on framebuffer size
* and audio sample settings in vnc_update_throttle_offset() */
size_t throttle_output_offset;
Buffer output;
Buffer input;
/* current output mode information */
VncWritePixels *write_pixels;
PixelFormat client_pf;
pixman_format_code_t client_format;
bool client_be;

CaptureVoiceOut *audio_cap;
struct audsettings as;

VncReadEvent *read_handler;
size_t read_handler_expect;

bool abort;
QemuMutex output_mutex;
QEMUBH *bh;
Buffer jobs_buffer;

/* Encoding specific, if you add something here, don't forget to
* update vnc_async_encoding_start()
*/
VncTight *tight;
VncZlib zlib;
VncHextile hextile;
VncZrle *zrle;
VncZywrle zywrle;

Notifier mouse_mode_notifier;

QemuClipboardPeer cbpeer;
QemuClipboardInfo *cbinfo;
uint32_t cbpending;

QTAILQ_ENTRY(VncState) next;
};

在同一个 VncDisplay (VNC Server)下的所有 VncState(VNC Client)连接成一个链表

III、VncJob - 单个 VNC 连接单次需要更新的图像信息

VncJob 结构体用来表示单个 VNC 连接(VncState)单次需要更新的图像信息,定义于 ui/vnc.h 中,如下:

1
2
3
4
5
6
7
struct VncJob
{
VncState *vs;

QLIST_HEAD(, VncRectEntry) rectangles;
QTAILQ_ENTRY(VncJob) next;
};

在单次的图像更新当中,单个矩形区域具体的的更新信息使用 VncRectEntry 结构体表示,单个 VncJob 中可以有多个 VncRectEntry,他们之间形成一个单向链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct VncRect
{
int x;
int y;
int w;
int h;
};

struct VncRectEntry
{
struct VncRect rect;
QLIST_ENTRY(VncRectEntry) next;
};

多个 VncJob 之间也构成一个单向链表,最终挂载到一个 VncJobQueue 结构体上,该结构体定义于 ui/vnc-jobs.c

1
2
3
4
5
6
7
8
9
struct VncJobQueue {
QemuCond cond;
QemuMutex mutex;
QemuThread thread;
bool exit;
QTAILQ_HEAD(, VncJob) jobs;
};

typedef struct VncJobQueue VncJobQueue;

同时我们存在着一个全局的 VncJobQueue,默认情况下我们会将 VncJob 挂载到上面,同时 vnc worker thread(负责将图像信息发送给 client 的线程)也是主要依赖于这个全局的 queue

1
2
3
4
5
/*
* We use a single global queue, but most of the functions are
* already reentrant, so we can easily add more than one encoding thread
*/
static VncJobQueue *queue;

一、vnc_display_init()

该函数主要作用便是初始化一个 VncDisplay 结构体,定义于 ui/vnc.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void vnc_display_init(const char *id, Error **errp)
{
VncDisplay *vd;

if (vnc_display_find(id) != NULL) {
return;
}
vd = g_malloc0(sizeof(*vd));

vd->id = strdup(id);
QTAILQ_INSERT_TAIL(&vnc_displays, vd, next); // 加入到全局链表中

QTAILQ_INIT(&vd->clients);
vd->expires = TIME_MAX;

if (keyboard_layout) { // 初始化键盘布局
trace_vnc_key_map_init(keyboard_layout);
vd->kbd_layout = init_keyboard_layout(name2keysym,
keyboard_layout, errp);
} else {
vd->kbd_layout = init_keyboard_layout(name2keysym, "en-us", errp);
}

if (!vd->kbd_layout) {
return;
}

vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE;
vd->connections_limit = 32;

qemu_mutex_init(&vd->mutex);
vnc_start_worker_thread(); // 启动 VNC 的 worker 线程

vd->dcl.ops = &dcl_ops; // 设置 DisplayChangeListener 的 ops
register_displaychangelistener(&vd->dcl);
vd->kbd = qkbd_state_init(vd->dcl.con);
}

大致流程如下:

  • 创建一个 VncDisplay,加入到全局链表中
  • 初始化键盘布局
  • 启动 VNC worker thread(若未启动),由 worker thread 将图像信息发送给 client
  • 设置该 VncDisplay 对应的 DisplayChangeListener 的 ops 为 dcl_ops
  • 调用 register_displaychangelistener() 注册该 VncDisplay 对应的 DisplayChangeListener

I、VNC worker thread - 将图像更新发送给客户端

vnc worker thread 是 Qemu 中 VNC 服务中的一个重要的线程,其用以持续地处理挂载在全局的 VncJobQueue 上的 VncJob,并将图像更新数据发送给 vnc 客户端

vnc_display_init() 中创建 VncDisplay 实例时,其还会调用 vnc_start_worker_thread() 启动 vnc worker thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static bool vnc_worker_thread_running(void)
{
return queue; /* Check global queue */
}

void vnc_start_worker_thread(void)
{
VncJobQueue *q;

if (vnc_worker_thread_running())
return ;

q = vnc_queue_init();
qemu_thread_create(&q->thread, "vnc_worker", vnc_worker_thread, q,
QEMU_THREAD_DETACHED);
queue = q; /* Set global queue */
}

该线程的本体便是 vnc_worker_thread 函数,主要就是一个重复调用 vnc_worker_thread_loop 的大循环:

1
2
3
4
5
6
7
8
9
10
static void *vnc_worker_thread(void *arg)
{
VncJobQueue *queue = arg;

qemu_thread_get_self(&queue->thread);

while (!vnc_worker_thread_loop(queue)) ;
vnc_queue_clear(queue);
return NULL;
}

vnc_worker_thread_loop 定义如下,该函数会一直等待到全局的 VncJobQueue 的 VncJob 链表非空,单次调用处理一个 VncJob:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
static int vnc_worker_thread_loop(VncJobQueue *queue)
{
VncJob *job;
VncRectEntry *entry, *tmp;
VncState vs = {};
int n_rectangles;
int saved_offset;

vnc_lock_queue(queue); // 等待全局 queue 不为空
while (QTAILQ_EMPTY(&queue->jobs) && !queue->exit) {
qemu_cond_wait(&queue->cond, &queue->mutex);
}
/* Here job can only be NULL if queue->exit is true */
job = QTAILQ_FIRST(&queue->jobs); // 取下第一个 job
vnc_unlock_queue(queue);
assert(job->vs->magic == VNC_MAGIC);

if (queue->exit) {
return -1;
}

vnc_lock_output(job->vs); // 锁上 VncState
if (job->vs->ioc == NULL || job->vs->abort == true) {
// io channel 为空或 abort 为真,断开连接
vnc_unlock_output(job->vs);
goto disconnected;
}
if (buffer_empty(&job->vs->output)) {
// 这里的 output 为 Buffer 类型,类似于 iovec,包含着长度与一个指向 buffer 的指针
// buffer_move_empty(*to, *from) 的作用就是释放 to 的 buffer,将 from 的 buffer 给到 to 的 buffer
/*
* Looks like a NOP as it obviously moves no data. But it
* moves the empty buffer, so we don't have to malloc a new
* one for vs.output
*/
buffer_move_empty(&vs.output, &job->vs->output);
}
vnc_unlock_output(job->vs);

/* Make a local copy of vs and switch output buffers */
vnc_async_encoding_start(job->vs, &vs);
vs.magic = VNC_MAGIC;

/* Start sending rectangles */
n_rectangles = 0;
vnc_write_u8(&vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE);
vnc_write_u8(&vs, 0);
saved_offset = vs.output.offset;
vnc_write_u16(&vs, 0);

vnc_lock_display(job->vs->vd);
// 遍历该 job 上的 VncRect,发送给 client
QLIST_FOREACH_SAFE(entry, &job->rectangles, next, tmp) {
int n;

if (job->vs->ioc == NULL) {
// io channel 为空,断开连接
vnc_unlock_display(job->vs->vd);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);
goto disconnected;
}

if (vnc_worker_clamp_rect(&vs, job, &entry->rect)) {
// 发送 buffer
n = vnc_send_framebuffer_update(&vs, entry->rect.x, entry->rect.y,
entry->rect.w, entry->rect.h);

if (n >= 0) {
n_rectangles += n;
}
}
g_free(entry);
}
trace_vnc_job_nrects(&vs, job, n_rectangles);
vnc_unlock_display(job->vs->vd);

/* Put n_rectangles at the beginning of the message */
vs.output.buffer[saved_offset] = (n_rectangles >> 8) & 0xFF;
vs.output.buffer[saved_offset + 1] = n_rectangles & 0xFF;

vnc_lock_output(job->vs);
if (job->vs->ioc != NULL) {
buffer_move(&job->vs->jobs_buffer, &vs.output);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);

qemu_bh_schedule(job->vs->bh);
} else {
buffer_reset(&vs.output);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);
}
vnc_unlock_output(job->vs);

disconnected:
vnc_lock_queue(queue);
QTAILQ_REMOVE(&queue->jobs, job, next);
vnc_unlock_queue(queue);
qemu_cond_broadcast(&queue->cond);
g_free(job);
vs.magic = 0;
return 0;
}

该函数的流程如下:

  • 检查全局 queue 的 job 链表是否为空,若是则进入睡眠等待
  • 锁上第一个 job,检查对应 VncState 的 IO channel 是否为空,若是则跳到结束发送
  • 若 VncState 的 output buffer (Buffer 类型,类似于 iovec,有长度和指向实际 buffer 的指针)的 offset 为 0,则将其给到函数内临时创建的 VncState 的 output buffer,原 buffer 设为 NULL
  • 遍历该 job 上的 VncRect,发送给 client,若 io channel 为空,则跳到结束发送
  • 若 VncState 的 io channel 不为空,则将函数内临时创建的 VncState 的 output buffer 给回原 VncState
  • (结束发送)从全局 queue 上取下该 job 并释放

II、VNC 的 dcl_ops

vnc_display_init() 中创建 VncDisplay 实例时会将其 DisplayChangeListener 的函数表初始化为 dcl_ops 函数表,定义如下:

1
2
3
4
5
6
7
8
9
static const DisplayChangeListenerOps dcl_ops = {
.dpy_name = "vnc",
.dpy_refresh = vnc_refresh,
.dpy_gfx_update = vnc_dpy_update,
.dpy_gfx_switch = vnc_dpy_switch,
.dpy_gfx_check_format = qemu_pixman_check_format,
.dpy_mouse_set = vnc_mouse_set,
.dpy_cursor_define = vnc_dpy_cursor_define,
};

可以看到的是其中主要都是与 vnc 相关的操作函数,后面涉及到具体函数时我们再展开分析

III、register_displaychangelistener - 注册 dcl,启动 timer

该函数定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void register_displaychangelistener(DisplayChangeListener *dcl)
{
QemuConsole *con;

assert(!dcl->ds);

trace_displaychangelistener_register(dcl, dcl->ops->dpy_name);
dcl->ds = get_alloc_displaystate();
QLIST_INSERT_HEAD(&dcl->ds->listeners, dcl, next);
gui_setup_refresh(dcl->ds);
if (dcl->con) {
dcl->con->dcls++;
con = dcl->con;
} else {
con = active_console;
}
displaychangelistener_display_console(dcl, con, dcl->con ? &error_fatal : NULL);
text_console_update_cursor(NULL);
}

主要做了这些事情:

  • 将 dcl 插到对应 DisplayState 的 listeners 链表上
  • 调用 gui_setup_refresh() 启动一个 Qemu Timer
  • 调用 displaychangelistener_display_console() 进行相关初始化操作,其中会调用 dcl->ops 中函数

我们主要关注 gui_setup_refresh(),其会启动一个 Qemu 定时器,定期地刷新 frame buffer,这也是更新显卡数据的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void gui_setup_refresh(DisplayState *ds)
{
DisplayChangeListener *dcl;
bool need_timer = false;
bool have_gfx = false;
bool have_text = false;

QLIST_FOREACH(dcl, &ds->listeners, next) {
if (dcl->ops->dpy_refresh != NULL) {
need_timer = true;
}
if (dcl->ops->dpy_gfx_update != NULL) {
have_gfx = true;
}
if (dcl->ops->dpy_text_update != NULL) {
have_text = true;
}
}

if (need_timer && ds->gui_timer == NULL) {
ds->gui_timer = timer_new_ms(QEMU_CLOCK_REALTIME, gui_update, ds);
timer_mod(ds->gui_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME));
}
if (!need_timer && ds->gui_timer != NULL) {
timer_free(ds->gui_timer);
ds->gui_timer = NULL;
}

ds->have_gfx = have_gfx;
ds->have_text = have_text;
}

当 dcl->ops 的 dpy_refresh 指针非空且当前 DisplayState 未设置 timer 时,便会调用 timer_new_ms() 新建一个毫秒级别的定时器,定时调用 gui_update() 刷新显存,接收的参数便为该 DisplayState

0x03.显示设备更新相关函数

前面我们讲到,在 Qemu 启动时会启动一个 Timer 定时进行显存的刷新,其代码调用关系如下图所示:

image.png

一、gui_update - timer 定时调用进行更新操作

我们先来看 gui_update() 这个由 timer 定时调用的函数,其定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void gui_update(void *opaque)
{
uint64_t interval = GUI_REFRESH_INTERVAL_IDLE;
uint64_t dcl_interval;
DisplayState *ds = opaque;
DisplayChangeListener *dcl;
QemuConsole *con;

ds->refreshing = true;
dpy_refresh(ds);
ds->refreshing = false;

QLIST_FOREACH(dcl, &ds->listeners, next) {
dcl_interval = dcl->update_interval ?
dcl->update_interval : GUI_REFRESH_INTERVAL_DEFAULT;
if (interval > dcl_interval) {
interval = dcl_interval;
}
}
if (ds->update_interval != interval) {
ds->update_interval = interval;
QTAILQ_FOREACH(con, &consoles, next) {
if (con->hw_ops->update_interval) {
con->hw_ops->update_interval(con->hw, interval);
}
}
trace_console_refresh(interval);
}
ds->last_update = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
timer_mod(ds->gui_timer, ds->last_update + interval);
}

主要流程如下:

  • 调用 dpy_refresh() ,该函数会遍历 DisplayState 中的所有 DisplayChangeListener 并调用 dcl->ops->dpy_refresh()
  • 遍历 DisplayState 中的所有 DisplayChangeListener,检查 dcl->update_interval
  • ds->update_interval != interval,遍历所有的 QemuConsole 并调用 con->hw_ops->update_interval() 进行硬件数据更新

二、dpy_refresh - 遍历 dcl 并调用 dcl->ops->dpy_refresh 进行更新

dpy_refresh() 比较简单,主要就是遍历 DisplayState 中的所有 DisplayChangeListener 并调用 dcl->ops->dpy_refresh(),如下:

1
2
3
4
5
6
7
8
9
10
static void dpy_refresh(DisplayState *s)
{
DisplayChangeListener *dcl;

QLIST_FOREACH(dcl, &s->listeners, next) {
if (dcl->ops->dpy_refresh) {
dcl->ops->dpy_refresh(dcl);
}
}
}

我们主要关注与 VNC 有关的部分,对于 VNC 而言,其 dcl->ops->dpy_refresh 应为 vnc_refresh,该函数定义于 ui/vnc.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void vnc_refresh(DisplayChangeListener *dcl)
{
VncDisplay *vd = container_of(dcl, VncDisplay, dcl);
VncState *vs, *vn;
int has_dirty, rects = 0;

if (QTAILQ_EMPTY(&vd->clients)) { // 没有 client,直接返回
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_MAX); //更新计时
return;
}

graphic_hw_update(vd->dcl.con); // 硬件更新

if (vnc_trylock_display(vd)) {
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE);
return;
}

has_dirty = vnc_refresh_server_surface(vd); // 检查是否有待更新数据
vnc_unlock_display(vd);

QTAILQ_FOREACH_SAFE(vs, &vd->clients, next, vn) {
rects += vnc_update_client(vs, has_dirty); // 遍历更新 client
/* vs might be free()ed here */
}

if (has_dirty && rects) {
vd->dcl.update_interval /= 2;
if (vd->dcl.update_interval < VNC_REFRESH_INTERVAL_BASE) {
vd->dcl.update_interval = VNC_REFRESH_INTERVAL_BASE;
}
} else {
vd->dcl.update_interval += VNC_REFRESH_INTERVAL_INC;
if (vd->dcl.update_interval > VNC_REFRESH_INTERVAL_MAX) {
vd->dcl.update_interval = VNC_REFRESH_INTERVAL_MAX;
}
}
}

该函数主要流程如下:

  • 检查是否有连接的客户端,若否则更新计时后直接返回
  • 调用 graphic_hw_update() 更新 dcl 对应的 QemuConsole 对应的硬件设备
  • 调用 vnc_refresh_server_surface() 检查是否有 dirty 区域(待更新区域)
  • 遍历 dcl 上的 client, 调用 vnc_update_client() 创建新的 VncJob 挂载到全局链表上,由 worker thread 进行推送

三、graphic_hw_update - 图形硬件数据更新

前面我们涉及到的都是 console、vnc 这一块的数据更新,并没有触及到实际的硬件方面(qemu 模拟显卡等)的更新,这一块实际的更新是由 graphic_hw_update() 来完成的,该函数定义于 ui/console.c 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void graphic_hw_update(QemuConsole *con)
{
bool async = false;
con = con ? con : active_console;
if (!con) {
return;
}
if (con->hw_ops->gfx_update) {
con->hw_ops->gfx_update(con->hw);
async = con->hw_ops->gfx_update_async;
}
if (!async) {
graphic_hw_update_done(con);
}
}

前面我们讲到一个 QemuConsole 对应一个显示设备,因此在 graphic_hw_update() 当中会直接调用 con->hw_ops->gfx_update() 来完成硬件方面的数据更新

如果是纯字符型显示设备,则为 text_console_ops,定义于 ui/console.c 中,该函数表没有 gfx_update 指针,故会直接返回:

1
2
3
4
static const GraphicHwOps text_console_ops = {
.invalidate = text_console_invalidate,
.text_update = text_console_update,
};

对于默认的图形化界面,con->hw->ops 应为 vga_ops,定义于 hw/display/vga.c 中,其 gfx_update 指针为 vga_update_display

1
2
3
4
5
static const GraphicHwOps vga_ops = {
.invalidate = vga_invalidate_display,
.gfx_update = vga_update_display,
.text_update = vga_update_text,
};

这里主要是根据指定的硬件决定的,默认的图形界面便是 vga_ops,如果你在启动时指定了一个 QXL 显卡,那么这里就应该是 qxl_ops,最终调用到 qxl_hw_update()

该函数定义于 hw/display/vga.c 中,主要作用就是根据显示模式调用不同的更新函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void vga_update_display(void *opaque)
{
VGACommonState *s = opaque;
DisplaySurface *surface = qemu_console_surface(s->con);
int full_update, graphic_mode;

qemu_flush_coalesced_mmio_buffer();

if (surface_bits_per_pixel(surface) == 0) {
/* nothing to do */
} else {
full_update = 0;
if (!(s->ar_index & 0x20)) {
graphic_mode = GMODE_BLANK;
} else {
graphic_mode = s->gr[VGA_GFX_MISC] & VGA_GR06_GRAPHICS_MODE;
}
if (graphic_mode != s->graphic_mode) {
s->graphic_mode = graphic_mode;
s->cursor_blink_time = qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL);
full_update = 1;
}
switch(graphic_mode) {
case GMODE_TEXT:
vga_draw_text(s, full_update);
break;
case GMODE_GRAPH:
vga_draw_graphic(s, full_update);
break;
case GMODE_BLANK:
default:
vga_draw_blank(s, full_update);
break;
}
}
}

这些 vga_draw_* 函数最后都会调用到 dpy_gfx_update() 将更新推送到显示设备上,其定义于 ui/console.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h)
{
DisplayState *s = con->ds;
DisplayChangeListener *dcl;
int width = qemu_console_get_width(con, x + w);
int height = qemu_console_get_height(con, y + h);

x = MAX(x, 0);
y = MAX(y, 0);
x = MIN(x, width);
y = MIN(y, height);
w = MIN(w, width - x);
h = MIN(h, height - y);

if (!qemu_console_is_visible(con)) {
return;
}
dpy_gfx_update_texture(con, con->surface, x, y, w, h);
QLIST_FOREACH(dcl, &s->listeners, next) {
if (con != (dcl->con ? dcl->con : active_console)) {
continue;
}
if (dcl->ops->dpy_gfx_update) {
dcl->ops->dpy_gfx_update(dcl, x, y, w, h);
}
}
}

主要流程如下:

  • 若 QemuConsole 不可视,直接返回
  • 调用 dpy_gfx_update_texture(),其最终会调用到 con->gl->ops->dpy_gl_ctx_update_texture(),这一块是与 GL 相关的操作,这里暂且不展开
  • 遍历 QemuConsole 上的 DisplayChangeListener,调用 dcl->ops->dpy_gfx_update(),更新 dcl 对应的 display 设备

对于 VNC 而言,dcl->ops->dpy_gfx_update 指针应为 vnc_dpy_update() 函数,定义于 ui/vnc.c 中,主要就是调用 vnc_set_area_dirty() 将一块区域标记为 dirty:

1
2
3
4
5
6
7
8
static void vnc_dpy_update(DisplayChangeListener *dcl,
int x, int y, int w, int h)
{
VncDisplay *vd = container_of(dcl, VncDisplay, dcl);
struct VncSurface *s = &vd->guest;

vnc_set_area_dirty(s->dirty, vd, x, y, w, h);
}

最终这些被标记为 dirty 的区域会在 vnc_refresh() 中调用 vnc_refresh_server_surface() 时被进一步处理,在 vnc_update_client() 中变为新的 VncJob 链到全局链表上

四、vnc_client_update - 创建新的 VncJob 挂载到全局链表上

当前面我们将特定的显示区域标记为 dirty 之后,最终会由 vnc_update_client() 函数来扫描 dirty 区域,并创建相应的 VncJob 挂载到全局的 queue 上,因此最后实际上还是由 vnc worker thread 完成推送的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int vnc_update_client(VncState *vs, int has_dirty)
{
VncDisplay *vd = vs->vd;
VncJob *job;
int y;
int height, width;
int n = 0;

if (vs->disconnecting) {
vnc_disconnect_finish(vs);
return 0;
}

vs->has_dirty += has_dirty;
if (!vnc_should_update(vs)) {
return 0;
}

if (!vs->has_dirty && vs->update != VNC_STATE_UPDATE_FORCE) {
return 0;
}

/*
* Send screen updates to the vnc client using the server
* surface and server dirty map. guest surface updates
* happening in parallel don't disturb us, the next pass will
* send them to the client.
*/
job = vnc_job_new(vs); // 创建新的 VncJob

height = pixman_image_get_height(vd->server);
width = pixman_image_get_width(vd->server);

y = 0;
for (;;) {
// 扫描 dirty 区域
int x, h;
unsigned long x2;
unsigned long offset = find_next_bit((unsigned long *) &vs->dirty,
height * VNC_DIRTY_BPL(vs),
y * VNC_DIRTY_BPL(vs));
if (offset == height * VNC_DIRTY_BPL(vs)) {
/* no more dirty bits */
break;
}
y = offset / VNC_DIRTY_BPL(vs);
x = offset % VNC_DIRTY_BPL(vs);
x2 = find_next_zero_bit((unsigned long *) &vs->dirty[y],
VNC_DIRTY_BPL(vs), x);
bitmap_clear(vs->dirty[y], x, x2 - x);
h = find_and_clear_dirty_height(vs, y, x, x2, height);
x2 = MIN(x2, width / VNC_DIRTY_PIXELS_PER_BIT);
if (x2 > x) {
// 创建新的 VncRect,挂载到 VncJob 上
n += vnc_job_add_rect(job, x * VNC_DIRTY_PIXELS_PER_BIT, y,
(x2 - x) * VNC_DIRTY_PIXELS_PER_BIT, h);
}
if (!x && x2 == width / VNC_DIRTY_PIXELS_PER_BIT) {
y += h;
if (y == height) {
break;
}
}
}

vs->job_update = vs->update;
vs->update = VNC_STATE_UPDATE_NONE;
vnc_job_push(job); // 挂载到全局 queue 上
vs->has_dirty = 0;
return n;
}

至此,Qemu 中 VNC 的工作流程基本分析完毕

0xFF.总览

最后让我们重新看一下 Qemu VNC 的基本架构,这里再放一张从知乎上偷来的一个 Qemu VNC 的基本架构图:

image.png

在 Qemu 中内部的显示结构如下图所示:

image.png

最终的更新调用链路则如下所示:

image.png


【VIRT.0x01】Qemu - II:VNC 模块源码分析
https://arttnba3.github.io/2022/07/22/VIRTUALIZATION-0X01-QEMU-PART-II/
作者
arttnba3
发布于
2022年7月22日
许可协议