原子操作:可以保证指令以原子的方式执行,即执行过程不被打断。
整数的原子操作只针对 atomic_t 类型。因为:
atomic_t 类型定义在 include/linux/types.h:
typedef struct {
volatile int counter;
} atomic_t;
在 Linux 上 atomic_t 整数类型都是 32 位,其中数据位为高 24 位,低 8 位嵌入了一个锁,因为 SPARC 体系结构对原子操作缺乏指令级支持,所以只能利用该锁来避免对原子类型数据的并发访问。因此在 SPARC 机器上只能用 24 位,在其它机器上可以用 32 位。
(貌似已经在 SPARC 机器上只能使用 24 位的问题已经被解决了)
原子操作在 arch/alpha/include/asm/atomic.h 中:
原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。若某个函数本就具备原子性,那么通常会被定义为一个宏。
在编写代码时,尽量使用原子操作,而不是使用复杂的加锁机制。相对于锁机制,原子操作可以给系统带来较小的开销,对高速缓存行的影响也小。
因为移植性的问题,atomic_t 变量无法在体系结构之间改变。因此 atmoic_t 类型即便在 64 位下也是 32 位的,要使用 64 位的,则要使用 atomic64_t 类型,其功能与 32 位无异。
#ifdef CONFIG_64BIT // 判断系统是不是 64 位的
typedef struct {
volatile long counter;
} atomic64_t;
#endif
开发者应该使用 32 位的 atomic_t 类型,因为为了可以在各体系结构之间可以移植代码。
路径:arch/alpha/include/asm/bitops.h
非原子位函数在其原子位函数名字前多了两条下划线,例如 test_bit() 的非原子位函数为 __test_bit()。
Linux 内中最常见的锁。
最多只能被一个线程持有。
若一个线程试图得到一个已被持有的锁,那么该线程就会一直循环等待该锁被释放为止。
在任意时间,自旋锁都可以防止多于一个线程同时进入临界区。
自旋锁因为会循环等待锁,因此特别浪费处理器时间,所有自旋锁不该被长时间占有。
也可以让请求线程休眠,直到锁重新可用为止。(这会带来一定的开销,如两次上下文切换)
自旋锁的设计初衷:在短期内进行轻量级加锁。
路径:arch/alpha/include/asm/spinlock.h
也可能是这个:include/linux/spinlock.h
自旋锁不可递归:你试图得到一个你持有的锁,必须自旋等待,可是你处于自旋等待中,因此无法释放锁,导致给自己锁死了。
根据此情况,可知在中断使用自旋锁时,一定要先关闭中断。
CONFIG_DEBUG_SPINLOCK 配置选择为使用自旋锁的代码加入了许多调试检测的手段。
从信号量的睡眠特性得出的一些结论:
信号量支持的两个原子操作:
路径:include/linux/semaphore.h
位于:include/linux/rwsem.h
down_read_trylock() 和 down_write_trylock() 方法,若成功获得了信号量锁,返回非0,若信号量锁被争用,则返回0,这与普通信号量情况相反,要小心。
信号量是内核中唯一允许睡眠的锁。
信号量适用于较复杂、未明情况下的互斥访问。简单的锁定,使用信号量并不方便,且信号量调试也不方便。
互斥体是比信号量更简单允许休眠的锁。
互斥体指的是任何可以睡眠的强制互斥锁,如计数是 1 的信号量。
互斥体是一种互斥信号。
mutex 是简化版的信号量,因为不再需要管理任何使用计数。
开启 mutex 的调试功能:CONFIG_DEBUG_MUTEXES
路径:include\linux\mutex.h
互斥体和信号量很相似,应该优先使用 mutex,不到万不得已就别用信号量,一般只有很底层的代码才需要信号量。因此首选 mutex,若发现不能满足其约束条件,且没有别的选择时,再考虑选择信号量。
在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。当一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
完成变量仅仅只是代替信号量的一个简单方案。例如,当子进程执行或退出时,vfork() 系统调用使用完成变量唤醒父进程。
路径:include\linux\completion.h
BKL 的特性:
路径:include/linux/seqlock.h
seq 锁使用场景:
重新排序的例子:
a = 1;
b = 2;
重新排序导致可能会在 a 中存放新的值之前就在 b 中存放新值。
编译器和处理器都看不出两者之间的关系。编译器会在编译时按这种顺序编译,这种顺序会是静态的,编译的目标代码就只把 a 放在 b 之前。但是处理器会重新动态排序,因为处理器在执行指令期间,会在取指令和分配时,把表面看似无关的指令按自认为最好的顺序排列。大多数情况下,这样的排序是最佳的。
处理器和编译器绝不会重新排序的情况:
a = 1;
b = a;
书中后面这小段我没懂,此处略…