CORE网络仿真软件分析

时间:2024-03-09 09:24:28

CORE采用LXC(Linux namespace Container)技术和Bridge技术实现虚拟主机和虚拟网络的仿真模拟。LXC利用cgroups子系统中的进程组资源管理框架将虚拟主机实现为同一个组相对独立的进程。LXC已加入到内核2.6.28版本,CORE对虚拟主机创建和管理,通过的C语言系统调用来的实现,具体代码实现在core/daemon/src文件夹下。

文件列表

文件名称

功能说明

vnoded_main.c

创建LXC容器运行的守护进程vnoded(PID 1)

vcmd_main.c

tool, 指定LXC容器运行命令

netns_main.c

tool, 创建LXC容器中运行指定程序

netns.h, netns.c

提供LXC容器管理操作

netnsmodule.c

提供LXC容器管理操作的Python库

vnode_server.h, .c

 

vnode_client.h, .c

 

vnode_cmd.h, .c

 

vnode_msg.h, .c

 

vnode_io.h, .c

提供了io基本操作

vnode_chnl.h, .c

提供了针对vnoded进程的连接管理操作

 

 

 

 

 

 

 

 

 

LXC容器(netns.h, netns.c)

Linux下通过进程克隆(syscall调用)来创建LXC容器。每个LXC容器PID、IPC、Network等系统资源不再是全局的,而是属于特定的Namespace,每个Namespace中的资源相对其它Namespace是透明的。

pid = syscall(SYS_clone, flags | NSCLONEFLGS, NULL, NULL, NULL, NULL)

在创建新的LXC容器时需要指定相应的flag。

#define NSCLONEFLGS                      \           //netns.c

  (                                      \

   SIGCHLD       |                       \

   CLONE_NEWNS   |                        \

   CLONE_NEWUTS  |                       \

   CLONE_NEWIPC  |                        \

   CLONE_NEWPID       |                          \

   CLONE_NEWNET                                  \

)

l        SIGCHLD表示创建进程退出后向父进程发送SIGCHLD信号

l        CLONE_NEWNS表示创建进程设置独立的文件层次视图

l        CLONE_NEWUTS表示创建进程设置独立的主机名称

l        CLONE_NEWIPC表示创建进程设置独立的IPC环境

l        CLONE_NEWPID表示创建进程设置独立的PID环境

l        CLONE_NEWNET表示创建进程独立的网络环境

以上flags可以组合使用,根据需要进行创建LXC容器。

 

vnoded进程(vnoded_main.c)

vnoded进程是虚拟主机创建后运行的第一个进程(pid=1),它由pycore调用/usr/sbin/vnoded命令来创建,命令执行时需要提供的参数有newnetns, ctrlchnlname, logfilename, pidfilename等。vnoded进程是一个守护进程,执行时会进入消息循环,直至退出。

ev_loop(vnodeserver->loop, 0);

       vnoded进程相当于LXC容器内运行的操作系统,因此它提供了简易操作系统功能:文件管理,用户访问,进程管理三个基本功能。vnoded进程执行分为三个步骤:

Step1. 创建用户访问ctrlchnl

CORE虚拟出来的节点需要通过ctrlchnl进行访问,ctrlchnl其实是SOCK_SEQPACKET 类型的Socket进程间通讯机制。ctrlchnl建立位于进程克隆之前,这样克隆出来的进程(子进程)自然可以访问该Socket句柄。

fd = socket(AF_UNIX, SOCK_SEQPACKET, 0)

bind(fd, (struct sockaddr *)&addr, sizeof(addr))

Step2. 创建容器(namespace)。

pid = nsfork(0);

子进程:

    关闭父进程打开的所有文件;设置输入/输出。

for (i = 3; i < openmax; i++)

if (i != ctrlfd) close(i);

DUPFILE("/dev/null", O_RDONLY, STDIN_FILENO);

setvbuf(stdout, NULL, _IOLBF, 0);

setvbuf(stderr, NULL, _IOLBF, 0);

父进程:

退出等待。

_exit(0);     /* nothing else for the parent to do */

Step3. 建立监听消息循环。

vnoded进程(运行在容器中)建立两个消息循环,一个是Ctrlchnl消息循环,接收用户命令。另一个是子进程消息,接收子进程信息。这两个消息循环的实现采用了libev库,封装在vnode_server_t结构体中来完成。

server = vnode_newserver(ev_default_loop(0), ctrlfd, ctrlchnlname);

 

vcmd命令(vcmd_main.c)

vcmd命令用于指定容器里运行程序,确切地说是向vnoded进程发送消息,让它clone出一个新的进程。关于网卡和链路的配置都需要通过vcmd命令进行。。为了指定LXC容器,vcmd执行时需要指定容器的ctrlchnl

Linux下的程序需要标准输入输出和出错输出,可以是管道(Pipe),也可以是终端(Pty),统称为I/O。用vcmd命令执行另一个程序,需要为该程序指定I/O,通过参数-I, -i, -q等。vmd定义了三种输入输出I/O类型:文件、管道、终端,V4.5版本中仅仅实现终端类型,即VCMD_IO_PTY。

typedef enum {

  VCMD_IO_NONE = 0,

  VCMD_IO_FD,

  VCMD_IO_PIPE,

  VCMD_IO_PTY,

} vnode_client_cmdiotype_t;

Vcmd命令执行分为以下四个步骤。

Step1. 初始化输入输出

根据命令参数,对终端、管道进行初始化。

vcmd.cmdio = vnode_open_clientcmdio(iotype);

Step2. 建立容器访问客户端。

       容器访问客户端封装在vnode_client_t结构体中。vcmd命令会创建vnode_client_t结构体,并通过它与服务器进程(vnoded)访问。

vcmd.client = vnode_client(ev_default_loop(0), ctrlchnlname,

                          vcmd_ioerrorcb, &vcmd);

Step3. 建立命令回复响应消息循环。

vcmd命令进程在执行时,接收用户输入,并将输入传递给容器中的进程,同时也将容器中的进程执行回复显示在用户终端,因此vcmd需要建立两个消息循环:自身I/O消息循环和容器中的进程的I/O消息循环。

vcmd->stdin_fwdfd = vcmd->cmdio->stdiopty.masterfd;

vcmd->stdin_watcher.data = &vcmd->stdin_fwdfd;

ev_io_init(&vcmd->stdin_watcher, vcmd_rwcb,

STDIN_FILENO, EV_READ);

ev_io_start(loop, &vcmd->stdin_watcher);

 

vcmd->ptymaster_fwdfd = STDOUT_FILENO;

vcmd->ptymaster_watcher.data = &vcmd->ptymaster_fwdfd;

ev_io_init(&vcmd->ptymaster_watcher, vcmd_rwcb,

              vcmd->cmdio->stdiopty.masterfd, EV_READ);

ev_io_start(loop, &vcmd->ptymaster_watcher);

Step4. 发送命令消息。

vcmd通过argc, argv获得用户想要在容器中执行程序的名称和参数,然后通过ctrlchnl发送给vnoded进程,然后由vnoded进程通过forkexec来执行。为了编程方便,vcmd作为如下层次抽象。

 

图1  vcmd消息传递机制

命令层cmd交给用户客户端vnode_client,用户客户端交给消息层msg,然后通过Socket发送给Vnode进程的消息层msg,上传给服务客户端,然后由命令层cmd解包获得命令参数进行执行。此外,Vcmd还与子进程建立了标准输入输出。

 

vnode_server_t结构体(vnoded_server.h, .c)

    vnoded进程启动的系统服务功能为接收用户请求连接,根据用户命令,在容器中执行(forkexec)相应程序,并将程序结果返回给用户。由于用户可能有多个,执行的程序也会有多个,vnode_server_t结构体用队列clientlist,cmdlist来存储。serverfd用于存储用户连接绑定的contrlchnl建立的Socket。fdwatcher和childwatcher分别是建立两个消息观察器,前者观察用户连接消息,后都观察子进程执行消息。

typedef struct {

  TAILQ_HEAD(clientlist, cliententry) clientlisthead;

  TAILQ_HEAD(cmdlist, cmdentry) cmdlisthead;

  struct ev_loop *loop;

  char ctrlchnlname[PATH_MAX];

  char pidfilename[PATH_MAX];

  int serverfd;

  ev_io fdwatcher;

  ev_child childwatcher;

} vnode_server_t;

用户连接消息循环

用户连接消息由回调函数vnode_server_cb处理。

static void vnode_server_cb(struct ev_loop *loop, ev_io *w, int revents)

该函数将生成vnode_client对象,加入到clientlist队列中,并启动vnode_client对象的msgio消息循环,之后该client对象与server的io处理交给msgio消息循环处理。

vnode_msgiostart(&client->msgio, server->loop, client->clientfd, client, client_ioerror, msghandler)

 

子进程消息循环

子进程消息由回调函数vnode_child_cb处理。

static void vnode_child_cb(struct ev_loop *loop, ev_child *w, int revents)

该函数根据子进程执行返回的状态,向用户发送状态信息。

vnode_send_cmdstatus(client->clientfd, cmd->cmdid, w->rstatus)

之后,从cmdlist中将相应的cmd移除。

TAILQ_REMOVE(&server->cmdlisthead, cmd, entries);

 

vnode_client_t结构体(vnoded_client.h, .c)

       vnode_client_t结构体用于创建用户客户端实体,对应于vnoded进程中一个vnode_cliententry_t表项。vnode_client_t接收用户命令vcmd_t,并将用户命令封装在vnode_msgio结构体中,并vnode_msgiostart启动该消息循环。

typedef struct vnode_client {

  TAILQ_HEAD(cmdlist, cmdentry) cmdlisthead;

  struct ev_loop *loop;

  int serverfd;

  struct vnode_msgio msgio;

  void *data;

  vnode_clientcb_t ioerrorcb;

  int32_t cmdid;

} vnode_client_t;

       cmdlist用于存储vcmd_t,但理解上一条命令对应一个用户客户端,不需要用列表来存储,检查代码后发现没有往该列表中插入任何对象,vcmd_t存在data中。serverfd用于存储vnoded进程ctrlchnl。msgio存储msg消息层结构体vnode_msgio。Ioerrorcb用于存储服务器端io出错回调函数。cmdid用于存储子进程id号。

用户客户端向服务客户端发送消息,应该向消息层指定消息响应回调。程序定义了四种消息类型。

typedef enum {

  VNODE_MSG_NONE = 0,

  VNODE_MSG_CMDREQ,

  VNODE_MSG_CMDREQACK,

  VNODE_MSG_CMDSTATUS,

  VNODE_MSG_CMDSIGNAL,

  VNODE_MSG_MAX,

} vnode_msgtype_t;

但实际只指定了两个类型的回调。

static const vnode_msghandler_t msghandler[VNODE_MSG_MAX] = {

    [VNODE_MSG_CMDREQACK] = vnode_clientrecv_cmdreqack,

    [VNODE_MSG_CMDSTATUS] = vnode_clientrecv_cmdstatus,

 };

 

vnode_msgio_t结构体(vnoded_msg.h, .c)

       vnode_msgio_t负责收发消息,它维护的重点是fd的处理,但不负责fd的初始化,且msghandler也是由client为它指定。

typedef struct vnode_msgio {

  struct ev_loop *loop;

  int fd;

  ev_io fdwatcher;

  vnode_msgbuf_t msgbuf;

  void *data;

  vnode_msghandler_t ioerror;

  vnode_msghandler_t msghandler[VNODE_MSG_MAX];

} vnode_msgio_t;

       vnode_msgio_t最关键的两个函数是收发消息函数。

ssize_t vnode_sendmsg(int fd, vnode_msgbuf_t *msgbuf);

ssize_t vnode_recvmsg(vnode_msgio_t *msgio);

vnode_sendmsg可由外部调用,收消息vnode_recvmsg由fdwatcher在消息触发时在vnode_msg_cb回调函数中调用,读取消息后根据消息类型,选择不同的msghandler处理。

msghandlefn = msgio->msghandler[msgio->msgbuf.msg->hdr.type];

msghandlefn(msgio);

 

vnode_cmd.h, .c, vcmdmodule.c

       定义了结构体vnode_cmdentry_t,但没有引用(可见代码是在其它代码上修改而来)。定义了命令层次收发函数。主要是发送命令请求vnode_send_cmdreq,接收命令请求vnode_recv_cmdreq,发送命令状态vnode_send_cmdstatus,发送命令信号vnode_send_cmdsignal,接收命令信号vnode_recv_cmdsignal。

void vnode_recv_cmdreq(vnode_msgio_t *msgio);

int vnode_send_cmdreq(int fd, int32_t cmdid, char *argv[], int infd, int outfd, int errfd);

int vnode_send_cmdstatus(int fd, int32_t cmdid, int32_t status);

int vnode_send_cmdsignal(int fd, int32_t cmdid, int32_t signum);

void vnode_recv_cmdsignal(vnode_msgio_t *msgio);

部分函数只有定义,并没有调用,估计在完善中,例如vnode_send_cmdstatus和

vnode_recv_cmdsignal。