我在上一篇日志用unshare创建轻量级虚拟环境中介绍了如何用系统调用unshare创建一个虚拟机. 这一个方法正是参照了mininet的实现方式, 我将其用C语言实现, 达到了mininet中创建虚拟网络节点(包括主机和交换机)的效果. 这一篇文章中, 我想直接引用mininet的代码来阐述实现的方法. 除此之外, 我还会介绍mininet识别网络节点输出的结束, 以及中断进程,接收命令, 创建虚拟网卡等功能的实现.

    

  • 1. 网络节点的创建
  •     在介绍之前, 我想介绍一下mnexec.c. 看过mininet代码的童鞋都知道它是用python写的, 但是它却有且只有一个C语言写的文件-mnexec.c, 十分碍眼. 在我看来, mnexec.c正是mininet的灵魂, 所有的操作就必须在它存活的时候进行. 在安装mininet的过程中, 这个C文件会被编译并输出成一个叫”mnexec”的文件, 放在 /usr/local/bin/目录下. 也就是说, 在mininet安装完成后, 除了我们熟悉的”mn”命令, 还生成了另外一个shell命令-“mnexec”(欢迎没事干的童鞋验证). 下面是这个命令的帮助说明:

    Execution utility for Mininet.
    usage: %s [-cdnp] [-a pid] [-g group] [-r rtprio] cmd args...
    -c: close all file descriptors except stdin/out/error
    -d: detach from tty by calling setsid()
    -n: run in new network namespace
    -p: print ^A + pid
    -a pid: attach to pid's network namespace
    -g group: add to cgroup
    -r rtprio: run with SCHED_RR (usually requires -g)

        “-c”/”-d”大概是为了避免意外错误, “-p”我会在后面介绍, “-a”是省事儿的时候用的(不常用), “-g”/”-r”我没研究过=,=. 我是不是漏了”-n”了? 我故意的! 如果加了这个参数的, 就基本能创建一个网络节点了. 来看看”-n”执行的是哪段代码:

    case 'n':
        /* run in network namespace */
        if (unshare(CLONE_NEWNET) == -1) {
            perror("unshare");
            return 1;
        }
        break;

        unshare! 看到没有! 这正是用来从操作系统中隔离并克隆出一个网络名字空间的系统调用啊. 有了它, 执行mnexec命令的那个进程就有了一块独立的网络环境, 离独立的网络节点就不远了.

        unshare! 读了我上一篇博文的童鞋可能知道, 还需要运行起一个bash, 这主要是靠mnexec.c下面这一段代码实现的

    if (optind < argc) {
        execvp(argv[optind], &argv[optind]);
        perror(argv[optind]);
        return 1;
    }

        这段代码会处理mnexec命令其他的参数.

        mnexec.c要介绍的大概就这么多, 接下来要讲它与mininet创建网络节点之间的关系. mininet中网络节点的操作主要是由Node()类来实现. 创建一个网络节点就是在Node()类初始化的时候进行的. 在Node().__init__()中, 可以看到下面的代码:

    # mnexec: (c)lose descriptors, (d)etach from tty,
    # (p)rint pid, and run in (n)amespace
    opts = '-cdp'
    if self.inNamespace:
        opts += 'n'
    # bash -m: enable job control
    cmd = [ 'mnexec', opts, 'bash', '-m' ]
    self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
        close_fds=True )

        这里Popen函数是Subprocess库里的Popen函数, 它的功能是执行shell命令”cmd”并且用一个管道将该命令的进程与使用该函数的进程连通. 根据之前我对mnexec的介绍, 这段代码的作用就是用mnexec命令创建一个有独立网络名字空间并且执行着bash的进程, 进程的标准输入/输出/错误都指向管道. 如此以来, 当创建一个Node()时, 就能得到一个有独立网络环境的节点.

        

  • 2. 网络节点输出的结束
  •     用户输入的shell命令会通过管道传给网络节点, 之后mininet会再从管道中获得网络节点的输出. 由于输出是不以EOF结束的, 所以判断输出的结束需要特殊的处理. 在mininet中, 为了识别出输出的结束, 它除了让网络节点打印出用户输入的shelle命令的输出外, 还让网络节点在输出结束后打印一个不常用的字符.

    if len( cmd ) > 0 and cmd[ -1 ] == '&':
        # print ^A{pid}n{sentinel}
        cmd += ' printf "01%dn177" $! n'
    else:
        # print sentinel
        cmd += '; printf "177"'
        if printPid and not isShellBuiltin( cmd ):
            cmd = 'mnexec -p ' + cmd
    self.write( cmd + 'n' )

        该代码中, 字符”177″就是那个字符, 按其注释的说法, 这叫一个sentinel(哨兵). 也就说, 由管道传给网络节点的命令除了用户命令外, 还有一个printf “177” 的命令, 这会让网络节点在执行完用户的命令后打印一个sentinel字符. 这样, 当mininet从管道中读到sentinel字符时, 就表明输出结束了.

        

  • 3. 中断进程的实现
  •     在上一段代码

    if len( cmd ) > 0 and cmd[ -1 ] == '&':
        # print ^A{pid}n{sentinel}
        cmd += ' printf "01%dn177" $! n'
    else:
        # print sentinel
        cmd += '; printf "177"'
        if printPid and not isShellBuiltin( cmd ):
            cmd = 'mnexec -p ' + cmd
    self.write( cmd + 'n' )

    除了进行输出结束的处理, 还有一个任务就是为用户随时可能中断进程的动作做准备. 所谓的准备就是在命令的输出的最开始地方打印该命令的进程的进程号(pid). 打印pid分为两种情况, 一种情况该命令是一条在后台执行的命令(即最后一个字符是”&”), 这时需要用printf “01%dn177″ $! n的方法获取pid并打印出来; 第二种情况该命令是前台执行的命令, 无法直接获取pid, 这时就需要再次用到mnexec命令的”-p”参数. 之前我说过要在后面介绍”-p”参数, it’s the time! 当使用”-p”参数时, mnexec在执行过程中会先打印出该进程的进程号, 再执行之后的命令(cmd). 代码如下:

    case 'p':
        /* print pid */
        printf("�01%dn", getpid());
        fflush(stdout);
        break;

        所以说, 当执行cmd = ‘mnexec -p ‘ + cmd这一条语句时, mininet会先执行mnexec打印出进程号,然后再执行命令. 当mininet从管道输出中读到符号”^A”时,会把之后的字符记录成pid, 以便做好中断的准备.

        所记录的pid会存在一个叫lastPid的变量中, 当用户用”CTRL+C”中断进程, 产生一个KeyboardInterrupt信号时, 会执行该网络节点的sendInt()调用. 该调用的原型如下:

    def sendInt( self, sig=signal.SIGINT ):
        "Interrupt running command."
        if self.lastPid:
            try:
                os.kill( self.lastPid, sig )
            except OSError:
                pass

        可以看出, 使用该调用会将lastPid所代表的进程杀掉, 达到中断进程的效果.

        

  • 4. 接收命令
  •     mininet直接用了python中的cmd库, 比较简单直接. 具体实现可以看cli.py.

        

  • 5. 创建虚拟网卡
  •     上篇日志中我已经介绍了, 可以用两条ip命令创建虚拟网卡. 在mininet中分别用了makeIntfPair()调用的代码

    # Delete any old interfaces with the same names
    quietRun( 'ip link del ' + intf1 )
    quietRun( 'ip link del ' + intf2 )
    # Create new pair
    cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2
    return quietRun( cmd )

    和moveIntfNoRetry()的代码

    cmd = 'ip link set ' + intf + ' netns ' + repr( node.pid )
    quietRun( cmd )

    来实现虚拟网卡的建立.

    lidemin OpenFlow , ,

    4 Replies

    1. 前面几位大神,受小弟一拜,越来越发现自己太年轻,懂的太少……还有好多要学习的啊!

    Leave a Reply

    Your email address will not be published. Required fields are marked *