DPDK シリーズ 26 バッファ キャッシュ管理

1. キャッシュの有用性

実際、私はこの問題を分析したいと思ったことはありません。主な理由は、問題が多すぎるからです。DPDK を学んでいなくても、マルチスレッドの擬似共有を含む、コンピューターの原理やオペレーティング システム、メモリベースのフレームワークなどでこの問題を避けることはできません。この問題は避けられず、常套句が曖昧になっているとも言えます。
したがって、ここで焦点を当てるのはキャッシュの原理について話すことではありません。読むべき本やインターネットが多すぎます。ここでは、DPDK でのキャッシュの使用方法、つまり DPDK でのキャッシュの用途を分析することに焦点を当てます。
1. メモリ ロックの同時競合を減らすため、読み取りおよび書き込み速度を向上させることも目的です。
2. 読み取りおよび書き込み速度を向上させること

2. DPDKにおけるキャッシュ処理

1. キャッシュヒュージページメモリのサポート
: ヒット率の向上
DIDO: ハードウェアバッファを直接処理し、メモリをスキップ
TLB: TLB とヒュージページの組み合わせは、依然としてキャッシュヒット率を向上させる方法です

2. プリフェッチ命令は
通常、すべてハードウェアであるキャッシュのレベルにあり、せいぜい OS の操作によって処理され、一般に上位層には公開されません。しかし、テクノロジーの発展に伴い、ソフトウェア開発者はプリフェッチ命令を操作することもでき、最下層もこれらのソフトウェア プリフェッチ命令をオープンします。DPDK はこのテクノロジーを使用してキャッシュのデータ読み込みを処理し、実行効率を向上させることができます。ただし、ソフトウェアで同じことをしたい場合は、虎を描いたときに犬にならないように、適切な戦略を検討する必要があることに注意してください。
プリフェッチ命令は通常アセンブリ コマンドですが、一部のプログラムではカプセル化された上位層 API ライブラリも提供します。
DPDK では、データ処理とクロック サイクルを一致させる、つまり最大の効率を達成するために、すべてのデータをキャッシュに確実に入れる必要があります。そうしないと、パフォーマンスが大幅に低下します。そして、このプリフェッチ命令は、このヒットに連携するための手段である。もちろん、DPDK にはこの目的を達成できる他の手段もあり、処理とデータ読み取りの一貫性を保つ結果は 1 つだけです。

3. DPDK のプリフェッチ一貫性処理
最近のコンピューターは基本的にマルチコアまたはマルチ CPU ですが、異なるコアが同じキャッシュにアクセスする場合、DPDK は競合をどのように処理しますか? 言い換えれば、データの一貫性をどのように確保するかということです。解決策は非常に単純かつ失礼で、各コアに個別のキャッシュを直接与えます。このように、読み取りと書き込みは独自のデータ キューのみを操作するため、競合は発生しません。ただし、これはデータの最終的な一貫性ももたらすため、設計によって解決する必要があります。競合を避ける必要がある場合は、ロックするか、いくつかのプロトコルを使用して競合を解決する必要があります。
さらに、競合を最小限に抑えるために、割り当て中にキャッシュ ライン (キャッシュの最小単位) が直接アライメントされます。これも別の手段です。つまり、1 つの Cache Line 内で 2 つに分割されないように注意してください。
次に、整合性プロトコルにはいくつかの種類があります。
ディレクトリベースのプロトコルとバス スヌーピング プロトコルですが、ここでは展開しません。興味があれば情報を確認してください。

3. ソースコード分析

Cache の関連するソース コードを見てみましょう。

//Ring
struct rte_ring {
	/*
	 * Note: this field kept the RTE_MEMZONE_NAMESIZE size due to ABI
	 * compatibility requirements, it could be changed to RTE_RING_NAMESIZE
	 * next time the ABI changes
	 */
	char name[RTE_MEMZONE_NAMESIZE] __rte_cache_aligned; /**< Name of the ring. */
	int flags;               /**< Flags supplied at creation. */
	const struct rte_memzone *memzone;
			/**< Memzone, if any, containing the rte_ring */
	uint32_t size;           /**< Size of ring. */
	uint32_t mask;           /**< Mask (size-1) of ring. */
	uint32_t capacity;       /**< Usable size of ring */

	char pad0 __rte_cache_aligned; /**< empty cache line */

	/** Ring producer status. */
	struct rte_ring_headtail prod __rte_cache_aligned;
	char pad1 __rte_cache_aligned; /**< empty cache line */

	/** Ring consumer status. */
	struct rte_ring_headtail cons __rte_cache_aligned;
	char pad2 __rte_cache_aligned; /**< empty cache line */
};

// librte_eal/common/include/rte_common.h
/** Force alignment to cache line. */
#define __rte_cache_aligned __rte_aligned(RTE_CACHE_LINE_SIZE)
#define __rte_aligned(a) __attribute__((__aligned__(a)))  

マクロ __rte_cache_aligned は基本データ構造によく見られますが、これは実際にはキャッシュ ラインの処理位置合わせ方法です。
各コアの構成データ構造定義をもう一度見てください。

struct lcore_conf {
	uint16_t nb_rx_queue;
	struct lcore_rx_queue rx_queue_list[MAX_RX_QUEUE_PER_LCORE];
	uint16_t tx_queue_id[RTE_MAX_ETHPORTS];
	struct buffer tx_mbufs[RTE_MAX_ETHPORTS];
	struct ipsec_ctx inbound;
	struct ipsec_ctx outbound;
	struct rt_ctx *rt4_ctx;
	struct rt_ctx *rt6_ctx;
	struct {
		struct rte_ip_frag_tbl *tbl;
		struct rte_mempool *pool_dir;
		struct rte_mempool *pool_indir;
		struct rte_ip_frag_death_row dr;
	} frag;
} __rte_cache_aligned;//总是行对齐,防止跨Cache Line
static struct lcore_conf lcore_conf[RTE_MAX_LCORE];

RTE_MAX_LCORE は現在の最大コア数です。複数のコアが同じデータ構造にアクセスする問題を回避するために、コアへのアクセスは数値によって制御されます。同様に、最新のネットワーク カードは一般にマルチキュー ネットワーク カードをサポートしており、DPDK はこれらの問題に対処するために複数の読み取りおよび書き込みキューを用意しています。以前のインストールから来た読者は、インストール中に構成を思い出す可能性があります。
受信したプリフェッチを見てみましょう。

uint16_t
ixgbe_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts,
		uint16_t nb_pkts)
{
	struct ixgbe_rx_queue *rxq;
	volatile union ixgbe_adv_rx_desc *rx_ring;
	volatile union ixgbe_adv_rx_desc *rxdp;
	struct ixgbe_rx_entry *sw_ring;
	struct ixgbe_rx_entry *rxe;
	struct rte_mbuf *rxm;
	struct rte_mbuf *nmb;
	union ixgbe_adv_rx_desc rxd;
	uint64_t dma_addr;
	uint32_t staterr;
	uint32_t pkt_info;
	uint16_t pkt_len;
	uint16_t rx_id;
	uint16_t nb_rx;
	uint16_t nb_hold;
	uint64_t pkt_flags;
	uint64_t vlan_flags;

	nb_rx = 0;
	nb_hold = 0;
	rxq = rx_queue;
	rx_id = rxq->rx_tail;
	rx_ring = rxq->rx_ring;
	sw_ring = rxq->sw_ring;
	vlan_flags = rxq->vlan_flags;
	while (nb_rx < nb_pkts) {
		/*
		 * The order of operations here is important as the DD status
		 * bit must not be read after any other descriptor fields.
		 * rx_ring and rxdp are pointing to volatile data so the order
		 * of accesses cannot be reordered by the compiler. If they were
		 * not volatile, they could be reordered which could lead to
		 * using invalid descriptor fields when read from rxd.
		 */
		rxdp = &rx_ring[rx_id];
		staterr = rxdp->wb.upper.status_error;
		if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
			break;
		rxd = *rxdp;

		/*
		 * End of packet.
		 *
		 * If the IXGBE_RXDADV_STAT_EOP flag is not set, the RX packet
		 * is likely to be invalid and to be dropped by the various
		 * validation checks performed by the network stack.
		 *
		 * Allocate a new mbuf to replenish the RX ring descriptor.
		 * If the allocation fails:
		 *    - arrange for that RX descriptor to be the first one
		 *      being parsed the next time the receive function is
		 *      invoked [on the same queue].
		 *
		 *    - Stop parsing the RX ring and return immediately.
		 *
		 * This policy do not drop the packet received in the RX
		 * descriptor for which the allocation of a new mbuf failed.
		 * Thus, it allows that packet to be later retrieved if
		 * mbuf have been freed in the mean time.
		 * As a side effect, holding RX descriptors instead of
		 * systematically giving them back to the NIC may lead to
		 * RX ring exhaustion situations.
		 * However, the NIC can gracefully prevent such situations
		 * to happen by sending specific "back-pressure" flow control
		 * frames to its peer(s).
		 */
		PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_id=%u "
			   "ext_err_stat=0x%08x pkt_len=%u",
			   (unsigned) rxq->port_id, (unsigned) rxq->queue_id,
			   (unsigned) rx_id, (unsigned) staterr,
			   (unsigned) rte_le_to_cpu_16(rxd.wb.upper.length));

		nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
		if (nmb == NULL) {
			PMD_RX_LOG(DEBUG, "RX mbuf alloc failed port_id=%u "
				   "queue_id=%u", (unsigned) rxq->port_id,
				   (unsigned) rxq->queue_id);
			rte_eth_devices[rxq->port_id].data->rx_mbuf_alloc_failed++;
			break;
		}

		nb_hold++;
		rxe = &sw_ring[rx_id];
		rx_id++;
		if (rx_id == rxq->nb_rx_desc)
			rx_id = 0;

		/* Prefetch next mbuf while processing current one. */
		rte_ixgbe_prefetch(sw_ring[rx_id].mbuf);

		/*
		 * When next RX descriptor is on a cache-line boundary,
		 * prefetch the next 4 RX descriptors and the next 8 pointers
		 * to mbufs.
		 */
		if ((rx_id & 0x3) == 0) {
			rte_ixgbe_prefetch(&rx_ring[rx_id]);
			rte_ixgbe_prefetch(&sw_ring[rx_id]);
		}

		rxm = rxe->mbuf;
		rxe->mbuf = nmb;
		dma_addr =
			rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb));
		rxdp->read.hdr_addr = 0;
		rxdp->read.pkt_addr = dma_addr;

		/*
		 * Initialize the returned mbuf.
		 * 1) setup generic mbuf fields:
		 *    - number of segments,
		 *    - next segment,
		 *    - packet length,
		 *    - RX port identifier.
		 * 2) integrate hardware offload data, if any:
		 *    - RSS flag & hash,
		 *    - IP checksum flag,
		 *    - VLAN TCI, if any,
		 *    - error flags.
		 */
		pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) -
				      rxq->crc_len);
		rxm->data_off = RTE_PKTMBUF_HEADROOM;
		rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
		rxm->nb_segs = 1;
		rxm->next = NULL;
		rxm->pkt_len = pkt_len;
		rxm->data_len = pkt_len;
		rxm->port = rxq->port_id;

		pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data);
		/* Only valid if PKT_RX_VLAN set in pkt_flags */
		rxm->vlan_tci = rte_le_to_cpu_16(rxd.wb.upper.vlan);

		pkt_flags = rx_desc_status_to_pkt_flags(staterr, vlan_flags);
		pkt_flags = pkt_flags |
			rx_desc_error_to_pkt_flags(staterr, (uint16_t)pkt_info,
						   rxq->rx_udp_csum_zero_err);
		pkt_flags = pkt_flags |
			ixgbe_rxd_pkt_info_to_pkt_flags((uint16_t)pkt_info);
		rxm->ol_flags = pkt_flags;
		rxm->packet_type =
			ixgbe_rxd_pkt_info_to_pkt_type(pkt_info,
						       rxq->pkt_type_mask);

		if (likely(pkt_flags & PKT_RX_RSS_HASH))
			rxm->hash.rss = rte_le_to_cpu_32(
						rxd.wb.lower.hi_dword.rss);
		else if (pkt_flags & PKT_RX_FDIR) {
			rxm->hash.fdir.hash = rte_le_to_cpu_16(
					rxd.wb.lower.hi_dword.csum_ip.csum) &
					IXGBE_ATR_HASH_MASK;
			rxm->hash.fdir.id = rte_le_to_cpu_16(
					rxd.wb.lower.hi_dword.csum_ip.ip_id);
		}
		/*
		 * Store the mbuf address into the next entry of the array
		 * of returned packets.
		 */
		rx_pkts[nb_rx++] = rxm;
	}
	rxq->rx_tail = rx_id;

	/*
	 * If the number of free RX descriptors is greater than the RX free
	 * threshold of the queue, advance the Receive Descriptor Tail (RDT)
	 * register.
	 * Update the RDT with the value of the last processed RX descriptor
	 * minus 1, to guarantee that the RDT register is never equal to the
	 * RDH register, which creates a "full" ring situtation from the
	 * hardware point of view...
	 */
	nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
	if (nb_hold > rxq->rx_free_thresh) {
		PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_tail=%u "
			   "nb_hold=%u nb_rx=%u",
			   (unsigned) rxq->port_id, (unsigned) rxq->queue_id,
			   (unsigned) rx_id, (unsigned) nb_hold,
			   (unsigned) nb_rx);
		rx_id = (uint16_t) ((rx_id == 0) ?
				     (rxq->nb_rx_desc - 1) : (rx_id - 1));
		IXGBE_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
		nb_hold = 0;
	}
	rxq->nb_rx_hold = nb_hold;
	return nb_rx;
}

上記のプリフェッチは非常に明確に記述されています。定義を見てください。ソース コードには 3 種類のプラットフォームがあります。ここでは X86 のみを見ていきます。

static inline void rte_prefetch0(const volatile void *p)
{
    
    
	asm volatile ("prefetcht0 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch1(const volatile void *p)
{
    
    
	asm volatile ("prefetcht1 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch2(const volatile void *p)
{
    
    
	asm volatile ("prefetcht2 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch_non_temporal(const volatile void *p)
{
    
    
	asm volatile ("prefetchnta %[p]" : : [p] "m" (*(const volatile char *)p));
}

他のプリフェッチについては、rte_packet_prefetch を検索すると、すべて問題ありません。ここでは詳細には触れません。

4. まとめ

実際、上記の分析から、どのような方法が使用されるとしても、目的は 1 つだけであり、流れが乱雑にならないように流れ作業を行う必要があることがわかります。このようにして、データを効率的に生成および処理できます。この種のデータ処理ソフトウェアのフレームワークはこれらに最も気を配っており、データが流れ続けて意図通りに進む限り、設計目標は達成されます。
結局のところ、外部からの介入がある限り、その介入時間は CPU にとって非常に長い時間であるため、効率はもはや問題になりません。介入がない場合、データの最大フローが保証されなければなりません。ネットワーク データのダウンロード、オンラインでのビデオの視聴、オンライン ビデオ会議など。
実際、これはソフトウェア設計がアプリケーション シナリオに焦点を当てていることも示しており、Redis などのいくつかのフレームワークもアプリケーション シナリオに明確に位置付けられており、それが非常に人気がある理由です。では、このことから何を学べるでしょうか? それは言うまでもない。

おすすめ

転載: blog.csdn.net/fpcc/article/details/132004583