《深入理解Linux内核(第三版)》笔记(六),第五章内核同步(1)

总述

目前来说,关于中断异常嵌套和内核抢占还有若干细节没有搞明白。
大概理解是,中断的嵌套是硬件更相关的事情,会正常的嵌套并串行调用,然后原路返回。

但是,“系统调用和异常”的处理情况则有所不同。
在《第三版》第五章中,“系统调用和普通异常”统称为异常。
首先,异常都是同步触发的,并且多数(缺页异常例外)是在用户态触发的(前提是内核代码没有bug)。
内核在进行异常处理时,有可能被中断抢占(当然中断有可能继续被中断抢占),这没有什么问题;问题出现在从中断返回时,如果允许“内核抢占”,那么从中断返回时,内核有可能会执行优先级更高的异常,而不一定是最初执行的那个异常。

内核抢占和 preempt_count

中断和异常是算在当前进程的时间片里的。获取当前的 preempt_count:

// include/linux/preempt.h/line: 23
#define preempt_count()	(current_thread_info()->preempt_count)

要开启下面这个预编译的宏定义,才能实现内核抢占:

// include/linux/preempt.h/line: 25
#ifdef CONFIG_PREEMPT

直接操作 preempt_count 的几个宏:

// include/linux/preempt.h/line: 29
#define preempt_disable()
...

间接操作 preempt_count 的几个宏:

// include/linux/smp.h/line: 141
#define get_cpu()		({ preempt_disable(); smp_processor_id(); })
...

解析下这个宏 preempt_enable():

#define preempt_enable()	\
do { \
	preempt_enable_no_resched(); \
	preempt_check_resched(); \
} while (0)

// 将这个宏展开
do {
	barrier();
	dec_preempt_count();
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
		preempt_schedule();
} while (0)

// kernel/sched.c/line: 2838
asmlinkage void __sched preempt_schedule(void)
{
	struct thread_info *ti = current_thread_info();
	...
	if (unlikely(ti->preempt_count || irqs_disabled()))
		return;
need_resched:
	add_preempt_count(PREEMPT_ACTIVE);
	...
	schedule();
	...
	sub_preempt_count(PREEMPT_ACTIVE);
	/* we could miss a preemption opportunity between schedule and now */
	barrier();
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
		goto need_resched;
}

还没看到进程的调度,preempt_enable() 会在什么时候被调用还不太了解。

同步原语

每 CPU 变量

// include/asm-generic/percpu.h/line: 11
#define DEFINE_PER_CPU(type, name) \
    __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name
...

暂时还没有看明白,这些宏是怎么实现的每个 CPU 单独使用一份数据。

原子操作

要点大概有:单条汇编指令完成操作,同时“锁定”内存总线。
锁总线,意味着,所有的 CPU 都不能在此刻访问这个变量了。
x86 中的操作码加 lock 前缀时,

// include/asm-i386/atomic.h/line: 24
typedef struct { volatile int counter; } atomic_t;

// include/asm-i386/atomic.h/line: 34
#define atomic_read(v)		((v)->counter)
...
// include/asm-i386/bitops.h/line: 20
#define LOCK_PREFIX "lock ; "

// include/asm-i386/bitops.h/line: 42
static inline void set_bit(int nr, volatile unsigned long * addr)
// 相关的操作函数都在这个文件里

从文件的路径上,可以推测,这些函数需要对硬件进行抽象。
大部分都是基于 x86 指令集,没必要分析太多。

优化屏障和内存屏障

平时写嵌入式代码时也会遇到一些奇怪的问题,比如说变量不按计划变化啦之类的。
我的解决方案一般都是把编译器的优化力度调到最小,代码大点慢点无所谓,出了那些奇怪的问题很麻烦。

现在知道了,原来有专门的技术手段来解决这样的问题啊。
关于这两个术语《第三版》有具体描述。

// include/asm-i386/system.h/line: 362
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
...	// 还有其他相关宏,也在相同的文件里 

自旋锁

大名鼎鼎的自旋锁。

// include/asm-i386/spinlock.h/line: 17
typedef struct {
	volatile unsigned int slock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
	unsigned int break_lock;
#endif
} spinlock_t;
// include/asm-i386/spinlock.h/line: 35
#define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 SPINLOCK_MAGIC_INIT }
#define spin_lock_init(x)	do { *(x) = SPIN_LOCK_UNLOCKED; } while(0)

spin_lock()

spin_lock() 相关代码及编译预处理配置等还是有点复杂的。

// include/linux/spinlock.h/line: 450
#define spin_lock(lock)		_spin_lock(lock)

基于是否支持 SMP,_spin_lock(lock) 有不同的实现。

// include/linux/spinlock.h/line: 40
#ifdef CONFIG_SMP
...
void __lockfunc _spin_lock(spinlock_t *lock)	__acquires(spinlock_t); 	// line: 49
...
#else	// line: 82
...
// line: 253
#define _spin_lock(lock)	\
do { \
	preempt_disable(); \
	_raw_spin_lock(lock); \
	__acquire(lock); \
} while(0)
...
#endif	// line: 439
// 如果支持 SMP 的话,_spin_lock() 是一个函数
// 如果不支持的话,_spin_lock() 是一个宏

《第三版》上只分析了支持 SMP 的情形。
在支持 SMP 的情形下,根据是否支持内核抢占,又分为两个分支:

// kernel/spinlock.c/line: 60
#ifndef CONFIG_PREEMPT
...
void __lockfunc _spin_lock(spinlock_t *lock)	// line: 150
{
	preempt_disable();
	_raw_spin_lock(lock);
}
...
#else /* CONFIG_PREEMPT: */		// line: 166
...
// line: 176
#define BUILD_LOCK_OPS(op, locktype)					\
void __lockfunc _##op##_lock(locktype##_t *lock)			\
...
BUILD_LOCK_OPS(spin, spinlock);		// line: 249
...
#endif /* CONFIG_PREEMPT */		// line: 253

// 如果不支持内核抢占,就使用 line:150 的函数

// 如果支持内核抢占的话,情况比较复杂:
// BUILD_LOCK_OPS 宏类似于 C++ 的模板,并且一个宏同时定义了三个函数
// line:249 的宏调用,生成了 _spin_lock() 等函数

把 BUILD_LOCK_OPS(spin, spinlock) 展开:

// 这段代码是宏展开的哦,源码里直接找不到哦
void __lockfunc _spin_lock(spinlock_t *lock)
{
	preempt_disable();
	for (;;) {
		if (likely(_raw_spin_trylock(lock)))
			break;
		preempt_enable();
		if (!(lock)->break_lock)
			(lock)->break_lock = 1;
		while (!spin_can_lock(lock) && (lock)->break_lock)
			cpu_relax();
		preempt_disable();
	}
}


// include/asm-i386/spinlock.h/line: 121
static inline int _raw_spin_trylock(spinlock_t *lock)
// 其中 _raw_spin_trylock() 是 x86 汇编实现,不必深究
// 大概实现是,用数值0去原子性的交换一个全局变量
// 如果这个全局变量之前是 1,那么就没有锁,这时已经把它替换成零了,别的进程再来访问的话拿到的就是0了;
// 然后返回全局变量之前的值,也就是1;这时候,这个进程就拿到这个锁了;好激动啊
// 如果这个全局变量之前就是0,那么就是锁上了,即使拿0去替换,它还是0;然后返回0,没有拿到锁

// 如果 _raw_spin_trylock() 返回 true,那么就得到锁了,直接 break
// 如果返回的是 false,那么就没有拿到锁,就需要打开内核抢占
// 然后再不停的去查询锁(注意,是查询锁而不是拿锁)
// 因为已经打开内核抢占了,所以这时候如果有优先级更高的进程就绪并且触发了调度的话,CPU 就去干别的了
// 就不在这里瞎等了
// 这里有个细节要注意,while 循环里,只是查询锁,而不是拿锁
// 为什么呢?因为 while 退出和 preempt_disable() 之间有空隙!!!
// 所以,实际的做法是,查询到锁空闲了,先禁内核抢占,在尝试加锁
// 如果运气差的话,for 循环重新开始的时候,锁已经被别人拿走了;不过没关系,再继续查询就好了呀

不支持内核抢占的函数《第三版》有解析。

读写自旋锁

读写自旋锁比普通自旋锁更精细一些,使用者需要更认真的对待。

// include/asm-i386/spinlock.h/line: 167
typedef struct {
	volatile unsigned int lock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
	unsigned int break_lock;
#endif
} rwlock_t;
// .lock 的第24位是标志,0~23位可以表示有多少个内核控制路径在读受保护数据

// include/asm-i386/spinlock.h/line: 185
#define RW_LOCK_UNLOCKED (rwlock_t) { RW_LOCK_BIAS RWLOCK_MAGIC_INIT }
#define rwlock_init(x)	do { *(x) = RW_LOCK_UNLOCKED; } while(0)

// include/asm-i386/rwlock.h/line: 20
#define RW_LOCK_BIAS		 0x01000000

read_lock()

同样也会按是否支持 SMP 和是否支持内核抢占,分为四个分支。
重点分析支持 SMP 且支持内核抢占的。

// kernel/spinlock.c/line: 176
#define BUILD_LOCK_OPS(op, locktype)					\
void __lockfunc _##op##_lock(locktype##_t *lock)			\
...

// kernel/spinlock.c/line: 250
BUILD_LOCK_OPS(read, rwlock);

把 BUILD_LOCK_OPS(read, rwlock) 的 _lock 部分展开:

// 这个是展开的,源码里没有
void __lockfunc _read_lock(rwlock_t *lock)
{
	preempt_disable();
	for (;;) {
		if (likely(_raw_read_trylock(lock)))
			break;
		preempt_enable();
		if (!(lock)->break_lock)
			(lock)->break_lock = 1;
		while (!read_can_lock(lock) && (lock)->break_lock)
			cpu_relax();
		preempt_disable();
	}
}
// 和 _spin_lock() 流程类似,差异化的地方在 _raw_read_trylock() 和 read_can_lock()
// include/asm-i386/spinlock.h/line: 231
static inline int _raw_read_trylock(rwlock_t *lock)
{
	atomic_t *count = (atomic_t *)lock;
	atomic_dec(count);
	if (atomic_read(count) >= 0)
		return 1;
	atomic_inc(count);
	return 0;
}
// 这个 atomic_dec() 需要讨论下
// 在执行这条语句之前,count 有可能是这么几种情况:
// 1)0x0100_0000,没有人读也没有人写,结果 >=0,可以读也可以写
// 2) 0x00ff_ffff~0x0000_0001,有人读了,结果 =0,
// 3) 0x0000_0000,有可能是在写,也有可能是可读的资源用光了

// 情况 1 和情况 2,可以顺利的拿到读权限
// 因为 sub 和 add 是成对出现的,所以只有减过的人才能加
// 所以只要这个进程不释放这个锁,它永远都会占有一个 1

// 对于情况3,有可能返回一个 -1,运气不好的话也有可能返回 -2、-3
// 这说明资源在占用,然后把数值加回去就好了

_raw_write_trylock()

// include/asm-i386/spinlock.h/line: 241
static inline int _raw_write_trylock(rwlock_t *lock)
{
	atomic_t *count = (atomic_t *)lock;
	if (atomic_sub_and_test(RW_LOCK_BIAS, count))
		return 1;
	atomic_add(RW_LOCK_BIAS, count);
	return 0;
}
// atomic_sub_and_test() 的特性是如果减完等于0,则返回1
// 就是说 *count == 0x0100_0000 的时候,才能拿到写锁

但是,可以想到,这会有一个小问题,如果 10000 个人在读,1 个人在写,那么这 1 个人要等那 10000 个人都不读了才能写;运气不好的话,它永远拿不到写权限的锁。

顺序锁

// include/linux/seqlock.h/line: 33
typedef struct {
	unsigned sequence;
	spinlock_t lock;
} seqlock_t;

// include/linux/seqlock.h/line: 42
#define SEQLOCK_UNLOCKED { 0, SPIN_LOCK_UNLOCKED }
#define seqlock_init(x)	do { *(x) = (seqlock_t) SEQLOCK_UNLOCKED; } while (0)
// 这几个函数都不是很复杂,不展开了

// include/linux/seqlock.h/line: 50
static inline void write_seqlock(seqlock_t *sl)
// include/linux/seqlock.h/line: 57
static inline void write_sequnlock(seqlock_t *sl) 
// include/linux/seqlock.h/line: 64
static inline int write_tryseqlock(seqlock_t *sl)
// 总的逻辑是,拿到写锁后,sequence 就被保护起来了;并且只有拿到锁后,才会对 sequence 进行操作
// 拿锁后加一次,放锁前加一次;所以拿到以后是奇数,释放以后是偶数

// include/linux/seqlock.h/line: 75
static inline unsigned read_seqbegin(const seqlock_t *sl)
// include/linux/seqlock.h/line: 91
static inline int read_seqretry(const seqlock_t *sl, unsigned iv)

读-拷贝-更新(RCU)

这也是个神奇的设计,具体描述见《第三版》。
这个有点复杂,没有实际的封装成套的函数供使用,需要使用者小心处理。
RCU 和其他模块的关联不是特别密切,《第三版》上描述主要用在网络层和虚拟文件系统中。
暂时不细究了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值