连接管理模块是Open vSwitch中非常重要的模块,如果能够熟悉掌握其中的关联结构,对于开发Open vSwitch起到事半功倍的效果。写这篇博客是比较难写的,因为里面涉及层次比较多,生怕梳理不到位,反而误导大家。
Open vSwitch中虽然注释不多,但是它的层次结构非常好而且函数也非常短小。如果阅读过此套代码的人会有比较深入的感触。通过阅读代码可知道,在Open vSwitch中所有函数名、结构体名字定义在头文件中都是接口类,相应的定义到源文件中函数、结构体都是内部类,而且函数都是静态函数。这里需要澄清一下概念,此处提到类并非是C++中的类,而是广义的概念,也就是说Open vSwitch是采用面向对象思想,采用C语言实现的一套软件。
一、结构体
我们还是从结构体入手把,因为结构体是软件的灵魂和桥梁。只有将结构体分析透彻才能梳理出整个软件的架构层次。
外部流结构(外部接口):
1
2
3
4
5
6
7
8
9
10
11
|
/* Active stream connection. 主动 流连接*/
/* Active stream connection.
*
* This structure should be treated as opaque by implementation. */
struct
stream
{
const
struct
stream_class
*
class
;
/* 实际 流对象 */
int
state
;
/* 连接状态 取值为SCS_CONNECTING等枚举 */
int
error
;
/* 错误码 */
char
*
name
;
/* 形式hostIP:port 例如name = "127.0.0.1:6653" */
}
;
|
流对象(外部接口):
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
|
struct
stream_class
{
/* Prefix for connection names, e.g. "tcp", "ssl", "unix". */
const
char
*
name
;
/* True if this stream needs periodic probes to verify connectivity. For
* streams which need probes, it can take a long time to notice the
* connection was dropped. */
bool
needs_probes
;
/* Attempts to connect to a peer. 'name' is the full connection name
* provided by the user, e.g. "tcp:1.2.3.4". This name is useful for error
* messages but must not be modified.
*
* 'suffix' is a copy of 'name' following the colon and may be modified.
* 'dscp' is the DSCP value that the new connection should use in the IP
* packets it sends.
*
* Returns 0 if successful, otherwise a positive errno value. If
* successful, stores a pointer to the new connection in '*streamp'.
*
* The open function must not block waiting for a connection to complete.
* If the connection cannot be completed immediately, it should return
* EAGAIN (not EINPROGRESS, as returned by the connect system call) and
* continue the connection in the background. */
int
(
*
open
)
(
const
char
*
name
,
char
*
suffix
,
struct
stream
*
*
streamp
,
uint8_t
dscp
)
;
/* Closes 'stream' and frees associated memory. */
void
(
*
close
)
(
struct
stream
*
stream
)
;
/* Tries to complete the connection on 'stream'. If 'stream''s connection
* is complete, returns 0 if the connection was successful or a positive
* errno value if it failed. If the connection is still in progress,
* returns EAGAIN.
*
* The connect function must not block waiting for the connection to
* complete; instead, it should return EAGAIN immediately. */
int
(
*
connect
)
(
struct
stream
*
stream
)
;
/* Tries to receive up to 'n' bytes from 'stream' into 'buffer', and
* returns:
*
* - If successful, the number of bytes received (between 1 and 'n').
*
* - On error, a negative errno value.
*
* - 0, if the connection has been closed in the normal fashion.
*
* The recv function will not be passed a zero 'n'.
*
* The recv function must not block waiting for data to arrive. If no data
* have been received, it should return -EAGAIN immediately. */
ssize_t
(
*
recv
)
(
struct
stream
*
stream
,
void
*
buffer
,
size
_t
n
)
;
/* Tries to send up to 'n' bytes of 'buffer' on 'stream', and returns:
*
* - If successful, the number of bytes sent (between 1 and 'n').
*
* - On error, a negative errno value.
*
* - Never returns 0.
*
* The send function will not be passed a zero 'n'.
*
* The send function must not block. If no bytes can be immediately
* accepted for transmission, it should return -EAGAIN immediately. */
ssize_t
(
*
send
)
(
struct
stream
*
stream
,
const
void
*
buffer
,
size
_t
n
)
;
/* Allows 'stream' to perform maintenance activities, such as flushing
* output buffers.
*
* May be null if 'stream' doesn't have anything to do here. */
void
(
*
run
)
(
struct
stream
*
stream
)
;
/* Arranges for the poll loop to wake up when 'stream' needs to perform
* maintenance activities.
*
* May be null if 'stream' doesn't have anything to do here. */
void
(
*
run_wait
)
(
struct
stream
*
stream
)
;
/* Arranges for the poll loop to wake up when 'stream' is ready to take an
* action of the given 'type'. */
void
(
*
wait
)
(
struct
stream
*
stream
,
enum
stream_wait_type
type
)
;
}
;
|
内部流结构(内部管理接口):
1
2
3
4
5
6
7
|
/* Active file descriptor stream. */
/* 主动接受连接 */
struct
stream_fd
{
struct
stream
stream
;
/* 流管理对象 */
int
fd
;
/* socket套接字描述符 */
}
;
|
从上面可知,前两个是对外接口 (定义在头文件中),最后一个是内部接口,用于实际管理。其中第二个结构的成员机会都是函数指针,从面向对象的角度来说,这个结构对应着抽象类 (或者接口) ,主要目的是实现多态。三者是如何管理是如何关联起来的呢? 是通过一个非常短的函数 new_fd_stream,如下所示:
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
|
/* Creates a new stream named 'name' that will send and receive data on 'fd'
* and stores a pointer to the stream in '*streamp'. Initial connection status
* 'connect_status' is interpreted as described for stream_init().
*
* Returns 0 if successful, otherwise a positive errno value. (The current
* implementation never fails.)
* name = "127.0.0.1:6653"
*/
int
new_fd_stream
(
const
char
*
name
,
int
fd
,
int
connect_status
,
struct
stream
*
*
streamp
)
{
struct
stream_fd
*
s
;
s
=
xmalloc
(
sizeof
*
s
)
;
/* 分配内部流管理 */
/* 关联stream_fd_class */
stream_init
(
&
s
->
stream
,
&
stream_fd_class
,
connect_status
,
name
)
;
s
->
fd
=
fd
;
*
streamp
=
&
s
->
stream
;
/* 返回外部流对象 这点很重要 */
return
0
;
}
/* Initializes 'stream' as a new stream named 'name', implemented via 'class'.
* The initial connection status, supplied as 'connect_status', is interpreted
* as follows:
*
* - 0: 'stream' is connected. Its 'send' and 'recv' functions may be
* called in the normal fashion.
*
* - EAGAIN: 'stream' is trying to complete a connection. Its 'connect'
* function should be called to complete the connection.
*
* - Other positive errno values indicate that the connection failed with
* the specified error.
*
* After calling this function, stream_close() must be used to destroy
* 'stream', otherwise resources will be leaked.
*
* The caller retains ownership of 'name'. */
void
stream_init
(
struct
stream
*
stream
,
const
struct
stream_class
*
class
,
int
connect_status
,
const
char
*
name
)
{
memset
(
stream
,
0
,
sizeof
*
stream
)
;
stream
->
class
=
class
;
stream
->
state
=
(
connect_status
==
EAGAIN
?
SCS_CONNECTING
:
!
connect
_status
?
SCS_CONNECTED
:
SCS_DISCONNECTED
)
;
stream
->
error
=
connect_status
;
stream
->
name
=
xstrdup
(
name
)
;
ovs_assert
(
stream
->
state
!=
SCS_CONNECTING
||
class
->
connect
)
;
}
|
针对上面两个函数需要说明三点:
1、stream_fd_class 是内部静态全局常量,类型是 stream_class。通过定义可知,内部函数指针实际指向的是流操作相关函数,例如: send,recv ,close等。换句话说,当进行真正的 socket数据传输过程中主要是通过下面的函数。函数指针 open为什么NULL?? 没有open函数怎么可能连接起来呢??
1
2
3
4
5
6
7
8
9
10
11
12
|
static
const
struct
stream_class
stream_fd_class
=
{
"fd"
,
/* name */
false
,
/* needs_probes */
NULL
,
/* open */
fd_close
,
/* close */
fd_connect
,
/* connect */
fd_recv
,
/* recv */
fd_send
,
/* send */
NULL
,
/* run */
NULL
,
/* run_wait */
fd_wait
,
/* wait */
}
;
|
2、连接socket套接字保存在内部流管理fd中并非在外部流管理中。
3、函数返回并非是内部流管理而是外部流管理。当进行最底层数据处理时候,在转换成内部流管理,可以参考一下fd_send函数。
这个在介绍一个全局stream管理结构,用面向对象思想这个结构体是抽象类(接口)的实现。此结构体是也静态局部常量,主要用于连接方式管理,例如tcp连接,unix连接,ssl方式连接,甚至跨平台window方式连接。
1
2
3
4
5
6
7
8
9
10
11
|
static
const
struct
stream_class
*
stream_classes
[
]
=
{
&
tcp_stream_class
,
#ifndef _WIN32
&
unix_stream_class
,
/* unix域 相同主机不同进程间通信 */
#else
&
windows_stream_class
,
#endif
#ifdef HAVE_OPENSSL
&
ssl_stream_class
,
#endif
}
;
|
这里我们在讲tcp_stream_class展开看一下内部结构:
1
2
3
4
5
6
7
8
9
10
11
12
|
const
struct
stream_class
tcp_stream_class
=
{
"tcp"
,
/* name */
true
,
/* needs_probes */
tcp_open
,
/* open */
NULL
,
/* close */
NULL
,
/* connect */
NULL
,
/* recv */
NULL
,
/* send */
NULL
,
/* run */
NULL
,
/* run_wait */
NULL
,
/* wait */
}
;
|
当我看到tcp_stream_class定义的时候,也是很奇怪,为什么tcp_stream_class只定义了open,定义没有其他函数呢??
但是当tcp_stream_class和stream_fd_class整合到一起,除了run和run_wait外其他函数指针都定义了,貌似这两者存在着某种关联??我只能深入梳理代码,去验证我的猜测。
二、连接创建
上面数据结构主要是用于Open vSwitch交换机与控制器连接(Socket连接),下面我们来分析一下,Open vSwitch是如何一步一步与控制器建立起来的socket连接。
下图是我通过Source Insight截取出来的函数调用关系。函数层次比较深入,有些地方讲解可能不够深入或者层次不清晰,因此对于这部读者需要自己深入看一下代码加深理解。
第一部分讲解是Open vSwitch中最底层socket连接管理层次,对于上层应用来说这个层次是不可见的。也就是说Open vSwitch不是直接操作stream类的,而是通过ofconn管理,或者在深一层是通过vconn管理。下面是函数流程图,针对此流程图有两点说明:
1)红色线代表返回上层函数,蓝色线表示回到最顶层函数。
2)由于函数层次较深,图中会有向右走向的函数,纯粹是为了节省篇幅
对于SDN 来说,理论上一个控制器可以管理多个交换机,一个交换机也可以被多个控制器管理。因此Open vSwitch在启动过程中,根据配置会主动连接每个控制器。
ofproto_set_controllers à add_controller 创建rconn 和ofcon,并且将 rconn挂在ofconn 中 à rconn_connect àreconnect à vconn_open 这个函数中会根据连接类型 (tcp连接,unix 连接等)在抽象类 vconn_classes中查找对应的实例。找到实例之后调用对应的 open函数,开始创建socket以及出事 tream流对象等一系列动作,最后返回到函数 connmgr_set_controller判断是否还有其他的控制器需要连接,如果有则再次执行此流程,如果没有则结束。
上面的流程比较复杂,需要我们认真去看一下代码,此处只是起到“抛砖引玉”的效果。最后展示一下数据结构关系图:
在ovs数据结构中,有很多这样的数据结构,比如说有结构 A,B ,结构B包含在结构 A中,对外使用的是结构B。类似代码中 struct vconn_stream,struct vconn 对外使用的是 struct vconn。至于ovs 为什么要这样设计的原因,我不是很清楚,只能猜测是为了封装和抽象。