DPDK L3fwd 源碼閱讀


代碼部分

整個L3fwd有三千多行代碼,但總體思想就是在L2fwd的基礎上,增加網絡層的根據 IP 地址進行路由查找的內容。

main.c 文件

int
main(int argc, char **argv)
{
        /*......*/

	/* init EAL */
	ret = rte_eal_init(argc, argv);
	if (ret < 0)
		rte_exit(EXIT_FAILURE, "Invalid EAL parameters\n");

	/*......*/

	/* parse application arguments (after the EAL ones) */
	// 解析命令行參數
	ret = parse_args(argc, argv);
	if (ret < 0)
		rte_exit(EXIT_FAILURE, "Invalid L3FWD parameters\n");

	 // 檢查 lcore 配置數組的正確性
	if (check_lcore_params() < 0)
		rte_exit(EXIT_FAILURE, "check_lcore_params failed\n");
	// 檢查 rx 隊列數是否過多,把 param 的東西放到 conf 里
	ret = init_lcore_rx_queues();
	if (ret < 0)
		rte_exit(EXIT_FAILURE, "init_lcore_rx_queues failed\n");

	nb_ports = rte_eth_dev_count(); // 獲取以太網端口數量

	if (check_port_config() < 0) // 驗證 port 掩碼、 port id 的正確性
		rte_exit(EXIT_FAILURE, "check_port_config failed\n");

	nb_lcores = rte_lcore_count(); // 獲取 lcore 數量

	/* Setup function pointers for lookup method. */
	setup_l3fwd_lookup_tables();// 設定是 Exact 還是 LPM 

	/* initialize all ports */
	// 初始化端口
	RTE_ETH_FOREACH_DEV(portid) {

	        /*......*/
		ret = rte_eth_dev_configure(portid, nb_rx_queue,
					(uint16_t)n_tx_queue, &local_port_conf); 
					// 配置以太網設備,rx 隊列數量根據 param 數組來,tx 數量則是和 lcore 數量相同

		/* init memory */
		// 分配內存,設置 LPM 或 Hash 表
		ret = init_mem(NB_MBUF);
		if (ret < 0)
			rte_exit(EXIT_FAILURE, "init_mem failed\n");

		/* init one TX queue per couple (lcore,port) */
		// 設置 Tx queue,每個端口只設置一條
		queueid = 0;
		for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
			
	                /*......*/
			ret = rte_eth_tx_queue_setup(portid, queueid, nb_txd,
						     socketid, txconf);
			/*......*/
		}
		printf("\n");
	}

	for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
                /*......*/
		// 配置 Rx 隊列,每個端口上設置多條。
		for(queue = 0; queue < qconf->n_rx_queue; ++queue) {
			/*......*/
			rte_eth_dev_info_get(portid, &dev_info);
			/*......*/
			ret = rte_eth_rx_queue_setup(portid, queueid, nb_rxd,
					socketid,
					&rxq_conf,
					pktmbuf_pool[socketid]);
			/*......*/
		}
	}

	printf("\n");

	/* start ports */
	// 啟用設備
	RTE_ETH_FOREACH_DEV(portid) {
		/*......*/
		/* Start device */
		ret = rte_eth_dev_start(portid);
		/*......*/
		if (promiscuous_on) // 設置混雜模式
			rte_eth_promiscuous_enable(portid);
	}

                /*......*/
		}
	}

	// 檢查鏈路狀態
	check_all_ports_link_status(enabled_port_mask);

	ret = 0;
	/* launch per-lcore init on every lcore */
	// 在每個lcore上執行函數,包括master lcore,根據選定的 LPM 還是 exact 執行對應的 main_loop 函數
	rte_eal_mp_remote_launch(l3fwd_lkp.main_loop, NULL, CALL_MASTER);
	RTE_LCORE_FOREACH_SLAVE(lcore_id) {
		if (rte_eal_wait_lcore(lcore_id) < 0) {
			ret = -1;
			break;
		}
	}

	/* stop ports */
	RTE_ETH_FOREACH_DEV(portid) {
		if ((enabled_port_mask & (1 << portid)) == 0)
			continue;
		printf("Closing port %d...", portid);
		rte_eth_dev_stop(portid);
		rte_eth_dev_close(portid);
		printf(" Done\n");
	}
	printf("Bye...\n");

	return ret;
}

main 函數中,代碼思路就是L2fwd+helloworld。首先分配內存,配置隊列、初始化端口等部分與L2fwd相似。除此之外,多出來的幾個部分就是L3層的事情:選取網絡層路由模式(精確匹配 Exact or 最長前綴匹配LPM);配置路由表(從代碼看就是靜態路由表);檢查設備是否支持IP協議(還有不支持IP協議的以太網網卡嗎?);在最后,用 rte_eal_mp_remote_launch() 在各個邏輯核上執行函數,這里使用了函數指針,根據你是選擇Exact 還是 LPM 會注冊不同的函數。

選取 LPM or Exact Match 的邏輯:

首先,程序會解析命令行參數,可以用命令行參數來選取路由模式。-E 就是精確匹配,-L 就是 LPM。具體可以參考L3 forward sample guide

/* Parse the argument given in the command line of the application */
static int
parse_args(int argc, char **argv)
{
	/*......*/

	while ((opt = getopt_long(argc, argvopt, short_options,
				lgopts, &option_index)) != EOF) {

		switch (opt) {
		/*......*/

		case 'P': // 開啟混雜模式
			promiscuous_on = 1;
			break;

		case 'E': // 精確匹配
			l3fwd_em_on = 1;
			break;

		case 'L': // 最長前綴匹配
			l3fwd_lpm_on = 1;
			break;

                /*......*/

	/* If both LPM and EM are selected, return error. */
	if (l3fwd_lpm_on && l3fwd_em_on) { // Exact match 和 LPM 只能選一種
		fprintf(stderr, "LPM and EM are mutually exclusive, select only one\n");
		return -1;
	}

	/*
	 * Nothing is selected, pick longest-prefix match
	 * as default match.
	 */
	if (!l3fwd_lpm_on && !l3fwd_em_on) { // 如果兩者都沒選,選擇 LPM
		fprintf(stderr, "LPM or EM none selected, default LPM on\n");
		l3fwd_lpm_on = 1;
	}

	/*
	 * ipv6 and hash flags are valid only for
	 * exact macth, reset them to default for
	 * longest-prefix match.
	 */
	if (l3fwd_lpm_on) { // 如果選擇了LPM,不適用ipv6 和 hash 
		ipv6 = 0;
		hash_entry_number = HASH_ENTRY_NUMBER_DEFAULT;
	}

	if (optind >= 0)
		argv[optind-1] = prgname;

	ret = optind-1;
	optind = 1; /* reset getopt lib */
	return ret;
}

LPM 和 精確匹配在 L3fwd 里分別用了兩套代碼來實現整個網絡層協議棧。根據你選擇的模式,會通過設置變量和函數指針的方式來調用特定的代碼塊。


struct l3fwd_lkp_mode { 
	void  (*setup)(int);// 各種函數指針
	int   (*check_ptype)(int);
	rte_rx_callback_fn cb_parse_ptype;
	int   (*main_loop)(void *);
	void* (*get_ipv4_lookup_struct)(int);
	void* (*get_ipv6_lookup_struct)(int);
};

static struct l3fwd_lkp_mode l3fwd_lkp; // 會根據是使用 lpm 還是 exact,賦值給下面兩個中的一個

static struct l3fwd_lkp_mode l3fwd_em_lkp = {
	.setup                  = setup_hash,
	.check_ptype		= em_check_ptype,
	.cb_parse_ptype		= em_cb_parse_ptype,
	.main_loop              = em_main_loop,
	.get_ipv4_lookup_struct = em_get_ipv4_l3fwd_lookup_struct,
	.get_ipv6_lookup_struct = em_get_ipv6_l3fwd_lookup_struct,
};

static struct l3fwd_lkp_mode l3fwd_lpm_lkp = {
	.setup                  = setup_lpm,
	.check_ptype		= lpm_check_ptype,
	.cb_parse_ptype		= lpm_cb_parse_ptype,
	.main_loop              = lpm_main_loop,
	.get_ipv4_lookup_struct = lpm_get_ipv4_l3fwd_lookup_struct,
	.get_ipv6_lookup_struct = lpm_get_ipv6_l3fwd_lookup_struct,
};


static void
setup_l3fwd_lookup_tables(void) // 設定 IP 查表方法是 Exact 還是 LPM 
{
	/* Setup HASH lookup functions. */
	if (l3fwd_em_on) 
		l3fwd_lkp = l3fwd_em_lkp; 
	/* Setup LPM lookup functions. */
	else 
		l3fwd_lkp = l3fwd_lpm_lkp;
}

LPM 相關

LPM 就是最長前綴匹配。在DPDK中有個一個專門的 LPM library 來實現LPM的相關模塊。LPM 的條目(或者說規則)是由三個部分組成,IP地址、前綴長度、下一跳。分別是 4、1、1個字節。而LPM table,也就是條目的集合,集合組成了路由表。

struct ipv4_l3fwd_lpm_route { // 路由表中一個 entry 的結構
	uint32_t ip;    // IP 地址
	uint8_t  depth;	// (掩碼)前綴位數
	uint8_t  if_out;// 下一跳
};

// LPM的路由表:IP地址、掩碼長度、下一跳
static struct ipv4_l3fwd_lpm_route ipv4_l3fwd_lpm_route_array[] = {
	{IPv4(1, 1, 1, 0), 24, 0},
	{IPv4(2, 1, 1, 0), 24, 1},
	{IPv4(3, 1, 1, 0), 24, 2},
	{IPv4(4, 1, 1, 0), 24, 3},
	{IPv4(5, 1, 1, 0), 24, 4},
	{IPv4(6, 1, 1, 0), 24, 5},
	{IPv4(7, 1, 1, 0), 24, 6},
	{IPv4(8, 1, 1, 0), 24, 7},
};

LPM library 中主要用到的幾個功能如下:

  1. 創建 LPM table 對象。
  2. 朝 LPM table 插入一個條目。拿上面那個條目的結構體和LPM table對象作為參數。如果表中沒有相同前綴的規則,則新規則將添加到LPM表中。如果表中已存在具有相同前綴的規則,則更新規則的下一跳。
  3. 路由功能。輸入就是目的IP地址,算法會選擇匹配的規則,並返回該規則的下一跳。如果LPM表中存在多個具有能匹配的規則,則算法選擇具有最長前綴位數的規則作為最佳匹配規則。

(以上都是TCP/IP的基礎知識)

LPM的思路是挺簡單的,然而具體的代碼實現算是一個大工程,思路也是非常有意思。具體的工程方法叫做 DIR-24-8,可以參考LPM library - implementation details(這足以讓一個Stanford phd 畢業~文末有給出2000年研究這個方法的phd論文)

Exact match 相關

實現精確匹配時,路由表的內容是<五元組,下一跳>,實現思路是將五元組信息過 hash 函數,得到一個hash value用作路由表的 index。只有五元組都相同才會匹配成功,才能得到正確的路由表的index,才能得到合法的下一跳地址。這樣就實現了精確匹配。DPDK中為了實現快速的hash查找有專門的 hash library。DPDK 為了性能要求,在 hash library 中對 hash table有特定的要求:hash key 的長度必須固定,hash 表的條目也是有限的。這兩點在創建 hash table 對象時必須配置。hash library 中有用到的幾個功能如下:

  1. 創建新的 hash table 對象
  2. 在 hash table 中加入一個條目,鍵值是 key。如果添加成功,返回值為一個正值,此值對於此鍵是唯一的。
  3. 以鍵值 key 查詢 hash table,若查詢成功,會返回一個正值。此正值對於此鍵是唯一的,並且與添加鍵時返回的值相同。使用這兩個API的返回的正值作為某個數組(下一跳數組)或者結構體數組的下標,就可以實現精確匹配了。

hash 庫中使用了所謂 cuckoo hash 的實現方法。

// 精確匹配的路由表,是五元組和下一跳
static struct ipv4_l3fwd_em_route ipv4_l3fwd_em_route_array[] = {
	// 目的IP地址          源IP地址            目的/源 端口號 協議類型   下一跳
	{{IPv4(101, 0, 0, 0), IPv4(100, 10, 0, 1),  101, 11, IPPROTO_TCP}, 0},
	{{IPv4(201, 0, 0, 0), IPv4(200, 20, 0, 1),  102, 12, IPPROTO_TCP}, 1},
	{{IPv4(111, 0, 0, 0), IPv4(100, 30, 0, 1),  101, 11, IPPROTO_TCP}, 2},
	{{IPv4(211, 0, 0, 0), IPv4(200, 40, 0, 1),  102, 12, IPPROTO_TCP}, 3},
	// 所謂精確匹配就是五元組的信息 hash后,只有五元組都相同才會匹配成功。
	// 所以 exact 就是配置 hash
};

三層轉發

// 檢查packet是否符合網絡層的要求:

static inline int
is_valid_ipv4_pkt(struct ipv4_hdr *pkt, uint32_t link_len)
{
	/* From http://www.rfc-editor.org/rfc/rfc1812.txt section 5.2.2 */
	/*
	 * 1. The packet length reported by the Link Layer must be large
	 * enough to hold the minimum length legal IP datagram (20 bytes).
	 */
	if (link_len < sizeof(struct ipv4_hdr)) // 正常的包長度不能短於 header 長度(20 bytes)
		return -1;

	/* 2. The IP checksum must be correct. */
	/* this is checked in H/W */

	/*
	 * 3. The IP version number must be 4. If the version number is not 4
	 * then the packet may be another version of IP, such as IPng or
	 * ST-II.
	 */
	if (((pkt->version_ihl) >> 4) != 4) // 版本號字段必須是 4
		return -3;
	/*
	 * 4. The IP header length field must be large enough to hold the
	 * minimum length legal IP datagram (20 bytes = 5 words).
	 */
	if ((pkt->version_ihl & 0xf) < 5) // IP header length 字段的值必須大於 5
		return -4;

	/*
	 * 5. The IP total length field must be large enough to hold the IP
	 * datagram header, whose length is specified in the IP header length
	 * field.
	 */
	if (rte_cpu_to_be_16(pkt->total_length) < sizeof(struct ipv4_hdr)) // 總長度字段的值要大於等於 20
		return -5;

	return 0;
}

// 網絡層的處理:
static __rte_always_inline void
l3fwd_lpm_simple_forward(struct rte_mbuf *m, uint16_t portid,
		struct lcore_conf *qconf)
{
	struct ether_hdr *eth_hdr;
	struct ipv4_hdr *ipv4_hdr;
	uint16_t dst_port;

	eth_hdr = rte_pktmbuf_mtod(m, struct ether_hdr *); // 從 pkt m 中獲取 MAC header

	if (RTE_ETH_IS_IPV4_HDR(m->packet_type)) { // 查看 pkt 的 L3 header type 是不是 IPv4 
		/* Handle IPv4 headers.*/
		ipv4_hdr = rte_pktmbuf_mtod_offset(m, struct ipv4_hdr *,
						   sizeof(struct ether_hdr)); // 從 pkt m 中獲取 IP header

#ifdef DO_RFC_1812_CHECKS
		/* Check to make sure the packet is valid (RFC1812) */
		
		if (is_valid_ipv4_pkt(ipv4_hdr, m->pkt_len) < 0) { // 根據 RFC1812 的內容對 pkt 進行驗證。
			rte_pktmbuf_free(m); // 如果不是合法的包就丟包
			return;
		}
#endif
		 dst_port = lpm_get_ipv4_dst_port(ipv4_hdr, portid,
						qconf->ipv4_lookup_struct); // 獲取“下一跳” (目的端口),這會根據你選擇的方法是LPM還是Exact而調用不同的代碼。

		if (dst_port >= RTE_MAX_ETHPORTS ||
			(enabled_port_mask & 1 << dst_port) == 0)
			dst_port = portid; // 如果成功獲取了目的端口,但端口沒有啟用或是超過了最大數量的限制,就設置目的端口與收包的端口一樣。
								//(從哪里收到的就原路返回。

#ifdef DO_RFC_1812_CHECKS
		/* Update time to live and header checksum */
		--(ipv4_hdr->time_to_live); // TTL 自減 1
		++(ipv4_hdr->hdr_checksum);
#endif
		/* dst addr */
		*(uint64_t *)&eth_hdr->d_addr = dest_eth_addr[dst_port]; 
		//根據查表得出的下一跳 port id,根據dest_eth_addr[dst_port]中的信息,改寫 eth_hdr 中的 目的 MAC 地址字段。

		/* src addr */
		ether_addr_copy(&ports_eth_addr[dst_port], &eth_hdr->s_addr);
		// 根據ports_eth_addr數組改寫 eth_hdr 中的 源 MAC 地址字段。

		send_single_packet(qconf, m, dst_port); // 協議棧的東西都處理完之后就加入發包隊列

	} else if (RTE_ETH_IS_IPV6_HDR(m->packet_type)) { // 如果不是 IPv4 而是 IPv6 的話
		/* Handle IPv6 headers.*/
		
                /*......*/
                
	} else { // 網絡層協議不是 IP
		/* Free the mbuf that contains non-IPV4/IPV6 packet */
		rte_pktmbuf_free(m); // 丟包
	}
}

這部分如果傳統TCP/IP網絡知識掌握的比較扎實,看懂是沒什么難度的。

二層發包

/* Enqueue a single packet, and send burst if queue is filled */
static inline int
send_single_packet(struct lcore_conf *qconf,
		   struct rte_mbuf *m, uint16_t port)
{
	uint16_t len;

	len = qconf->tx_mbufs[port].len;
	qconf->tx_mbufs[port].m_table[len] = m;
	len++; // 將該 pkt 進入發包隊列

	/* enough pkts to be sent */
	if (unlikely(len == MAX_PKT_BURST)) { // 當 tx queue 長度達到 Burst 就一次性發出
		send_burst(qconf, MAX_PKT_BURST, port);
		len = 0; // 清空發包隊列長度
	}

	qconf->tx_mbufs[port].len = len;
	return 0;
}

/* Send burst of packets on an output interface */
static inline int
send_burst(struct lcore_conf *qconf, uint16_t n, uint16_t port)
{
	struct rte_mbuf **m_table;
	int ret;
	uint16_t queueid;

	queueid = qconf->tx_queue_id[port];
	m_table = (struct rte_mbuf **)qconf->tx_mbufs[port].m_table;

	ret = rte_eth_tx_burst(port, queueid, m_table, n); // 參數:從哪個端口/哪條隊列/發出的pkt 的mbuf/發多少個包
	if (unlikely(ret < n)) {
		do {
			rte_pktmbuf_free(m_table[ret]);
		} while (++ret < n);
	}

	return 0;
}

// 邏輯核上的main函數
/* main processing loop */
int
lpm_main_loop(__attribute__((unused)) void *dummy)
{
	struct rte_mbuf *pkts_burst[MAX_PKT_BURST];
	unsigned lcore_id;
	uint64_t prev_tsc, diff_tsc, cur_tsc;
	int i, nb_rx;
	uint16_t portid;
	uint8_t queueid;
	struct lcore_conf *qconf;
	const uint64_t drain_tsc = (rte_get_tsc_hz() + US_PER_S - 1) /
		US_PER_S * BURST_TX_DRAIN_US; // 每隔一段時間發包的計時器

	prev_tsc = 0;

	lcore_id = rte_lcore_id();
	qconf = &lcore_conf[lcore_id];

	if (qconf->n_rx_queue == 0) { // 該 lcore 沒有配置收包隊列。
		RTE_LOG(INFO, L3FWD, "lcore %u has nothing to do\n", lcore_id);
		return 0;
	}

	RTE_LOG(INFO, L3FWD, "entering main loop on lcore %u\n", lcore_id);

	for (i = 0; i < qconf->n_rx_queue; i++) { // 打印該 lcore 負責的每條收包隊列

		portid = qconf->rx_queue_list[i].port_id;
		queueid = qconf->rx_queue_list[i].queue_id;
		RTE_LOG(INFO, L3FWD,
			" -- lcoreid=%u portid=%u rxqueueid=%hhu\n", 
			lcore_id, portid, queueid);
	}

	while (!force_quit) {

		cur_tsc = rte_rdtsc();

		/*
		 * TX burst queue drain
		 */
		diff_tsc = cur_tsc - prev_tsc;
		if (unlikely(diff_tsc > drain_tsc)) { // 該發包了

			for (i = 0; i < qconf->n_tx_port; ++i) {
				portid = qconf->tx_port_id[i];
				if (qconf->tx_mbufs[portid].len == 0)
					continue;
				send_burst(qconf,
					qconf->tx_mbufs[portid].len,
					portid); // 該函數見 l3fwd.h
				qconf->tx_mbufs[portid].len = 0;
			}

			prev_tsc = cur_tsc;
		}

		/*
		 * Read packet from RX queues
		 */
		for (i = 0; i < qconf->n_rx_queue; ++i) { // 對 lcore 負責的每條 rx queue
			portid = qconf->rx_queue_list[i].port_id;
			queueid = qconf->rx_queue_list[i].queue_id;
			nb_rx = rte_eth_rx_burst(portid, queueid, pkts_burst,
				MAX_PKT_BURST); // 收包
			if (nb_rx == 0)
				continue;

// 轉發
#if defined RTE_ARCH_X86 || defined RTE_MACHINE_CPUFLAG_NEON \
			 || defined RTE_ARCH_PPC_64 
			 // 對於 x86 系統架構的,使用優化 buffer 的轉發方法(沒看
			l3fwd_lpm_send_packets(nb_rx, pkts_burst,
						portid, qconf);
#else
			// 否則是普通的tx buffer轉發方法
			l3fwd_lpm_no_opt_send_packets(nb_rx, pkts_burst,
							portid, qconf); // 參數:包數量/包的mbuf/收包的port/收包的qconf
#endif /* X86 */
		}
	}

	return 0;
}

二層發包就參考L2fwd。

參考

todo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM