kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

HAProxy 研究笔记 -- rules 实现

http://blog.chinaunix.net/uid-10167808-id-3775567.html

本文研究 haproxy-1.5-dev17 中 rules 的相关实现。

1
2
3
4
1. ACL
2. rule 的组成
3. rule 的执行
4. rule 的种类

1. ACL

如果要实现功能丰富的 rules,需要有配套的 ACL 机制。

ACL 的格式如下:

1
acl [flags] [operator] ... 

haproxy 中 ACL 数据结构的定义:

1
2
3
4
5
6
7
8
/* The acl will be linked to from the proxy where it is declared */
struct acl {
	struct list list;           /* chaining */
	char *name;           /* acl name */
	struct list expr;     /* list of acl_exprs */
	int cache_idx;              /* ACL index in cache */
	unsigned int requires;      /* or'ed bit mask of all acl_expr's ACL_USE_* */
};

其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
name: ACL 的名字
expr: ACL 定义的表达式。就是定义的 ACL 名字后面的表达式。这是一个链表结构。因此,可以定义多条表达式不同但是名字相同的 ACL。这样,多个表达式都属于同一个 ACL。
requires: 所有表达式中关键字对应的作用域(该关键字可以用在什么场合)的集合

函数 parse_acl() 负责解析定义好的 ACL:

查找 ACL 的名字,如果不存在的话,则 alloc 一个新的 acl 结构
通过调用 parse_acl_expr() 对表达式进行解析,并返回 struct acl_expr 结构
	ACL 中的表达式应该只有一个 kw
	查找该关键字,必须是已经注册好的。并返回该关键字注册时的数据结构 struct acl_expr
	alloc 一个 struct acl_expr 结构体,记录下返回的 kw 的数据结构,并作成员的初始化
	调用对应 kw 的 parse 方法,将解析的结果保存在 struct acl_pattern 结构体中,并将该结构体加入到 expr->patterns 的链表中
将解析到的表达式插入到 acl 中的 expr 链表中

总结: 一个 ACL 包含一到多个表达式。每个表达式包含一个 kw及一到多个 pattern。

2. rule 的组成

这里简要描述 rule 与 acl 之间的逻辑关系:

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
rule 应该是 action + condition 组成
	有些动作自身可能也需要记录一些信息。不同的 rule 对应动作的信息可能不同,比如 reqirep 等
	block rules 的动作比较单一, condition 满足之后处理结果均相同
condition,完成 rule 检测的判断条件 对应数据结构: struct acl_cond

		struct acl_cond {
			struct list list;           /* Some specific tests may use multiple conditions */
			struct list suites;         /* list of acl_term_suites */
			int pol;                    /* polarity: ACL_COND_IF / ACL_COND_UNLESS */
			unsigned int requires;      /* or'ed bit mask of all acl's ACL_USE_* */
			const char *file;           /* config file where the condition is declared */
			int line;                   /* line in the config file where the condition is declared */
		};
		

condition 包含多个 ACL 组。组的分割逻辑是逻辑或(|| 或者 or),即 struct list suites 的成员,组的数据结构 struct acl_term_suite

	struct acl_term_suite {
		struct list list;           /* chaining of term suites */
		struct list terms;          /* list of acl_terms */
	};

	该数据结构可以包含多个 ACL,以及每个 ACL 可能的一个取反标识 '!'
	所有表达式中相邻的 ACL 且其逻辑关系为逻辑与(&&) 的构成一个 ACL 组
		比如 if acl1 !acl2 or acl3 acl4,则构成两个 acl_term_suite,分别是 acl1 !acl2 和 acl3 acl4
		每个 ACL 及其可能的取反标记对应的数据结构: struct acl_term

			struct acl_term {
				struct list list;           /* chaining */
				struct acl *acl;            /* acl pointed to by this term */
				int neg;                    /* 1 if the ACL result must be negated */
			};

	一个 ACL 包含多个 expr

3. rule 的执行

概括起来很简单,执行判断条件。符合条件,然后执行对应动作。

下面是 rspadd 的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* add response headers from the rule sets in the same order */
list_for_each_entry(wl, &rule_set->rsp_add, list) {
	if (txn->status < 200)
		break;
	if (wl->cond) {
		int ret = acl_exec_cond(wl->cond, px, t, txn, SMP_OPT_DIR_RES|SMP_OPT_FINAL);
		ret = acl_pass(ret);
		if (((struct acl_cond *)wl->cond)->pol == ACL_COND_UNLESS)
			ret = !ret;
		if (!ret)
			continue;
	}
	if (unlikely(http_header_add_tail(&txn->rsp, &txn->hdr_idx, wl->s) < 0))
		goto return_bad_resp;
}

对于同一个种类的 rules,执行逻辑如下:

1
2
3
4
5
6
主要遍历 rule,调用 acl_exec_cond 执行该 rule 的检测条件。该检测结果只给出匹配与否。
	逐个遍历 cond 上的 ACL 组,即cond->suites。任一 suite 匹配成功,则认为匹配成功
	同一个 ACL 组上,遍历所有 suite->terms (ACL + 取反逻辑)。任意一个 ACL 匹配失败,则跳到下一个 ACL 组继续匹配。同一组全部 ACL 匹配成功,则认为该 ACL 组匹配
		同一个 ACL 上的匹配,则是逐一遍历 ACL 的 expr。只要任意一个 expr 匹配成功,则认为该 ACL 匹配成功
结合 condition 中的条件 if/unless,确定最终匹配结果
如果匹配则执行对应的 action,否则检测下一条规则。

4. rule 的种类

从 proxy 结构体可以看出 rule 的种类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct proxy {
	...
	struct list acl;                        /* ACL declared on this proxy */
	struct list http_req_rules;       /* HTTP request rules: allow/deny/http-auth */
	struct list block_cond;                 /* early blocking conditions (chained) */
	struct list redirect_rules;             /* content redirecting rules (chained) */
	struct list switching_rules;            /* content switching rules (chained) */
	struct list persist_rules;        /* 'force-persist' and 'ignore-persist' rules (chained) */
	struct list sticking_rules;             /* content sticking rules (chained) */
	struct list storersp_rules;             /* content store response rules (chained) */
	struct list server_rules;               /* server switching rules (chained) */
	struct {                                /* TCP request processing */
		unsigned int inspect_delay;     /* inspection delay */
		struct list inspect_rules;      /* inspection rules */
		struct list l4_rules;           /* layer4 rules */
	} tcp_req;
	struct {                                /* TCP request processing */
		unsigned int inspect_delay;     /* inspection delay */
		struct list inspect_rules;      /* inspection rules */
	} tcp_rep;
	...
}

其中, 函数 http_process_req_common 中处理的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http_process_req_common
{
	... 
	1. process block rules
	...
	2. process http req rules
	...
	3. execute regular exp if any
	...
	4. req add
	...
	5. process redirect rules
	...
}

这里没有详细的介绍各种具体用途的 rules。随后具体分析代码的时候总结一下再加上。

HAProxy 研究笔记 -- TCP 连接处理流程

http://blog.chinaunix.net/uid-10167808-id-3771148.html

本文基于 HAProxy 1.5-dev7 版本。

1
2
3
4
5
6
7
8
9
10
11
12
目录
1. 关键数据结构 session
2. 相关初始化
	2.1. 初始化处理 TCP 连接的方法
	2.2. 初始化 listener
	2.3. 绑定所有已注册协议上的 listeners
	2.4. 启用所有已注册协议上的 listeners
3. TCP 连接的处理流程
	3.1. 接受新建连接
	3.2. TCP 连接上的接收事件
	3.3. TCP 连接上的发送事件
	3.4. http 请求的处理

1. 关键数据结构 session

haproxy 负责处理请求的核心数据结构是 struct session,本文不对该数据结构进行分析。

从业务的处理的角度,简单介绍一下对 session 的理解:

1
2
3
haproxy 每接收到 client 的一个连接,便会创建一个 session 结构,该结构一直伴随着连接的处理,直至连接被关闭,session 才会被释放
haproxy 其他的数据结构,大多会通过引用的方式和 session 进行关联
一个业务 session 上会存在两个 TCP 连接,一个是 client 到 haproxy,一个是 haproxy 到后端 server。

此外,一个 session,通常还要对应一个 task,haproxy 最终用来做调度的是通过 task。

2. 相关初始化

在 haproxy 正式处理请求之前,会有一系列初始化动作。这里介绍和请求处理相关的一些初始化。

2.1. 初始化处理 TCP 连接的方法

初始化处理 TCP 协议的相关数据结构,主要是和 socket 相关的方法的声明。详细见下面 proto_tcpv4 (proto_tcp.c)的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct protocol proto_tcpv4 = {
	.name = "tcpv4",
	.sock_domain = AF_INET,
	.sock_type = SOCK_STREAM,
	.sock_prot = IPPROTO_TCP,
	.sock_family = AF_INET,
	.sock_addrlen = sizeof(struct sockaddr_in),
	.l3_addrlen = 32/8,
	.accept = &stream_sock_accept,
	.read = &stream_sock_read,
	.write = &stream_sock_write,
	.bind = tcp_bind_listener,
	.bind_all = tcp_bind_listeners,
	.unbind_all = unbind_all_listeners,
	.enable_all = enable_all_listeners,
	.listeners = LIST_HEAD_INIT(proto_tcpv4.listeners),
	.nb_listeners = 0,
};
2.2. 初始化 listener

listener,顾名思义,就是用于负责处理监听相关的逻辑。

在 haproxy 解析 bind 配置的时候赋值给 listener 的 proto 成员。函数调用流程如下:

1
2
3
4
5
cfgparse.c
	-> cfg_parse_listen
		-> str2listener
			-> tcpv4_add_listener
				-> listener->proto = &proto_tcpv4;

由于这里初始化的是 listener 处理 socket 的一些方法。可以推断, haproxy 接收 client 新建连接的入口函数应该是 protocol 结构体中的 accpet 方法。对于tcpv4 来说,就是 stream_sock_accept() 函数。该函数到 1.5-dev19 中改名为 listener_accept()。这是后话,暂且不表。

listener 的其他初始化

1
2
3
4
5
cfgparse.c
	-> check_config_validity
		-> listener->accept = session_accept;
listener->frontend = curproxy; (解析 frontend 时,会执行赋值: curproxy->accept = frontend_accept)
listener->handler = process_session;

整个 haproxy 配置文件解析完毕,listener 也已初始化完毕。可以简单梳理一下几个 accept 方法的设计逻辑:

1
2
3
stream_sock_accept(): 负责接收新建 TCP 连接,并触发 listener 自己的 accept 方法 session_accept()
session_accept(): 负责创建 session,并作 session 成员的初步初始化,并调用 frontend 的 accept 方法 front_accetp()
frontend_accept(): 该函数主要负责 session 前端的 TCP 连接的初始化,包括 socket 设置,log 设置,以及 session 部分成员的初始化

下文分析 TCP 新建连接处理过程,基本上就是这三个函数的分析。

2.3. 绑定所有已注册协议上的 listeners
1
2
3
4
5
6
haproxy.c 
	-> protocol_bind_all 
		-> all registered protocol bind_all
			-> tcp_bind_listeners (TCP)
				-> tcp_bind_listener 
					-> [ fdtab[fd].cb[DIR_RD].f = listener->proto->accept ]

该函数指针指向 proto_tcpv4 结构体的 accept 成员,即函数 stream_sock_accept

2.4. 启用所有已注册协议上的 listeners

把所有 listeners 的 fd 加到 polling lists 中 haproxy.c -> protocol_enable_all -> all registered protocol enable_all -> enable_all_listeners (TCP) -> enable_listener 函数会将处于 LI_LISTEN 的 listener 的状态修改为 LI_READY,并调用 cur poller 的 set 方法, 比如使用 sepoll,就会调用 __fd_set

3. TCP 连接的处理流程

3.1. 接受新建连接

前面几个方面的分析,主要是为了搞清楚当请求到来时,处理过程中实际的函数调用关系。以下分析 TCP 建连过程。

1
2
3
4
5
6
7
8
haproxy.c 
	-> run_poll_loop 
		-> cur_poller.poll 
			-> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
				-> fdtab[fd].cb[DIR_RD].f(fd) (TCP 协议的该函数指针指向 stream_sock_accept )
					-> stream_sock_accept
						-> 按照 global.tune.maxaccept 的设置尽量可能多执行系统调用 accept,然后再调用 l->accept(),即 listener 的 accept 方法 session_accept
							-> session_accept

session_accept 主要完成以下功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
调用 pool_alloc2 分配一个 session 结构
调用 task_new 分配一个新任务
将新分配的 session 加入全局 sessions 链表中
session 和 task 的初始化,若干重要成员的初始化如下
	t->process = l->handler: 即 t->process 指向 process_session
	t->context = s: 任务的上下文指向 session
	s->listener = l: session 的 listener 成员指向当前的 listener
	s->si[] 的初始化,记录 accept 系统调用返回的 cfd 等
	初始化 s->txn
	为 s->req 和 s->rep 分别分配内存,并作对应的初始化
		s->req = pool_alloc2(pool2_buffer)
		s->rep = pool_alloc2(pool2_buffer)
		从代码上来看,应该是各自独立分配 tune.bufsize + sizeof struct buffer 大小的内存
	新建连接 cfd 的一些初始化
		cfd 设置为非阻塞
		将 cfd 加入 fdtab[] 中,并注册新建连接 cfg 的 read 和 write 的方法
		fdtab[cfd].cb[DIR_RD].f = l->proto->read,设置 cfd 的 read 的函数 l->proto->read,对应 TCP 为 stream_sock_read,读缓存指向 s->req,
		fdtab[cfd].cb[DIR_WR].f = l->proto->write,设置 cfd 的 write 函数 l->proto->write,对应 TCP 为 stream_sock_write,写缓冲指向 s->rep
p->accept 执行 proxy 的 accept 方法即 frontend_accept
	设置 session 结构体的 log 成员
	根据配置的情况,分别设置新建连接套接字的选项,包括 TCP_NODELAY/KEEPALIVE/LINGER/SNDBUF/RCVBUF 等等
	如果 mode 是 http 的话,将 session 的 txn 成员做相关的设置和初始化
3.2. TCP 连接上的接收事件
1
2
3
4
5
6
haproxy.c 
	-> run_poll_loop 
		-> cur_poller.poll 
			-> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
				-> fdtab[fd].cb[DIR_RD].f(fd) (该函数在建连阶段被初始化为四层协议的 read 方法,对于 TCP 协议,为 stream_sock_read )
					-> stream_sock_read

stream_sock_read 主要完成以下功能

找到当前连接的读缓冲,即当前 session 的 req buffer:

1
struct buffer *b = si->ib
1
2
3
4
5
6
7
8
9
根据配置,调用 splice 或者 recv 读取套接字上的数据,并填充到读缓冲中,即填充到从 b->r(初始位置应该就是 b->data)开始的内存中
如果读取到 0 字节,则意味着接收到对端的关闭请求,调用 stream_sock_shutr 进行处理
	读缓冲标记 si->ib->flags 的 BF_SHUTR 置位,清除当前 fd 的 epoll 读事件,不再从该 fd 读取
	如果写缓冲 si->ob->flags 的 BF_SHUTW 已经置位,说明应该是由本地首先发起的关闭连接动作
		将 fd 从 fdset[] 中清除,从 epoll 中移除 fd,执行系统调用 close(fd), fd.state 置位 FD_STCLOSE
		stream interface 的状态修改 si->state = SI_ST_DIS
唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务

##### 3.3. TCP 连接上的发送事件
haproxy.c 
    -> run_poll_loop 
        -> cur_poller.poll 
            -> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
                -> fdtab[fd].cb[DIR_WR].f(fd) (该函数在建连阶段被初始化为四层协议的 write 方法,对于 TCP 协议,为 stream_sock_write )
                    -> stream_sock_write
1
2
3
4

stream_sock_write主要完成以下功能

  找到当前连接的写缓冲,即当前 session 的 rep buffer:
struct buffer *b = si->ob
1
2
3
4
5

```
  将待发送的数据调用 send 系统调用发送出去  
  或者数据已经发送完毕,需要发送关闭连接的动作 stream_sock_shutw-> 系统调用 shutdown  
  唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务  
3.4. http 请求的处理
1
2
3
4
haproxy.c 
	-> run_poll_loop 
		-> process_runnable_tasks,查找当前待处理的任务所有 tasks, 然后调用 task->process(大多时候就是 process_session) 进行处理
			-> process_session

process_session 主要完成以下功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
处理连接需要关闭的情形,分支 resync_stream_interface
处理请求,分支 resync_request (read event)
	根据 s->req->analysers 的标记位,调用不同的 analyser 进行处理请求
	ana_list & AN_REQ_WAIT_HTTP: http_wait_for_request
	ana_list & AN_REQ_HTTP_PROCESS_FE: http_process_req_common
	ana_list & AN_REQ_SWITCHING_RULES:process_switching_rules
处理应答,分支 resync_response (write event)
	根据 s->rep->analysers 的标记位,调用不同的 analyser 进行处理请求
	ana_list & AN_RES_WAIT_HTTP: http_wait_for_response
	ana_list & AN_RES_HTTP_PROCESS_BE:http_process_res_common
处理 forward buffer 的相关动作
关闭 req 和 rep 的 buffer,调用 pool2_free 释放 session 及其申请的相关内存,包括读写缓冲 (read 0 bytes)
	pool_free2(pool2_buffer, s->req);
	pool_free2(pool2_buffer, s->rep);
	pool_free2(pool2_session, s);
task 从运行任务队列中清除,调用 pool2_free 释放 task 申请的内存: task_delete(); task_free();

本文简单分析了 TCP 连接的处理过程,不侧重细节分析,而且缺少后端 server 的选择以及建连等,重在希望展示出一个 haproxy 处理 TCP 连接的框架。

编译qemu-kvm和安装qemu-kvm

http://smilejay.com/2012/06/qemu-kvm_compilation_installation/

3.4 编译和安装qemu-kvm

除了在内核空间的KVM模块之外,在用户空间需要QEMU[注6]来模拟所需要CPU和设备模型以及用于启动客户机进程,这样才有了一个完整的KVM运行环境。而qemu-kvm是为了针对KVM专门做了修改和优化的QEMU分支[注7],在本书写作的2012年,qemu-kvm分支里面的小部分特性还没有完全合并进入到qemu的主干代码之中,所以本书中采用qemu-kvm来讲解。

在编译和安装了KVM并且启动到编译的内核之后,下面来看一下qemu-kvm的编译和安装。

3.4.1 下载qemu-kvm源代码

目前的QEMU项目针对KVM的代码分支qemu-kvm也是由Redhat公司的Gleb Natapov和Marcelo Tosatti作维护者(Maintainer),代码也是托管在kernel.org上。

qemu-kvm开发代码仓库的网页连接为:http://git.kernel.org/?p=virt/kvm/qemu-kvm.git

其中,可以看到有如下3个URL连接可供下载开发中的最新qemu-kvm的代码仓库。 git://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git
http://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git
https://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git

可以根据自己实际需要选择3个中任一个用git clone命令下载即可,它们是完全一样的。

另外,可以到sourceforge.net的如下链接中根据需要下载qemu-kvm各个发布版本的代码压缩包(作者建议使用最新的正式发布版本,因为它的功能更多,同时也比较稳定)。

http://sourceforge.net/projects/kvm/files/qemu-kvm/

在本节讲解编译时,以下载开发中的最新的qemu-kvm.git为例,获取其代码仓库过程如下:

1
2
3
4
5
6
7
8
9
10
[root@jay-linux kvm_demo]# git clone\ git://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git qemu-kvm.git
Initialized empty Git repository in /root/kvm_demo/qemu-kvm.git/.git/
remote: Counting objects: 145222, done.
remote: Compressing objects: 100% (35825/35825), done.
remote: Total 145222 (delta 114656), reused 137663 (delta 107444)
Receiving objects: 100% (145222/145222), 40.83 MiB | 10.33 MiB/s, done.
Resolving deltas: 100% (114656/114656), done.
[root@jay-linux kvm_demo]# cd qemu-kvm.git
[root@jay-linux kvm.git]# pwd
/root/kvm_demo/qemu-kvm.git
3.4.2 配置和编译qemu-kvm

qemu-kvm的配置并不复杂,通常情况下,可以直接运行代码仓库中configure文件进行配置即可。当然,如果对其配置并不熟悉,可以运行“./configure –help”命令查看配置的一些选项及其帮助信息。

显示配置的帮助手册信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@jay-linux qemu-kvm.git]# ./configure --help
Usage: configure [options]
Options: [defaults in brackets after descriptions]
 
Standard options:
--help                   print this message
--prefix=PREFIX          install in PREFIX [/usr/local]
--interp-prefix=PREFIX   where to find shared libraries, etc.
use %M for cpu name [/usr/gnemul/qemu-%M]
--target-list=LIST       set target list (default: build everything)
Available targets: i386-softmmu x86_64-softmmu
<!- 此处省略百余行帮助信息的输出 ->
--disable-guest-agent    disable building of the QEMU Guest Agent
--enable-guest-agent     enable building of the QEMU Guest Agent
--with-coroutine=BACKEND coroutine backend. Supported options:
gthread, ucontext, sigaltstack, windows

NOTE: The object files are built at the place where configure is launched

执行configure文件进行配置的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@jay-linux qemu-kvm.git]# ./configure
Install prefix    /usr/local
BIOS directory    /usr/local/share/qemu
binary directory  /usr/local/bin
library directory /usr/local/lib
include directory /usr/local/include
config directory  /usr/local/etc
Manual directory  /usr/local/share/man
ELF interp prefix /usr/gnemul/qemu-%M
Source path       /root/kvm_demo/qemu-kvm.git
C compiler        gcc
Host C compiler   gcc
<!– 此处省略数十行 –>
VNC support       yes     #通常需要通过VNC连接到客户机中
<!– 此处省略十余行 –>
KVM support       yes     #这是对KVM的支持
TCG interpreter   no
KVM device assig. yes    #这是对KVM中VT-d功能的支持
<!– 此处省略十余行 –>
OpenGL support    yes
libiscsi support  no
build guest agent yes
coroutine backend ucontext

需要注意的是,上面命令行输出的KVM相关的选项需要是配置为yes,另外,一般VNC的支持也是配置为yes的(因为通常需要用VNC连接到客户机中)。

【2013.05.13 updated】 在configure时,可能遇到“glib-2.12 required to compile QEMU”的错误,是需要安装glib2和glib2-dev软件包,在RHEL上的安装命令为“yum install glib2 glib2-devel”,在Ubuntu上安装的过程为“apt-get install libglib2.0 libglib2.0-dev”。

经过配置之后,进行编译就很简单了,直接执行make即可进行编译,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@jay-linux qemu-kvm.git]# make -j 20
GEN   config-host.h
GEN   trace.h
GEN   qemu-options.def
GEN   qmp-commands.h
GEN   qapi-types.h
GEN   qapi-visit.h
GEN   tests/test-qapi-types.h
GEN   tests/test-qapi-visit.h
GEN   tests/test-qmp-commands.h
CC    libcacard/cac.o
CC    libcacard/event.o
<!– 此处省略数百行的编译时输出信息 –>
CC    x86_64-softmmu/target-i386/cpu.o
CC    x86_64-softmmu/target-i386/machine.o
CC    x86_64-softmmu/target-i386/arch_memory_mapping.o
CC    x86_64-softmmu/target-i386/arch_dump.o
CC    x86_64-softmmu/target-i386/kvm.o
CC    x86_64-softmmu/target-i386/hyperv.o
LINK  x86_64-softmmu/qemu-system-x86_64

可以看到,最后有编译生成qemu-system-x86_64文件,它就是我们常用的qemu-kvm的命令行工具(在多数Linux发行版中自带的qemu-kvm软件包的命令行是qemu-kvm,只是名字不同而已)。

3.4.2 安装qemu-kvm

编译完成之后,运行“make install”命令即可安装qemu-kvm,其过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@jay-linux qemu-kvm.git]# make install | tee make-install.log
install -d -m 0755 “/usr/local/share/qemu”
install -d -m 0755 “/usr/local/etc/qemu”
install -c -m 0644 /root/kvm_demo/qemu-kvm.git/sysconfigs/target/target-x86_64.conf “/usr/local/etc/qemu”
install -c -m 0644 /root/kvm_demo/qemu-kvm.git/sysconfigs/target/cpus-x86_64.conf “/usr/local/share/qemu”
install -d -m 0755 “/usr/local/bin”
install -c -m 0755  vscclient qemu-ga qemu-nbd qemu-img qemu-io  “/usr/local/bin”
install -d -m 0755 “/usr/local/libexec”
<!– 此处省略数行的安装时输出信息 –>
make[1]: Entering directory `/root/kvm_demo/qemu-kvm.git/x86_64-softmmu’
install -m 755 qemu-system-x86_64 “/usr/local/bin”
strip “/usr/local/bin/qemu-system-x86_64″
make[1]: Leaving directory `/root/kvm_demo/qemu-kvm.git/x86_64-softmmu’

qemu-kvm的安装过程的主要任务有这几个:创建qemu的一些目录,拷贝一些配置文件到相应的目录下,拷贝一些firmware文件(如:sgabios.bin, kvmvapic.bin)到目录下以便qemu-kvm的命令行启动时可以找到对应的固件提供给客户机使用,拷贝keymaps到相应的目录下以便在客户机中支持各种所需键盘类型,拷贝qemu-system-x86_64、qemu-img等可执行程序到对应的目录下。下面的一些命令行检查了qemu-kvm被安装了之后的系统状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@jay-linux qemu-kvm.git]# which qemu-system-x86_64
/usr/local/bin/qemu-system-x86_64
[root@jay-linux qemu-kvm.git]# which qemu-img
/usr/local/bin/qemu-img
[root@jay-linux qemu-kvm.git]# ls /usr/local/share/qemu/
bamboo.dtb        mpc8544ds.dtb     petalogix-ml605.dtb       pxe-pcnet.rom    slof.bin            vgabios-vmware.bin
bios.bin          multiboot.bin     petalogix-s3adsp1800.dtb  pxe-rtl8139.rom  spapr-rtas.bin
cpus-x86_64.conf  openbios-ppc      ppc_rom.bin               pxe-virtio.rom   vgabios.bin
keymaps           openbios-sparc32  pxe-e1000.rom             qemu-icon.bmp    vgabios-cirrus.bin
kvmvapic.bin      openbios-sparc64  pxe-eepro100.rom          s390-zipl.rom    vgabios-qxl.bin
linuxboot.bin     palcode-clipper   pxe-ne2k_pci.rom          sgabios.bin      vgabios-stdvga.bin
[root@jay-linux qemu-kvm.git]# ls /usr/local/share/qemu/keymaps/
ar    common  de     en-gb  es  fi  fr     fr-ca  hr  is  ja  lv  modifiers  nl-be  pl  pt-br  sl  th
bepo  da      de-ch  en-us  et  fo  fr-be  fr-ch  hu  it  lt  mk  nl         no     pt  ru     sv  tr

由于qemu-kvm是用户空间的程序,安装之后不用重启系统,直接用qemu-system-x86_64、qemu-img这样的命令行工具即可使用qemu-kvm了。

KVM源代码分析4:内存虚拟化

http://www.oenhan.com/kvm-src-4-mem

在虚拟机的创建与运行中pc_init_pci负责在qemu中初始化虚拟机,内存初始化也是在这里完成的,还是一步步从qemu说起,在vl.c的main函数中有ram_size参数,由qemu入参标识QEMU_OPTION_m设定,顾名思义就是虚拟机内存的大小,通过machine->init一步步传递给pc_init1函数。在这里分出了above_4g_mem_size和below_4g_mem_size,即高低端内存(也不一定是32bit机器..),然后开始初始化内存,即pc_memory_init,内存通过memory_region_init_ram下面的qemu_ram_alloc分配,使用qemu_ram_alloc_from_ptr。

插播qemu对内存条的模拟管理,是通过RAMBlock和ram_list管理的,RAMBlock就是每次申请的内存池,ram_list则是RAMBlock的链表,他们结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct RAMBlock {
	//对应宿主的内存地址
	uint8_t *host;
	//block在ramlist中的偏移
	ram_addr_t offset;
	//block长度
	ram_addr_t length;
	uint32_t flags;
	//block名字
	char idstr[256];
	QLIST_ENTRY(RAMBlock) next;
#if defined(__linux__) && !defined(TARGET_S390X)
	int fd;
#endif
} RAMBlock;

typedef struct RAMList {
	//看代码理解就是list的head,但是不知道为啥叫dirty...
	uint8_t *phys_dirty;
	QLIST_HEAD(ram, RAMBlock) blocks;
} RAMList;

下面再回到qemu_ram_alloc_from_ptr函数,使用find_ram_offset赋值给new block的offset,find_ram_offset具体工作模型已经在KVM源代码分析2:虚拟机的创建与运行中提到了,不赘述。然后是一串判断,在kvm_enabled的情况下使用new_block->host = kvm_vmalloc(size),最终内存是qemu_vmalloc分配的,使用qemu_memalign干活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *qemu_memalign(size_t alignment, size_t size)
{
	void *ptr;
	//使用posix进行内存针对页大小对齐
#if defined(_POSIX_C_SOURCE) && !defined(__sun__)
	int ret;
	ret = posix_memalign(&ptr, alignment, size);
	if (ret != 0) {
		fprintf(stderr, "Failed to allocate %zu B: %s\n",
				size, strerror(ret));
		abort();
	}
#elif defined(CONFIG_BSD)
	ptr = qemu_oom_check(valloc(size));
#else
	//所谓检查oom就是看memalign对应malloc申请内存是否成功
	ptr = qemu_oom_check(memalign(alignment, size));
#endif
	trace_qemu_memalign(alignment, size, ptr);
	return ptr;
}

以上qemu_vmalloc进行内存申请就结束了。在qemu_ram_alloc_from_ptr函数末尾则是将block添加到链表,realloc整个ramlist,用memset初始化整个ramblock,madvise对内存使用限定。 然后一层层的退回到pc_memory_init函数。

此时pc.ram已经分配完成,ram_addr已经拿到了分配的内存地址,MemoryRegion ram初始化完成。下面则是对已有的ram进行分段,即ram-below-4g和ram-above-4g,也就是高端内存和低端内存。用memory_region_init_alias初始化子MemoryRegion,然后将memory_region_add_subregion添加关联起来,memory_region_add_subregion具体细节“KVM源码分析2”中已经说了,参考对照着看吧,中间很多映射代码过程也只是qemu遗留的软件实现,没看到具体存在的意义,直接看到kvm_set_user_memory_region函数,内核真正需要kvm_vm_ioctl传递过去的参数是什么, struct kvm_userspace_memory_region mem而已,也就是

1
2
3
4
5
6
7
struct kvm_userspace_memory_region {
	__u32 slot;
	__u32 flags;
	__u64 guest_phys_addr;
	__u64 memory_size;    /* bytes */
	__u64 userspace_addr; /* start of the userspace allocated memory */
};

kvm_vm_ioctl进入到内核是在KVM_SET_USER_MEMORY_REGION参数中,即执行kvm_vm_ioctl_set_memory_region,然后一直向下,到__kvm_set_memory_region函数,check_memory_region_flags检查mem->flags是否合法,而当前flag也就使用了两位,KVM_MEM_LOG_DIRTY_PAGES和KVM_MEM_READONLY,从qemu传递过来只能是KVM_MEM_LOG_DIRTY_PAGES,下面是对mem中各参数的合规检查,(mem->memory_size & (PAGE_SIZE – 1))要求以页为单位,(mem->guest_phys_addr & (PAGE_SIZE – 1))要求guest_phys_addr页对齐,而((mem->userspace_addr & (PAGE_SIZE – 1)) || !access_ok(VERIFY_WRITE,(void __user *)(unsigned long)mem->userspace_addr,mem->memory_size))则保证host的线性地址页对齐而且该地址域有写权限。

id_to_memslot则是根据qemu的内存槽号得到kvm结构下的内存槽号,转换关系来自id_to_index数组,那映射关系怎么来的,映射关系是一一对应的,在kvm_create_vm虚拟机创建过程中,kvm_init_memslots_id初始化对应关系,即slots->id_to_index[i] = slots->memslots[i].id = i,当前映射是没有意义的,估计是为了后续扩展而存在的。

扩充了new的kvm_memory_slot,下面直接在代码中注释更方便:

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
//映射内存有大小,不是删除内存条
if (npages) {
	//内存槽号没有虚拟内存条,意味内存新创建
	if (!old.npages)
		change = KVM_MR_CREATE;
	else { /* Modify an existing slot. */
		//修改已存在的内存修改标志或者平移映射地址
		//下面是不能处理的状态(内存条大小不能变,物理地址不能变,不能修改只读)
		if ((mem->userspace_addr != old.userspace_addr) ||
			(npages != old.npages) ||
			((new.flags ^ old.flags) & KVM_MEM_READONLY))
			goto out;
		//guest地址不同,内存条平移
		if (base_gfn != old.base_gfn)
			change = KVM_MR_MOVE;
		else if (new.flags != old.flags)
			//修改属性
			change = KVM_MR_FLAGS_ONLY;
		else { /* Nothing to change. */
			r = 0;
			goto out;
		}
	}
} else if (old.npages) {
	//申请插入的内存为0,而内存槽上有内存,意味删除
	change = KVM_MR_DELETE;
} else /* Modify a non-existent slot: disallowed. */
	goto out;

另外看kvm_mr_change就知道memslot的变动值了:

1
2
3
4
5
6
enum kvm_mr_change {
	KVM_MR_CREATE,
	KVM_MR_DELETE,
	KVM_MR_MOVE,
	KVM_MR_FLAGS_ONLY,
};

在往下是一段检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) {
	/* Check for overlaps */
	r = -EEXIST;
	kvm_for_each_memslot(slot, kvm->memslots) {
		if ((slot->id >= KVM_USER_MEM_SLOTS) ||
			//下面排除掉准备操作的内存条,在KVM_MR_MOVE中是有交集的
			(slot->id == mem->slot))
			continue;
		//下面就是当前已有的slot与new在guest线性区间上有交集
		if (!((base_gfn + npages <= slot->base_gfn) ||
			  (base_gfn >= slot->base_gfn + slot->npages)))
			goto out;
			//out错误码就是EEXIST
	}
}

如果是新插入内存条,代码则走入kvm_arch_create_memslot函数,里面主要是一个循环,KVM_NR_PAGE_SIZES是分页的级数,此处是3,第一次循环,lpages = gfn_to_index(slot->base_gfn + npages – 1,slot->base_gfn, level) + 1,lpages就是一级页表所需要的page数,大致是npages>>09,然后为slot->arch.rmap[i]申请了内存空间,此处可以猜想,rmap就是一级页表了,继续看,lpages约为npages>>19,此处又多为lpage_info申请了同等空间,然后对lpage_info初始化赋值,现在看不到lpage_info的具体作用,看到后再补上。整体上看kvm_arch_create_memslot做了一个3级的软件页表。

如果有脏页,并且脏页位图为空,则分配脏页位图, kvm_create_dirty_bitmap实际就是”页数/8″.

1
2
3
4
if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) {
	if (kvm_create_dirty_bitmap(&new) < 0)
		goto out_free;
}

当内存条的改变是KVM_MR_DELETE或者KVM_MR_MOVE,先申请一个slots,把kvm->memslots暂存到这里,首先通过id_to_memslot获取准备插入的内存条对应到kvm的插槽是slot,无论删除还是移动,将其先标记为KVM_MEMSLOT_INVALID,然后是install_new_memslots,其实就是更新了一下slots->generation的值,