内核开发笔记

内核开发笔记

内核空间和用户空间的概念以及内核空间和用户空间的数据拷贝

可以调用copy_from_user和copy_to_user来进行用户和内核空间的读写

image-20211020195117124

内核空间与用户空间的地址界限不是固定的,内核里有个配置叫config_page_offset来设置界限

image-20211020195408772

image-20211020203514422

x86段页式内存管理和页表映射机制

image-20211020210949787

image-20211020214828697

一个页目录有1024个节点,一个节点4字节,总共4K,正好一个页,然后页表也是一样,也就是页目录1024×页表1024×物理页4KB,正好就是32位系统的最大内存限制4GB

CR3寄存器存放了页目录表的物理内存基址,每当进程切换时,Linux就会把下一个将要运行进程的页目录表物理内存基地址等信息存放到CR3寄存器中。按我自己的理解,每个进程都保存了页目录表项(索引),加上CR3的基址,就找到了对应的页目录

linux内核同步机制之semaphore使用

image-20211022155124475

先定义一个信号量struct semaphore sema;,在临界区前面加down(&sema);加锁,等出临界区时用up(&sema);解锁

image-20211022155807575

semaphore的内核源码实现

测试程序

image-20211022204944477

semaphore结构体

image-20211022160800421

双向链表在include/linux里面的types.h定义了

image-20211022161202988

初始化函数

image-20211022161812678

image-20211022161934709

image-20211022190751298

down函数

image-20211022193918380

count在初始化的时候设置了,当第一个进程调用down的时候,检测sem->count是否大于0,如果是count就自减1,如果不是,也就是第二个进程调用的时候,锁被占用了,就进入__down函数

image-20211022194341804

​ TASK_INTERRUPTIBLE: 处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但可以被中断唤醒.一般情况下,进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE状态.毕竟皇帝只有一个(单个CPU时),后宫佳丽几千;如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来. TASK_UNINTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但不可以被中断唤醒.

​ MAX_SCHEDULE_TIMEOUT设置等待时间,一般为很大很大的数

__down_common函数

image-20211022195152512

image-20211022195033477

首先将当前进程赋值给task,然后下面三句话总的意思将当前进程加入到等待队列

image-20211022195339343

然后进入for循环,判断是否有信号打断当前进程,因为state是TASK_UNINTERRUPTIBLE,所以直接就return 0了,前面两个if都可以不用看

image-20211022200641252

__set_task_state函数将当前进程的状态设置为TASK_UNINTERRUPTIBLE,让其进入睡眠不可中断状态,释放出资源给别的进程

image-20211022201556925

然后是raw_spin_unlock_irq,这里释放了一个锁,实际上是释放的下图中的锁,原因1是因为自旋锁的开销是非常大的,所以应该让尽可能少的代码放到被自旋锁保护的临界区,

image-20211022193918380

原因2:schedule_timeout(timeout)该方法会让需要延迟的任务睡眠到指定的延迟时间耗尽后再重新运行。但该方法也不能保证睡眠时间正好等于指定的延迟时间,只能尽量使睡眠时间接近指定的延迟时间。当指定的时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列。唯一的参数是延迟的相对时间,单位为jiffies,上列中将相应的任务推入可中断睡眠队列,睡眠s秒。因为任务处于可中断状态,所以如果任务收到信号将被唤醒。如果睡眠任务不想接受信号,可以将任务状态设置为TASK_UNINTERRUPTIBLE,然后睡眠。注意,在调用schedule_timeout()函数前必须首先将任务设置成上面两种状态之一,否则任务不会睡眠。

注意,由于schedule_timeout()函数需要调度程序,所以调用它的代码必须保证能够睡眠。简而言之,调用代码必须处于进程上下文中,并且不能持有锁。在这个过程中发生睡眠,而自旋锁是不允许睡眠的

所以呢,schedule_timeout(timeout)这句代码直到当前进程重新被调度了才会返回,然后继续往下执行代码,又加了一把锁,检查waiter.up是否为true,如果不是就一直循环

那什么时候waiter.up等于true呢,还记得前面加信号量锁的步骤吗,先down(&sema)加锁,up(&sema)解锁,就是这个up函数将waiter.up等于true的

image-20211022203732667

up函数

image-20211022203825612

首先看wait_list里边是否为空,如果是就count++,如果不是就调用__up函数

__up函数

image-20211022204047933

首先将wait_list将等待的进程拿出来,从waiter->list里边删掉,然后将up设为true,唤醒该进程,这样之前的for循环就结束掉了,锁就解开了

内核原子变量的说明和使用

由于信号量涉及到进程的调度导致开销很大,如果仅涉及int变量修改同步,可以使用原子变量,使其变成一个原子操作,这样就不会被其他进程打断

image-20211023122037224

image-20211023122049625

image-20211023122105329

image-20211023122114801

image-20211023122148057

atomic_dec_and_test(&can_open)会将can_open减一,然后检查是否等于0,如果等于0就返回true

atomic_inc(&can_open)会加一,让其变回原样

atomic的内核源码实现

1
2
3
4
5
6
7
#define ATOMIC_OPS(op, c_op, asm_op)					\
	ATOMIC_OP(op, c_op, asm_op)					\
	ATOMIC_OP_RETURN(op, c_op, asm_op)				\
	ATOMIC_FETCH_OP(op, c_op, asm_op)

ATOMIC_OPS(add, +=, add)
ATOMIC_OPS(sub, -=, sub)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*适用于SMP多核系统*/
#define ATOMIC_OP(op, c_op, asm_op)					\
static inline void atomic_##op(int i, atomic_t *v)			\
{									\
	unsigned long tmp;						\
	int result;							\
									\
	prefetchw(&v->counter);						\
	__asm__ __volatile__("@ atomic_" #op "\n"			\
"1:	ldrex	%0, [%3]\n"						\ /*ldrex是load指令,将v->counter的值放到result里面*/
"	" #asm_op "	%0, %0, %4\n"					\ /*#asm_op根据设置可以是add或sub,*/
"	strex	%1, %0, [%3]\n"						\/*store指令,将result的值放到v->counter,如果操作成功tmp为0*/
    /* ldrex指令在读取时会设置一个独占标记,防止其他执行单元去访问,然后strex存储完就会将独占标记清除*/
"	teq	%1, #0\n"						\  /*teq跟x86汇编test一样,测试%1和0是否相等*/
"	bne	1b"							\      /*根据teq的结果,如果不相等,则返回标号1处执行*/
	: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)		\ /*嵌入式汇编语法,第一行是输出寄存器*/
	: "r" (&v->counter), "Ir" (i)					\       /*输入寄存器*/
	: "cc");							\                  /*cc表示自动将变量放到相应的寄存器,标号排序,也就是result放到%0,tmp是%1,v->counter是%2,&v->counter是%3,i是%4*/
}
/*内嵌汇编手册http://blog.chinaunix.net/uid-20543672-id-3194385.html*/
1
2
3
4
5
6
7
8
9
10
/*适用于UP单核系统,就靠开中断关中断实现原子操作*/
#define ATOMIC_OP(op, c_op, asm_op)					\
static inline void atomic_##op(int i, atomic_t *v)			\
{									\
	unsigned long flags;						\
									\
	raw_local_irq_save(flags);					\
	v->counter c_op i;						\
	raw_local_irq_restore(flags);					\
}

spinlock的说明和使用方法

image-20211023152617915

image-20211023152643849

初始化spin lock

image-20211023152819416

image-20211023152746145

使用

image-20211023152758808

image-20211023152842383

spinlock内核源码(UP版)

image-20211023154033706

image-20211023154047938

image-20211023154135629

__ ARMEB__ 表示大端序

image-20211023154517712

初始化函数

略过check

image-20211023155131545

先不管debug的spinlock

image-20211023155216757

image-20211023155259123

image-20211023155316863

image-20211023155502781

就一个目的,把owner和next设置为0

spin_lock()函数

image-20211023160418997

image-20211023160515205

image-20211023160532879

image-20211023160544766

主要是将抢占关闭

image-20211023160846216

spinlock在SMP下的源码实现

image-20211023161150670

image-20211023161201645

关闭抢占,同步核心信息

image-20211023161400181

实际上就是第三个参数调用第一个参数,do_raw_spin_lock(lock)

image-20211023161606798

image-20211023161700383

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"
"	add	%1, %0, %4\n"  /*next+1*/
"	strex	%2, %1, [%3]\n"
"	teq	%2, #0\n"
"	bne	1b"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)  /*TICKET_SHIFT 16*/
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {  /*判断next是否等于owner,如果等于就获得了自旋锁*/
		wfe();/*等待指令*/
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);  /*更新获取owner的值,直到next==owner跳出循环获得自旋锁*/
	}

	smp_mb();
}

unlock就是onwer++

image-20211023163952863

spinlock、rw spinlock、seqlock、rcu机制比较

image-20211023172445430

image-20211023172605280

image-20211023172753375

当多个cpu进入临界区前,会涉及到共享内存lock的访问,当CPU0得到了lock,就会操作lock改变lock的值,当lock的值发生变化时,其它CPU的L1缓存中的lock值就会失效,我们都知道,cpu会不断的访问内存获取lock的值,那么当L1失效了,它们就得去访问L2,如果L2也失效,就会去主存找,这样就会造成性能的开销,主要还是CPU和内存之间性能的发展不平衡造成的。

image-20211023173820811

页框和伙伴算法以及slab机制

image-20211025141616928

一个4K的页对应一个struct page结构体,所以有多少个页就有多少个struct page结构体

image-20211025142735858

image-20211025143353884

image-20211025143404118

image-20211025144207168

image-20211025144413676

linux内核内存管理和分配方法概述

image-20211025152428599

image-20211025153350864

image-20211025153946090

image-20211025154953959

img

https://www.cnblogs.com/wangzahngjun/p/4977425.html

kmalloc()的内核源码实现

image-20211025213408968

首先从kzalloc看起

1
2
3
4
5
在include/linux/gfp.h中定义,GFP_KERNEL是内核内存分配时最常用的,无内存可用时可引起休眠.
#define GFP_KERNEL(__GFP_WAIT | __GFP_IO | __GFP_FS)
__GFP_WAIT : 缺内存页的时候可以睡眠;
__GFP_IO : 允许启动磁盘IO;
__GFP_FS : 允许启动文件系统IO。

image-20211025213522328

kzalloc调用了kmalloc将内存都设置为0

image-20211025213654057

__builtin_constant_p()判断括号里的值是否为预定义的常量,但是一般size都是我们自己定义的,所以先省略不看,直接看__kamlloc,在slab.c里

image-20211026112831015

然后是__do_kmalloc

image-20211026112900129

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
/**
 * __do_kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 * @caller: function caller for debug tracking of the caller
 */
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
					  unsigned long caller)
{
	struct kmem_cache *cachep;
	void *ret;

	if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
		return NULL;
	cachep = kmalloc_slab(size, flags);  /*通过size得到高速缓存结构体  kmalloc-xxx*/
	if (unlikely(ZERO_OR_NULL_PTR(cachep)))
		return cachep;
	ret = slab_alloc(cachep, flags, caller);  /*slab分配器分配对象*/

	kasan_kmalloc(cachep, ret, size, flags);
	trace_kmalloc(caller, ret,
		      size, cachep->size, flags);

	return ret;
}

kmalloc_slab

在slab_common.c里

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
/*
 * Find the kmem_cache structure that serves a given size of
 * allocation
 */
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
	int index;

	if (size <= 192) {
		if (!size)
			return ZERO_SIZE_PTR;

		index = size_index[size_index_elem(size)];
	} else {
		if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) {
			WARN_ON(1);
			return NULL;
		}
		index = fls(size - 1);
	}

#ifdef CONFIG_ZONE_DMA
	if (unlikely((flags & GFP_DMA)))
		return kmalloc_dma_caches[index];

#endif
	return kmalloc_caches[index];
}
1
2
3
4
static inline int size_index_elem(size_t bytes)
{
	return (bytes - 1) / 8; /*与8对齐,0-7就是0,8-15就是1,以此类推*/
}
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
/*
 * Conversion table for small slabs sizes / 8 to the index in the
 * kmalloc array. This is necessary for slabs < 192 since we have non power
 * of two cache sizes there. The size of larger slabs can be determined using
 * fls.
 */
static s8 size_index[24] = {
	3,	/* 8 */
	4,	/* 16 */
	5,	/* 24 */
	5,	/* 32 */
	6,	/* 40 */
	6,	/* 48 */
	6,	/* 56 */
	6,	/* 64 */
	1,	/* 72 */
	1,	/* 80 */
	1,	/* 88 */
	1,	/* 96 */
	7,	/* 104 */
	7,	/* 112 */
	7,	/* 120 */
	7,	/* 128 */
	2,	/* 136 */
	2,	/* 144 */
	2,	/* 152 */
	2,	/* 160 */
	2,	/* 168 */
	2,	/* 176 */
	2,	/* 184 */
	2	/* 192 */
};

__clz会返回32位数最高位有多少个0,fls()就会32减去__clz返回0的个数

image-20211025215219710

这个时候就要去了解kmalloc_caches[index]这个数组是什么了

1
2
3
4
struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1];/*KMALLOC_SHIFT_HIGH + 1=23*/

#define KMALLOC_SHIFT_HIGH	((MAX_ORDER + PAGE_SHIFT - 1) <= 25 ? \  /*MAX_ORDER是11 PAGE_SHIFT是12,1<<12就是4K嘛*/
				(MAX_ORDER + PAGE_SHIFT - 1) : 25)  /*最多只能25*/

看一下如何初始化kmalloc_caches[]

1
2
3
4
5
static void __init new_kmalloc_cache(int idx, unsigned long flags)
{
	kmalloc_caches[idx] = create_kmalloc_cache(kmalloc_info[idx].name,
					kmalloc_info[idx].size, flags);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
 * kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
 * kmalloc_index() supports up to 2^26=64MB, so the final entry of the table is
 * kmalloc-67108864.
 */
static struct {   
	const char *name;
	unsigned long size;
}const kmalloc_info[] __initconst = {
	{NULL,                      0},		{"kmalloc-96",             96},
	{"kmalloc-192",           192},		{"kmalloc-8",               8},
	{"kmalloc-16",             16},		{"kmalloc-32",             32},
	{"kmalloc-64",             64},		{"kmalloc-128",           128},
	{"kmalloc-256",           256},		{"kmalloc-512",           512},
	{"kmalloc-1024",         1024},		{"kmalloc-2048",         2048},
	{"kmalloc-4096",         4096},		{"kmalloc-8192",         8192},
	{"kmalloc-16384",       16384},		{"kmalloc-32768",       32768},
	{"kmalloc-65536",       65536},		{"kmalloc-131072",     131072},
	{"kmalloc-262144",     262144},		{"kmalloc-524288",     524288},
	{"kmalloc-1048576",   1048576},		{"kmalloc-2097152",   2097152},
	{"kmalloc-4194304",   4194304},		{"kmalloc-8388608",   8388608},
	{"kmalloc-16777216", 16777216},		{"kmalloc-33554432", 33554432},
	{"kmalloc-67108864", 67108864}
};  /*一共25个*/

初始化一个高速缓存,名字是name,大小是size

image-20211026104608460

然后看一下new_kmalloc_cache(int idx, unsigned long flags)参数idx和高速缓存的关系

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
/*
 * Create the kmalloc array. Some of the regular kmalloc arrays
 * may already have been created because they were needed to
 * enable allocations for slab creation.
 */
void __init create_kmalloc_caches(unsigned long flags)
{
	int i;

	for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) { /*KMALLOC_SHIFT_LOW=5,KMALLOC_SHIFT_HIGH=22*/
		if (!kmalloc_caches[i])   /*如果高速缓存没有被建立,就调用new_kmalloc_cache建立一个*/
			new_kmalloc_cache(i, flags);

		/*
		 * Caches that are not of the two-to-the-power-of size.
		 * These have to be created immediately after the
		 * earlier power of two caches
		 */
		if (KMALLOC_MIN_SIZE <= 32 && !kmalloc_caches[1] && i == 6)   /*下标1和2分别是96和192,他们都不是2的N次方,所以要单独创建*/
			new_kmalloc_cache(1, flags);
		if (KMALLOC_MIN_SIZE <= 64 && !kmalloc_caches[2] && i == 7)
			new_kmalloc_cache(2, flags);
	}

	/* Kmalloc array is now usable */
	slab_state = UP;

#ifdef CONFIG_ZONE_DMA
	for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
		struct kmem_cache *s = kmalloc_caches[i];

		if (s) {
			int size = kmalloc_size(i);
			char *n = kasprintf(GFP_NOWAIT,
				 "dma-kmalloc-%d", size);

			BUG_ON(!n);
			kmalloc_dma_caches[i] = create_kmalloc_cache(n,
				size, SLAB_CACHE_DMA | flags);
		}
	}
#endif
}

slab_alloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static __always_inline void *
slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
	unsigned long save_flags;
	void *objp; /*slab管理的对象指针*/

	flags &= gfp_allowed_mask;
	cachep = slab_pre_alloc_hook(cachep, flags);
	if (unlikely(!cachep))
		return NULL;

	cache_alloc_debugcheck_before(cachep, flags);
	local_irq_save(save_flags);/*关中断*/
	objp = __do_cache_alloc(cachep, flags);/*主要看这个*/
	local_irq_restore(save_flags);/*开中断*/
	objp = cache_alloc_debugcheck_after(cachep, flags, objp, caller);
	prefetchw(objp);

	if (unlikely(flags & __GFP_ZERO) && objp)
		memset(objp, 0, cachep->object_size);

	slab_post_alloc_hook(cachep, flags, 1, &objp);
	return objp;
}

image-20211026115423625

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
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
	void *objp;
	struct array_cache *ac;

	check_irq_off();

	ac = cpu_cache_get(cachep);  /*使ac指向cachep->array[smp_processor_id()]*/
	if (likely(ac->avail)) {  /*如果avail>0说明当前处理器的高速缓存内存阵列中有内存对象可以分配*/
		ac->touched = 1;     /*标记该CPU已被使用*/
		objp = ac->entry[--ac->avail];/*由于ac是记录着这次struct arrary_cache结构体存放地址,通过ac_entry()后,我们就得到下一紧接地址,这个地址可以看做是为本高速缓存内存的内存对象指针存放首地址,这里可以看出,我们是从最后一个对象开始分配的*/

		STATS_INC_ALLOCHIT(cachep);/*cachep的allochit成员加1,说明分配命中*/
		goto out;
	}

	STATS_INC_ALLOCMISS(cachep);/*如果没有可用的对象,将cachep->allocmiss加1,表示分配失败计数*/
	objp = cache_alloc_refill(cachep, flags);/*为高速缓存内存空间增加新的内存对象*/
	/*
	 * the 'ac' may be updated by cache_alloc_refill(),
	 * and kmemleak_erase() requires its correct value.
	 */
	ac = cpu_cache_get(cachep);

out:
	/*
	 * To avoid a false negative, if an object that is in one of the
	 * per-CPU caches is leaked, we need to make sure kmemleak doesn't
	 * treat the array pointers as a reference to the object.
	 */
	if (objp)
		kmemleak_erase(&ac->entry[ac->avail]);
	return objp;
}

然后是cache_alloc_refill,它实现如果没有对象可以用来分配时,就使用它来新增新的对象。

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
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
	int batchcount;
	struct kmem_cache_node *n;
	struct array_cache *ac, *shared;
	int node;
	void *list = NULL;
	struct page *page;

	check_irq_off();//检测IRQ中断是否关闭了。
	node = numa_mem_id(); //获取当前节点kmem_cache_node,每个NODE关联三条SLAB链表,分别表示full,partial,free的slab

	ac = cpu_cache_get(cachep);//使ac指向cachep->array[smp_processor_id](当前cpu对应的预留的高速缓存内存阵列)
	batchcount = ac->batchcount;//batchcount是每次批处理变量
	if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {//如果本缓存是没有使用过的同时批处理还大于最大的限制
		/*
		 * If there was little recent activity on this cache, then
		 * perform only a partial refill.  Otherwise we could generate
		 * refill bouncing.
		 */
		batchcount = BATCHREFILL_LIMIT;//使批处理等于最大限制值
	}
	n = get_node(cachep, node); // n = cachep->node[node]

	BUG_ON(ac->avail > 0 || !n);
	shared = READ_ONCE(n->shared);
	if (!n->free_objects && (!shared || !shared->avail))
		goto direct_grow;

	spin_lock(&n->list_lock);
	shared = READ_ONCE(n->shared);

	/* See if we can refill from the shared array */
	if (shared && transfer_objects(ac, shared, batchcount)) {
		shared->touched = 1;
		goto alloc_done;
	}

	while (batchcount > 0) {
		/* Get slab alloc is to come from. */
		page = get_first_slab(n, false);
		if (!page)
			goto must_grow;

		check_spinlock_acquired(cachep);

		batchcount = alloc_block(cachep, ac, page, batchcount);
		fixup_slab_list(cachep, n, page, &list);
	}

must_grow:
	n->free_objects -= ac->avail;
alloc_done:
	spin_unlock(&n->list_lock);
	fixup_objfreelist_debug(cachep, &list);

direct_grow:
	if (unlikely(!ac->avail)) {
		/* Check if we can use obj in pfmemalloc slab */
		if (sk_memalloc_socks()) {
			void *obj = cache_alloc_pfmemalloc(cachep, n, flags);

			if (obj)
				return obj;
		}

		page = cache_grow_begin(cachep, gfp_exact_node(flags), node);

		/*
		 * cache_grow_begin() can reenable interrupts,
		 * then ac could change.
		 */
		ac = cpu_cache_get(cachep);
		if (!ac->avail && page)
			alloc_block(cachep, ac, page, batchcount);
		cache_grow_end(cachep, page);

		if (!ac->avail)
			return NULL;
	}
	ac->touched = 1;

	return ac->entry[--ac->avail];
}
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
struct kmem_cache {
	struct array_cache *array[NR_CPUS];//per_cpu数据,记录了本地高速缓存的信息,也是用于跟踪最近释放的对象,每次分配和释放都要直接访问它。每个CPU都有它们自己的硬件高速缓存,当此CPU上释放对象时,可能这个对象很可能还在这个CPU的硬件高速缓存中,所以内核为每个CPU维护一个这样的链表,当需要新的对象时,会优先尝试从当前CPU的本地CPU空闲对象链表获取相应大小的对象。
	unsigned int batchcount;//本地高速缓存转入和转出的大批数据数量
	unsigned int limit;//本地高速缓存中空闲对象的最大数目
	unsigned int shared;/* 是否存在CPU共享高速缓存,CPU共享高速缓存指针保存在kmem_cache_node结构中 */

	unsigned int buffer_size;/*buffer的大小,就是对象的大小*/
	u32 reciprocal_buffer_size;/* size的倒数,加快计算 */

	unsigned int flags;		/* 高速缓存永久属性的标识,如果SLAB描述符放在外部(不放在SLAB中),则CFLAGS_OFF_SLAB置1 */
	unsigned int num;		/* # of objs per slab *//* 每个SLAB中对象的个数(在同一个高速缓存中slab中对象个数相同) */

	/* order of pgs per slab (2^n) */
	unsigned int gfporder;/* 一个单独SLAB中包含的连续页框数目的对数 */

	gfp_t gfpflags;       /*与伙伴系统交互时所提供的分配标识*/  

	size_t colour;			/* cache colouring range *//*slab中的着色*/
	unsigned int colour_off;	/* colour offset *///SLAB中基本对齐偏移,当新SLAB着色时,偏移量的值需要乘上这个基本对齐偏移量,理解就是1个偏移量等于多少个B大小的值
	struct kmem_cache *slabp_cache;/* 空闲对象链表放在外部时使用,其指向的SLAB高速缓存来存储空闲对象链表 */
	unsigned int slab_size;              //slab管理区的大小
	unsigned int dflags;		/* dynamic flags */

	/* 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;

	int obj_offset; /* 对象间的偏移 */
	int obj_size;
#endif /* CONFIG_DEBUG_SLAB */
    //用于组织该高速缓存中的slab
	struct kmem_list3 *nodelists[MAX_NUMNODES];/*最大的内存节点*/
			/*如果是NUMA架构就是kmem_cache_node*/
};

/* Size description struct for general caches. */
struct cache_sizes {
	size_t		 	cs_size;
	struct kmem_cache	*cs_cachep;
#ifdef CONFIG_ZONE_DMA
	struct kmem_cache	*cs_dmacachep;
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct kmem_list3 {
/*三个链表中存的是一个高速缓存slab*/
/*在这三个链表中存放的是cache*/
	struct list_head slabs_partial;	//包含空闲对象和已经分配对象的slab描述符
	struct list_head slabs_full;//只包含非空闲的slab描述符
	struct list_head slabs_free;//只包含空闲的slab描述符
	unsigned long free_objects;  /*高速缓存中空闲对象的个数(包括slabs_partial链表中和slabs_free链表中所有的空闲对象)*/
	unsigned int free_limit;   //空闲对象的上限
	unsigned int colour_next;	/* Per-node cache coloring *//*即将要着色的下一个*/
	spinlock_t list_lock;
	struct array_cache *shared;	/* shared per node */ /* 指向这个结点上所有CPU共享的一个本地高速缓存 */
	struct array_cache **alien;	/* on other nodes */
	unsigned long next_reap;	/* updated without locking *//* 两次缓存收缩时的间隔,降低次数,提高性能 */
	int free_touched;		/* updated without locking */
};
1
2
3
4
5
6
7
8
struct slab {
	struct list_head list;   //用于将slab连入keme_list3的链表
	unsigned long colouroff;   //该slab的着色偏移
	void *s_mem;		/* 指向slab中的第一个对象*/
	unsigned int inuse;	/* num of objs active in slab */已经分配出去的对象
	kmem_bufctl_t free;       //下一个空闲对象的下标
	unsigned short nodeid;   //节点标识符
};
1
2
3
4
5
6
7
8
9
10
11
12
struct array_cache {
	unsigned int avail;/*当前cpu上有多少个可用的对象*/
	unsigned int limit;/*per_cpu里面最大的对象的个数,当超过这个值时,将对象返回给伙伴系统*/
	unsigned int batchcount;/*一次转入和转出的对象数量*/
	unsigned int touched;/*标示本地cpu最近是否被使用*/
	spinlock_t lock;/*自旋锁*/
	void *entry[];	/*entry指向该缓存的对象数组,数组中保存的是对象的地址,这里就表示缓存块的地址。有一点就是从这里分配缓存对象不是从数组起始位置分配,而是从数组末尾分配,avail就表示分配的对象在entry数组中的下标
			 * Must have this definition in here for the proper
			 * alignment of array_cache. Also simplifies accessing
			 * the entries.
			 */
};

img

  • 每个CPU都有它们自己的硬件高速缓存,当此CPU上释放对象时,可能这个对象很可能还在这个CPU的硬件高速缓存中,所以内核为每个CPU维护一个这样的链表,当需要新的对象时,会优先尝试从当前CPU的本地CPU空闲对象链表获取相应大小的对象。
  • 减少锁的竞争,试想一下,假设多个CPU同时申请一个大小的slab,这时候如果没有本地CPU空闲对象链表,就会导致分配流程是互斥的,需要上锁,就导致分配效率低。

这个本地CPU空闲对象链表在系统初始化完成后是一个空的链表,只有释放对象时才会将对象加入这个链表。当然,链表对象个数也是有所限制,其最大值就是limit,链表数超过这个值时,会将batchcount个数的对象返回到所有CPU共享的空闲对象链表(也是这样一个结构)中。

所有CPU共享的空闲对象链表

  原理和本地CPU空闲对象链表一样,唯一的区别就是所有CPU都可以从这个链表中获取对象,一个常规的对象申请流程是这样的:系统首先会从本地CPU空闲对象链表中尝试获取一个对象用于分配;如果失败,则尝试来到所有CPU共享的空闲对象链表链表中尝试获取;如果还是失败,就会从SLAB中分配一个;这时如果还失败,kmem_cache会尝试从页框分配器中获取一组连续的页框建立一个新的SLAB,然后从新的SLAB中获取一个对象。对象释放过程也类似,首先会先将对象释放到本地CPU空闲对象链表中,如果本地CPU空闲对象链表中对象过多,kmem_cache会将本地CPU空闲对象链表中的batchcount个对象移动到所有CPU共享的空闲对象链表链表中,如果所有CPU共享的空闲对象链表链表的对象也太多了,kmem_cache也会把所有CPU共享的空闲对象链表链表中batchcount个数的对象移回它们自己所属的SLAB中,这时如果SLAB中空闲对象太多,kmem_cache会整理出一些空闲的SLAB,将这些SLAB所占用的页框释放回页框分配器中。

  这个所有CPU共享的空闲对象链表也不是肯定会有的,kmem_cache中有个shared字段如果为1,则这个kmem_cache有这个高速缓存,如果为0则没有。

一般一级缓存叫本地CPU缓存,别的核心访问不了,二级缓存就是共享的,核心之间可以共享数据,都可以访问

关于SLAB着色

  看名字很难理解,其实又很好理解,我们知道内存需要处理时要先放入CPU硬件高速缓存中,而CPU硬件高速缓存与内存的映射方式有多种。在同一个kmem_cache中所有SLAB都是相同大小,都是相同连续长度的页框组成,这样的话在不同SLAB中相同对象号对于页框的首地址的偏移量也相同,这样有很可能导致不同SLAB中相同对象号的对象放入CPU硬件高速缓存时会处于同一行,当我们交替操作这两个对象时,CPU的cache就会交替换入换出,效率就非常差。SLAB着色就是在同一个kmem_cache中对不同的SLAB添加一个偏移量,就让相同对象号的对象不会对齐,也就不会放入硬件高速缓存的同一行中,提高了效率。

进程虚拟地址空间管理机制

image-20211101190637743

image-20211101193109906

mm_users表示正在引用该地址空间的thread数目。是一个线程级的计数器。

mm_counter:表示这个地址空间被内核线程引用的次数+1

当 mm_user 和 mm_counter 都等于0的时候才会free这一块mm_struct,代表此时既没有用户级进程使用此地址空间,也没有内核级线程引用。

image-20211101193742602

image-20211101193805489

image-20211101193935930

image-20211101193950542

image-20211101200145673

image-20211101200638185

linux下同一个进程的不同线程之间如何共享虚拟地址空间?

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
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk) //tsk是子进程
{
	struct mm_struct *mm, *oldmm;
	int retval;

	tsk->min_flt = tsk->maj_flt = 0;
	tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
	tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

	tsk->mm = NULL;
	tsk->active_mm = NULL;

	/*
	 * Are we cloning a kernel thread?
	 *
	 * We need to steal a active VM for that..
	 */
	oldmm = current->mm;  //父进程的mm
	if (!oldmm)
		return 0;

	/* initialize the new vmacache entries */
	vmacache_flush(tsk);

	if (clone_flags & CLONE_VM) {  //如果flags设置了CLONE_VM位,就是把进程标记成了线程
		atomic_inc(&oldmm->mm_users);   //mm_users+1
		mm = oldmm;
		goto good_mm;
	}

	retval = -ENOMEM;
	mm = dup_mm(tsk);
	if (!mm)
		goto fail_nomem;

good_mm:
	tsk->mm = mm;  //把父进程的mm给了子进程(线程),共享同一虚拟地址空间
	tsk->active_mm = mm;
	return 0;

fail_nomem:
	return retval;
}

mmap()的内核实现

image-20211101202611744

image-20211101202954707

image-20211101203944336

image-20211101204208202

进程的用户栈和内核栈

image-20211101211239148

image-20211101205211856

image-20211101205631373

image-20211101205957228

image-20211101210015436

image-20211102150347190

进程上下文和中断上下文

在thread_info里边

image-20211102153038246

为了防止进程被softirq所抢占,关闭/禁止softirq的次数,比如每使用一次local_bh_disable(),softirq count高7个bits(bit 9到bit 15)的值就会加1,使用local_bh_enable()则会让softirq count高7个bits的的值减1。

irq_enter()用于标记hardirq的进入,此时hardirq count的值会加1。irq_exit()用于标记hardirq的退出,hardirq count的值会相应的减1

在中断上下文中,调度是关闭的,不会发生进程的切换,这属于一种隐式的禁止调度,而在代码中,也可以使用preempt_disable()来显示地关闭调度,关闭次数由第0到7个bits组成的preemption count(注意不是preempt count)来记录。每使用一次preempt_disable(),preemption count的值就会加1,使用preempt_enable()则会让preemption count的值减1。preemption count占8个bits,因此一共可以表示最多256层调度关闭的嵌套。

如何获取preempt_count

1
2
3
4
static __always_inline int preempt_count(void)
{
	return READ_ONCE(current_thread_info()->preempt_count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * how to get the current stack pointer in C
 */
register unsigned long current_stack_pointer asm ("sp");  //获取当前栈顶指针

/*
 * how to get the thread information struct from C
 */
static inline struct thread_info *current_thread_info(void) __attribute_const__;

static inline struct thread_info *current_thread_info(void)
{
	return (struct thread_info *)
		(current_stack_pointer & ~(THREAD_SIZE - 1));// ~(THREAD_SIZE - 1))为-8192,然后用sp指针对齐就能得到thread_info的起始地址
}

获取中断上下文conut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
 * Are we doing bottom half or hardware interrupt processing?
 *
 * in_irq()       - We're in (hard) IRQ context
 * in_softirq()   - We have BH disabled, or are processing softirqs
 * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
 * in_serving_softirq() - We're in softirq context
 * in_nmi()       - We're in NMI context
 * in_task()	  - We're in task context
 *
 * Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really
 *       should not be used in new code.
 */
#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())
#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi()		(preempt_count() & NMI_MASK)
#define in_task()		(!(preempt_count() & \
				   (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
1
2
3
4
5
6
7
8
9
#define hardirq_count()	(preempt_count() & HARDIRQ_MASK)
#define softirq_count()	(preempt_count() & SOFTIRQ_MASK)
#define irq_count()	(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
				 | NMI_MASK))  //不管是hardirq上下文还是softirq上下文,都属于我们俗称的中断上下文(interrupt context)。
#define in_task()  (!(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_OFFSET | NMI_MASK)))//判断进程上下文(process context)  
 *         PREEMPT_MASK:	0x000000ff
 *         SOFTIRQ_MASK:	0x0000ff00
 *         HARDIRQ_MASK:	0x000f0000
 *             NMI_MASK:	0x00100000

中断上下文、进程上下文环境的进入时机

硬件中断上下文

img

当外设产生一个中断以后,信号会传给CPU,然后CPU收到中断以后就会调用

image-20211103134657967

image-20211103134716686

image-20211103134903830

image-20211103135109129

image-20211103135126371

1
2
3
4
5
6
#define __irq_enter()					\
	do {						\
		account_irq_enter_time(current);	\
		preempt_count_add(HARDIRQ_OFFSET);	\  //HARDIRQ_OFFSET为16,正好是preempt_count的hardirq的位置
		trace_hardirq_enter();			\
	} while (0)

此时调用in_irq()就会返回一个大于0的数,表示就处于硬件中断上下文中,那什么时候退出呢

image-20211103135644687

image-20211103135740522

1
2
3
preempt_count_sub(HARDIRQ_OFFSET); //减一退出中断top half
	if (!in_interrupt() && local_softirq_pending())//查看pending里有没有softirq
		invoke_softirq();

中间的注册好的handler处于硬件中断上下文

软中断上下文

image-20211103140841038

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
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC s current task context is borrowed for the
	 * softirq. A softirq handled such as network RX might set PF_MEMALLOC
	 * again if the socket is related to swap
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending();  //获取当前CPU未决的软中断  谁触发就谁执行
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);  //给preempt_count的bit 8加一,表示有软中断handler在执行
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0); //将pending清零

	local_irq_enable(); //打开硬件中断,因为在软中断执行中是可以被硬中断打断的

	h = softirq_vec;

	while ((softirq_bit = ffs(pending))) {  //ffs是返回整型的最低位1的位置
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1; //获取对应软中断类型的结构体地址

		vec_nr = h - softirq_vec;
		prev_count = preempt_count();

		kstat_incr_softirqs_this_cpu(vec_nr);

		trace_softirq_entry(vec_nr);
		h->action(h);  //调用软中断注册的处理函数
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {
			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count);
		}
		h++;
		pending >>= softirq_bit;//继续找pending下一位是1的软中断,循环执行  所以按顺序执行,优先级是安排好的
	}
	//while循环结束,但是如果想网络传输这样的中断十分频繁,pending又有值了,就会往下继续判断
	rcu_bh_qs();
	local_irq_disable(); 

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&  //!need_resched()当前没有更高优先级的进程在处理
		    --max_restart)//如果时间超过了2ms或者次数超过了10次,就调用wakeup_softirqd(),交由ksoftirqd来处理。
			goto restart;

		wakeup_softirqd();  //唤醒ksoftirqd内核线程,ksoftirqd内核线程和普通进程优先级相同
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);//开软中断,减一
	WARN_ON_ONCE(in_interrupt());
	tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

image-20211103141346435

软中断特点以及softirq注册和触发源码分析

https://zhuanlan.zhihu.com/p/80371745

image-20211103195034005

中断的来源很多,所以softirq的种类也不少。内核的限制是不能超过32个,目前实际用到的有9个,其中两个用来实现tasklet(HI_SOFTIRQ和TASKLET_SOFTIRQ),两个用于网络的发送和接收操作(NET_TX_SOFTIRQ和NET_RX_SOFTIRQ),一个用于调度器(SCHED_SOFTIRQ),实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个HRTIMER_SOFTIRQ。

为了有效地管理不同的softirq中断源,Linux采用的是一个名为softirq_vec[]的数组,数组的大小由NR_SOFTIRQS 表示,这是在编译时就确定了的,不能在系统运行过程中动态添加。为了便于多核的并行处理,它还被设计成了per-cpu类型的数组,也就是每个processor对应一个softirq_vec[]数组。每个数组元素代表一种softirq的种类,而数组里存放的内容则是其各自对应的执行函数。

1
2
3
4
5
struct softirq_action
{
    void (*action)(struct softirq_action *);
};
struct softirq_action softirq_vec[NR_SOFTIRQS];

image-20211103210815880

软中断的触发

image-20211103210954899

image-20211103211030732

image-20211103211237666

image-20211103211306223

这里就是当前CPU的softiq_pending这个“中断寄存器”或上nr(软中断号?)

image-20211103211458545

image-20211103211616526

image-20211103211707673

意思是哪个CPU触发了软中断号就由哪个CPU去执行软中断程序

软中断处理函数do_softirq()的执行时机

image-20211108114256232

首先是ksoftirqd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,  //运行函数
	.thread_comm		= "ksoftirqd/%u",  //有多少个CPU就有多少个ksoftirqd
};

static __init int spawn_ksoftirqd(void)
{
	cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
				  takeover_tasklets);
	BUG_ON(smpboot_register_percpu_thread(&softirq_threads));//softirq线程结构体

	return 0;
}
early_initcall(spawn_ksoftirqd);//初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void run_ksoftirqd(unsigned int cpu)
{
	local_irq_disable();  //关硬中断
	if (local_softirq_pending()) { //上面有写  取CPU对应的pending 查看当前是否有软中断发生了
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();//软中断最终的处理函数
		local_irq_enable();//开硬件中断
		cond_resched_rcu_qs();
		return;
	}
	local_irq_enable();
}

##