# UPGW 代码分析 ## 代码结构概览 UPGW 程序主要由 NES_MAIN 程序启动 NES_IO 、 NIS_IO 、 NTS_IO 、 NES_CTRL 四个线程来实现,另外有 nes_client 程序作为命令行客户端单独运行。各程序功能大致如下图: ![image-1](../../_static/nes_code_analyse__revise_1.png) ## 主程序 NES_MAIN 1. 功能:初始化 NES 程序,启动 NES_IO 、 NIS_IO 、 NTS_IO 、 NES_CTRL 线程。 2. 程序入口: ![image-table1](../../_static/nes_code_analyse__nes_main_table.png) 3. 代码逻辑: ```bash # 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 环初始化** ```bash # 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](../../_static/nes_code_analyse__nes_io_table.png) 3. 代码逻辑: ```bash # 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](../../_static/nes_code_analyse__nes_io_main_data_struct.png) ```C // 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 ) ```bash # 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 系列函数: ```bash # 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](../../_static/nes_code_analyse__nis_io_table.png) 3. 代码逻辑: ```bash # 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 指针初始化: ```bash # 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 不在此赋值。 ``` 2. 调用逻辑: ```bash # 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](../../_static/nes_code_analyse__nts_io_table.png) 3. 代码调用逻辑: ```bash # 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; ```C // 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 指针初始化: ```bash # 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 ``` 2. 实现逻辑: ```bash # 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 ``` 3. 以 nts_flow_upstream_ip 为例 ```bash # 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](../../_static/nes_code_analyse__nts_flow_table.png) 5. 总结: NTS_IO 线程负责数据包的协议业务处理,主要通过调用 flow 成员函数实现,根据数据包方向和协议类型做具体的操作。 ## 线程 NES_CTRL [本地 server 端] 1. 功能:监听远程调用。 2. 程序入口: ![image-table6](../../_static/nes_code_analyse__nes_ctrl_table.png) 3. 代码调用逻辑: ```bash # 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); ``` ```bash # 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 的使用** ```bash # 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](../../_static/nes_code_analyse__nes_client_table.png) 3. 代码调用逻辑: ```bash # 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](../../_static/nes_code_analyse__nes_ctrl_data_struct.png) 4. 关键代码说明: **client 线程与 server 线程之间通过 socket 通信** ```bash # 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