您当前的位置:首页 > 电脑百科 > 软件技术 > 操作系统 > linux

Linux内核快速处理路径尽量多用kmem_cache而慎用kmalloc

时间:2021-05-18 10:02:07  来源:  作者:linux内核

题目是一个典型 《Effective C++》 的风格。

事情是这样的,我大致说一下。

我在开发一个Netfilter模块,在PREROUTING匹配一些数据包,显而易见,都能想到使用哈希表hlist作为数据结构的容器,其中装有下面的结构体:

struct item {    struct hlist_node hnode;    char padding[16];};

生成item的时候,我先用 kmalloc 接口分配内存

item_nd = (struct item *)kmalloc(sizeof(struct item), GFP_KERNEL);

然后我用hlist_add/del接口将分配好的结构体插入到hlist中。

仅仅为了测试是否会宕机,所以我的所有的数据结构的hash值均是一样的,这样插入200个项的话,它们会hash冲突,从而仅仅添加到同一个hlist链表中,这样整个匹配过程就退化成了遍历200个项的链表。

虽然是万恶的遍历操作,但200个项一切还OK,性能几乎是无损的,无论是吞吐,还是pps。

这个时候,我想扩充一些功能,于是乎为item结构体增加了一个字段:

struct item {    struct hlist_node hnode;    char padding[16];    void *private;};

仅仅增加了一个private,其它均和之前完全一致,同样的200个项插入同一条hlist,同样遍历,吞吐和pps下降达到15%~20%!

为什么增加了一个指针变量,就出现了如此巨大的性能差异?!


事情的端倪就隐藏在kmalloc接口中!

事情的真相是,在不添加private指针时,item结构的大小是32,添加一个指针,其大小变成了40,别小看这8个字节:

  • 32字节大小的所有200个item在内存中几乎都是连续的。
  • 40字节大小的所有200个item在内存中几乎都是不连续的。

为什么会造成这个结果?32和40有什么特殊性吗?

我们还要继续向下看。

kmalloc的背后其实是一系列的kmem_cache:

  • 8字节的kmem_cache
  • 16字节的kmem_cache
  • 32字节的kmem_cache
  • 64字节的kmem_cache
  • 92字节的kmem_cache
  • 128字节的kmem_cache
  • ...

我们从/proc/slabinfo里可以一窥究竟:

[root@localhost test]# cat /proc/slabinfo |grep ^kmallockmalloc-8192          52     52   8192    4    8 : tunables    0    0    0 : slabdata     13     13      0kmalloc-4096         274    288   4096    8    8 : tunables    0    0    0 : slabdata     36     36      0kmalloc-2048         578    608   2048   16    8 : tunables    0    0    0 : slabdata     38     38      0kmalloc-1024        1105   1120   1024   16    4 : tunables    0    0    0 : slabdata     70     70      0kmalloc-512         1466   1584    512   16    2 : tunables    0    0    0 : slabdata     99     99      0kmalloc-256         2289   2560    256   16    1 : tunables    0    0    0 : slabdata    160    160      0kmalloc-192         1630   1785    192   21    1 : tunables    0    0    0 : slabdata     85     85      0kmalloc-128         1632   1632    128   32    1 : tunables    0    0    0 : slabdata     51     51      0kmalloc-96          1344   1344     96   42    1 : tunables    0    0    0 : slabdata     32     32      0kmalloc-64         25408  25408     64   64    1 : tunables    0    0    0 : slabdata    397    397      0kmalloc-32          3072   3072     32  128    1 : tunables    0    0    0 : slabdata     24     24      0kmalloc-16          3072   3072     16  256    1 : tunables    0    0    0 : slabdata     12     12      0kmalloc-8           5120   5120      8  512    1 : tunables    0    0    0 : slabdata     10     10      0

当你调用 kmalloc(size, flags) 申请内存时,系统会根据你的size向上寻找一个最接近的kmem_cache,然后在其中为你分配所需的内存。

我们知道kmemcache是针对特定数据结构的独享内存池子,它以 *最小化碎片* 的原则为特定的场合提供 *可高效访问* 的内存,比如sock,skbuff这些。

然而kmalloc接口所依托的kmem_cache则是全局(同一个NUMA node)共享的内存池子,它并不针对特定场合,仅仅针对特定大小!也即是说, 最小化碎片 是针对所有调用kmalloc接口的线程的。

我们回头看上面的slabinfo,可以注意到,64字节大小的kmem_cache,即kmalloc-64已经包含了非常多的object,因此如果你调用kmalloc申请40字节的内存,其实你是在kmalloc-64里分配。

其实32和40没有什么特殊性,32字节大小的item之所以还可以保持连续,那是因为kmalloc-32几乎没有被重度使用,而kmalloc-64则已经被其它使用者打散。

我们可以试一下,看看分别申请32字节和40字节的效果:

#include <linux/module.h>struct stub32 {    unsigned char m[32];};struct stub40 {    unsigned char m[40];};#define SIZE 20struct stub32 *array32[SIZE] = {NULL};struct stub40 *array40[SIZE] = {NULL};%}function alloc_test()%{    int i;    for (i = 0; i < SIZE; i ++) {        array32[i] = kmalloc(sizeof(struct stub32), GFP_KERNEL);        printk("32bytes [%d]:%p ", i, array32[i]);        if (i > 0) {            unsigned long hi = (unsigned long)array32[i];            unsigned long lo = (unsigned long)array32[i - 1];            signed long delta = hi - lo;            if (delta < 0)                delta = lo - hi;            printk("delta [%lx] n", delta);        } else            printk("delta [0] n");    }    printk("------------------n");    for (i = 0; i < SIZE; i ++) {        array40[i] = kmalloc(sizeof(struct stub40), GFP_KERNEL);        printk("40bytes [%d]:%p ", i, array40[i]);        if (i > 0) {            unsigned long hi = (unsigned long)array40[i];            unsigned long lo = (unsigned long)array40[i - 1];            signed long delta = hi - lo;            if (delta < 0)                delta = lo - hi;            printk("delta [%lx] n", delta);        } else            printk("delta [0] n");    }    for (i = 0; i < SIZE; i ++) {        kfree(array32[i]);        kfree(array40[i]);    }%}probe begin{    alloc_test();    exit(); // oneshot模式}

以下是结果:

[  466.933100] 32bytes [1]:ffff881f9649caa0 delta [20][  466.938206] 32bytes [2]:ffff881f9649cac0 delta [20][  466.943314] 32bytes [3]:ffff881f9649cae0 delta [20][  466.948586] 32bytes [4]:ffff881f9649cb00 delta [20][  466.953732] 32bytes [5]:ffff881f9649cb20 delta [20][  466.958863] 32bytes [6]:ffff881f9649cb40 delta [20][  466.963977] 32bytes [7]:ffff881f9649cb60 delta [20][  466.969095] 32bytes [8]:ffff881f9649cb80 delta [20][  466.974222] 32bytes [9]:ffff881f9649cba0 delta [20][  466.979329] 32bytes [10]:ffff881f9649cbc0 delta [20][  466.984731] 32bytes [11]:ffff881f9649cbe0 delta [20][  466.990124] 32bytes [12]:ffff881f9649cc00 delta [20][  466.995510] 32bytes [13]:ffff881f9649cc20 delta [20][  467.000907] 32bytes [14]:ffff881f9649cc40 delta [20][  467.006294] 32bytes [15]:ffff881f9649cc60 delta [20][  467.011685] 32bytes [16]:ffff881f9649cc80 delta [20][  467.017086] 32bytes [17]:ffff881f9649cca0 delta [20][  467.022483] 32bytes [18]:ffff881f9649ccc0 delta [20][  467.027881] 32bytes [19]:ffff881f9649cce0 delta [20][  467.033286] ------------------[  467.036610] 40bytes [0]:ffff881d0c904d40 delta [0][  467.041828] 40bytes [1]:ffff881d0c904680 delta [6c0][  467.047216] 40bytes [2]:ffff881d0c904140 delta [540][  467.052607] 40bytes [3]:ffff881d0c904d00 delta [bc0][  467.058001] 40bytes [4]:ffff881d0c9043c0 delta [940][  467.063399] 40bytes [5]:ffff881d0c904940 delta [580][  467.068801] 40bytes [6]:ffff881d0c9048c0 delta [80][  467.074107] 40bytes [7]:ffff881d0c904e80 delta [5c0][  467.079496] 40bytes [8]:ffff881d0c904200 delta [c80][  467.084888] 40bytes [9]:ffff881d0c904980 delta [780][  467.090282] 40bytes [10]:ffff881fcd725dc0 delta [2c0e21440][  467.096280] 40bytes [11]:ffff881fcd7250c0 delta [d00][  467.101763] 40bytes [12]:ffff881fcd725440 delta [380][  467.107235] 40bytes [13]:ffff881fcd725340 delta [100][  467.112722] 40bytes [14]:ffff881f8398ee80 delta [49d964c0][  467.118633] 40bytes [15]:ffff881f8398ecc0 delta [1c0][  467.124110] 40bytes [16]:ffff881f8398e100 delta [bc0][  467.129589] 40bytes [17]:ffff881f8398ed40 delta [c40][  467.135062] 40bytes [18]:ffff881f8398efc0 delta [280][  467.140542] 40bytes [19]:ffff881f8398e700 delta [8c0]

我们可以看到,32字节的结构体,kmalloc分配的完全都是连续的,而40字节的结构体,完全就散乱碎片化了。

如果以上的这些地址是需要在网络协议栈的Netfilter hook中被遍历的,可想而知,如果地址非连续且布局无迹可寻,cache miss将会非常高。

值得一提的是,并不是说32字节的结构体分配就一定会获得连续的内存,而64字节的就不会, 这完全取决于你的系统当前的整体kmalloc使用情况。

kmalloc并不适合快速路径的内存分配,它只适合稳定的,离散的管理结构体的内存分配。

大家之所以普遍喜欢用kmalloc,因为它简单,快捷,少了kmem_cache的create和destroy的维护操作。

kmalloc有个副作用,就是它只有固定的大小,比如你分配一个24字节大小的结构体,事实上系统会给你32字节。具体的细节就参考kmalloc的kmem_cache数组吧。


在诸如网络协议栈处理这种相对快速的路径中,比如skbuff,sock,nfconntrack等结构体均是在自行维护的独享kmem_cache中被管理的,这保证了内存分配的 尽可能的连续性,尽可能的最少碎片。

这是通过kmem_cache的 栈式管理 实现的:

  • kmem_cache的obj可以随意释放。
  • kmem_cache的obj按照释放的逆序进行分配。
  • kmem_cache的free相当于push操作,而alloc相当于pop操作。

我再用例子给出直观的效果,依然采用专家模式的stap:

// alloc_free.stp%{#include <linux/module.h>struct stub {    unsigned char m[40];};%}function kmemcache_stack_test()%{    int i;    struct kmem_cache *memcache;    struct stub *array[10];    struct stub *new[10] = {NULL};    memcache = kmem_cache_create("test_", sizeof(struct stub), 0, 0, NULL);    if (!memcache)        return;    for (i = 0; i < 10; i ++) {        array[i] = kmem_cache_alloc(memcache, GFP_KERNEL);        STAP_PRINTF("[%d]:%llx n", i, array[i]);    }    STAP_PRINTF("Let's playn");    kmem_cache_free(memcache, array[4]);    STAP_PRINTF("free [4]:%llx n", array[4]);    array[4] = NULL;    new[0] = kmem_cache_alloc(memcache, GFP_KERNEL);    STAP_PRINTF("new [x]:%llx n", new[0]);    kmem_cache_free(memcache, array[1]);    STAP_PRINTF("free [1]:%llx n", array[1]);    array[1] = NULL;    kmem_cache_free(memcache, array[8]);    STAP_PRINTF("free [8]:%llx n", array[8]);    array[8] = NULL;    new[1] = kmem_cache_alloc(memcache, GFP_KERNEL);    STAP_PRINTF("new [x]:%llx n", new[1]);    new[2] = kmem_cache_alloc(memcache, GFP_KERNEL);    STAP_PRINTF("new [x]:%llx n", new[2]);    for (i = 0; i < 10; i++) {        if (new[i]) {            kmem_cache_free(memcache, new[i]);            new[i] = NULL;        }    }    STAP_PRINTF("Batch freen");    for (i = 0; i < 10; i++) {        if (array[i]) {            kmem_cache_free(memcache, array[i]);            STAP_PRINTF("free [i]:%llx n", array[i]);            array[i] = NULL;        }    }    STAP_PRINTF("Batch allocn");    for (i = 0; i < 10; i++) {        new[i] = kmem_cache_alloc(memcache, GFP_KERNEL);        STAP_PRINTF("new [%d]:%llx n", i, new[i]);    }    for (i = 0; i < 10; i++) {        if (new[i]) {            kmem_cache_free(memcache, new[i]);            new[i] = NULL;        }    }    kmem_cache_destroy(memcache);%}probe begin{    kmemcache_stack_test();    exit(); // oneshot模式}

很简单的实验,就是分配,释放的操作,我们运行一下:

[root@localhost test]# stap -g ./alloc_free.stp[0]:ffff88003bc4bf28[1]:ffff88003bc4bf00[2]:ffff88003bc4beb0[3]:ffff88003bc4be38[4]:ffff88003bc4be88[5]:ffff88003bc4be60[6]:ffff88003bc4bdc0[7]:ffff88003bc4be10[8]:ffff88003bc4bde8[9]:ffff88003bc4bd48Let's playfree [4]:ffff88003bc4be88new [x]:ffff88003bc4be88free [1]:ffff88003bc4bf00free [8]:ffff88003bc4bde8new [x]:ffff88003bc4bde8new [x]:ffff88003bc4bf00Batch freefree [i]:ffff88003bc4bf28free [i]:ffff88003bc4beb0free [i]:ffff88003bc4be38free [i]:ffff88003bc4be60free [i]:ffff88003bc4bdc0free [i]:ffff88003bc4be10free [i]:ffff88003bc4bd48Batch allocnew [0]:ffff88003bc4bd48new [1]:ffff88003bc4be10new [2]:ffff88003bc4bdc0new [3]:ffff88003bc4be60new [4]:ffff88003bc4be38new [5]:ffff88003bc4beb0new [6]:ffff88003bc4bf28new [7]:ffff88003bc4bf00new [8]:ffff88003bc4bde8new [9]:ffff88003bc4be88[root@localhost test]#

从地址上可以看出, kmem_cache就是按照一个栈的形式进行管理的,即便由于随机的free操作造成了空洞,后续的alloc会尽快将其填充。 这样的结果如下:

  • 尽可能节省内存,保持内存的紧凑。
  • 提高CPU dcache的命中率,最大化preload效果。

即便我们使用自行维护的kmem_cache slab,当从中分配的对象插入链表时,也要尽量按照其内存地址的升序插入链表确定的位置,这样在遍历链表时可以达到最大化预取的效果。实测过程这里从略。

一个事实是:

  • 在连续的内存上进行遍历,其性能远超在离散的内存上进行遍历!

这是因为CPU在访问内存地址P时,会把一个cacheline的数据预取到cache,在连续的内存上,随着遍历的进行,链表项的访问和预取将形成一个流水化作业,这个流水线只要不被打断,遍历就好像在cache中进行一样。

我建议,根据slab对象的内存使用hlistaddbefore[rcu],hlistaddbebind[rcu]将对象插入hlist的特定位置,而不是简单使用hlistaddhead。



Tags:Linux内核   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
什么是linux内核linux就像是一个哲学的最佳实践。如果非要对它评价,我真的不知道该怎么赞叹,我只能自豪地说着:“linux的美丽简直让人沉醉。”我只能说是我处在linux学习的修炼...【详细内容】
2021-12-23  Tags: Linux内核  点击:(15)  评论:(0)  加入收藏
内核空间 和用户空间申请的内存最终和buddy怎么交互?以及在页表映射上的区别?虚拟地址到物理地址,什么时候开始映射?Buddy的问题分配的力度太大 buddy算法把空闲页面分成1,2,4页,bu...【详细内容】
2021-12-07  Tags: Linux内核  点击:(25)  评论:(0)  加入收藏
1、设备树的概念在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_b...【详细内容】
2021-11-26  Tags: Linux内核  点击:(31)  评论:(0)  加入收藏
自旋锁:如果内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,但是对于UP系统,自旋锁仅做抢占和中断操作,没有实现真正的“自旋”。如果配置了CONFIG_DEBUG_SPI...【详细内容】
2021-10-21  Tags: Linux内核  点击:(37)  评论:(0)  加入收藏
1. Linux内核时钟系统和定时器实现Linux 2.6.16之前,内核只支持低精度时钟,内核定时器的工作方式: 系统启动后,会读取时钟源设备(RTC, HPET,PIT&hellip;),初始化当前系统时间; 内...【详细内容】
2021-09-29  Tags: Linux内核  点击:(50)  评论:(0)  加入收藏
1. 红黑树1.1 红黑树概述红黑树和我们以前学过的AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。不过自从红黑树出来后,AVL...【详细内容】
2021-06-24  Tags: Linux内核  点击:(119)  评论:(0)  加入收藏
UDP报文接收概述UDP数据报的接收要分两部分来看: 网络层接收完数据包后递交给UDP后,UDP的处理过程。该过程UDP需要做的工作就是接收数据包并对其进行校验,校验成功后将其放入接...【详细内容】
2021-06-04  Tags: Linux内核  点击:(93)  评论:(0)  加入收藏
题目是一个典型 《Effective C++》 的风格。事情是这样的,我大致说一下。我在开发一个Netfilter模块,在PREROUTING匹配一些数据包,显而易见,都能想到使用哈希表hlist作为数据结...【详细内容】
2021-05-18  Tags: Linux内核  点击:(200)  评论:(0)  加入收藏
需求在Linux SMP(对称多处理器)环境下,每个CPU对应一个run_queue(可执行队列)。如果一个进程处于TASK_RUNNING状态(可执行状态),则它会被加入到其中一个run_queue(且同一时刻仅会被加...【详细内容】
2021-04-01  Tags: Linux内核  点击:(225)  评论:(0)  加入收藏
Linux内核是GNU/Linux操作系统的核心组件。它是一个免费、开源、庞大、模块化、多任务的类Unix的操作系统内核。它最初是由Linus Torvalds在1991年为他的i386 PC创造的。实...【详细内容】
2021-03-18  Tags: Linux内核  点击:(274)  评论:(0)  加入收藏
▌简易百科推荐
作用显示文件或目录所占用的磁盘空间使用命令格式du [option] 文件/目录命令功能显示文件或目录所占用的磁盘空间一些写法的区别du -sh xxx 显示总目录的大小,但是不会列出...【详细内容】
2021-12-23  mitsuhide1992    Tags:du命令   点击:(12)  评论:(0)  加入收藏
什么是linux内核linux就像是一个哲学的最佳实践。如果非要对它评价,我真的不知道该怎么赞叹,我只能自豪地说着:“linux的美丽简直让人沉醉。”我只能说是我处在linux学习的修炼...【详细内容】
2021-12-23  linux上的码农    Tags:linux内核   点击:(15)  评论:(0)  加入收藏
本文将比较 Linux 中 service 和 systemctl 命令,先分别简单介绍这两个命令的基础用法,然后进行比较。从 CentOS 7.x 开始,CentOS 开始使用 systemd 服务来代替 service服务(dae...【详细内容】
2021-12-23  软件架构    Tags:systemctl   点击:(13)  评论:(0)  加入收藏
mv是move的缩写,可以用来移动文件或者重命名文件名,经常用来备份文件或者目录。命令格式mv [选项] 源文件或者目录 目标文件或者目录命令功能mv命令中第二个参数类型的不同(...【详细内容】
2021-12-17  入门小站    Tags:mv命令   点击:(23)  评论:(0)  加入收藏
大数据技术AI Flink/Spark/Hadoop/数仓,数据分析、面试,源码解读等干货学习资料 98篇原创内容 -->公众号 Linux sed 命令是利用脚本来处理文本文件。sed 可依照脚本的指令来处...【详细内容】
2021-12-17  仙风道骨的宝石骑士    Tags:sed命令   点击:(21)  评论:(0)  加入收藏
Node是个啥?  写个东西还是尽量面面俱到吧,所以有关基本概念的东西我也从网上选择性地拿了下来,有些地方针对自己的理解有所改动,对这些概念性的东西有过了解的可选择跳过这段...【详细内容】
2021-12-15  linux上的码农    Tags:node   点击:(21)  评论:(0)  加入收藏
难道只有我一个人觉得Ubuntu的unity桌面非常好用吗?最近把台式机上面的Ubuntu 16.04格式化了,装了黑苹果用了一周,不得不说,MacOS确实很精美,软件生态比Linux丰富很多,比Windows简...【详细内容】
2021-12-14  地球末日村    Tags:ubuntu   点击:(34)  评论:(0)  加入收藏
简介Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。输出信息含义执行net...【详细内容】
2021-12-13  窥镜天    Tags:Linux netstat   点击:(26)  评论:(0)  加入收藏
对于较多数量的文件描述符的监听无论是select还是poll系统调用都显得捉襟见肘,poll每次都需要将所有的文件描述符复制到内核,内核本身不会对这些文件描述符加以保存,这样的设计...【详细内容】
2021-12-13  深度Linux    Tags:Linux   点击:(16)  评论:(0)  加入收藏
今天,我们来了解下 Linux 系统的革命性通用执行引擎-eBPF,之所以聊着玩意,因为它确实牛逼,作为一项底层技术,在现在的云原生生态领域中起着举足轻重的作用。截至目前,业界使用范...【详细内容】
2021-12-10  架构驿站    Tags:eBPF   点击:(24)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条