ILD

skb内存碎片化导致oom问题定位解决
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2022-9-8 站点:Inside Linux Development

路由器开启无线,使用5g一段时间后,free 内存会慢慢的往下降,使用过程就是刷抖音,玩王者荣耀,流量都比较低。

前面写过一篇博客,使用page owner来分析内存泄露,使用该方法定位出了,内存泄露的原因是大量的skb没有释放,对应的分配栈为:


times 304->878 order 3 mask 0xd2a20
 __alloc_pages+0x134/0x1064
 page_frag_alloc_align+0x84/0x180
 __netdev_alloc_skb+0x158/0x190
 ath11k_ce_rx_post_pipe+0x1c0/0x364 [ath11k]
 ath11k_ce_per_engine_service+0x2d8/0x38c [ath11k]
 ath11k_pci_ce_tasklet+0x18/0x34 [ath11k_ahb]
 tasklet_action_common.constprop.0+0xc8/0xf0
 __do_softirq+0x13c/0x444

times 0->302 order 3 mask 0xd2a20
 __alloc_pages+0x134/0x1064
 page_frag_alloc_align+0x84/0x180
 __netdev_alloc_skb+0x158/0x190
 ath11k_dp_rxbufs_replenish+0x150/0x3e8 [ath11k]
 ath11k_dp_process_rx+0x370/0x48c [ath11k]
 ath11k_dp_service_srng+0x80/0x30c [ath11k]
 ath11k_pci_ext_grp_napi_poll+0x20/0x88 [ath11k_ahb]
 __napi_poll+0x28/0x208
 net_rx_action+0xe4/0x270
 __do_softirq+0x13c/0x444

times 0->739 order 3 mask 0xd2a20
 __alloc_pages+0x134/0x1064
 page_frag_alloc_align+0x84/0x180
 __netdev_alloc_skb+0x158/0x190
 syn_dp_rx_refill+0x120/0x1bc [nss_dp]
 syn_dp_napi_poll_rx+0x2c/0x84 [nss_dp]
 __napi_poll+0x28/0x208
 net_rx_action+0xe4/0x270
 __do_softirq+0x13c/0x444


ath11k内存相关代码分析

ath11k debug fs可以查看内存统计:

/ # cat /sys/kernel/debug/ath11k/qcn6122_1/memory_stats
MEMORY STATS IN BYTES:
malloc size : 786432
ce_ring_alloc size: 32076
dma_alloc size:: 6719115
htc_skb_alloc size: 3648
wmi tx skb alloc size: 4240818176
per peer object: 1428
rx_post_buf size: 1623040
Total size: 4249983915


hw参数

drivers/net/wireless/ath/ath11k/hw.h


#define TARGET_NUM_VDEVS        8
#define TARGET_NUM_PEERS_PDEV   (128 + TARGET_NUM_VDEVS)
#define TARGET_NUM_STATIONS     128
#define ATH11K_QMI_TARGET_MEM_MODE      ATH11K_QMI_TARGET_MEM_MODE_256M
#define ATH11K_DP_TX_COMP_RING_SIZE     8192
#define ATH11K_DP_RXDMA_MON_STATUS_RING_SIZE    512
#define ATH11K_DP_RXDMA_MONITOR_BUF_RING_SIZE   128
#define ATH11K_DP_RXDMA_MONITOR_DST_RING_SIZE   128
#define ATH11K_DP_RXDMA_REFILL_RING_SIZE        2048
#define ATH11K_DP_RXDMA_NSS_REFILL_RING_SIZE    1816

host ce

咋也不知道这是啥,但是看代码,这一块分配很多内存

drivers/net/wireless/ath/ath11k/ce.c


分配调用栈:

int ath9k_ahb_probe(struct platform_device *pdev)

    ret = ath11k_ce_alloc_pipes(ab);

        for (i = 0; i < ab->hw_params.ce_count; i++)

            ret = ath11k_ce_alloc_pipe(ab, i);


int ath11k_ce_alloc_pipe(struct ath11k_base *ab, int ce_id)

调用ath11k_ce_alloc_ring()为每个ce分配src和dst ring.

    ring = ath11k_ce_alloc_ring(ab, nentries, desc_sz);


ath11k_ce_alloc_ring(struct ath11k_base *ab, int nentries, int desc_sz)

分配nentries个skb指针。

分配nentries*desc_sz内存的dma.



drivers/net/wireless/ath/ath11k/core.c

的 ath11k_hw_params[] 数组中,定义了各个芯片的硬件参数。

qcn6122的.host_ce_config用的是qcn9074的ce配置


const struct ce_attr ath11k_host_ce_config_qcn9074[] = {
        /* CE0: host->target HTC control and raw streams */
        {
                .flags = CE_ATTR_FLAGS,
                .src_nentries = 16,
                .src_sz_max = 2048,
                .dest_nentries = 0,
        },

        /* CE1: target->host HTT + HTC control */
        {    
                .flags = CE_ATTR_FLAGS,
                .src_nentries = 0,
                .src_sz_max = 2048,
                .dest_nentries = 256,
                .recv_cb = ath11k_htc_rx_completion_handler,
        },

        /* CE2: target->host WMI */
        {
                .flags = CE_ATTR_FLAGS,
                .src_nentries = 0,
                .src_sz_max = 2048,
                .dest_nentries = 128,
                .recv_cb = ath11k_htc_rx_completion_handler,
        },

        /* CE3: host->target WMI (mac0) */
        {
                .flags = CE_ATTR_FLAGS,
                .src_nentries = 32,
                .src_sz_max = 2048,
                .dest_nentries = 0,
                .send_cb = ath11k_htc_tx_completion_handler,
        },

        /* CE4: host->target HTT */
        {
                .flags = CE_ATTR_FLAGS | CE_ATTR_DIS_INTR,
                .src_nentries = 512,
                .src_sz_max = 256,
                .dest_nentries = 0,
        },

        /* CE5: target->host pktlog */
        {
                .flags = CE_ATTR_FLAGS,
                .src_nentries = 0,
                .src_sz_max = 2048,
                .dest_nentries = 256,
                .recv_cb = ath11k_dp_htt_htc_t2h_msg_handler,
        },
};


就ring这个,占用的内存其实不多,就几十k:

[   12.480524] =======ce 0 src entries 16
[   12.491351] ring size: 100 256
[   12.501859] =======ce 1 dst entries 256
[   12.512082] ring size: 1060 2048
[   12.522251] ring size: 1060 4096
[   12.532328] =======ce 2 dst entries 128
[   12.535416] ring size: 548 1024
[   12.540287] ring size: 548 2048
[   12.542113] =======ce 3 src entries 32
[   12.545224] ring size: 164 512
[   12.549085] =======ce 4 src entries 512
[   12.552082] ring size: 2084 8192
[   12.555837] =======ce 5 dst entries 256
[   12.559310] ring size: 1060 2048
[   12.562861] ring size: 1060 4096


RX

drivers/net/wireless/ath/ath11k/dp_rx.c


rx分配调用过程:

在firmware加载完毕

ret = ath11k_core_qmi_firmware_ready(ab);

    ret = ath11k_core_pdev_create(ab);

        ret = ath11k_dp_pdev_alloc(ab);

            ath11k_dp_rx_pdev_alloc(ab, i)


定位过程

测试发现,在出现内存紧张的时候,卸载掉ath11k_ahb模式,内存就只消耗了20M。那就说明skb没有泄露,是被ath11k追踪起来了,在卸载的模块的时候释放后,内存就正常了。


于是在卸载过程中,使用下面的接口获取可用内存:

global_zone_page_state(NR_FREE_PAGES)


添加打印,发现下面的函数释放了大量的内存,释放了13K page,50多M内存。

[  128.878227] a111==2 free 7019
[  128.881166] a1112==1 free 7019
[  128.896926] a1112==2 free 20225
[  128.897332] a1112==3 free 20273
[  128.900290] a1112==4 free 20273


static int ath11k_dp_rxdma_buf_ring_free(struct ath11k *ar,
                                         struct dp_rxdma_ring *rx_ring)
{
        struct sk_buff *skb;
        int buf_id;

        spin_lock_bh(&rx_ring->idr_lock);
        idr_for_each_entry(&rx_ring->bufs_idr, skb, buf_id) {
                idr_remove(&rx_ring->bufs_idr, buf_id);
                /* TODO: Understand where internal driver does this dma_unmap
                 * of rxdma_buffer.
                 */
                dma_unmap_single(ar->ab->dev, ATH11K_SKB_RXCB(skb)->paddr,
                                 skb->len + skb_tailroom(skb), DMA_FROM_DEVICE);
                dev_kfree_skb_any(skb);
        }

        idr_destroy(&rx_ring->bufs_idr);
        spin_unlock_bh(&rx_ring->idr_lock);

        return 0;
}


skb存放在idr数据结构体中,rx dma ring,通过id来映射skb。所以使用idr结构体来存储skb。一开始以为skb的个数很多,

添加个数统计,发现内存很紧张的时候,也只有4095个skb。其实idr中skb的个数,就是rx dma ring的ring的个数对应的。

而ring的大小是4096,因此idr中的skb不会海量。


分配skb的时候,DP_RX_BUFFER_SIZE是2048,  DP_RX_BUFFER_ALIGN_SIZE 是 128. 一个skb也就2K多,4095个skb也就8M多

怎么就消耗了50多M内存呢。于是怀疑不是释放skb导致的内存突降,将dev_kfree_skb_any(skb)注释,发现内存没有下降,那就实锤了,是skb占用的内存。分配skb的代码:


        while (num_remain > 0) {

               skb = dev_alloc_skb(DP_RX_BUFFER_SIZE +
                                   DP_RX_BUFFER_ALIGN_SIZE);

               if (!skb)
                        break;

                if (!IS_ALIGNED((unsigned long)skb->data,
                                DP_RX_BUFFER_ALIGN_SIZE)) {
                        skb_pull(skb,
                                 PTR_ALIGN(skb->data, DP_RX_BUFFER_ALIGN_SIZE) -
                                 skb->data);
                }

                paddr = dma_map_single(ab->dev, skb->data,
                                       skb->len + skb_tailroom(skb),
                                       DMA_FROM_DEVICE);


这个skb有什么奥秘吗?于是来看内核的 dev_alloc_skb() 函数的实现 (net/core/skbuff.c):

struct sk_buff *__netdev_alloc_skb(struct net_device *dev, unsigned int len,
                                   gfp_t gfp_mask)
{
        struct page_frag_cache *nc;
        struct sk_buff *skb;
        bool pfmemalloc;
        void *data;

        len += NET_SKB_PAD;

        /* If requested length is either too small or too big,
         * we use kmalloc() for skb->head allocation.
         */
        if (len <= SKB_WITH_OVERHEAD(1024) ||
            len > SKB_WITH_OVERHEAD(PAGE_SIZE) ||
            (gfp_mask & (__GFP_DIRECT_RECLAIM | GFP_DMA))) {
                skb = __alloc_skb(len, gfp_mask, SKB_ALLOC_RX, NUMA_NO_NODE);
                if (!skb)
                        goto skb_fail;
                goto skb_success;
        }

        len += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
        len = SKB_DATA_ALIGN(len);

        if (sk_memalloc_socks())
                gfp_mask |= __GFP_MEMALLOC;

        if (in_hardirq() || irqs_disabled()) {
                nc = this_cpu_ptr(&netdev_alloc_cache);
                data = page_frag_alloc(nc, len, gfp_mask);
                pfmemalloc = nc->pfmemalloc;
        } else {
                local_bh_disable();
                nc = this_cpu_ptr(&napi_alloc_cache.page);
                data = page_frag_alloc(nc, len, gfp_mask);
                pfmemalloc = nc->pfmemalloc;
                local_bh_enable();
        }    

        if (unlikely(!data))
                return NULL;

        skb = __build_skb(data, len);
        if (unlikely(!skb)) {
                skb_free_frag(data);
                return NULL;
        } 


可以看到如果len较小,或者len较大,就调用__alloc_skb() 来分配skb, 其是用kmalloc来分配内存。


如果len不大不小,如 2048 + 128就属于,这种情况用kmalloc,直接就是分配一个page, 此时会非常浪费内存。

因此,内核开发者搞了一种方法:page_frag_alloc(nc, len, gfp_mask)。它就是分配多个页,来容纳多个片段,就可以避免

内存浪费。


比如分配3K内存,那我就使用3个页,就是12K, 那就可以分配4个对象,不浪费一点内存。


所以在我们开头看到的栈:

times 304->878 order 3 mask 0xd2a20
 __alloc_pages+0x134/0x1064
 page_frag_alloc_align+0x84/0x180
 __netdev_alloc_skb+0x158/0x190


分配的是order 3 也就是8个连续页,32K内存。此时,我就想到了一个点,如果这32K内存,其它skb都释放了,只剩一个skb.

那这一个skb就占用了 32 K 内存,那就非常浪费内存了,这只是我的一个猜想。


page_frag_alloc()的第一个参数是struct page_frag_cache *,它用来保存缓存记录,它是一个全局变量:

nc = this_cpu_ptr(&napi_alloc_cache.page);


是否, 会有碎片化的问题呢,为了验证我的猜想,修改 __netdev_alloc_skb(),强制使用__alloc_skb(),测试发现,内存稳定起来了。

不会在用着用着内存不断下降。这就证明了,是内存碎片化导致的问题。因此解决办法是,在ath11k里面,将dev_alloc_skb()修改为

alloc_skb(), 如下:


        while (num_remain > 0) {
                /*
                 * use alloc_skb instead of dev_alloc_skb
                 * solve the memory exhaused problem on 256M device.
                 *
                 * the dev_alloc_skb() alloc order 3 page to store
                 * multiple skb data, this can lead to fragment
                 * problem, which a skb hold 8 pages memory.
                 */
                skb = alloc_skb(DP_RX_BUFFER_SIZE +
                                DP_RX_BUFFER_ALIGN_SIZE, GFP_ATOMIC);


此时,skb和skb data都用kmalloc分配,可以通过slab info来查看个数:

/ # cat /proc/slabinfo

skbuff_head_cache  12156  12411    192   21    1 : tunables    0    0    0 : slabdata    591    591      0

kmalloc-4k         10922  10992   4096    8    8 : tunables    0    0    0 : slabdata   1374   1374      0


总结

之前的ath11k代码分析,没有太大的用,在发现卸载ath11k模块,内存恢复了之后,就找到了定位的方向,看看是卸载过程释放哪个资源导致了内存显著下降,顺藤摸瓜即可。

Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.