vpp/vpp一点又一点のvppctl

概述

我们在运行vpp之后,通常使用vppctl作为vpp的CLI,实现和vpp之间的交互。 比如:


[root@localhost ~]# systemctl start vpp
[root@localhost ~]# 
[root@localhost ~]# ps -aef | grep vpp
root      1274     1 99 09:47 ?        00:00:13 /usr/bin/vpp -c /etc/vpp/startup.conf
root      1324 32737  0 09:48 pts/1    00:00:00 grep --color=auto vpp
[root@localhost ~]# 
[root@localhost ~]# 
[root@localhost ~]# vppctl 
    _______    _        _   _____  ___ 
 __/ __/ _ \  (_)__    | | / / _ \/ _ \
 _/ _// // / / / _ \   | |/ / ___/ ___/
 /_/ /____(_)_/\___/   |___/_/  /_/    

vpp# 
vpp# show version 
vpp v20.05-release built by root on localhost.localdomain at 2020-07-15T09:05:09
vpp# 

今天简单的来探究一下vppctl是如何工作的,我们参考的代码是 vpp20.05,vppctl的代码入口位于目录 src/app/vppctl.c 中。

代码走读

文件 src/app/vppctl.c 很小,不到400行,实际调用的库函数也不多。
简单描述来说,vppctl基本上在用户和vpp之间起到了命令和响应的传递作用。 我们分成x部分来看:

初始化

变量定义部分:


int main (int argc, char *argv[])
{
  clib_socket_t _s = {0}, *s = &_s;
  clib_error_t *error = 0;
  struct epoll_event event;
  struct sigaction sa;
  struct termios tio;
  int efd = -1;
  u8 *str = 0;
  u8 *cmd = 0;
  int do_quit = 0;
  int is_interactive = 0;
  int acked = 1; /* counts messages from VPP; starts at 1 */
  int sent_ttype = 0;

这里用到了 vpp 的几个变量,以及 linux 的 epoll、signal 等机制,我们逐一描述。

clib_socket_t

这个结构体定义如下,大概把socket中需要的内容都包括进去了,包括实际的fd、fd对应的host:port、对端的地址peer,以及发送、接收指针,发送、接收、关闭等函数。
这里并没有标明是什么类型的socket,可以是tcp、udp或者unix等。


typedef struct _socket_t
{
  /* File descriptor. */
  i32 fd;

  /* Config string for socket HOST:PORT or just HOST. */
  char *config;

  u32 flags;
#define CLIB_SOCKET_F_IS_SERVER (1 << 0)
#define CLIB_SOCKET_F_IS_CLIENT (0 << 0)
#define CLIB_SOCKET_F_RX_END_OF_FILE (1 << 2)
#define CLIB_SOCKET_F_NON_BLOCKING_CONNECT (1 << 3)
#define CLIB_SOCKET_F_ALLOW_GROUP_WRITE (1 << 4)
#define CLIB_SOCKET_F_SEQPACKET (1 << 5)
#define CLIB_SOCKET_F_PASSCRED  (1 << 6)


  /* Transmit buffer.  Holds data waiting to be written. */
  u8 *tx_buffer;

  /* Receive buffer.  Holds data read from socket. */
  u8 *rx_buffer;

  /* Peer socket we are connected to. */
  struct sockaddr_in peer;

  /* Credentials, populated if CLIB_SOCKET_F_PASSCRED is set */
  pid_t pid;
  uid_t uid;
  gid_t gid;

  clib_error_t *(*write_func) (struct _socket_t * sock);
  clib_error_t *(*read_func) (struct _socket_t * sock, int min_bytes);
  clib_error_t *(*close_func) (struct _socket_t * sock);
  clib_error_t *(*recvmsg_func) (struct _socket_t * s, void *msg, int msglen,
                 int fds[], int num_fds);
  clib_error_t *(*sendmsg_func) (struct _socket_t * s, void *msg, int msglen,
                 int fds[], int num_fds);
  uword private_data;
} clib_socket_t;

clib_error_t

这是一个比较常用的结构体,一般用来描述函数返回的状态之类的。
结构体可以保存的内容包括:错误代码code、以及什么错误what和在哪里发生的错误where,what和where其实都是指针。
这里需要注意的是,如果一个函数调用正常,那么应该返回 (clib_error_t *)0,而不是返回一个 clib_error_t->code = 0。


typedef struct
{
  /* Error message. */
  u8 *what;

  /* Where error occurred (e.g. __FUNCTION__ __LINE__) */
  const u8 *where;

  uword flags;

  /* Error code (e.g. errno for Unix errors). */
  any code;
} clib_error_t;

Linuxの epoll_event、sigaction和termios

后面具体代码中描述其使用情况。

函数参数初始化和使用

标准 main 函数带了两个参数 argc 和 argv,argc 表示程序执行时携带了多少个参数(包括程序名自己),argv 依次记录了程序名称和参数。
vppctl的第一部分就是表明了两点:

  1. 如果通过参数指定和vpp之间的通讯接口,那么第一个和第二个参数应该是分别是 -s 和通讯接口,通讯接口被写入了 s->config,在结构体 clib_socket_t 定义的注释中也说明了 config 用于保存host:port,当然也可以保存unix socket的文件路径。 如果没有指定通讯接口,则使用了默认通讯接口 SOCKET_FILE,是一个unix socket文件。
  2. 如果没有指定通讯接口的参数、或者除了通讯接口的参数外,其它参数都通过空格组成一个字符串cmd,其实就是一个命令行,即直接执行某一条命令,而不是进入交互模式。

这部分还有两个小功能:

  1. clib_mem_init (0, 64ULL << 10); 这是初始化部分,参考后面 函数调用的 《### clib_mem_init》
  2. s->flags = CLIB_SOCKET_F_IS_CLIENT; 标明自己是client,对应vpp那端打开的socket应该就是 server。

  clib_mem_init (0, 64ULL << 10);

  /* process command line */
  argc--;
  argv++;

  if (argc > 1 && strncmp (argv[0], "-s", 2) == 0)
    {
      s->config = argv[1];
      argc -= 2;
      argv += 2;
    }
  else
    s->config = SOCKET_FILE;

  while (argc--)
    cmd = format (cmd, "%s%c", (argv++)[0], argc ? ' ' : 0);

  s->flags = CLIB_SOCKET_F_IS_CLIENT;

和用户之间通讯接口的建立和初始化

作为一个CLI,一方面作为MMI连接着用户、另一方面连接后台程序(通常是一个Daemon程序),vppctl首先建立的是这两个接口。
vppctl和vpp之间可以用unix socket或者socket进行通讯,这个有vppctl执行时的参数决定,建立连接的函数就一个 clib_socket_init,在后面 函数调用的 《### clib_socket_init》 中有对这个函数做描述。
接着根据需要,确定是否要建立和用户之间的接口、或者是直接执行cmd,只有在当前交互接口存在(标准输入未被关闭)并且vppctl没有指定命令的情况下,才会进入交互模式(初始化和用户之间的接口)。

和用户的交互接口,其实就是标准输入输出,所以不存在打开文件或者socket的说法,初始化包括两部分:

  1. 注册信号处理函数,包括窗口大小变化消息 SIGWINCH、以及结束消息 SIGTERM
  2. 设置标准输入接口的属性:不允许回显字符、不允许输入特殊字符等。

  error = clib_socket_init (s);
  if (error)
    goto done;

  is_interactive = isatty (STDIN_FILENO) && cmd == 0;

  if (is_interactive)
    {
      /* Capture terminal resize events */
      clib_memset (&sa, 0, sizeof (struct sigaction));
      sa.sa_handler = signal_handler_winch;
      if (sigaction (SIGWINCH, &sa, 0) < 0)
        {
          error = clib_error_return_unix (0, "sigaction");
          goto done;
        }

      /* Capture SIGTERM to reset tty settings */
      sa.sa_handler = signal_handler_term;
      if (sigaction (SIGTERM, &sa, 0) < 0)
        {
          error = clib_error_return_unix (0, "sigaction");
          goto done;
        }

      /* Save the original tty state so we can restore it later */
      if (tcgetattr (STDIN_FILENO, &orig_tio) < 0)
        {
          error = clib_error_return_unix (0, "tcgetattr");
          goto done;
        }

      /* Tweak the tty settings */
      tio = orig_tio;
      /* echo off, canonical mode off, ext'd input processing off */
      tio.c_lflag &= ~(ECHO | ICANON | IEXTEN);
      tio.c_cc[VMIN] = 1;  /* 1 byte at a time */
      tio.c_cc[VTIME] = 0; /* no timer */

      if (tcsetattr (STDIN_FILENO, TCSAFLUSH, &tio) < 0)
        {
          error = clib_error_return_unix (0, "tcsetattr");
          goto done;
        }
    }

初始化epool

将 STDIN_FILENO 和 s->fd(和vpp之间的接口)分别加入epool的efd中,主要监听 输入、错误等事件


  efd = epoll_create1 (0);

  /* register STDIN */
  event.events = EPOLLIN | EPOLLPRI | EPOLLERR;
  event.data.fd = STDIN_FILENO;
  if (epoll_ctl (efd, EPOLL_CTL_ADD, STDIN_FILENO, &event) != 0)
    {
      /* ignore EPERM; it means stdin is something like /dev/null */
      if (errno != EPERM)
        {
          error = clib_error_return_unix (0, "epoll_ctl[%d]", STDIN_FILENO);
          goto done;
        }
    }

  /* register socket */
  event.events = EPOLLIN | EPOLLPRI | EPOLLERR;
  event.data.fd = s->fd;
  if (epoll_ctl (efd, EPOLL_CTL_ADD, s->fd, &event) != 0)
    {
      error = clib_error_return_unix (0, "epoll_ctl[%d]", s->fd);
      goto done;
    }

主循环

初始化完成后,就进入了主循环,相对来说,主循环部分代码稍长一些,但是逻辑也很简单,就是基于epool,检查 STDIN_FILENO 和 s->fd 是否有数据,有数据则取出来进行处理。


  while (1)
    {
      int n;

    // 重新设置窗口大小,主要是设置 STDOUT_FILENO 输出内容的换行
      if (window_resized)
        {
          window_resized = 0;
          send_naws (s);
        }

    // 类似于select,有fd被触发的时候、或者有错误的时候,会返回
      if ((n = epoll_wait (efd, &event, 1, -1)) < 0)
        {
          /* maybe we received signal */
          if (errno == EINTR)
            continue;

          error = clib_error_return_unix (0, "epoll_wait");
          goto done;
        }

      if (n == 0)
        continue;

    // 如果是标准输入有内容,则读取数据并发送给s(vpp)
      if (event.data.fd == STDIN_FILENO)
        {
          int n;
          char c[100];    // 长度只有100,很明显有问题! 命名过于简单,也是不规范的代码

    // ?
          if (!sent_ttype)
            continue; /* not ready for this yet */

            //    读取数据
          n = read (STDIN_FILENO, c, sizeof (c));
          if (n > 0)
            {
                // clib_socket_tx_add 和 clib_socket_tx 函数后续有说明,这里只需要理解数据发给s(vpp)了。
              memcpy (clib_socket_tx_add (s, n), c, n);
              error = clib_socket_tx (s);
              if (error)
                goto done;
            }
          else if (n < 0)
            clib_warning ("read rv=%d", n);
          else /* EOF */
            do_quit = 1;
        }

    // 如果是s有数据,则进行处理
      else if (event.data.fd == s->fd)
        {
    // 后续有说明
          error = clib_socket_rx (s, 100);
          if (error)
            break;

    // s读取到文件结束,针对socket来说,就是对端close了
          if (clib_socket_rx_end_of_file (s))
            break;

        // 处理数据,具体有说明
          str = process_input (str, s, is_interactive, &sent_ttype);

    // vpp强调使用宏、而不是直接访问结构体的变量,确保兼容性
          if (vec_len (str) > 0)
            {
              int len = vec_len (str);
              u8 *p = str, *q = str;

              while (len)
                {
                  /* Search for and skip NUL bytes */
                  while (q < (p + len) && *q)
                    q++;

                  n = write (STDOUT_FILENO, p, q - p);
                  if (n < 0)
                    {
                      error = clib_error_return_unix (0, "write");
                      goto done;
                    }

                  while (q < (p + len) && !*q)
                    {
                      q++;
                      acked++; /* every NUL is an acknowledgement */
                    }
                  len -= q - p;
                  p = q;
                }

              vec_reset_length (str);
            }

    // 退出条件:所有命令都得到了响应
          if (do_quit && do_quit < acked)
            {
              /* Ask the other end to close the connection */
              clib_socket_tx_add_formatted (s, "quit\n");
              clib_socket_tx (s);
              do_quit = 0;
            }
          if (cmd && sent_ttype)
            {
              /* We wait until after the TELNET TTYPE option has been sent.
               * That is to make sure the session at the VPP end has switched
               * to line-by-line mode, and thus avoid prompts and echoing.
               * Note that it does also disable further TELNET option
               * processing.
               */
              clib_socket_tx_add_formatted (s, "%s\n", cmd);
              clib_socket_tx (s);
              vec_free (cmd);
              do_quit = acked; /* quit after the next response */
            }
        }
      else
        {
          error = clib_error_return (0, "unknown fd");
          goto done;
        }
    }

主循环之 process_input

这个process_input是用来处理vpp输出的数据的,应该结合vpp的相关代码来研究(待补充)。


static u8 *process_input (u8 *str, clib_socket_t *s, int is_interactive,
                          int *sent_ttype)
{
  int i = 0;

  while (i < vec_len (s->rx_buffer))
    {
      if (s->rx_buffer[i] == IAC)
        {
          if (s->rx_buffer[i + 1] == SB)
            {
              u8 *sb = 0;
              char opt = s->rx_buffer[i + 2];
              i += 3;
              while (s->rx_buffer[i] != IAC)
                vec_add1 (sb, s->rx_buffer[i++]);

#if DEBUG
              clib_warning ("SB %s\n  %U", TELOPT (opt), format_hexdump, sb,
                            vec_len (sb));
#endif
              vec_free (sb);
              i += 2;
              if (opt == TELOPT_TTYPE)
                {
                  send_ttype (s, is_interactive);
                  *sent_ttype = 1;
                }
              else if (is_interactive && opt == TELOPT_NAWS)
                send_naws (s);
            }
          else
            {
#if DEBUG
              clib_warning ("IAC at %d, IAC %s %s", i,
                            TELCMD (s->rx_buffer[i + 1]),
                            TELOPT (s->rx_buffer[i + 2]));
#endif
              i += 3;
            }
        }
      else
        vec_add1 (str, s->rx_buffer[i++]);
    }
  vec_reset_length (s->rx_buffer);
  return str;
}

退出条件

  1. STDIN_FILENO 输入EOF,比如ctrl+c,即可直接退出
  2. 单独执行命令的时候,执行完成会退出
  3. 但是没找到输入q会退出的代码。。。


调用的函数

clib_mem_init




clib_socket_init、clib_socket_tx_add、clib_socket_tx、clib_socket_rx

要理解这几个函数,就要先回头看看一个结构体clib_socket_t,该结构体定义了config用于保存配置(比如要和对端服务器建立连接,config保存了对端服务器的信息)、还有发送接收buffer以及读、写接口、关闭等接口函数。
相关代码在vppinfra/socket.c中出现的这个函数,意味着这是基础架构的一部分,不会轻易变动、且底层支持常见的socket(比如linux kernel、DPDK等)。


clib_error_t *clib_socket_init (clib_socket_t * s)
{
  union
  {
    struct sockaddr sa;
    struct sockaddr_un su;
  } addr;
  socklen_t addr_len = 0;
  int socket_type, rv;
  clib_error_t *error = 0;
  word port;

  error = socket_config (s->config, &addr.sa, &addr_len,
             (s->flags & CLIB_SOCKET_F_IS_SERVER
              ? INADDR_LOOPBACK : INADDR_ANY));
  if (error)
    goto done;

  socket_init_funcs (s);

  socket_type = s->flags & CLIB_SOCKET_F_SEQPACKET ?
    SOCK_SEQPACKET : SOCK_STREAM;

  s->fd = socket (addr.sa.sa_family, socket_type, 0);
  if (s->fd < 0)
    {
      error = clib_error_return_unix (0, "socket (fd %d, '%s')",
                      s->fd, s->config);
      goto done;
    }

  port = 0;
  if (addr.sa.sa_family == PF_INET)
    port = ((struct sockaddr_in *) &addr)->sin_port;

  if (s->flags & CLIB_SOCKET_F_IS_SERVER)
    {
      uword need_bind = 1;

      if (addr.sa.sa_family == PF_INET)
    {
      if (port == 0)
        {
          port = find_free_port (s->fd);
          if (port < 0)
        {
          error = clib_error_return (0, "no free port (fd %d, '%s')",
                         s->fd, s->config);
          goto done;
        }
          need_bind = 0;
        }
    }
      if (addr.sa.sa_family == PF_LOCAL)
    unlink (((struct sockaddr_un *) &addr)->sun_path);

      /* Make address available for multiple users. */
      {
    int v = 1;
    if (setsockopt (s->fd, SOL_SOCKET, SO_REUSEADDR, &v, sizeof (v)) < 0)
      clib_unix_warning ("setsockopt SO_REUSEADDR fails");
      }

#if __linux__
      if (addr.sa.sa_family == PF_LOCAL && s->flags & CLIB_SOCKET_F_PASSCRED)
    {
      int x = 1;
      if (setsockopt (s->fd, SOL_SOCKET, SO_PASSCRED, &x, sizeof (x)) < 0)
        {
          error = clib_error_return_unix (0, "setsockopt (SO_PASSCRED, "
                          "fd %d, '%s')", s->fd,
                          s->config);
          goto done;
        }
    }
#endif

      if (need_bind && bind (s->fd, &addr.sa, addr_len) < 0)
    {
      error = clib_error_return_unix (0, "bind (fd %d, '%s')",
                      s->fd, s->config);
      goto done;
    }

      if (listen (s->fd, 5) < 0)
    {
      error = clib_error_return_unix (0, "listen (fd %d, '%s')",
                      s->fd, s->config);
      goto done;
    }
      if (addr.sa.sa_family == PF_LOCAL
      && s->flags & CLIB_SOCKET_F_ALLOW_GROUP_WRITE)
    {
      struct stat st = { 0 };
      if (stat (((struct sockaddr_un *) &addr)->sun_path, &st) < 0)
        {
          error = clib_error_return_unix (0, "stat (fd %d, '%s')",
                          s->fd, s->config);
          goto done;
        }
      st.st_mode |= S_IWGRP;
      if (chmod (((struct sockaddr_un *) &addr)->sun_path, st.st_mode) <
          0)
        {
          error =
        clib_error_return_unix (0, "chmod (fd %d, '%s', mode %o)",
                    s->fd, s->config, st.st_mode);
          goto done;
        }
    }
    }
  else
    {
      if ((s->flags & CLIB_SOCKET_F_NON_BLOCKING_CONNECT)
      && fcntl (s->fd, F_SETFL, O_NONBLOCK) < 0)
    {
      error = clib_error_return_unix (0, "fcntl NONBLOCK (fd %d, '%s')",
                      s->fd, s->config);
      goto done;
    }

      while ((rv = connect (s->fd, &addr.sa, addr_len)) < 0
         && errno == EAGAIN)
    ;
      if (rv < 0 && !((s->flags & CLIB_SOCKET_F_NON_BLOCKING_CONNECT) &&
              errno == EINPROGRESS))
    {
      error = clib_error_return_unix (0, "connect (fd %d, '%s')",
                      s->fd, s->config);
      goto done;
    }
    }

  return error;

done:
  if (s->fd > 0)
    close (s->fd);
  return error;
}

分析和猜测