最近realtek 8125发布了驱动9.012.04,从这里尝试分析一波page reuse,也方便大家查阅。
1 什么是page reuse?
这里可以参考这篇文章的网卡驱动收包代码分析之 page reuse 的第四节。我们为每一个desc分配的page是要能够满足两倍包大小的,比如包大小是1500,那我分配一个4096大小的页面,这就是足够的。
这里的理解其实也很简单,假设有一堆A4纸,比如小徐在不停地写字,而另一个人小季需要帮助他,写好的内容需要给主管看一遍。要保证小徐总有纸可以写,小季可以怎么做呢?1、每次都拿新的纸给小徐,同时把主管看过的纸给丢掉。2、每次都把用过的纸反过来,再给小徐写,且在主管看完内容之后,擦除页面上的东西,这样就可以反复使用了。
方法1和方法2对小徐来说没有差别,他只需要一直不停地写就可以,而同样对于主管来说,他只需要看就可以,而不在乎纸是否是同一张纸(当然,这里需要假设纸可以无限次地写和擦XD)。那么,在不考虑小季的情况下,方法2显然可以节省纸张,降低成本。而这,就是page reuse。
2 初始化
首先呢,需要在驱动初始化的时候分配 page。第一步,确定要分配的page的大小。
在rtl8125_set_rxbufsize中会调用函数rtl8125_rx_page_order来确定order,而这个order其实就是一个指数,如下,最左侧是order的值,右侧是需要的页面的大小区间,因为分配page是要向上兼容的,这样才能确保一定够用。
0 -> 2^0 * PAGE_SIZE 及以下
1 -> 2^1 * PAGE_SIZE 到 2^0 * PAGE_SIZE + 1
2 -> 2^2 * PAGE_SIZE 到 2^1 * PAGE_SIZE + 1
3 -> 2^3 * PAGE_SIZE 到 2^2 * PAGE_SIZE + 1
4 -> 2^4 * PAGE_SIZE 到 2^3 * PAGE_SIZE + 1
而最后分配的page size也就是 PAGE_SIZE << order,这里的PAGE_SIZE是内核里的一个值,一般是4096。
第二步,为每一个描述符(descriptor)分配对应的page。这在函数rtl8125_rx_fill中,主要是函数rtl8125_alloc_rx_page,这个函数我们仔细看一下。
static int
rtl8125_alloc_rx_page(struct rtl8125_private *tp, struct rtl8125_rx_ring *ring,
struct rtl8125_rx_buffer *rxb)
{
struct page *page;
dma_addr_t dma;
unsigned int order = tp->rx_buf_page_order;
//根据order来alloc一个page
page = dev_alloc_pages(order);
if (unlikely(!page))
return -ENOMEM;
//dma映射
dma = dma_map_page_attrs(&tp->pci_dev->dev, page, 0,
tp->rx_buf_page_size,
DMA_FROM_DEVICE,
(DMA_ATTR_SKIP_CPU_SYNC | DMA_ATTR_WEAK_ORDERING));
if (unlikely(dma_mapping_error(&tp->pci_dev->dev, dma))) {
__free_pages(page, order);
return -ENOMEM;
}
rxb->page = page;
//page的虚拟地址
rxb->data = page_address(page);
//这里的offset就是指偏移到正面还是反面了。假设page大小为4096
//offset为0,那就是正面,offset为2048,那就是反面。
//当然,这里的正面反面也只是帮助大家理解的。
rxb->page_offset = ring->rx_offset;
rxb->dma = dma;
//after page alloc, page refcount already = 1
//这个后面需要用到的
return 0;
}
那么到此为止,初始化的任务就已经完成了,下面是收包的流程。
3 收包
还记得我们之前提过的收包函数吗?那就是rtl8125_rx_interrupt。在该函数中,首先确定不enable PTP,然后enable page reuse and rx fragment,把代码精简一下,然后我们开始。
首先呢,就是rtl8125_rx_buffer这个结构体了,在上面已经介绍过,除了一个skb,这个skb就是我们packet所在的buffer。
struct rtl8125_rx_buffer {
struct page *page;
u32 page_offset;
dma_addr_t dma;
void* data;
struct sk_buff *skb;
};
然后我们看:
static int
rtl8125_rx_interrupt(struct net_device *dev,
struct rtl8125_private *tp,
struct rtl8125_rx_ring *ring,
napi_budget budget)
{
unsigned int cur_rx, rx_left;
unsigned int delta, count = 0;
unsigned int entry;
struct RxDesc *desc;
struct sk_buff *skb;
u32 status;
u32 rx_quota;
u32 ring_index = ring->index;
struct rtl8125_rx_buffer *rxb;
unsigned int total_rx_multicast_packets = 0;
unsigned int total_rx_bytes = 0, total_rx_packets = 0;
assert(dev != NULL);
assert(tp != NULL);
if (ring->RxDescArray == NULL)
goto rx_out;
rx_quota = RTL_RX_QUOTA(budget);
//cur_rx就是当前所用过的desc的number,前面的都是clean的,也就是可以用的
//之后如果要继续使用desc也是从cur_rx开始
cur_rx = ring->cur_rx;
//dirty_rx就是当前已处理过的desc的number,其前面直到cur_rx之间的都是dirty的
//所以这里计算的rx_left就是clean的desc的数目
rx_left = ring->num_rx_desc + ring->dirty_rx - cur_rx;
//这里取了一个最小值,得到了最终我们这次rx_interrupt可以收的包数目
rx_left = rtl8125_rx_quota(rx_left, (u32)rx_quota);
for (; rx_left > 0; rx_left--, cur_rx++) {
u32 pkt_size;
//desc是“围成”一个ring的,所以要得到ring index就要除以desc number
entry = cur_rx % ring->num_rx_desc;
//这里抓到这个desc
desc = rtl8125_get_rxdesc(tp, ring->RxDescArray, entry);
status = le32_to_cpu(rtl8125_rx_desc_opts1(tp, desc));
//如果desc own bit为1表示这个desc为nic所拥有,nic只有在已经fill了
//这个rx buffer之后clear它,表示这个desc已经为host system own,可以
//进行软件收包流程了
if (status & DescOwn)
break;
rmb();
if (unlikely(rtl8125_check_rx_desc_error(dev, tp, status) < 0)) {
if (netif_msg_rx_err(tp)) {
printk(KERN_INFO
"%s: Rx ERROR. status = %08x\n",
dev->name, status);
}
RTLDEV->stats.rx_errors++;
if (!(dev->features & NETIF_F_RXALL))
goto release_descriptor;
}
//这里得到pkt size的大小
pkt_size = status & 0x00003fff;
if (likely(!(dev->features & NETIF_F_RXFCS))) {
//这里查看是否是end of packet(eop),如果是就返回0.否则返回1.
//如果不是最后一个skb,那么要比较一下pkt size,如果和buffer size相同
//那么表示它确实是中间的包,进入循环
if (rtl8125_is_non_eop(tp, status) &&
pkt_size == tp->rx_buf_sz) {
struct RxDesc *desc_next;
unsigned int entry_next;
int pkt_size_next;
u32 status_next;
entry_next = (cur_rx + 1) % ring->num_rx_desc;
desc_next = rtl8125_get_rxdesc(tp, ring->RxDescArray, entry_next);
status_next = le32_to_cpu(rtl8125_rx_desc_opts1(tp, desc_next));
if (!(status_next & DescOwn)) {
pkt_size_next = status_next & 0x00003fff;
//最终呢到达这里,他的目的就是确定真正的pkt size,这在后面要用于计算
if (pkt_size_next < ETH_FCS_LEN)
pkt_size -= (ETH_FCS_LEN - pkt_size_next);
}
}
//如果是eop,那么也要做一下确认
if (!rtl8125_is_non_eop(tp, status)) {
if (pkt_size < ETH_FCS_LEN)
pkt_size = 0;
else
pkt_size -= ETH_FCS_LEN;
}
}
if (unlikely(pkt_size > tp->rx_buf_sz))
goto drop_packet;
rxb = &ring->rx_buffer[entry];
skb = rxb->skb;
rxb->skb = NULL;
//如果这里的skb为空则需要build skb
if (!skb) {
skb = RTL_BUILD_SKB_INTR(rxb->data + rxb->page_offset - ring->rx_offset, tp->rx_buf_page_size / 2);
if (!skb) {
//netdev_err(tp->dev, "Failed to allocate RX skb!\n");
goto drop_packet;
}
skb->dev = dev;
if (!R8125_USE_NAPI_ALLOC_SKB)
skb_reserve(skb, R8125_RX_ALIGN);
skb_put(skb, pkt_size);
} else
//确保网络堆栈可以通过最大限度地减少数据拷贝的需求来有效地接收和处理数据包
//这主要是针对rx fragment的情形的
skb_add_rx_frag(skb, skb_shinfo(skb)->nr_frags, rxb->page,
rxb->page_offset, pkt_size, tp->rx_buf_page_size / 2);
//这就是page reuse的函数了
rtl8125_put_rx_buffer(tp, ring, cur_rx, rxb);
dma_sync_single_range_for_cpu(tp_to_dev(tp),
rxb->dma,
rxb->page_offset,
tp->rx_buf_sz,
DMA_FROM_DEVICE);
//如果当前不是eop,那么就是rx fragment的情形,所以需要继续上述步骤
if (rtl8125_is_non_eop(tp, status)) {
unsigned int entry_next;
entry_next = (entry + 1) % ring->num_rx_desc;
rxb = &ring->rx_buffer[entry_next];
rxb->skb = skb;
continue;
}
#ifdef ENABLE_RSS_SUPPORT
rtl8125_rx_hash(tp, (struct RxDescV3 *)desc, skb);
#endif
rtl8125_rx_csum(tp, skb, desc, status);
skb->protocol = eth_type_trans(skb, dev);
total_rx_bytes += skb->len;
if (skb->pkt_type == PACKET_MULTICAST)
total_rx_multicast_packets++;
if (rtl8125_rx_vlan_skb(tp, desc, skb) < 0)
rtl8125_rx_skb(tp, skb, ring_index);
#if LINUX_VERSION_CODE < KERNEL_VERSION(4,11,0)
dev->last_rx = jiffies;
#endif //LINUX_VERSION_CODE < KERNEL_VERSION(4,11,0)
total_rx_packets++;
rxb->skb = NULL;
continue;
release_descriptor:
if (tp->InitRxDescType == RX_DESC_RING_TYPE_3) {
rtl8125_set_desc_dma_addr(tp, desc,
ring->RxDescPhyAddr[entry]);
wmb();
}
rtl8125_mark_to_asic(tp, desc, tp->rx_buf_sz);
continue;
drop_packet:
RTLDEV->stats.rx_dropped++;
RTLDEV->stats.rx_length_errors++;
goto release_descriptor;
}
count = cur_rx - ring->cur_rx;
ring->cur_rx = cur_rx;
//这里的rx fill在alloc的时候我们看过,
delta = rtl8125_rx_fill(tp, ring, dev, ring->dirty_rx, ring->cur_rx, 1);
if (!delta && count && netif_msg_intr(tp))
printk(KERN_INFO "%s: no Rx buffer allocated\n", dev->name);
ring->dirty_rx += delta;
RTLDEV->stats.rx_bytes += total_rx_bytes;
RTLDEV->stats.rx_packets += total_rx_packets;
RTLDEV->stats.multicast += total_rx_multicast_packets;
/*
* FIXME: until there is periodic timer to try and refill the ring,
* a temporary shortage may definitely kill the Rx process.
* - disable the asic to try and avoid an overflow and kick it again
* after refill ?
* - how do others driver handle this condition (Uh oh...).
*/
if ((ring->dirty_rx + ring->num_rx_desc == ring->cur_rx) && netif_msg_intr(tp))
printk(KERN_EMERG "%s: Rx buffers exhausted\n", dev->name);
rx_out:
return total_rx_packets;
}
我们重点讲一下reuse page的函数rtl8125_put_rx_buffer。首先需要为大家补充一下page ref count的知识。
在 Linux 中,page ref count是跟踪对该页面的引用数量。 如果一个页面的计数下降到0,则意味着不再有对其的引用,并且它可能被系统释放[1]。 需要注意的是,只有在没有其他条件阻止该页被释放的情况下(例如该页因 IO 操作或其他内核活动而被锁定),该页才会被释放。此外,页面在活动和非活动列表上进行管理,引用和活动跟踪有助于确定是否应回收(释放)页面。 当系统寻求释放内存时,它通常会从非活动列表中回收页面,其中包含被认为是“clod”或不经常访问的页面[2]。这个进程是Linux内核内存管理系统的一个组成部分,它不断地平衡内存分配和回收,以保持最佳的系统性能。
在Linux内核中,page_ref_count()函数用于检索特定内存页的当前引用计数,该引用计数在该页面被使用时递增,在不再需要时递减。 当第一次分配页面时,其引用计数通常设置为 1。 随着页面映射到不同进程的地址空间或被内核组件使用,它会增加。
知道了这个知识我们继续rtl8125_put_rx_buffer的分析。
这里的entry用的是dirty rx,代表这是要clean的。首先我们要看是否可以reuse,一个是要避免reuse remote pages,另一个是,我们的确保我们是唯一的owner of page。page ref count上面已经解释过。而这个判断用之前的例子来说,也就是主管还在看呢,你不能把这张纸拿过来擦掉呀。
如果page不能reuse,那么我们就unmap该page,之后重新再分配一个page就是了。这里为什么不free page呢?对,系统会自动free的。
static void rtl8125_put_rx_buffer(struct rtl8125_private *tp,
struct rtl8125_rx_ring *ring,
u32 cur_rx,
struct rtl8125_rx_buffer *rxb)
{
struct rtl8125_rx_buffer *nrxb;
struct page *page = rxb->page;
u32 entry;
entry = ring->dirty_rx % ring->num_rx_desc;
nrxb = &ring->rx_buffer[entry];
if (likely(rtl8125_reuse_rx_ok(page))) {
/* hand second half of page back to the ring */
rtl8125_reuse_rx_buffer(tp, ring, cur_rx, rxb);
} else {
tp->page_reuse_fail_cnt++;
dma_unmap_page_attrs(&tp->pci_dev->dev, rxb->dma,
tp->rx_buf_page_size,
DMA_FROM_DEVICE,
(DMA_ATTR_SKIP_CPU_SYNC | DMA_ATTR_WEAK_ORDERING));
//the page ref is kept 1, uniquely owned by kernel now
rxb->page = NULL;
return;
}
dma_sync_single_range_for_device(tp_to_dev(tp),
nrxb->dma,
nrxb->page_offset,
tp->rx_buf_sz,
DMA_FROM_DEVICE);
rtl8125_map_to_asic(tp, ring, ring->RxDescArray + entry,
nrxb->dma + nrxb->page_offset,
tp->rx_buf_sz, entry);
ring->dirty_rx++;
}
接下来就是具体执行page reuse的函数了。在Linux内核中,page_ref_inc函数被用来增加给定内存页的引用计数。这是因为我们在用到这个page,自然是要+1的。然后主要就是“翻转”page。可以看到,我们对之取了一半页面大小的异或值。大家可以拿0带入计算一下,最终会在0和2048之间来回翻转,像不像一张A4纸的正反面?也就是小季在做的第一件事。
然后就是把rxb的dma、page offset和data给nrxb,这里怎么理解呢?rxb对应的是cur rx而nrxb对应的是dirty rx。那么这里在做的就是用cur rx的反面替换dirty rx的正面,这样dirty rx的clean就完成了,后续dirty rx就可以++了。
那么最后那个判断该怎么理解呢?假如我们每次page reuse都是成功的,那么其实是同一个page,这样的话,我们只需要进行翻转就可以了。但是,如果我们中间有page reuse fail,这时候,dirty rx的值和cur rx的值是不相等的。那么就需要把page的值也给nrxb,且同时要清空cur rx对应的desc对应的page,因为它已经给了dirty rx了,后续需要重新给cur rx分配一个page才行。于是乎,在rx_interrupt函数的最后,我们需要执行一次rx_fill,也就是为dirty rx之前直到cur rx之间的desc重新“填充”page。
static void
rtl8125_reuse_rx_buffer(struct rtl8125_private *tp, struct rtl8125_rx_ring *ring, u32 cur_rx, struct rtl8125_rx_buffer *rxb)
{
struct page *page = rxb->page;
u32 dirty_rx = ring->dirty_rx;
u32 entry = dirty_rx % ring->num_rx_desc;
struct rtl8125_rx_buffer *nrxb = &ring->rx_buffer[entry];
u32 noffset;
//the page gonna be shared by us and kernel, keep page ref = 2
page_ref_inc(page);
//flip the buffer in page to use next
noffset = rxb->page_offset ^ (tp->rx_buf_page_size / 2); //one page, two buffer, ping-pong
nrxb->dma = rxb->dma;
nrxb->page_offset = noffset;
nrxb->data = rxb->data;
if (cur_rx != dirty_rx) {
//move the buffer to other slot
nrxb->page = page;
rxb->page = NULL;
}
}
那么关于瑞昱R8125的page reuse的分析就结束了。
如果觉得这篇文章有用的话,可以点赞、评论或者收藏,万分感谢,goodbye~