目录
总述
目前来说,关于中断异常嵌套和内核抢占还有若干细节没有搞明白。
大概理解是,中断的嵌套是硬件更相关的事情,会正常的嵌套并串行调用,然后原路返回。
但是,“系统调用和异常”的处理情况则有所不同。
在《第三版》第五章中,“系统调用和普通异常”统称为异常。
首先,异常都是同步触发的,并且多数(缺页异常例外)是在用户态触发的(前提是内核代码没有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 和其他模块的关联不是特别密切,《第三版》上描述主要用在网络层和虚拟文件系统中。
暂时不细究了。