kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

linux TCP/IP协议栈-IP层

linux TCP/IP协议栈 —ip_rcv()
linux TCP/IP协议栈 —ip_rcv_finish()
linux TCP/IP协议栈 —ip_local_deliver()
linux TCP/IP协议栈 —ip_local_deliver_finish()
linux TCP/IP协议栈 —ip_defrag()
linux TCP/IP协议栈 —ip_find()
linux TCP/IP协议栈 —inet_frag_find()

ip_rcv()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* 主要功能:对IP头部合法性进行严格检查,然后把具体功能交给ip_rcv_finish。*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	struct iphdr *iph;
	u32 len;
	/* 网络名字空间,忽略 */
	if (dev->nd_net != &init_net)
		goto drop;
	/*
	 *当网卡处于混杂模式时,收到不是发往该主机的数据包,由net_rx_action()设置。
	 *在调用ip_rcv之前,内核会将该数据包交给嗅探器,所以该函数仅丢弃该包。
	 */
	if (skb->pkt_type == PACKET_OTHERHOST)
		goto drop;
	/* SNMP所需要的统计数据,忽略 */
	IP_INC_STATS_BH(IPSTATS_MIB_INRECEIVES);

	/*
	 *ip_rcv是由netif_receive_skb函数调用,如果嗅探器或者其他的用户对数据包需要进
	 *进行处理,则在调用ip_rcv之前,netif_receive_skb会增加skb的引用计数,既该引
	 *用计数会大于1。若如此次,则skb_share_check会创建sk_buff的一份拷贝。
	 */
	if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
		IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
		goto out;
	}
	/*
	 *pskb_may_pull确保skb->data指向的内存包含的数据至少为IP头部大小,由于每个
	 *IP数据包包括IP分片必须包含一个完整的IP头部。如果小于IP头部大小,则缺失
	 *的部分将从数据分片中拷贝。这些分片保存在skb_shinfo(skb)->frags[]中。
	 */
	if (!pskb_may_pull(skb, sizeof(struct iphdr)))
		goto inhdr_error;
	/* pskb_may_pull可能会调整skb中的指针,所以需要重新定义IP头部*/
	iph = ip_hdr(skb);

	/*
	 *    RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the checksum.
	 *
	 *    Is the datagram acceptable?
	 *
	 *    1.    Length at least the size of an ip header
	 *    2.    Version of 4
	 *    3.    Checksums correctly. [Speed optimisation for later, skip loopback checksums]
	 *    4.    Doesn't have a bogus length
	 */
	/* 上面说的很清楚了 */
	if (iph->ihl < 5 || iph->version != 4)
		goto inhdr_error;
	/* 确保IP完整的头部包括选项在内存中 */
	if (!pskb_may_pull(skb, iph->ihl*4))
		goto inhdr_error;
	
	iph = ip_hdr(skb);
	/* 验证IP头部的校验和 */
	if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
		goto inhdr_error;
	/* IP头部中指示的IP数据包总长度 */
	len = ntohs(iph->tot_len);
	/*
	 *确保skb的数据长度大于等于IP头部中指示的IP数据包总长度及数据包总长度必须
	 *大于等于IP头部长度。
	 */
	if (skb->len < len) {
		IP_INC_STATS_BH(IPSTATS_MIB_INTRUNCATEDPKTS);
		goto drop;
	} else if (len < (iph->ihl*4))
		goto inhdr_error;

	/* Our transport medium may have padded the buffer out. Now we know it
	 * is IP we can trim to the true length of the frame.
	 * Note this now means skb->len holds ntohs(iph->tot_len).
	 */
	/* 注释说明的很清楚,该函数成功执行完之后,skb->len = ntohs(iph->tot_len). */
	if (pskb_trim_rcsum(skb, len)) {
		IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
		goto drop;
	}

	/* Remove any debris in the socket control block */
	memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
	/* 忽略与netfilter子系统的交互,调用为ip_rcv_finish(skb) */
	return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
		 ip_rcv_finish);

inhdr_error:
	IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
drop:
	kfree_skb(skb);
out:
	return NET_RX_DROP;
}

ip_rcv_finish()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static int ip_rcv_finish(struct sk_buff *skb)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct rtable *rt;

	/*
	 *    Initialise the virtual path cache for the packet. It describes
	 *    how the packet travels inside Linux networking.
	 */
	/*
	 * 通常从外界接收的数据包,skb->dst不会包含路由信息,暂时还不知道在何处会设置
	 * 这个字段。ip_route_input函数会根据路由表设置路由信息,暂时不考虑路由系统。
	 */
	if (skb->dst == NULL) {
		int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
					 skb->dev);
		if (unlikely(err)) {
			if (err == -EHOSTUNREACH)
				IP_INC_STATS_BH(IPSTATS_MIB_INADDRERRORS);
			else if (err == -ENETUNREACH)
				IP_INC_STATS_BH(IPSTATS_MIB_INNOROUTES);
			goto drop;
		}
	}
/* 更新流量控制所需要的统计数据,忽略 */
#ifdef CONFIG_NET_CLS_ROUTE
	if (unlikely(skb->dst->tclassid)) {
		struct ip_rt_acct *st = ip_rt_acct + 256*smp_processor_id();
		u32 idx = skb->dst->tclassid;
		st[idx&0xFF].o_packets++;
		st[idx&0xFF].o_bytes+=skb->len;
		st[(idx>>16)&0xFF].i_packets++;
		st[(idx>>16)&0xFF].i_bytes+=skb->len;
	}
#endif
	/* 如果IP头部大于20字节,则表示IP头部包含IP选项,需要进行选项处理.暂时忽略,毕竟很少用 */
	if (iph->ihl > 5 && ip_rcv_options(skb))
		goto drop;

	/* skb->dst包含路由信息。根据路由类型更新SNMP统计数据 */
	rt = (struct rtable*)skb->dst;
	if (rt->rt_type == RTN_MULTICAST)
		IP_INC_STATS_BH(IPSTATS_MIB_INMCASTPKTS);
	else if (rt->rt_type == RTN_BROADCAST)
		IP_INC_STATS_BH(IPSTATS_MIB_INBCASTPKTS);
	/*
	 * dst_input实际上会调用skb->dst->input(skb).input函数会根据路由信息设置为合适的
	 * 函数指针,如果是递交到本地的则为ip_local_deliver,若是转发则为ip_forward.
	 * 暂时仅先考虑ip_local_deliver。
	 */
	return dst_input(skb);

drop:
	kfree_skb(skb);
	return NET_RX_DROP;
}

ip_local_deliver()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 *     Deliver IP Packets to the higher protocol layers.
 */
主要功能:收集IP分片,然后调用ip_local_deliver_finish将一个完整的数据包传送给上层协议。
int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *    Reassemble IP fragments.
	 */
	/*
	 * 判断该IP数据包是否是一个分片,如果IP_MF置位,则表示该包是分片之一,其
	 * 后还有更多分片,最后一个IP分片未置位IP_MF但是其offset是非0。
	 * 如果是一个IP分片,则调用ip_defrag重新组织IP数据包。
	 */
	if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
		if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
			return 0;
	}
	/* 调用ip_local_deliver_finish(skb) */
	return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
		 ip_local_deliver_finish);
}

ip_local_deliver_finish()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* 如果忽略掉原始套接字和IPSec,则该函数仅仅是根据IP头部中的协议字段选择上层L4协议,并交给它来处理 */
static int ip_local_deliver_finish(struct sk_buff *skb)
{
	/* 跳过IP头部 */
	__skb_pull(skb, ip_hdrlen(skb));

	/* Point into the IP datagram, just past the header. */
	/* 设置传输层头部位置 */
	skb_reset_transport_header(skb);

	rcu_read_lock();
	{
		/* Note: See raw.c and net/raw.h, RAWV4_HTABLE_SIZE==MAX_INET_PROTOS */
		int protocol = ip_hdr(skb)->protocol;
		int hash;
		struct sock *raw_sk;
		struct net_protocol *ipprot;

	resubmit:
	/* 这个hash根本不是哈希值,仅仅只是inet_protos数组中的下表而已 */
		hash = protocol & (MAX_INET_PROTOS - 1);
		raw_sk = sk_head(&raw_v4_htable[hash]);

		/* If there maybe a raw socket we must check - if not we
		 * don't care less
		 */
	/* 原始套接字?? 忽略... */
		if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash))
			raw_sk = NULL;
	/* 查找注册的L4层协议处理结构。 */
		if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) {
			int ret;
	/* 启用了安全策略,则交给IPSec */
			if (!ipprot->no_policy) {
				if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					kfree_skb(skb);
					goto out;
				}
				nf_reset(skb);
			}
	/* 调用L4层协议处理函数 */
	/* 通常会是tcp_v4_rcv, udp_rcv, icmp_rcv和igmp_rcv */
	/* 如果注册了其他的L4层协议处理,则会进行相应的调用。 */
			ret = ipprot->handler(skb);
			if (ret < 0) {
				protocol = -ret;
				goto resubmit;
			}
			IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
		} else {
			if (!raw_sk) {    /* 无原始套接字,提交给IPSec */
				if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS);
					icmp_send(skb, ICMP_DEST_UNREACH,
						 ICMP_PROT_UNREACH, 0);
				}
			} else
				IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
			kfree_skb(skb);
		}
	}
 out:
	rcu_read_unlock();

	return 0;
}

ip_defrag()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Process an incoming IP datagram fragment. */
int ip_defrag(struct sk_buff *skb, u32 user)
{
	struct ipq *qp;

	IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);

	/* Start by cleaning up the memory. */
	/*
	 * 首先检查所有IP分片所消耗的内存是否大于系统允许的最高阀值,如果是,则调用
	 * ip_evictor()丢弃未完全到达的IP分片,从最旧的分片开始释放。此举一来是为了节
	 * 约内存,二来是未了防止黑客的恶意攻击。使分片在系统中累计,降低系统性能。
	 */
	if (atomic_read(&ip4_frags.mem) > ip4_frags_ctl.high_thresh)
		ip_evictor();

	/* Lookup (or create) queue header */
	/* 如果该分片是数据报的第一个分片,则ip_find返回一个新的队列来搜集分片,否则
	 * 返回其所属于的分片队列。 */
	if ((qp = ip_find(ip_hdr(skb), user)) != NULL) {
		int ret;

		spin_lock(&qp->q.lock);
	/* 将该分片加入到队列中,重组分片队列,如果所有的包都收到了,则该函数
	 * 负责重组IP包 */
		ret = ip_frag_queue(qp, skb);

		spin_unlock(&qp->q.lock);
		ipq_put(qp);    /* 引用计数减1 */
		return ret;
	}

	IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
	kfree_skb(skb);
	return -ENOMEM;
}

ip_find()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Find the correct entry in the "incomplete datagrams" queue for
 * this IP datagram, and create new one, if nothing is found.
 */
/* u32 user这个参数有点迷惑,其表示以何种理由需要对数据包进行重组,在ip_local_deliver的调用序列当中,这个值是IP_DEFRAG_LOCAL_DELIVER。*/
static inline struct ipq *ip_find(struct iphdr *iph, u32 user)
{
	struct inet_frag_queue *q;
	struct ip4_create_arg arg;
	unsigned int hash;

	arg.iph = iph;
	arg.user = user;
	/*
	 * hash算法,该算法除了使用所给的这四个参数之外,还使用了一个随机值
	 * ip4_frags.rnd,,其初始化为
	 * (u32) ((num_physpages ^ (num_physpages>>7)) ^ (jiffies ^ (jiffies >> 6)));
	 * 这是为了防止黑客根据固定的hash算法,通过设置ip头部的这些字段,生成同样
	 * HASH值,从而使某一HASH队列长度急剧增大而影响性能。
	 */
	hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);
	/* 若存在该分片所属的分片队列则返回这个队列,否则创建一个新的队列 */
	q = inet_frag_find(&ip4_frags, &arg, hash);
	if (q == NULL)
		goto out_nomem;

	return container_of(q, struct ipq, q);

out_nomem:
	LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left !\n");
	return NULL;
}

inet_frag_find()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct inet_frag_queue *inet_frag_find(struct inet_frags *f, void *key,
		unsigned int hash)
{
	struct inet_frag_queue *q;
	struct hlist_node *n;

	/* f->lock是读写锁,先搜索是否存在该IP分段所属的队列 */
	read_lock(&f->lock);
	hlist_for_each_entry(q, n, &f->hash[hash], list) { /* 扫描该HASH槽中所有节点 */
	/* f->match中match字段在ipfrag_init中初始化为ip4_frag_match函数。*/
	/* 对比分片队列中的散列字段和user是否和key相等,key指向的是struct ip4_create_arg
	 * 结构,包含IP头部和user字段。 */
		if (f->match(q, key)) {
			atomic_inc(&q->refcnt);     /* 若找到,则增加该队列引用计数。 */
			read_unlock(&f->lock);
			return q;                /* 返回该队列 */
		}
	}
	read_unlock(&f->lock);
	/* 该分片是第一个IP分片,创建一个新的分片队列并添加到合适的HASH队列 */
	return inet_frag_create(f, key, hash);
}

Linux slab 分配器,注意RCU

Linux Slab分配器(一)-概述
Linux Slab分配器(二)-初始化
Linux Slab分配器(三)-创建缓存
Linux Slab分配器(四)-分配对象
Linux Slab分配器(五)–释放对象
Linux Slab分配器(六)–创建slab和销毁slab
Linux Slab分配器(七)–销毁缓存

一、概述

slab分配器是Linux内存管理中非常重要和复杂的一部分,其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内碎片,而且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统。slab分配对象时,会使用最近释放的对象内存块,因此其驻留在CPU高速缓存的概率较高。

用于描述和管理cache的数据结构是struct kmem_cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
struct kmem_cache {
/* 1) per-cpu data, touched during every alloc/free */
	/*per-CPU数据,记录了本地高速缓存的信息,也用于跟踪最近释放的对象,每次分配和释放都要直接访问它*/
	struct array_cache *array[NR_CPUS];
/* 2) Cache tunables. Protected by cache_chain_mutex */
	unsigned int batchcount;  /*本地高速缓存转入或转出的大批对象数量*/
	unsigned int limit;       /*本地高速缓存中空闲对象的最大数目*/
	unsigned int shared;

	unsigned int buffer_size;/*管理对象的大小*/
	u32 reciprocal_buffer_size;/*buffer_size的倒数值*/
/* 3) touched by every alloc & free from the backend */

	unsigned int flags;          /* 高速缓存的永久标识*/
	unsigned int num;         /* 一个slab所包含的对象数目 */

/* 4) cache_grow/shrink */
	/* order of pgs per slab (2^n) */
	unsigned int gfporder;   /*一个slab包含的连续页框数的对数*/

	/* force GFP flags, e.g. GFP_DMA */
	gfp_t gfpflags;          /*与伙伴系统交互时所提供的分配标识*/

	size_t colour;         /* 颜色的个数*/
	unsigned int colour_off; /* 着色的偏移量 */

	/*如果将slab描述符存储在外部,该指针指向存储slab描述符的cache,
	  否则为NULL*/
	struct kmem_cache *slabp_cache;
	unsigned int slab_size;  /*slab管理区的大小*/
	unsigned int dflags;     /*动态标识*/

	/* constructor func */
	void (*ctor)(void *obj); /*创建高速缓存时的构造函数指针*/

/* 5) cache creation/removal */
	const char *name;         /*高速缓存名*/
	struct list_head next;    /*用于将高速缓存链入cache chain*/

/* 6) statistics */
#ifdef CONFIG_DEBUG_SLAB /*一些用于调试用的变量*/
	unsigned long num_active;
	unsigned long num_allocations;
	unsigned long high_mark;
	unsigned long grown;
	unsigned long reaped;
	unsigned long errors;
	unsigned long max_freeable;
	unsigned long node_allocs;
	unsigned long node_frees;
	unsigned long node_overflow;
	atomic_t allochit;
	atomic_t allocmiss;
	atomic_t freehit;
	atomic_t freemiss;

	/*
	 * If debugging is enabled, then the allocator can add additional
	 * fields and/or padding to every object. buffer_size contains the total
	 * object size including these internal fields, the following two
	 * variables contain the offset to the user object and its size.
	 */
	int obj_offset;
	int obj_size;
#endif /* CONFIG_DEBUG_SLAB */

	/*
	 * We put nodelists[] at the end of kmem_cache, because we want to size
	 * this array to nr_node_ids slots instead of MAX_NUMNODES
	 * (see kmem_cache_init())
	 * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
	 * is statically defined, so we reserve the max number of nodes.
	 */
	 /*struct kmem_list3用于组织该高速缓存中的slab*/
	struct kmem_list3 *nodelists[MAX_NUMNODES];
	/*
	 * Do not add fields after nodelists[]
	 */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
struct kmem_list3 {
	struct list_head slabs_partial;/*slab链表,包含空闲对象和已分配对象的slab描述符*/
	struct list_head slabs_full;   /*slab链表,只包含非空闲的slab描述符*/
	struct list_head slabs_free;   /*slab链表,只包含空闲的slab描述符*/
	unsigned long free_objects;    /*高速缓存中空闲对象的个数*/
	unsigned int free_limit;       /*空闲对象的上限*/
	unsigned int colour_next;       /*下一个slab使用的颜色*/
	spinlock_t list_lock;
	struct array_cache *shared; /* shared per node */
	struct array_cache **alien; /* on other nodes */
	unsigned long next_reap;    /* updated without locking */
	int free_touched;       /* updated without locking */
};
描述和管理单个slab的结构是struct slab
1
2
3
4
5
6
7
8
struct slab {
	struct list_head list;  /*用于将slab链入kmem_list3的链表*/
	unsigned long colouroff;/*该slab的着色偏移*/
	void *s_mem;            /*指向slab中的第一个对象*/
	unsigned int inuse;     /*已分配出去的对象*/
	kmem_bufctl_t free;     /*下一个空闲对象的下标*/
	unsigned short nodeid;  /*节点标识号*/
};

Linux slab 分配器剖析

http://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/

动态内存管理

内存管理的目标是提供一种方法,为实现各种目的而在各个用户之间实现内存共享。内存管理方法应该实现以下两个功能:
1. 最小化管理内存所需的时间 2. 最大化用于一般应用的可用内存(最小化管理开销)

内存管理实际上是一种关于权衡的零和游戏。您可以开发一种使用少量内存进行管理的算法,但是要花费更多时间来管理可用内存。也可以开发一个算法来有效地管理内存,但却要使用更多的内存。最终,特定应用程序的需求将促使对这种权衡作出选择。

每个内存管理器都使用了一种基于堆的分配策略。在这种方法中,大块内存(称为 堆)用来为用户定义的目的提供内存。当用户需要一块内存时,就请求给自己分配一定大小的内存。堆管理器会查看可用内存的情况(使用特定算法)并返回一块内存。搜索过程中使用的一些算法有 first-fit(在堆中搜索到的第一个满足请求的内存块 )和 best-fit(使用堆中满足请求的最合适的内存块)。当用户使用完内存后,就将内存返回给堆。

这种基于堆的分配策略的根本问题是碎片(fragmentation)。当内存块被分配后,它们会以不同的顺序在不同的时间返回。这样会在堆中留下一些洞,需要花一些时间才能有效地管理空闲内存。这种算法通常具有较高的内存使用效率(分配需要的内存),但是却需要花费更多时间来对堆进行管理。

另外一种方法称为 buddy memory allocation,是一种更快的内存分配技术,它将内存划分为 2 的幂次方个分区,并使用 best-fit 方法来分配内存请求。当用户释放内存时,就会检查 buddy 块,查看其相邻的内存块是否也已经被释放。如果是的话,将合并内存块以最小化内存碎片。这个算法的时间效率更高,但是由于使用 best-fit 方法的缘故,会产生内存浪费。

本文将着重介绍 Linux 内核的内存管理,尤其是 slab 分配提供的机制。

slab 缓存

Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。

Linux slab 分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。

图 1 给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池。

每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:

slabs_full
完全分配的 slab
slabs_partial
部分分配的 slab
slabs_empty
空 slab,或者没有对象被分配

注意 slabs_empty 列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。

slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。这些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单位,因此如果需要对 slab 进行扩展,这也就是所扩展的最小值。通常来说,每个 slab 被分配为多个对象。

由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当所有对象都被释放之后,就从 slabs_partial 列表移动到 slabs_empty 列表中。

slab 背后的动机

与传统的内存管理模式相比, slab 缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab 分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

API 函数

现在来看一下能够创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 中对对象进行分配和释放操作的函数。

第一个步骤是创建 slab 缓存结构,您可以将其静态创建为:

1
struct struct kmem_cache *my_cachep;

然后其他 slab 缓存函数将使用该引用进行创建、删除、分配等操作。kmem_cache 结构包含了每个中央处理器单元(CPU)的数据、一组可调整的(可以通过 proc 文件系统访问)参数、统计信息和管理 slab 缓存所必须的元素。

kmem_cache_create

内核函数 kmem_cache_create 用来创建一个新缓存。这通常是在内核初始化时执行的,或者在首次加载内核模块时执行。其原型定义如下:

1
2
3
4
5
struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,
			unsigned long flags;
			void (*ctor)(void*, struct kmem_cache *, unsigned long),
			void (*dtor)(void*, struct kmem_cache *, unsigned long));

name 参数定义了缓存名称,proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。 size 参数指定了为这个缓存创建的对象的大小, align 参数定义了每个对象必需的对齐。 flags 参数指定了为缓存启用的选项。这些标志如表 1 所示。

表 1. kmem_cache_create 的部分选项(在 flags 参数中指定)
选项 说明
SLAB_RED_ZONE 在对象头、尾插入标志,用来支持对缓冲区溢出的检查。
SLAB_POISON 使用一种己知模式填充 slab,允许对缓存中的对象进行监视(对象属对象所有,不过可以在外部进行修改)。
SLAB_HWCACHE_ALIGN 指定缓存对象必须与硬件缓存行对齐。

ctor 和 dtor 参数定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象时,可以通过构造器进行初始化。

在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内存。相反,在试图从缓存(最初为空)分配对象时,refill 操作将内存分配给它。当所有对象都被使用掉时,也可以通过相同的操作向缓存添加内存。

kmem_cache_destroy

内核函数 kmem_cache_destroy 用来销毁缓存。这个调用是由内核模块在被卸载时执行的。在调用这个函数时,缓存必须为空。

1
void kmem_cache_destroy( struct kmem_cache *cachep );
kmem_cache_alloc

要从一个命名的缓存中分配一个对象,可以使用 kmem_cache_alloc 函数。调用者提供了从中分配对象的缓存以及一组标志:

1
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );

这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 向缓存中增加内存。 kmem_cache_alloc 的 flags 选项与 kmalloc 的 flags 选项相同。表 2 给出了标志选项的部分列表。

表 2. kmem_cache_alloc 和 kmalloc 内核函数的标志选项
标志 说明
GFP_USER 为用户分配内存(这个调用可能会睡眠)。
GFP_KERNEL 从内核 RAM 中分配内存(这个调用可能会睡眠)。
GFP_ATOMIC 使该调用强制处于非睡眠状态(对中断处理程序非常有用)。
GFP_HIGHUSER 从高端内存中分配内存。

kmem_cache_zalloc

内核函数 kmem_cache_zalloc 与 kmem_cache_alloc 类似,只不过它对对象执行 memset 操作,用来在将对象返回调用者之前对其进行清除操作。

kmem_cache_free

要将一个对象释放回 slab,可以使用 kmem_cache_free。调用者提供了缓存引用和要释放的对象。

1
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
kmalloc 和 kfree

内核中最常用的内存管理函数是 kmalloc 和 kfree 函数。这两个函数的原型如下:

1
2
void *kmalloc( size_t size, int flags );
void kfree( const void *objp );

注意在 kmalloc 中,惟一两个参数是要分配的对象的大小和一组标志(请参看 表 2 中的部分列表)。但是 kmalloc 和 kfree 使用了类似于前面定义的函数的 slab 缓存。kmalloc 没有为要从中分配对象的某个 slab 缓存命名,而是循环遍历可用缓存来查找可以满足大小限制的缓存。找到之后,就(使用 __kmem_cache_alloc)分配一个对象。要使用 kfree 释放对象,从中分配对象的缓存可以通过调用 virt_to_cache 确定。这个函数会返回一个缓存引用,然后在 __cache_free 调用中使用该引用释放对象。

其他函数

slab 缓存 API 还提供了其他一些非常有用的函数。 kmem_cache_size 函数会返回这个缓存所管理的对象的大小。您也可以通过调用 kmem_cache_name 来检索给定缓存的名称(在创建缓存时定义)。缓存可以通过释放其中的空闲 slab 进行收缩。这可以通过调用 kmem_cache_shrink 实现。注意这个操作(称为回收)是由内核定期自动执行的(通过 kswapd)。

1
2
3
unsigned int kmem_cache_size( struct kmem_cache *cachep );
const char *kmem_cache_name( struct kmem_cache *cachep );
int kmem_cache_shrink( struct kmem_cache *cachep );

slab 缓存的示例用法

下面的代码片断展示了创建新 slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程。首先,必须要定义一个 kmem_cache 对象,然后对其进行初始化(请参看清单 1)。这个特定的缓存包含 32 字节的对象,并且是硬件缓存对齐的(由标志参数 SLAB_HWCACHE_ALIGN 定义)。
清单 1. 创建新 slab 缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct kmem_cache *my_cachep;

static void init_my_cache( void )
{

	my_cachep = kmem_cache_create( 
				"my_cache",            /* Name */
				32,                    /* Object Size */
				0,                     /* Alignment */
				SLAB_HWCACHE_ALIGN,    /* Flags */
				NULL, NULL );          /* Constructor/Deconstructor */

	return;
}

使用所分配的 slab 缓存,您现在可以从中分配一个对象了。清单 2 给出了一个从缓存中分配和释放对象的例子。它还展示了两个其他函数的用法。
清单 2. 分配和释放对象

1
2
3
4
5
6
7
8
9
10
11
12
13
int slab_test( void )
{
	void *object;

	printk( "Cache name is %s\n", kmem_cache_name( my_cachep ) );
	printk( "Cache object size is %d\n", kmem_cache_size( my_cachep ) );

	object = kmem_cache_alloc( my_cachep, GFP_KERNEL );
	if (object) {
		kmem_cache_free( my_cachep, object );
	}
	return 0;
}

最后,清单 3 演示了 slab 缓存的销毁。调用者必须确保在执行销毁操作过程中,不要从缓存中分配对象。
清单 3. 销毁 slab 缓存

1
2
3
4
5
static void remove_my_cache( void )
{
	if (my_cachep) kmem_cache_destroy( my_cachep );
	return;
}

slab 的 proc 接口

proc 文件系统提供了一种简单的方法来监视系统中所有活动的 slab 缓存。这个文件称为 /proc/slabinfo,它除了提供一些可以从用户空间访问的可调整参数之外,还提供了有关所有 slab 缓存的详细信息。当前版本的 slabinfo 提供了一个标题,这样输出结果就更具可读性。对于系统中的每个 slab 缓存来说,这个文件提供了对象数量、活动对象数量以及对象大小的信息(除了每个 slab 的对象和页面之外)。另外还提供了一组可调整的参数和 slab 数据。

要调优特定的 slab 缓存,可以简单地向 /proc/slabinfo 文件中以字符串的形式回转 slab 缓存名称和 3 个可调整的参数。下面的例子展示了如何增加 limit 和 batchcount 的值,而保留 shared factor 不变(格式为 “cache name limit batchcount shared factor”):

1
# echo "my_cache 128 64 8" > /proc/slabinfo

limit 字段表示每个 CPU 可以缓存的对象的最大数量。 batchcount 字段是当缓存为空时转换到每个 CPU 缓存中全局缓存对象的最大数量。 shared 参数说明了对称多处理器(Symmetric MultiProcessing,SMP)系统的共享行为。

注意您必须具有超级用户的特权才能在 proc 文件系统中为 slab 缓存调优参数。

SLOB 分配器

对于小型的嵌入式系统来说,存在一个 slab 模拟层,名为 SLOB。这个 slab 的替代品在小型嵌入式 Linux 系统中具有优势,但是即使它保存了 512KB 内存,依然存在碎片和难于扩展的问题。在禁用 CONFIG_SLAB 时,内核会回到这个 SLOB 分配器中。更多信息请参看 参考资料 一节。