UPGW 代码分析

代码结构概览

UPGW 程序主要由 NES_MAIN 程序启动 NES_IO 、 NIS_IO 、 NTS_IO 、 NES_CTRL 四个线程来实现,另外有 nes_client 程序作为命令行客户端单独运行。各程序功能大致如下图:

image-1

主程序 NES_MAIN

  1. 功能:初始化 NES 程序,启动 NES_IO 、 NIS_IO 、 NTS_IO 、 NES_CTRL 线程。

  2. 程序入口:

    image-table1

  3. 代码逻辑:

    # file : nes_main.c
    NES_MAIN
    |_ rte_eal_init
    |_ getenv("NES_SERVER_CONF")
    |_ nes_cfgfile_load
    |_ nes_cfgfile_entry # 初始化 redis 服务器数据库的连接
    |_ init_redis_config
    |_ nes_mempool_init
    |_ nes_ring_init # 初始化 ring 环
    |_ nes_init_interfaces # 初始化接口
    |_ rte_eal_remote_launch(nes_io_main, NULL, LCORE_IO);     # NES_IO 四个线程分开四个核
    |_ rte_eal_remote_launch(nts_io_main, NULL, LCORE_NTS);    # NTS_IO
    |_ rte_eal_remote_launch(nis_io_main, NULL, LCORE_NIS);    # NIS_IO
    |_ rte_eal_remote_launch(nes_ctrl_main, NULL, LCORE_CTRL); # NES_CTRL
    |_ is_avp_enabled
       |_ nes_cfgfile_has_section
          |_ nes_dev_kni_init
    |_ for (;;)
       |_ for (i = 1; i <= lcores_count; i++)
          |_ rte_eal_get_lcore_state
          |_ if (state != RUNNING)
             |_ rte_vhost_driver_unregister
             |_ nes_cfgfile_close
             |_ nes_routefile_close
             |_ rte_eal_wait_lcore(i)
       |_ rte_pause
    |_ nes_cfgfile_close
    |_ nes_routefile_close
    
  4. 具体代码说明:

    ring 环初始化

    # file : nes_ring.c
    # nes_ring_init : ring 环初始化函数
    # 关键数据结构  static nes_ring_params_t nes_ring_params_table[ ] 
    nes_ring_init
    |_ nes_ring_lookup_init
    |_ nes_ctrl_ctor_ring_list
    |_ for (i = 0; nes_ring_params_table[i].name != NULL; i++) # nes_ring_params_table 是依据方向和协议定义的全局表
       |_ nes_ring_instantiate(&newring, &nes_ring_params_table[i]) # 1、初始化所有 ring 环的成员函数
          |_ nes_ring_lookup_entry_get(params->name,newring)
          |_ (*newring)->ctor = nes_ring_ctor # 构造函数(仅指针,非调用)
          |_ (*newring)->dtor = nes_ring_dtor;
          |_ (*newring)->deq       =  nes_ring_deq_sc;
          |_ (*newring)->deq_burst =  nes_ring_deq_burst_sc;
          |_ (*newring)->enq = (YES == params->multiproducer ?nes_ring_enq_mp : nes_ring_enq_sp);
          |_ (*newring)->enq_burst=(YES==params->multiproducer?nes_ring_enq_burst_mp:nes_ring_enq_burst_sp);
       |_ newring->ctor(newring, &nes_ring_params_table[i]) # 2、调用 ring 环的初始化函数,即 nes_ring_ctor
    
    
    # nes_ring_ctor
    # NES_STATIC int nes_ring_ctor(nes_ring_t *self, void *arg)
    # 该函数仅初始化 nes ring
    nes_ring_ctor
    |_ rte_ring_create # 依据 nes_ring_params_table 全局标量的 name 和 count 成员创建 ring
    |_ nes_ring_set_flow
       |_ nts_edit_ring_flow_set # 依据不同队列名调用队列处理函数,详见 NTS_IO 的 flow 初始化内容
    |_ nts_io_routing_tables_get
    |_ nes_ctrl_add_ring(self, self->ring->name)
    
  5. 总结:

    NES_MAIN 主程序负责内存、接口、ring 环等初始化,然后启动子模块的线程。有些在线程中直接使用的数据结构初始化操作在 NES_MAIN 中线程启动前可以找到。

线程 NES_IO

  1. 功能:收发数据包,分发 ring 环。

  2. 程序入口:

    image-table2

  3. 代码逻辑:

    # file : nes_io.c
    static nes_queue_t nes_io_devices;
    
    nes_io_main
    |_ nes_io_init
    |_ while (THREADS_MASK != rte_atomic32_read(&threads_started)) # 等触发
    |_ for (;;)
       |_ NES_QUEUE_FOREACH(node, &nes_io_devices)
       |_ device->recv(node->data,NULL); # 调用接收函数
       |_ device->scatter(node->data,NULL); # 调用分发函数
       |_ device->send(node->data,NULL); # 调用发送函数
       |_ nes_queue_remove(&nes_io_devices,node) # 调用移除函数
    
    # nes_queue_t -> nes_queue_node_t -> data === nes_dev_t [recv/scatter/send/remove]
    

    主要数据结构:struct nes_queue_t nes_io_devices

    image-2

    // file : nes_dev.h
    typedef struct nes_dev_s {
        nes_dev_id_t dev;
        nes_dev_id_type dev_type;
    
        nes_ring_t **rx_rings;
        int rx_ring_cnt;
    
        nes_ring_t *rx_default_ring;
        nes_ring_t *tx_ring;
    
        struct rte_mbuf *rx_pkts[MAX_BURST_SIZE];
        int rx_cnt;
    
        struct rte_mbuf *tx_buffer[TX_BUFFER_SIZE];
        int tx_buffer_cnt;
        uint64_t retry_send_start;
        uint64_t retry_timeout_cycles;
    
        struct ether_addr mac_address;
    
        nes_dev_traffic_type traffic_type;
        nes_dev_traffic_dir traffic_dir;
        int egres_port;
        char *name;
        uint8_t nes_port_id;
        uint8_t remove;
        struct rte_ip_frag_tbl *frag_tbl;
        struct rte_ip_frag_death_row death_row;
        uint16_t MTU;
    
        struct nes_ctrl_dev_s *dev_stats;
        int (*ctor)(struct nes_dev_s *self, void *data);
        int (*dtor)(struct nes_dev_s *self, void *data);
        int (*recv)(struct nes_dev_s *self, void *data);
        int (*send)(struct nes_dev_s *self, void *data);
        int (*scatter)(struct nes_dev_s *self, void *data);
    
    } nes_dev_t;
    
    // file : libnes_queue.h
    typedef struct nes_queue_node_s {
        void              *data;  // 数据部分是 nes_dev_t 的格式
        struct nes_queue_node_s *next;
        rte_atomic16_t     busy; // 忙闲状态值
        int                num;
    } nes_queue_node_t;
    
    // file : libnes_queue.h
    typedef struct nes_queue_s {
        nes_queue_node_t *begin; // 开始节点指针
        nes_queue_node_t *end;  // 结束节点指针
        rte_spinlock_t lock;
        int cnt;
    } nes_queue_t; // 队列
    
  4. 关键代码分析:

    scatter 分发处理函数

    作用:将数据包分发到下一个处理的 ring 环中。

    处理流程:

    1. NES_MAIN 初始化时,把 ring 的 scatter 指针指向各函数( scatter_eth_DIRECTION_TYPE )

      # file : nes_main.c
      nes_init_interfaces
      |_ nes_dev_port_new_device # file : nes_dev_port.c
         |_ for (i:0->count_port_devices)
            |_ nes_cfgfile_entry(port_name, TRAFFIC_DIRECTION, &buffer) #从文件读取流量方向 direction
            |_ nes_cfgfile_entry(port_name, TRAFFIC_TYPE, &buffer) # 从文件读取流量类型 type
            |_ switch (port_dev->traffic_dir)
               |_ switch (port_dev->traffic_type)
                  |_  port_dev->scatter = &scatter_eth_"direction"_"type" # 注意此处仅是指针,非调用
      
    2. NES_IO 获取报文时,调用 scatter_eth_DIRECTION_TYPE 系列函数:

      # file : nes_dev_port.c
      # static FORCE_INLINE void scatter_eth_packets(struct nes_dev_s *self, int flags)
      scatter_eth_"direction"_"type"
      |_ scatter_eth_packet # 先判断 VLAN 和非 IP4 ,然后才判断类型是否为 LTE 继而处理 GTPU 等。
         |_ foreach i:0 -> self->rx_cnt
            |_ if ETHER_TYPE_VLAN # VLAN (局域网、二层分离)
            |_ if not ETHER_TYPE_IPv4 # IP4
            |  |_ self->rx_default_ring
            |  |_ continue;
            |_ if SCATTER_FL_LTE
            |  |_ if SCATTER_FL_BOTH
            |  |_ if IP_PROTO_SCTP
            |  |  |_ if SCATTER_FL_BOTH
            |  |  |_ else if SCATTER_FL_UPSTREAM
            |  |  |_ else if SCATTER_FL_DOWNSTREAM
            |  |_ if IP_PROTO_UDP
            |  |  |_ if SCATTER_FL_IP
            |  |     |_ if SCATTER_FL_BOTH
            |  |     |_ else if SCATTER_FL_UPSTREAM
            |  |     |_ else if SCATTER_FL_DOWNSTREAM
            |  |_ if SCATTER_FL_BOTH
            |  |_ else if SCATTER_FL_DOWNSTREAM
            |  |_ if UDP_GTPU_PORT && GTPU_MSG_GPDU
            |  |  |_ if SCATTER_FL_BOTH
            |  |  |_ else if SCATTER_FL_UPSTREAM
            |  |_ else if UDP_GTPC_PORT
            |  |  |_ if SCATTER_FL_BOTH
            |  |  |_ else if SCATTER_FL_UPSTREAM
            |  |_ else
            |  |  |_ if SCATTER_FL_IP
            |  |     |_ if SCATTER_FL_BOTH
            |  |     |_ else if SCATTER_FL_UPSTREAM
            |  |     |_ else if SCATTER_FL_DOWNSTREAM
            |  |_ else
            |_ else if SCATTER_FL_IP
               |_ if SCATTER_FL_BOTH
                  |_ else if SCATTER_FL_UPSTREAM
                  |_ else if SCATTER_FL_DOWNSTREAM
      
  5. 总结

    NES_IO 线程主要负责数据包从物理接口收到之后的第一道分发处理操作,通过调用 recv 、 scatter 、 send 等函数指针实现,具体的函数实例化则是在 NES_MAIN 中初始化时完成。

线程 NIS_IO

  1. 功能:接收 NES_IO 分发过来的数据包,出队进行第二道处理,负责 GTPUC 、 GTPC 、 RNIS 、 SCTP 协议的 ring 环处理。

  2. 程序入口:

    image-table3

  3. 代码逻辑:

    # file : nis_io.c
    nis_io_main
    |_ nis_io_init # 初始化 nis_ring 等
    |_ rte_atomic32_add
    |_ for (;;)
       |_ NES_QUEUE_FOREACH(node, &nis_io_rings) # 数据结构: nes_queue_t nis_io_rings
       |_ in_ring->deq_burst # 调用批量出队函数
       |_ in_ring->flow # 调用流处理函数
    
  4. 关键代码分析:

    flow 流处理函数

    作用:依据流量方向操作数据包

    处理流程:

    1. flow 指针初始化:

    # file : nis_io.c
    nis_io_init
    |_ nis_io_init_traffic_rings
       |_ nes_ring_find( NIS_'direction'_'protocol' ) # nes_ring_params_table 中又定义
       |_ for ( 0->NIS_RX_RINGS_CNT )
          |_ nes_queue_enqueue
    |_ nis_io_init_flows
       |_ nis_io_ring_flow_set
          |_ rings->flow = nis_io_'direction'_flow
             |_ nes_dev_get_egressring_from_port_idx
                |_ return devices_tab[id].device->rx_default_ring # 数据结构 nes_tabq_t devices_tab
    
    # file : nes_ring.c
    # nes_ring_init 将根据 nes_ring_params_table 创建 ring
    nes_ring_init
    |_ nes_ring_instantiate
    |_ newring->ctor(newring, &nes_ring_params_table[i]) # 调用初始化指针给flow赋值,但 NIS 不在此赋值。
    
    1. 调用逻辑:

      # nis_io.c
      rings->flow = nis_io_'direction'_flow
      |_ nes_dev_get_egressring_from_port_idx
      |_ return devices_tab[id].device->rx_default_ring # 数据结构 nes_tabq_t devices_tab
      
  5. 总结:

    NIS_IO 线程在 NES_IO 之后负责数据包的第二道分发处理操作,依据数据包上行或下行处理数据包出口。

线程 NTS_IO

  1. 功能:匹配规则,分流。

  2. 程序入口:

    image-table4

  3. 代码调用逻辑:

    # file : nts_io.c
    static nes_queue_t nts_io_rings;
    
    nts_io_main
    |_ nts_io_init
    |_ rte_atomic32_add(&threads_started, THREAD_NTS_IO_ID);
    |_ for (;;)
       |_ NES_QUEUE_FOREACH(node, &nts_io_rings)  # nes_queue_node_t *node
          |_ in_ring->deq_burst # 出队处理 nes_ring_t *in_ring = node->data;
          |_ in_ring->flow # 调用处理函数
          |_ nes_queue_remove(&nts_io_rings, node) # 删除处理
    

    主要数据结构:nes_queue_t nts_io_rings;

    // file : nes_ring.h
    /**
    * Abstract ring object
    */
    typedef struct nes_ring_s {
        struct rte_ring *ring;
        nts_lookup_tables_t *routing_tables;
        uint8_t remove;
    
        struct nes_ctrl_ring_s *ring_stats;
        int (*ctor)(struct nes_ring_s *, void *);  // 构造
        int (*enq)(struct nes_ring_s *, void *);   // 入队
        int (*enq_burst)(struct nes_ring_s *, void **, int); // 批量入队
        int (*flow)(struct nes_ring_s *, void **, int);  //
        int (*deq)(struct nes_ring_s *, void **);  // 出队
        int (*deq_burst)(struct nes_ring_s *, void **, int);  // 批量出队
        int (*dtor)(struct nes_ring_s *, void *);  // 析构
    } nes_ring_t;
    
  4. 关键代码分析:

    flow 流处理函数

    作用:依据流量方向和协议类型操作数据包

    处理流程:

    1. flow 指针初始化:

    # file : nts_io.c
    nts_io_init
    |_ nts_lookup_init
    |_ nts_edit_init
    
    # file : nes_ring.c
    nes_ring_init
    |_ newring->ctor(newring, &nes_ring_params_table[i]) # int nes_ring_ctor(nes_ring_t *self, void *arg)
       |_ nes_ring_set_flow(self) # flow
       |_ self->routing_tables = nts_io_routing_tables_get(); # routing_table
    
    1. 实现逻辑:

      # nes_ring.c
      # NES_STATIC int nes_ring_ctor(nes_ring_t *self, void *arg)
      nes_ring_ctor
      |_ rte_ring_create # 依据 nes_ring_params_table 全局标量的 name 和 count 成员创建 ring
      |_ nes_ring_set_flow
         |_ nes_ring_name
         |_ nts_edit_ring_flow_set
            |_ if NTS_DWSTR_GTP
               |_ ring->flow = nts_flow_downstream_gtp # flow 指针赋值(注意,非调用)
            |_ else if NTS_UPSTR_GTP
               |_ ring->flow = nts_flow_upstream_gtp
            |_ else if NTS_DWSTR_IP
               |_ ring->flow = nts_flow_downstream_ip
            |_ else if NTS_UPSTR_IP
               |_ ring->flow = nts_flow_upstream_ip
            |_ else if NTS_LBP_PREFIX || NTS_VM_PREFIX || NTS_KNI_PREFIX || NTS_AVP_PREFIX
               |_ ring->flow = nts_flow_vm
            |_ else if EDGE_DNS_RING_NAME
               |_ ring->flow = nts_flow_edge_dns
      
    2. 以 nts_flow_upstream_ip 为例

      # file : nts_edit.c
      nts_flow_upstream_ip
      |_ for (i:0 -> mbuf_num ) # 处理 upstream 包
         |_ nts_egde_dns_hdr_parse_ip # IP 头处理
         |_ nes_dev_get_egressring_from_port_idx # 查找端口
         |_ nes_lookup_entry_find # 在 nes_lookup_table_t 中查找入口   ????
         |_ nes_lookup_entry_add # 如果表中没有,则添加
         |_ domainname_check # 检查域名
      |_ nes_acl_lookup # 查找 acl 规则
      |_ nes_ring_find # 查找边缘 ring 环
      |_ for (i:0 -> mbuf_num ) # 处理发送边缘包
         |_ ring->enq(ring, mbufs[i]) # 是边缘的包则入队到边缘 ring 中
         |_ nts_packet_edit_enq # 非边缘包,但需要转发,则改写包的路由信息
         |_ egress_ring->enq(egress_ring, mbufs[i]) # 直连透传,无需路由转发
         |_ NES_STATS_RING_UPDATE # 其他情况 drop 并统计弃包个数
      

      其他处理也类似,有两个 for 循环,一个处理入包,一个处理出包。

    image-table5

  5. 总结:

    NTS_IO 线程负责数据包的协议业务处理,主要通过调用 flow 成员函数实现,根据数据包方向和协议类型做具体的操作。

线程 NES_CTRL [本地 server 端]

  1. 功能:监听远程调用。

  2. 程序入口:

    image-table6

  3. 代码调用逻辑:

    # file : nes_ctrl.c
    nes_ctrl_main
    |_ nes_ctrl_init # 初始化(查找 ring 环)
    |_ nes_domainame_lookup_init # dns 查找初始化
    |_ nes_lookup_ctor(server_table, &lookup_table_params)
    |_ for (NES_FOREVER_LOOP)
       |_ analyze_buffer
          |_ nes_handle_msg
          |_ write(client->fd, buf_ptr, resp_size)
    |_ nes_lookup_dtor(server_table); # 析构
    |_ rte_free(server_table);
    
    # file : nes_ctrl.c
    nes_handle_msg
    |_ switch (api_msg->function_id)
       |_ nes_ctrl_stats_dev
       |_ nes_ctrl_show_dev_all
       |_ nes_ctrl_show_list
       |_ nes_ctrl_get_mac_addr
       |_ nes_ctrl_route_add
       |_ nes_ctrl_mirror_add
       |_ nes_ctrl_route_del
       |_ nes_ctrl_route_del_by_uuid
       |_ nes_ctrl_clear_routes
       |_ nes_ctrl_route_show
       |_ nes_ctrl_route_show_by_uuid
       |_ nes_ctrl_route_list
       |_ nes_ctrl_clear_stats
       |_ nes_ctrl_stats_ring
       |_ nes_ctrl_show_ring_all
       |_ nes_ctrl_kni_add
       |_ nes_ctrl_kni_del
       |_ nes_domainname_add
       |_ nes_domainame_del
       |_ nes_domainame_show
       |_ nes_domainame_list
       |_ nes_sync_data
    |_ switch ((enum nes_flow_function_id) api_msg->function_id)
       |_ nes_ctrl_flow_add
       |_ nes_ctrl_flow_show
       |_ nes_ctrl_flow_del
       |_ nes_ctrl_routing_data_add
       |_ nes_ctrl_routing_data_del
       |_ nes_ctrl_routing_data_show
       |_ nes_ctrl_encap_show
    
  4. 关键代码分析:

    epoll 的使用

    # file : nes_ctrl.c
    nes_ctrl_main
    |_ set_sock_non_blocking # 设置 socket 监听非阻塞
    |_ epoll_create1 # 创建 epoll
    |_ epoll_ctl # 设置 epoll 事件
    |_ for (NES_FOREVER_LOOP)
       |_ n = epoll_wait # 等待 epoll 时间
       |_ for (i = 0 -> n)
          |_ if (conn.listen_sock == events[i].data.fd) # 处理建链
             |_ nes_lookup_entry_get # 把监听到的 fd 添加到 server_table ,为它分配空间?
          |_ else if ((events[i].events & EPOLLIN)) # 接收数据
             |_ analyze_buffer
          |_ else if events[i].events === EPOLLERR || EPOLLHUP || EPOLLIN # epoll error
             |_ close (events[i].data.fd);
    
  5. 总结:

    NES_CTRL 线程负责创建命令行 server 端,通过 epoll 方式监听客户端的请求,在命令函数中实现相关命令行的操作。

独立程序 NES_CLIENT [ client 端]

  1. 功能:作为命令行供用户操作配置信息使用,其中包括路由配置、 ring 环配置、 flow 配置、domain 配置等。

  2. 程序入口:

    image-table7

  3. 代码调用逻辑:

    # file : nes_client.c
    main
    |_ nes_cmdline_file_manager # 信息输出重定向到文件中
       |_ cmdline_new(main_ctx, "# ", fd, fd_out) # 创建命令行实例,数据结构: main_ctx
       |_ cmdline_interact # 进入命令行交互
    |_ nes_cmdline_manager # 信息输出到标准输出中
       |_ cmdline_stdin_new(main_ctx, "# ") # 创建命令行实例,数据结构: main_ctx
       |_ cmdline_interact # 进入命令行交互
    

    数据结构:

    image-3

  4. 关键代码说明:

    client 线程与 server 线程之间通过 socket 通信

    # file : nes_cli.c
    
    # nes_conn_init_parsed
    # 初始化 socket 链接
    nes_conn_init_parsed
    |_ nes_conn_init # 依据 ip 地址和 port 获取 socket fd
       |_ nes_conn_start # 使用 select 方式建立连接
    
    # nes_send_api_msg
    # 在具体命令实现函数中,使用 nes_send_api_msg 发送并接收数据
    nes_send_api_msg
    |_ send
    |_ recv
    
    # nes_conn_close
    # 关闭 socket 连接
    nes_conn_close
    |_ close
    
  5. 总结

    nes_client 是独立的命令行客户端程序,与 nes_ctrl 服务端通过 socket 方式通信,启动时需要先启动 nes 以使服务端监听端口,再启动 nes_client 并建立连接。

reference

DPDK ACL 算法介绍: https://www.jianshu.com/p/0f71f814d73e

DPDK ACL 的使用:http://jiangzhuti.me/posts/DPDK%E4%B8%ADACL(Access-Control-List)%E7%9A%84%E4%BD%BF%E7%94%A8

DPDK CMD 相关函数:https://doc.dpdk.org/api-2.1/cmdline_8h_source.html