一、概述

linux内存管理核心是伙伴系统,slab,slub,slob是基于伙伴系统之上提供api,用于内核内存分配释放管理,适用于小内存(小于1页)分配与释放,当然大于1页,也是可以的.小于一页的,我们也可以直接用伙伴系统api申请内存和释放,但伙伴系统最小单位是页,如果我们只需要100byte,伙伴系统申请内存最小1页(一般情况1页是4k, 具体看系统PAGE_SHIFT定义),显然就很浪费.

二、slub核心原理

slub是从伙伴系统中分配连续一块内存页作为缓存池,再将其分成大小相等,多块小内存块,当申请内存时,返回其中一小块(返回小块内存起始地址.如下图),假设分成9小块内存(简单的数值表示地址):

每块小内存的首地址,存放下一小块内存起始地址,最后一小块内存首地址存放null.
缓存池(如上图,9个地址连续小内存块,可视为缓存池),从开始分配到小块内存分配完的过程:
假设指针p=0,表示指向第0小块内存.首次分配内存时,首先addr=p, p=*p(从第0小块内存首地址中获取下一小块内存起始地址,并写入p指针中)之后p=1指向第1小块内存起始地址,addr(指向第0小块内存起始地址)返回给申请分配内存的程序;第二次分配内存时,首先addr=p,p=*p,此后p=2,以此类推,直到最后,将第8小块内存地址分配完后,若再次发起内存申请时,发现p=null,表示内存分配完了,此时,会再次向伙伴系统申请分配连续的,一块大内存(缓存池).

一块缓存池,释放过程:
释放内存时,假设有个指针pa, 首次释放的内存,是第5小块内存,pa=5,*pa=null(第5小块内存首地址存放null), 然后第8小块内存被释放,这时候pa=8,*pa=5(第8小块内存首地址存放5),第5小块内存首地址仍然存放null,, 以此类推.

假如释放顺序是5,8,2,0,6,4,3,1,7释放完后,pa=7,内存情况如下:

释放完或者只释放部分,随时都可以加入到分配中去.

slub就是负责:缓存池之间的切换,以及小内存块分配与释放.

那么,小内存块(如上面9小块)大小,块数是怎么计算,缓存池大小又是怎么计算?

小内存块大小计算:

假如,申请x_size大小的内存,那么:

n = fls(x_size - 1);
j = 1 << n;
or = j < x_size ? 1 << n+1 : j;

or就是小块内存大小
fls()返回最高位为1的位置,起始是从1开始,返回0表示没有找到为1的位置
小块内存大小,也可以直接调用include/linux/slab.h里面定义的kmalloc_index()得到:

a = kmalloc_index(x_size);
or = a < 0 ? 0 : 1 << a;

由此,可看出,最终分配的小内存块大小,不一定是申请时指定的大小,而是2的n次方,不足往大的计算,如申请5字节,最终计算下来,会分配8字节返回.在申请内存时,大小按照2的n次方申请最佳,否则会出现浪费.如我们申请5字节,分配8字节,多余的3字节,申请者,不知道,就不会去使用这3字节,出现浪费.

缓存池大小计算:


int rem = 0;
int order = 0;
int max_order;
unsigned long slab_size;//2的order次幂,就是要向伙伴系统申请的,连续大块内存,单位页
for(order = 0; order < max_order; order++)//max_order最大限制
{slab_size = PAGE_SIZE << order;//将order,转化为字节(byte)大小//reserved,一般为0//x_size小块内存大小rem = (slab_size - reserved) % x_size;//碎片,残留rem字节大小内存,是无法分配的if (rem <= slab_size / fract_leftover)//fract_leftover会分别用16,8,4试探,看是否满足条件break;//表示成功
}printk("order:%d\n",order)

上面的代码,是个人理解后的简单描述,实现在mm/slub.c中calculate_order()函数.
2^order(2的order次幂)个页,就是缓存池大小.
简而言之,以2^order个页,循环试探,找到满足,对碎片要求的值order.

缓存池中小内存块,块数计算:
slab_size = PAGE_SIZE << order;
num = slab_size / or;
num就是小内存块,块数.

slub最大只能申请分配2页内存空间(小内存块),在include/linux/slab.h中定义:
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
其中的PAGE_SHIFT,需要看具体系统配置,一般会定义在arch/xxx/include/asm/page.h里面:
#define PAGE_SHIFT 12

slub最小能分配内存空间(小内存块),由宏KMALLOC_SHIFT_LOW定义,在include/linux/slab.h,KMALLOC_SHIFT_LOW会受平台定义的ARCH_DMA_MINALIGN影响,如果平台没定义ARCH_DMA_MINALIGN,slub默认:
#define KMALLOC_SHIFT_LOW 3
即,最小可以申请8个字节(1 << 3),假如申请3个字节,也会分配8字节.

三、slub code实现

为了便于描述,如下定义:

同类缓存池:struct kmem_cache *s;
小缓存池:struct page *page;
当前小缓存池:s->cpu_slab->page;
当前小内存块链:s->cpu_slab->freelist
一级(小缓存池)回收链表:s->cpu_slab->partial
二级(小缓存池)回收链表:s->node[x]->partial

小内存块:"slub核心原理"描述的,分成多个内存块,其中的一块,就叫做小内存块,即每次向slub申请分配,获取到的内存,就是一块小内存块,小内存块大小由struct kmem_cache成员size指定.
同类缓存池:一个struct kmem_cache实例化对象,管理着同一类缓存池(一个或多个缓存池),这里说的同一类,指相同大小小内存块,即可以叫做相同size小内存块,struct kmem_cache成员size(object_size与size相等)就是小内存块的大小.例如:size=1024的小内存块,与size=2048的小内存块,就属于不同类.(个人觉得,struct kmem_cache成员size,命名为size_type,或许更合适)
小缓存池:slub每次向伙伴系统申请分配内存页(单个或多个连续页),就可以叫做小缓存池.向伙伴系统分配的页大小由s->oo决定.如上"slub核心原理"提到的0到8的九个小块内存,组成一个小缓存池.
大缓存池:为方便描述和理解,同类缓存池取个别名,叫做大缓存池,里面有多个小缓存池.

1、同类缓存池初始化

同类缓存池结构初始化后,形成的大致结构图:
                图一
slab_caches是链表头,为了便于描述,我们取名为同类缓存池链表,或者大缓存池链表,定义在mm/slab_common.c中:
LIST_HEAD(slab_caches);
kmem_cache是指向struct kmem_cache的一个实体对象,定义在mm/slab_common.c中:
struct kmem_cache *kmem_cache;
kmem_cache_node是指向struct kmem_cache_node的一个实体对象,定义在mm/slub.c中;
static struct kmem_cache *kmem_cache_node;

kmem_cache,kmem_cache_node,kmalloc_caches[0], kmalloc_caches[1], …kmalloc_caches[i]都是struct kmem_cache实体,以slab_caches为列表头,通过链表(struct list_head)形式连接起来.

s0,s1,s2…sn分别代表不同size的,同类缓存池管理结构(即struct kmem_cache一个实体对象)

memory0, memory1, memory2…memoryN代表缓存池,即实际内存空间,图一方框大小并不代表缓存池空间大小

kmem_cache, s0代表同一个struc kmem_cache实体,即同类缓存池管理结构,kmem_cache和s0不分彼此,图一只是为了更好的描述.有意思的地方,mermory0用来存放小内存块s0, s1, s2…sn;而s0用来管理memory0缓存池里面其他小内存块分配释放.
依次,有如下关系:
kmem_cache_node对应s1,管理memory1
kmalloc_caches[0]对应s2,管理memory2
kmalloc_caches[1]对应s3,管理memory3

如下:
kmalloc_caches[6]管理所有大小为64byte的内存块(本文称为小内存块)分配释放.
kmalloc_caches[7]管理所有大小为128byte的内存块分配释放.
kmalloc_caches[8]管理所有大小为256byte的内存块分配释放.

在分析代码是如何描述图一结构前,先了解下同类缓存池struct kmem_cache结构体(定义在include/linux/slub_def.h里面):

struct kmem_cache {//cpu_slab管理当前小缓存池和一级回收链表struct kmem_cache_cpu __percpu *cpu_slab;/* Used for retriving partial slabs etc */unsigned long flags;//二级回收链表上小缓存池数量超过min_partial时,//将会把二级回收链表超过的部分,且inuse为0时,释放回伙伴系统//unfreeze_partials()函数做的处理unsigned long min_partial;//size同类缓存池关键,小内存块空间大于等于sizeint size;        /* The size of an object including meta data *///size == object_sizeint object_size;  /* The size of an object without meta data */int offset;        /* Free pointer offset. *///一级回收链表上小内存块数量超过cpu_partial时,//将会把一级回收链表上所有小缓存池移动到二级回收链表上//unfreeze_partials()函数会做这样的处理int cpu_partial;   /* Number of per cpu partial objects to keep around *///oo低16位存放小缓存池小内存块数量,//oo高16位,存放小缓存池空间大小(高16位是2的幂指数,2^(oo>>16)得到的值,就是小缓存池空间大小)struct kmem_cache_order_objects oo;/* Allocation and freeing of slabs *///max是oo>>16的上限值,slub向伙伴系统申请内存页时,不得超过这个值struct kmem_cache_order_objects max;// oo高16位的最小值,当缓存池向伙伴系统申请分配页时,按照oo申请不成功时,//会降低为min的值,再次向伙伴系统申请页struct kmem_cache_order_objects min;gfp_t allocflags;    /* gfp flags to use on each alloc */int refcount;       /* Refcount for slab cache destroy */void (*ctor)(void *);//inuse == sizeint inuse;       /* Offset to metadata */int align;      /* Alignment *///reserved一般为0int reserved;      /* Reserved bytes at the end of slabs *///同类缓存池名字,便于管理和查询,debugconst char *name; /* Name (only for display!) *///所有同类缓存池都是通过list连接到slab_caches(同类缓存池链表头)struct list_head list;   /* List of slab caches */
#ifdef CONFIG_SYSFSstruct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_MEMCG_KMEMstruct memcg_cache_params *memcg_params;int max_attr_size; /* for propagation, maximum size of a stored attr */
#endif#ifdef CONFIG_NUMA/** Defragmentation by allocating from a remote node.*/int remote_node_defrag_ratio;
#endif//node管理二级回收链表//多核CPU,MAX_NUMNODES代表CPU核数量,每个CPU核都有自己独立的一个nodestruct kmem_cache_node *node[MAX_NUMNODES];
};struct kmem_cache_cpu {//freelist指向当前小内存块链头void **freelist;   /* Pointer to next available object */unsigned long tid;    /* Globally unique transaction id *///page指向当前小缓存池struct page *page;    /* The slab from which we are allocating *///partial指向一级回收链表struct page *partial;   /* Partially allocated frozen slabs */
#ifdef CONFIG_SLUB_STATSunsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};//min的作用(在mm/slub.c中函数allocate_slab())如下:
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{.........page = alloc_slab_page(alloc_gfp, node, oo);if (unlikely(!page)) {//按照oo申请失败,降低为min再次试图向伙伴系统申请oo = s->min;/** Allocation may have failed due to fragmentation.* Try a lower order alloc if possible*/page = alloc_slab_page(flags, node, oo);if (page)stat(s, ORDER_FALLBACK);}.........
}

slub初始化
函数调用情况:
start_kernel() --> mm_init() --> kmem_cache_init()
start_kernel(), mm_init()都定义在init/main.c中
kmem_cache_init()定义在mm/slub.c中,函数如下:

void __init kmem_cache_init(void)
{//boot_kmem_cache为启动图一里面kmem_cache(s0)铺路或者引路//boot_kmem_cache_node为启动kmem_cache_node(s1)铺路或引路static __initdata struct kmem_cache boot_kmem_cache,boot_kmem_cache_node;if (debug_guardpage_minorder())slub_max_order = 0;kmem_cache_node = &boot_kmem_cache_node;kmem_cache = &boot_kmem_cache;//初始化boot_kmem_cache_node//第二个参数,作为缓存池管理结构名字boot_kmem_cache_node.name//第三个参数,作为缓存池管理结构boot_kmem_cache_node大小,即小内存块大小//这个函数,还会计算小内存块数量 create_boot_cache(kmem_cache_node, "kmem_cache_node",sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);register_hotmemory_notifier(&slab_memory_callback_nb);/* Able to allocate the per node structures */slab_state = PARTIAL;//初始化缓存池管理结构boot_kmem_cache//第二个参数,作为缓存池管理结构名字boot_kmem_cache.name//第三个参数,作为缓存池管理结构boot_kmem_cache大小,即小内存块空间大小//这个函数,还会计算出小内存块数量   create_boot_cache(kmem_cache, "kmem_cache",offsetof(struct kmem_cache, node) +nr_node_ids * sizeof(struct kmem_cache_node *),SLAB_HWCACHE_ALIGN);//将从memory0中分配s0,然后将boot_kmem_cache内容拷贝到s0   //当bootstrap()返回后,kmem_cache将不再指向boot_kmem_cache,//而是指向从伙伴系统分配来的内存空间s0,//之后kmem_cache(s0)管理的缓存池memory0,将用存放所有同类缓存池管理结构实体的,//(需要注意,同类缓存池管理结构struct kmem_cache实体,本身就是需要空间存放,//所有管理结构实体都存放在memory0中,而s0用来管理memory0分配释放)kmem_cache = bootstrap(&boot_kmem_cache);/** Allocate kmem_cache_node properly from the kmem_cache slab.* kmem_cache_node is separately allocated so no need to* update any list pointers.*///从memory0中分配出s1小内存块,然后把boot_kmem_cache_node拷贝到s1里面//bootstrap()返回后,kmem_cache_node指向s1kmem_cache_node = bootstrap(&boot_kmem_cache_node);/* Now we can use the kmem_cache to allocate kmalloc slabs *///主要初始化kmalloc_caches[n]//后期分配内存,通过申请size,调用函数kmalloc_index()或者kmalloc_slab()//计算查找出n,并得出kmalloc_caches[x]同类缓存池管理结构实体//其中n,表示将分配出大小为1<<n的内存块空间(小内存块),即2的n次方create_kmalloc_caches(0);#ifdef CONFIG_SMPregister_cpu_notifier(&slab_notifier);
#endifprintk(KERN_INFO"SLUB: HWalign=%d, Order=%d-%d, MinObjects=%d,"" CPUs=%d, Nodes=%d\n",cache_line_size(),slub_min_order, slub_max_order, slub_min_objects,nr_cpu_ids, nr_node_ids);
}void __init create_boot_cache(struct kmem_cache *s, const char *name, size_t size,unsigned long flags)
{int err;s->name = name;//同类缓存池管理结构名字s->size = s->object_size = size;//小内存块空间大小,这很关键哦//内存对齐s->align = calculate_alignment(flags, ARCH_KMALLOC_MINALIGN, size);//同类缓存池管理结构主要初始化err = __kmem_cache_create(s, flags);if (err)panic("Creation of kmalloc slab %s size=%zu failed. Reason %d\n",name, size, err);s->refcount = -1;    /* Exempt from merging for now */
}int __kmem_cache_create(struct kmem_cache *s, unsigned long flags)
{int err;err = kmem_cache_open(s, flags);if (err)return err;...
}static int kmem_cache_open(struct kmem_cache *s, unsigned long flags)
{s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);s->reserved = 0;if (need_reserve_slab_rcu && (s->flags & SLAB_DESTROY_BY_RCU))s->reserved = sizeof(struct rcu_head);//小缓存池里面小内存块数量计算,计算方法,可参考以上"slub核心原理"if (!calculate_sizes(s, -1))goto error;...  //二级回收链表上,小缓存池数量min_partial上限初始化set_min_partial(s, ilog2(s->size) / 2);...    //一级回收链表上小内存块数量上限cpu_partial初始化//超过cpu_partial,将会把一级回收链表上所有小缓存池移动到二级回收链表上if (kmem_cache_debug(s))s->cpu_partial = 0;else if (s->size >= PAGE_SIZE)s->cpu_partial = 2;else if (s->size >= 1024)s->cpu_partial = 6;else if (s->size >= 256)s->cpu_partial = 13;elses->cpu_partial = 30;...//二级回链表初始化if (!init_kmem_cache_nodes(s))goto error;//当前缓存池,一级回收链表初始化if (alloc_kmem_cache_cpus(s))return 0;//当前缓存池,一级回收链表,二级回收链表初始化失败时,将会释放掉二级回收链表free_kmem_cache_nodes(s);
error:if (flags & SLAB_PANIC)panic("Cannot create slab %s size=%lu realsize=%u ""order=%u offset=%u flags=%lx\n",s->name, (unsigned long)s->size, s->size, oo_order(s->oo),s->offset, flags);return -EINVAL;
}static int calculate_sizes(struct kmem_cache *s, int forced_order)
{unsigned long flags = s->flags;unsigned long size = s->object_size;int orde...size = ALIGN(size, sizeof(void *));...s->inuse = size;...size = ALIGN(size, s->align);s->size = size;if (forced_order >= 0)order = forced_order;else//calculate_order()计算方式,见"slub核心原理"描述order = calculate_order(size, s->reserved);if (order < 0)return 0;...//oo高16位存放order,其中order是向伙伴系统申请内存空间的大小,2的order次方页//oo低16位存放,小内存块数量,即小缓存池里面有多少个小内存块(2的order次方除以size)s->oo = oo_make(order, size, s->reserved); s->min = oo_make(get_order(size), size, s->reserved);if (oo_objects(s->oo) > oo_objects(s->max))s->max = s->oo;return !!oo_objects(s->oo);
}static int init_kmem_cache_nodes(struct kmem_cache *s)
{int node;//node cpu核编号(多核cpu)for_each_node_state(node, N_NORMAL_MEMORY) {struct kmem_cache_node *n;...//从memory1中分配一个二级回收链表struct kmem_cache_node实体内存空间n = kmem_cache_alloc_node(kmem_cache_node,GFP_KERNEL, node);if (!n) {//分配失败,释放二级回收链表free_kmem_cache_nodes(s);return 0;}s->node[node] = n;//二级回收链表指针,指向实体struct kmem_cache_nodeinit_kmem_cache_node(n);//初始化二级回收链表}return 1;
}static inline int alloc_kmem_cache_cpus(struct kmem_cache *s)
{...//为cpu_slab分配内存块空间s->cpu_slab = __alloc_percpu(sizeof(struct kmem_cache_cpu),2 * sizeof(void *));if (!s->cpu_slab)return 0;//初始tidinit_kmem_cache_cpus(s);return 1;
}static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{int node;//从memory0中分配struct kmem_cache实体(这主要指s0或s1)struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);memcpy(s, static_cache, kmem_cache->object_size);...for_each_node_state(node, N_NORMAL_MEMORY) {struct kmem_cache_node *n = get_node(s, node);struct page *p;if (n) {list_for_each_entry(p, &n->partial, lru)p->slab_cache = s;//二级回收链表上小缓存池归属于那类大缓存池...}...}list_add(&s->list, &slab_caches);//加入同类缓存池链表return s;
}

2、slub分配和释放分析

首先看下同类缓存池,小缓存池,小内存块组成的结构,或者说模型,如下图:
                  图二
代码是如何描述图中的结构,看下面慢慢分析:

每次分配内存时,都是通过申请size查找到对应的同类缓存池,再从同类缓存池中找到小缓存池,最后从小缓存池中找到空闲小内存块,并将小内存块起始地址返回给申请者.那么究竟是从s->cpu_slab->page, s->cpu_slab->partial, s->node[x]->partial三个中那个获取到小内存块,且看下面分析:

s->cpu_slab:
当前小内存块链头指针freelist
当前小缓存池指针page
一级回收链表partial

s->cpu_slab->freelist :
如图二,我们把freelist指向的,多个相互连接的小内存块,叫做小内存块链.
s->cpu_slab->freelist始终指向当前小缓存池里面小内存块链上第一个可用小内存块,即小缓存池首个小内存块.如图,小缓存池里面小内存块是按照"slub核心原理"描述方式串联在一起的,关于分配和释放,在"slub核心原理"节也有描述.

s->cpu_slab->page :
指向当前小缓存池首页.
在当前小缓存池上分配内存,主要函数slab_alloc_node(),定义在mm/slub.c中:
函数调用过程:
kmalloc() --> slab_alloc() --> slab_alloc_node()

static __always_inline void *slab_alloc_node(struct kmem_cache *s,gfp_t gfpflags, int node, unsigned long addr)
{void **object;struct kmem_cache_cpu *c;struct page *page;unsigned long tid;... c = __this_cpu_ptr(s->cpu_slab);...object = c->freelist;//小缓存池上首个可用小内存块page = c->page;//当前小缓存池首页if (unlikely(!object || !node_match(page, node)))//object为NULL时表示当前小缓存池没有可用小内存块,需要从其他地方分配内存object = __slab_alloc(s, gfpflags, node, addr, c);else {//当前小缓存池有空闲小内存块,从当前小缓存池中分配获取小内存块void *next_object = get_freepointer_safe(s, object);//获取当前小缓存池上,下一个可用小内存块地址.../*this_cpu_cmpxchg_double()将做如下操作:s->cpu_slab->freelist = next_object;s->cpu_slab->tid = tid;即s->cpu_slab->freelist指向小缓存池中下一个可用小内存块       */if (unlikely(!this_cpu_cmpxchg_double(s->cpu_slab->freelist, s->cpu_slab->tid,object, tid,next_object, next_tid(tid)))) {...}...}...    return object;//返回可用小内存块首地址
}

小内存块释放,发生在当前小缓存池上,主要函数slab_free(), 定义在mm/slub.c:
函数调用过程:
kfree() --> slab_free()

void kfree(const void *x)
{struct page *page;void *object = (void *)x;//释放内存地址   ...page = virt_to_head_page(x);//通过释放内存虚拟地址,查找到小内存所在小缓存池,即小缓存池首页地址...slab_free(page->slab_cache, page, object, _RET_IP_);
}static __always_inline void slab_free(struct kmem_cache *s,struct page *page, void *x, unsigned long addr)
{void **object = (void *)x;//释放内存地址首地址struct kmem_cache_cpu *c;unsigned long tid;...c = __this_cpu_ptr(s->cpu_slab);...if (likely(page == c->page)) {//释放内存,属于当前小缓存池set_freepointer(s, object, c->freelist);//c->freelist被写入到释放小内存块object空间头部,//这也是小内存块串联在一起的关键,就如"slub核心原理"描述的//this_cpu_cmpxchg_double执行主要操作://s->cpu_slab->freelist = object;刚释放的小内存块,被放在小内存块链上第一个位置//s->cpu_slab->tid = next_tid(tid);     if (unlikely(!this_cpu_cmpxchg_double(s->cpu_slab->freelist, s->cpu_slab->tid,c->freelist, tid,object, next_tid(tid)))) {...         }...} else__slab_free(s, page, x, addr);
}

从以上可以看出:

if (likely(page == c->page)) {//释放内存,属于当前小缓存池

当释放的小内存块,属于当前缓存池c->page时,释放才会发生在当前小缓存池上,即把释放的小内存块,回收到当前缓存池.

内存分配和释放,发生在当前缓存池,速度是最快的.
下面,将会分析,不是发生在当前缓存池的情况.

s->cpu_slab->partial :
如图二,多个相互有连接的小缓存池,我们取名为小缓存池链
s->cpu_slab->partial始终指向小缓存池链头
当前缓存池s->cpu_slab->page指向的小缓存池里面小内存块被分配使用完时, 会从一级回收链表s->cpu_slab->partial上取小缓存池, 给到s->cpu_slab->page上,即切换到新的小缓存池,s->cpu_slab->freelist也会指向新小缓存池里面小内存块链上头个小内存块.

函数slab_alloc_node()在当前缓存池耗尽时,会调用__slab_alloc()进一步找寻空闲内存:

static __always_inline void *slab_alloc_node(struct kmem_cache *s,gfp_t gfpflags, int node, unsigned long addr)
{void **object;struct kmem_cache_cpu *c;struct page *page;unsigned long tid;...c = __this_cpu_ptr(s->cpu_slab);...object = c->freelist;page = c->page;...if (unlikely(!object || !node_match(page, node)))//object为NULL时,表示小缓存池里面小内存块耗尽object = __slab_alloc(s, gfpflags, node, addr, c);else {//从当前小缓存池分配内存...}...
}

__slab_alloc()首先从一级回收链表s->cpu_slab->partial上取小缓存池,并在小缓存池中找寻空闲小内存块:

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,unsigned long addr, struct kmem_cache_cpu *c)
{void *freelist;struct page *page;unsigned long flags;...c = this_cpu_ptr(s->cpu_slab);...page = c->page;//当前小缓存池if (!page)goto new_slab;
redo:...freelist = get_freelist(s, page);//第一次执行时,当前小缓存池已经没有了空闲小内存块,返回的freelist为NULL,//只有通过下面goto redo跳转,再次执行时,//page指向新的小缓存池,如存在空闲小内存块,返回的freelist才会是非NULLif (!freelist) {c->page = NULL;//清空指向当前小缓存池的指针stat(s, DEACTIVATE_BYPASS);goto new_slab;//跳转到切换当前小缓存池的地方}...
load_freelist://执行到这里,表示已经找到空闲小内存块...c->freelist = get_freepointer(s, freelist);//freelist是指向小内存块链上第一块,//get_freepointer返回后,c->freelist指向小内存块链上第二块//get_freepointer()函数,就是从小内存块链上取小内存块的过程,并返回下一块c->tid = next_tid(c->tid);...return freelist;//成功获取小内存块,返回new_slab:if (c->partial) {//小缓存池链上,如有小缓存池page = c->page = c->partial;//从c->partial上取新的小缓存池,即切换当前小缓存池c->partial = page->next;//c->partial指向小缓存池链上,下一个小缓存池stat(s, CPU_PARTIAL_ALLOC);c->freelist = NULL;//清空当前小内存块链指针,为切换当前小内存块链做准备goto redo;//切换当前小缓存池后,跳转准备再次查找空闲小内存块.}...
}

函数get_freelist():

static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{struct page new;unsigned long counters;void *freelist;do {freelist = page->freelist;//从当前小缓存池上取小内存块链头//page->freelist始终指向小内存块链头//小缓存池从伙伴系统分配来,切换为当前小缓存//池时,page->freelist会被置为NULLcounters = page->counters;new.counters = counters;VM_BUG_ON(!new.frozen);new.inuse = page->objects;new.frozen = freelist != NULL;//page为当前小缓存池时,freelist为//NULL,那么new.frozen=0,//frozen为0表示解冻,可被释放,为1时//表示冻住,小缓存池不可被释放,//这里的释放,是指释放回伙伴系统//page不为当前小缓存池时,如果有空闲//小内存块,则new.fozen=1,没有空闲//小内存块时,new.fozen=0//__cmpxchg_double_slab函数主要作用: //page->freelist=NULL//page->counters=new.counters,即修改page->counter值} while (!__cmpxchg_double_slab(s, page,freelist, counters,NULL, new.counters,"get_freelist"));return freelist;//返回小内存块链头地址
}

在上面分配过程中,s->cpu_slab->page指向的当前小缓存池,在切换到新小缓存池时,旧的小缓存池,似乎直接被抛弃,那么,slub后面是怎么知道旧小缓存池的存在?请看小内存块释放,是怎么发生在一级回收链表s->cpu_slab->partial上:
函数调用过程:
kfree() --> slab_free() --> __slab_free() --> put_cpu_partial()

void kfree(const void *x)
{struct page *page;void *object = (void *)x;//释放小内存块的地址...page = virt_to_head_page(x);//通过内存地址,查找到内存所在页的首页地址,//即,被释放小内存块,所在小缓存池首页地址...//同类缓存池page->slab_cache//被释放内存所在小缓存池首页page//被释放object小内存块起始地址slab_free(page->slab_cache, page, object, _RET_IP_);
}static __always_inline void slab_free(struct kmem_cache *s,struct page *page, void *x, unsigned long addr)
{void **object = (void *)x;struct kmem_cache_cpu *c;unsigned long tid;...c = __this_cpu_ptr(s->cpu_slab)...if (likely(page == c->page)) {//当前缓存池...}else__slab_free(s, page, x, addr);//释放内存,不在当前小缓存池中
}//__slab_free需要关注:
//同一个page小缓存池里面小内存块分配完后,
//是否为首次发生小内存块释放
static void __slab_free(struct kmem_cache *s, struct page *page,void *x, unsigned long addr)
{void *prior;void **object = (void *)x;int was_frozen;struct page new;unsigned long counters;struct kmem_cache_node *n = NULL;unsigned long uninitialized_var(flags);...do {...prior = page->freelist;//page小缓存池,里面小内存块分配完后,//首次发生小内存块释放时,page->freelist为NULL//不是首次释放时,表示小缓存池,前面发生过小内存块释放,page->freelist不为NULLcounters = page->counters;set_freepointer(s, object, prior);//将prior写入到object空间开头地方new.counters = counters;was_frozen = new.frozen;//page小缓存池,里面小内存块分配完后,//首次发生小内存块释放时,new.frozen=0,可看切换当前//小缓存池时get_freelist()函数的调用//不是首次释放时,new.frozen=1new.inuse--;//inuse表示小缓存池里面有多少个小内存块被分配使用中,//释放时,自然就需要减去1if ((!new.inuse || !prior) && !was_frozen) {//此条件,在page小缓存池,里面小内存块分配完,//首次发生小内存块释放时,将会为真//不是首次释放小内存块时,将为假//kmem_cache_debug(s)主要用于slub调试时打开CONFIG_SLUB_DEBUG,//正常情况下CONFIG_SLUB_DEBUG是不被打开的           if (!kmem_cache_debug(s) && !prior)//page上首次发生小内存块释放时,此条件为真,//不是首次发生小内存块释放,则为假/** Slab was on no list before and will be partially empty* We can defer the list move and instead freeze it.*/new.frozen = 1;else { /* Needs to be taken off a list */n = get_node(s, page_to_nid(page));/** Speculatively acquire the list_lock.* If the cmpxchg does not succeed then we may* drop the list_lock without any processing.** Otherwise the list_lock will synchronize with* other processors updating the list of slabs.*/spin_lock_irqsave(&n->list_lock, flags);}}} while (!cmpxchg_double_slab(s, page,//cmpxchg_double_slab主要完成://page->freelist=object//page->counters=new.countersprior, counters,object, new.counters,"__slab_free"));if (likely(!n)) {//此条件,基本为真/** If we just froze the page then put it onto the* per cpu partial list.*/if (new.frozen && !was_frozen) {//小缓存池page在一级回收链表上,//首次发生小内存块释放时,//此条件为真,不是首次释放时,为假put_cpu_partial(s, page, 1);//将小缓存池page放到s->cpu_slab->partial链表上          stat(s, CPU_PARTIAL_FREE);}/** The list lock was not taken therefore no list* activity can be necessary.*/if (was_frozen)stat(s, FREE_FROZEN);return;}...
}//put_cpu_partial将把小缓存池,放到s->cpu_slab->partial
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{struct page *oldpage;int pages;int pobjects;do {pages = 0;pobjects = 0;oldpage = this_cpu_read(s->cpu_slab->partial);//一级回收链表if (oldpage) {//同类缓存池,首次发生小内存块释放,oldpage为NULL//之后,发生的小内存块释放,oldpage将为非NULLpobjects = oldpage->pobjects;//一级回收链表上,小内存块总数pages = oldpage->pages;//一级回收链表上,小缓存池总数//一级回收链表上,小内存块总数超过cpu_partial时,//将会把一级回收链表上的小缓存池,取出放入到二级回收链表上//注意:__slab_free()调用本函数,drain是为1if (drain && pobjects > s->cpu_partial) {unsigned long flags;/** partial array is full. Move the existing* set to the per node partial list.*/local_irq_save(flags);//将一级回收链表上小缓存池,取出放入到二级unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));local_irq_restore(flags);oldpage = NULL;pobjects = 0;pages = 0;stat(s, CPU_PARTIAL_DRAIN);}}pages++;//统计一级回收链表上,小缓存池数pobjects += page->objects - page->inuse;//统计一级回收链表上,空闲小内存块数//page->objects小缓存池上小内存块总数,从伙伴系统分配时确定,之后一直不变//page->inuse小缓存池已分配使用的小内存块数page->pages = pages;//将统计的小缓存池数信息,存放到一级回收链表头中//由此可看出,一级回收链表上小缓存池总数信息,始终存放在链表头中page->pobjects = pobjects;//将统计的小内存块数,存放到一级回收链表头中//由此可看出,一级回收链表上小内存块总数信息,始终存放在链表头中page->next = oldpage;//新加入的小缓存池,放入一级回收链表头//this_cpu_cmpxchg主要操作://s->cpu_slab->partial=page} while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page) != oldpage);
}

s->node[x]->partial :
二级回收链表
在什么情况下,才会从二级回收链表分配获取小缓存池呢?
在什么情况下,才会把小缓存池放入到二级回收链表呢?

从上面分析中,可以看到分配调用过程,有个函数__slab_alloc(),在这个函数里面,根据一定条件满足,将从二级回收链表分配内存:

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,unsigned long addr, struct kmem_cache_cpu *c)
{void *freelist;struct page *page;unsigned long flags;...c = this_cpu_ptr(s->cpu_slab);...
load_freelist://执行到这里,表示已经成功切换到有空闲小内存块的小缓存池/** freelist is pointing to the list of objects to be used.* page is pointing to the page from which the objects are obtained.* That page must be frozen for per cpu allocations to work.*/VM_BUG_ON(!c->page->frozen);c->freelist = get_freepointer(s, freelist);c->tid = next_tid(c->tid);local_irq_restore(flags);return freelist;new_slab:if (c->partial) {//c->partial为NULL时,表示一级级回收链表无小缓存池,//将进入下面的code,从二级回收链表上分配获取小缓存池...}//将有可能从二级回收链表上获取小缓存池freelist = new_slab_objects(s, gfpflags, node, &c);...page = c->page;//此时当前小缓存池已经切换到新小缓存池if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))goto load_freelist;//跳转...
}static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,int node, struct kmem_cache_cpu **pc)
{void *freelist;struct kmem_cache_cpu *c = *pc;struct page *page;freelist = get_partial(s, flags, node, c);//从二级回收链表上分配获取内存if (freelist)return freelist;page = new_slab(s, flags, node);//当前小缓存池,一级,二级回收链表上都没//有空闲小内存块,将会从伙伴系统分配//获取新小缓存池if (page) {//成功从伙伴系统中分配获取到小缓存池c = __this_cpu_ptr(s->cpu_slab);if (c->page)flush_slab(s, c);//清空当前小缓存池和小内存块链//并且根据条件,是否将小缓存池释放会伙伴系统/** No other reference to the page yet so we can* muck around with it freely without cmpxchg*/freelist = page->freelist;//新小缓存池中小内存块链page->freelist = NULL;//情况当前小缓存池中小内存块链,表示小内存块都被用//page->freelist始终指向未被用小内存块链stat(s, ALLOC_SLAB);c->page = page;//新小缓存池,作为当前小缓存池*pc = c;} elsefreelist = NULL;return freelist;
}static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,struct kmem_cache_cpu *c)
{void *object;int searchnode = (node == NUMA_NO_NODE) ? numa_node_id() : node;//从二级回收链表上分配获取小缓存池object = get_partial_node(s, get_node(s, searchnode), c, flags);if (object || node != NUMA_NO_NODE)return object;...
}static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,struct kmem_cache_cpu *c, gfp_t flags)
{struct page *page, *page2;void *object = NULL;int available = 0;int objects;/** Racy check. If we mistakenly see no partial slabs then we* just allocate an empty slab. If we mistakenly try to get a* partial slab and there is none available then get_partials()* will return NULL.*/if (!n || !n->nr_partial)//二级回收链表是否为空//n指向二级回收链表//n->nr_partial二级回收链表上小缓存池数量return NULL;spin_lock(&n->list_lock);//page指向前一个小缓存池,page2指向下一个小缓存池//n->partial二级回收链表//这个循环将把二级回收链表上,第一个小缓存池作为当前小缓存池,其他的放到二级回收链表list_for_each_entry_safe(page, page2, &n->partial, lru) {void *t;if (!pfmemalloc_match(page, flags))continue;//从二级回收链表上获取空闲小内存块链tt = acquire_slab(s, n, page, object == NULL, &objects);if (!t)break;available += objects;//统计小内存块数量if (!object) {c->page = page;//第一个,从二级回收链表获取的小缓存池作为当前小缓存池stat(s, ALLOC_FROM_PARTIAL);object = t;} else {put_cpu_partial(s, page, 0);//将后续从二级回收链表上获取的小缓存//池全部取出来放入到一级回收链表上stat(s, CPU_PARTIAL_NODE);}if (kmem_cache_debug(s) || available > s->cpu_partial / 2)break;}spin_unlock(&n->list_lock);return object;
}static inline void *acquire_slab(struct kmem_cache *s,struct kmem_cache_node *n, struct page *page,int mode, int *objects)
{void *freelist;unsigned long counters;struct page new;/** Zap the freelist and set the frozen bit.* The old freelist is the list of objects for the* per cpu allocation list.*/freelist = page->freelist;counters = page->counters;new.counters = counters;*objects = new.objects - new.inuse;//计算空闲小内存块数量if (mode) {//mode非0,表示二级回收链表上第一个小缓存池new.inuse = page->objects;new.freelist = NULL;} else {//二级回收链表上,其他小缓存池new.freelist = freelist;}VM_BUG_ON(new.frozen);new.frozen = 1;//表示小缓存池被冻结,不能释放回伙伴系统//page->freelist = new.freelist//page->counters=new.countersif (!__cmpxchg_double_slab(s, page,freelist, counters,new.freelist, new.counters,"acquire_slab"))return NULL;remove_partial(n, page);//从二级回收链表上移除小缓存池WARN_ON(!freelist);return freelist;
}

slub释放内存时,如何将内存释放到二级回收链表上?看如下:
函数调用过程:
kfree() --> slab_free() --> __slab_free() --> put_cpu_partial()

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{struct page *oldpage;int pages;int pobjects;...//pobjeccts表示一级回收链表上空闲小内存块数量//s->cpu_partial表示一级回收链表上空闲小内存块数量上限,大于上限将会移动到二级回收链表if (drain && pobjects > s->cpu_partial) {unsigned long flags;...//将一级回收链表上小缓存池,移动到二级回收链表unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));...}...
}static void unfreeze_partials(struct kmem_cache *s,struct kmem_cache_cpu *c)
{struct kmem_cache_node *n = NULL, *n2 = NULL;struct page *page, *discard_page = NULL;while ((page = c->partial)) {//循环从一级回收链表上获取小缓存池struct page new;struct page old;c->partial = page->next;//修改一级回收链表头指针n2 = get_node(s, page_to_nid(page));//n2指向二级回收链表if (n != n2) {if (n)spin_unlock(&n->list_lock);n = n2;spin_lock(&n->list_lock);}do {old.freelist = page->freelist;old.counters = page->counters;VM_BUG_ON(!old.frozen);new.counters = old.counters;new.freelist = old.freelist;new.frozen = 0;//清楚冻结标志//page->freelist=new.freelist//page->counters=new.counters} while (!__cmpxchg_double_slab(s, page,old.freelist, old.counters,new.freelist, new.counters,"unfreezing slab"));//n->nr_partial二级回收链表上,小缓存池数量// s->min_partial二级回收链表上,小缓存池数量上限,超过将被释放回到伙伴系统//new.inuse为0时,表示小缓存池上,没有小内存块被分配使用if (unlikely(!new.inuse && n->nr_partial > s->min_partial)) {page->next = discard_page;discard_page = page;} else {add_partial(n, page, DEACTIVATE_TO_TAIL);//将小缓存池page加入到二级回收链表n上stat(s, FREE_ADD_PARTIAL);}}if (n)spin_unlock(&n->list_lock);while (discard_page) {//将到达n->nr_partial > s->min_partial条件的//二级缓存回收链表上小缓存池将被释放回伙伴系统page = discard_page;discard_page = discard_page->next;stat(s, DEACTIVATE_EMPTY);discard_slab(s, page);//释放回伙伴系统stat(s, FREE_SLAB);}
}

从当前小缓存池,一级,二级回收链表上,都无法寻找到具有空闲小内存块的缓存池时,将会从伙伴系统分配获取小缓存池:
函数调用:
kmalloc() --> slab_alloc() --> slab_alloc_node() --> __slab_alloc() --> new_slab_objects() --> new_slab()

static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{struct page *page;void *start;void *last;void *p;int order;...//从伙伴系统中分配内存页,作为小缓存池page = allocate_slab(s,flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);if (!page)goto out...page->slab_cache = s;//小缓存池page,属于同类缓存池s__SetPageSlab(page);if (page->pfmemalloc)SetPageSlabPfmemalloc(page);start = page_address(page);//通过内存页计算内存页首地址...last = start;//将分配获取到的小缓存池,按照"slub核心原理"方式初始化,也可以视为格式化小缓存池for_each_object(p, s, start, page->objects) {setup_object(s, page, last);set_freepointer(s, last, p);last = p;}setup_object(s, page, last);set_freepointer(s, last, NULL);//最后一个小内存块,头部空间写入NULLpage->freelist = start;//小缓存池中,第一个小内存块起始地址page->inuse = page->objects;//小缓存池中小内存块总数量objectspage->frozen = 1;//小缓存池被冻结,不能释放回伙伴系统
out:return page;
}static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{struct page *page;struct kmem_cache_order_objects oo = s->oo;gfp_t alloc_gf...flags |= s->allocflags;/** Let the initial higher-order allocation fail under memory pressure* so we fall-back to the minimum order allocation.*/alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;page = alloc_slab_page(alloc_gfp, node, oo);//小缓存池空间大小,oo是2的幂指数if (unlikely(!page)) {//按照oo大小分配失败,将降低为s->min继续分配oo = s->min;/** Allocation may have failed due to fragmentation.* Try a lower order alloc if possible*/page = alloc_slab_page(flags, node, oo);if (page)stat(s, ORDER_FALLBACK);}...page->objects = oo_objects(oo);//计算小缓存池里面小内存块总数...return page;
}

简单总结:

slub分配时小内存块计算:
也可以说,申请size小内存块,查找属于那类大缓存池(同类缓存池).
第一种办法:
(1)kmalloc_index()得到kmalloc_caches[]角标index.
(2)然后kmalloc_caches[index]就是同类缓存池,之后见上面"slub分配释放"分析.
第二种办法:
通过调用kmalloc_slab()直接获取,同样,之后见上面"slub分配释放"分析.

slub分配内存时:
首先在当前小缓存池上检查有没有空闲小内存块,有则直接分配,如没有,检查一级回收链表上有没有小缓存池,有,则把该小缓存池切换为当前小缓存池,然后分配,如果一级回收链表还是没有,继续检查二级回收链表上小缓存池,同样,有,则切换为当前小缓存池,然后分配,要是二级回收链表上都还是没有,最后,向伙伴系统申请分配新的小缓存池,并将新分配的小缓存池切换为当前小缓存池,再分配.

slub释放内存:
释放小内存块,属于当前小缓存池时,直接释放回当前小缓存池.
释放小内存块,不属于当前小缓存池时,会被释放回所所属小缓存池,如果该小缓存池是分配使用完后,首次发生小内存块释放,那么将会把小缓存池挂接到一级回收链表上.每当有其他小缓存池发生挂接到一级回收链表时,会借此时机,先检查一级回收链表上小内存块总数是否大于s->cpu_partial,如果大于,将会把一级回收链表上所有小缓存池移动到二级回收链表上,移动过程中,还会检查二级回收链表上小缓存池数,如果超过s->min_partial,将会把超过的小缓存池(里面小内存块都未被分配使用情况)释放回伙伴系统,然后再将该小缓存池放入到一级回收链表.

linux内核内存管理slub相关推荐

  1. linux 内核内存管理

    物理内存 相关数据结构 page(页) Linux 内核内存管理的实现以 page 数据结构为核心,其他的内存管理设施都基于 page 数据结构,如 VMA 管理.缺页中断.RMAP.页面分配与回收等 ...

  2. 【Linux 内核 内存管理】虚拟地址空间布局架构 ③ ( 内存描述符 mm_struct 结构体成员分析 | mmap | mm_rb | task_size | pgd | mm_users )

    文章目录 一.mm_struct 结构体成员分析 1.mmap 成员 2.mm_rb 成员 3.get_unmapped_area 函数指针 4.task_size 成员 5.pgd 成员 6.mm_ ...

  3. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  4. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  5. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  6. 【Linux 内核 内存管理】RCU 机制 ② ( RCU 机制适用场景 | RCU 机制特点 | 使用 RCU 机制保护链表 )

    文章目录 一.RCU 机制适用场景 二.RCU 机制特点 三.使用 RCU 机制保护链表 一.RCU 机制适用场景 在上一篇博客 [Linux 内核 内存管理]RCU 机制 ① ( RCU 机制简介 ...

  7. pae扩展内存 linux,浅析linux内核内存管理之PAE

    浅析linux内核内存管理之PAE 早期Intel处理器从80386到Pentium使用32位物理地址,理论上,这样可以访问4GB的RAM.然而,大型服务器需要大于4GB的RAM来同时运行数以千计的进 ...

  8. Linux内核内存管理(3):kmemcheck介绍

    Linux内核内存管理 kmemcheck介绍 rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 5.10.13不存在kmemcheck的概念,取代的是k ...

  9. Linux内核内存管理(1):内存块 - memblock

    Linux内核内存管理 内存块 - memblock rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 1. 简介 内存管理是操作系统内核中最复杂的部分之 ...

最新文章

  1. How does SGD weight_decay work?
  2. Appium的环境搭建和配置
  3. animate inater插件_C4D R20插件下载 旧版插件C4D R20桥接插件INSYDIUMS Plug-In Bridge Cinema 4D R20 免费版 下载-脚本之家...
  4. lgg7深度详细参数_深度学习平均场理论第七讲:Batch Normalization会导致梯度爆炸?...
  5. VTK:可视化之Camera
  6. 【F3简介】一张图看懂FPGA-F3实例
  7. node中使用短信验证功能(阿里云为例)
  8. 信息学奥赛一本通(2068:【例2.6】鸡兔同笼)
  9. 数据结构---AVL树调整方法(详)
  10. [Java] 蓝桥杯ADV-205 算法提高 拿糖果
  11. 从头学习Drupal--基本架构三
  12. 怎么让HTML的属性横着排,css标签怎么设置横向排列
  13. react17.x+MDUI实现todo小案例,react动态添加与删除元素属性
  14. 在Excel中批量生成二维码标签,标签中可添加二维码或者条形码
  15. 笔记本电脑触摸板手势教程——快捷操作
  16. 机器学习(六)统计学习理论
  17. vue导入音乐_vue-music:添加歌曲到队列add-song.vue
  18. 人事管理系统实现(一)
  19. 微信开放平台开源_开源需要开放徽章的3个原因
  20. 背阔肌(04):杠铃俯身划船

热门文章

  1. arduino使用oled代码_Arduino指纹传感器模块使用方法(FPM10A)
  2. IEEE754转换规则
  3. Oracle EBS MTL_SUPPLY作用
  4. 数据治理系列:浅谈数据质量管理
  5. HttpWebRequest和HttpWebResponse
  6. VLAN 、PVLAN
  7. javaMap集合 详解
  8. 【读书笔记】商业自传-耐克科技,鞋狗:耐克创始人菲尔.奈特亲笔自传_2020.06.01
  9. 服务器cpu型号大变更,英特尔新世代Xeon Scalable服务器处理器登场,架构大翻新拥有超多28核心,更改采分级制推4大产品线...
  10. 本地服务器模板网站怎么安装,使用dedecms搭建自己的本地网站(全程图解)