Byte Queue Limits算法试图控制网卡发送队列的大小,如下Intel的e1000驱动的网卡显示当前发送ring有256个,如果填满256个1500字节的报文,对于带宽为1G的e1000完全发送完成,需要256*1500/1G的时间,大约为3.84ms。意味值最后一个报文要在本地经历大约3.84ms的延迟,另一方面对于TCP协议,将对其RTT的估算等造成不利影响。
BQL与TCP的TSQ功能类似,但是TSQ位于更高层的协议栈,来控制发往Qdisc/Device队列中的报文的量。
$ sudo ethtool --show-ring ens33
Ring parameters for ens33:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 256
RX Mini: 0
RX Jumbo: 0
TX: 256
对于BQL,存在如何确定网卡发送队列大小限值的问题,首先,限值的表示方式,如果使用报文数量,有的报文可能包含很少的字节,占用很少的发送时间,而又存在比较大的报文,所以,采用报文数量进行控制造成不准确。BQL最终使用的是字节数量,但是其不是一个固定的限值,将根据系统负荷而改变,因为对于负荷重的系统,发送队列的填充可能会延迟,造成网卡的闲置。相反,对于负荷轻的系统,使用较小的限值,发送队列填充及时的话,也可保持网卡的持续工作。内核中实现此功能的为DQL(dynamic queue limits),BQL实现在此功能之上。
BQL在内核中增加了两类接口,第一个在网卡驱动程序中,当报文添加到网卡队列中后,通知BQL,其进行限值判断。第二个是在网卡报文发送完成之后,通知BQL,进行相应处理,比如,之前停止的发送队列是否可开启,以及更新BQL的队列限值。
BQL发送接口
核心函数为netdev_tx_sent_queue,如果DQL判段达到限值,发送队列设置__QUEUE_STATE_STACK_XOFF,停止发送。DQL算法暂不介绍(不懂)。这里,在再次检查之前设置STACK_OFF标志位,是因为在BQL的发送完成处理函数netdev_tx_completed_queue中,完成空间的释放之后(dql_completed),进行STACK_OFF的检查,如果为真,将再次进行发送调度。所以这里先将此标志设置为真。
static inline void netdev_tx_sent_queue(struct netdev_queue *dev_queue, unsigned int bytes)
{
#ifdef CONFIG_BQL
dql_queued(&dev_queue->dql, bytes);
if (likely(dql_avail(&dev_queue->dql) >= 0))
return;
set_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state);
/*
* The XOFF flag must be set before checking the dql_avail below,
* because in netdev_tx_completed_queue we update the dql_completed before checking the XOFF flag.
*/
smp_mb();
/* check again in case another CPU has just made room avail */
if (unlikely(dql_avail(&dev_queue->dql) >= 0))
clear_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state);
#endif
}
对于一些网卡驱动,目前主要是Mellanox的mlx4驱动,以下封装函数__netdev_tx_sent_queue用于在发送batch型的skb报文时,仅在最后一个skb处理时,进行是否要设置__QUEUE_STATE_STACK_XOFF标志的判断。
static inline bool __netdev_tx_sent_queue(struct netdev_queue *dev_queue, unsigned int bytes, bool xmit_more)
{
if (xmit_more) {
#ifdef CONFIG_BQL
dql_queued(&dev_queue->dql, bytes);
#endif
return netif_tx_queue_stopped(dev_queue);
}
netdev_tx_sent_queue(dev_queue, bytes);
return true;
}
对于Intel网卡的i40e驱动,在发送函数i40e_tx_map中,调用BQL函数netdev_tx_sent_queue进行处理。
static inline int i40e_tx_map(struct i40e_ring *tx_ring, struct sk_buff *skb,
struct i40e_tx_buffer *first, u32 tx_flags, const u8 hdr_len, u32 td_cmd, u32 td_offset)
{
...
netdev_tx_sent_queue(txring_txq(tx_ring), first->bytecount);
BQL发送完成接口
核心函数为netdev_tx_completed_queue如下,首先是DQL的处理函数dql_completed,其次,注意这里的smp_mb,如果没有此语句,将由可能导致发送队列永远的停止,对比netdev_tx_sent_queue中的smp_mb语句的位置,如果在发送完成之后,存在可使用的发送空间,netdev_tx_sent_queue将清空STACK_XOFF标志,而这里的test_and_clear_bit将返回假,导致调度函数netif_schedule_queue不能够执行。
static inline void netdev_tx_completed_queue(struct netdev_queue *dev_queue, unsigned int pkts, unsigned int bytes)
{
#ifdef CONFIG_BQL
if (unlikely(!bytes))
return;
dql_completed(&dev_queue->dql, bytes);
/*
* Without the memory barrier there is a small possiblity that
* netdev_tx_sent_queue will miss the update and cause the queue to be stopped forever
*/
smp_mb();
if (dql_avail(&dev_queue->dql) < 0)
return;
if (test_and_clear_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state))
netif_schedule_queue(dev_queue);
#endif
}
仍然使用Intel的i40e为例,在函数i40e_clean_tx_irq中,调用BQL的函数netdev_tx_completed_queue。
static bool i40e_clean_tx_irq(struct i40e_vsi *vsi, struct i40e_ring *tx_ring, int napi_budget)
{
/* notify netdev of completed buffers */
netdev_tx_completed_queue(txring_txq(tx_ring),
total_packets, total_bytes);
BQL重置接口
驱动程序可使用BQL的函数netdev_tx_reset_queue重置BQL,包括清空STACK_XOFF标志,重置DQL算法。
static inline void netdev_tx_reset_queue(struct netdev_queue *q)
{
#ifdef CONFIG_BQL
clear_bit(__QUEUE_STATE_STACK_XOFF, &q->state);
dql_reset(&q->dql);
#endif
}
对于Intel的i40e驱动,在函数i40e_clean_tx_ring中,调用BQL系统的函数netdev_tx_reset_queue进行重置。
void i40e_clean_tx_ring(struct i40e_ring *tx_ring)
{
...
/* Zero out the descriptor ring */
memset(tx_ring->desc, 0, tx_ring->size);
tx_ring->next_to_use = 0;
tx_ring->next_to_clean = 0;
if (!tx_ring->netdev)
return;
/* cleanup Tx queue statistics */
netdev_tx_reset_queue(txring_txq(tx_ring));
}
内核版本 5.0