原子操做能够保证指令以原子的方式执行----执行过程不被打断。linux
针对整数的原子操做只能对atomic_t类型的数据进行处理。安全
atomic_t类型定义在文件<linux/types.h> 中数据结构
typedef struct { volatile int counter; } atomic_t;
使用原子整型操做须要的声明都在<asm/atomic.h>文件中。并发
定义一个atomic_t类型的数据方法很日常,还能够在定义时给它设定初值:ide
atomic_t v; /* 定义v */ atomic_t u = ATOMIC_INIT(0); /* 定义u并把它初始化为0 */ atomic_set(&v, 4); /* v = 4 */ atomic_add(2, &v); /* v = v + 2 = 6 */ atomic_inc(&v); /* v = v + 1 = 7 */
若是须要atomic_t转换成int,则须要atomic_read()来完成。函数
还能够用原子整数操做原子的执行一个操做并检查结果。优化
printk("%d\n", atomic_read(&v)); /* 会打印"7" */ int atomic_dec_and_test(atomic_t *v)
某种特定的体系结构上实现的全部操做能够在文件<asm/atomic.h>中找到atom
ATOMIC_INIT(int i); /* 在声明一个atomic_t变量时,将它初始化为i */ int atomic_read(atomic_t *v); /* 原子地读取整数变量v */ void atomic_set(atomic_t *v, int i); /* 原子地设置v值为i */ void atomic_add(int i, atomic_t *v) /* 原子地给v加i */ void atomic_sub(int i, atomic_t *v) /* 原子地从v减i */ void atomic_inc(atomic_t *v) /* 原子地给v加1 */ void atomic_dec(atomic_t *v) /* 原子地从v减1 */ int atomic_sub_and_test(int i, atomic_t *v) /* 原子地从v减i,若是结果等于0,返回真,不然返回假 */ int atomic_add_negative(int i, atomic_t *v) /* 原子地给v加i,若是结果是负数,返回真,不然返回假 */ int atomic_add_return(int i, atomic_t *v) /* 原子地给v加i,且返回结果 */ int atomic_sub_return(int i,atomic_t *v) /* 原子地从v减i,且返回结果 */ int atomic_inc_return(int i, atomic_t *v) /* 原子地给v加1,且返回结果 */ int atomic_dec_return(int i, atomic_t *v) /* 原子地给v减1,且返回结果 */ int atomic_dec_and_test(atomic_t *v) /* 原子地从v减1,若是结果等于0,返回真,不然返回假 */ int atomic_inc_and_test(atomic_t *v) /* 原子地给v加1,若是结果等于0,返回真,不然返回假 */
在编写代码时,能使用原子操做时,就尽可能不要使用复杂的加锁机制。spa
随着64位操做系统的普及,由于atomic_t便令没法在体系结构之间改变。因此atomic_t类型即使在64位下也是32位的,须要使用64位的原子变量操作系统
atomic64_t类型,其功能和32位原子操做无异,不一样的只有整型变量大小从32位变成了64位,atomic64_t类型实际上是对长整型的一个简单封装类。
ATOMIC64_INIT(int i); /* 在声明一个atomic_t变量时,将它初始化为i */ int atomic64_read(atomic_t *v); /* 原子地读取整数变量v */ void atomic64_set(atomic_t *v, int i); /* 原子地设置v值为i */ void atomic64_add(int i, atomic_t *v) /* 原子地给v加i */ void atomic64_sub(int i, atomic_t *v) /* 原子地从v减i */ void atomic64_inc(atomic_t *v) /* 原子地给v加1 */ void atomic64_dec(atomic_t *v) /* 原子地从v减1 */ int atomic64_sub_and_test(int i, atomic_t *v) /* 原子地从v减i,若是结果等于0,返回真,不然返回假 */ int atomic64_add_negative(int i, atomic_t *v) /* 原子地给v加i,若是结果是负数,返回真,不然返回假 */ int atomic64_add_return(int i, atomic_t *v) /* 原子地给v加i,且返回结果 */ int atomic64_sub_return(int i,atomic_t *v) /* 原子地从v减i,且返回结果 */ int atomic64_inc_return(int i, atomic_t *v) /* 原子地给v加1,且返回结果 */ int atomic64_dec_return(int i, atomic_t *v) /* 原子地给v减1,且返回结果 */ int atomic64_dec_and_test(atomic_t *v) /* 原子地从v减1,若是结果等于0,返回真,不然返回假 */ int atomic64_inc_and_test(atomic_t *v) /* 原子地给v加1,若是结果等于0,返回真,不然返回假 */
定义在文件<asm/bitops.h>中,例子:
unsigned long word = 0; set_bit(0, &word); /* 第0位被设置(原子地) */ set_bit(1, &work); /* 第1位被设置(原子地) */ printk("%u1\n", word); /* 打印3 */ clear_bit(1, &word); /* 清空第1位 */ change_bit(0, &word); /* 反转第0位的值,这里它被清空 */ /* 原子地设置第0位而且返回设置前的值(0) */ if(test_and_set_bit(0, &word) { /* 永远不为真 */ } /* 下面的语句是合法的,你能够把原子位指令与通常的C语言混在一块儿 */ word = 7;
标准原子位操做列表:
void set_bit(int nr, void *addr) /* 原子地设置addr所指对象的第nr位 */ void clear_bit(int nr, void *addr) /* 原子地清空addr所指对象的第nr位 */ void change_bit(int nr, void *addr) /* 原子地翻转addr所指对象的第nr位 */ int test_and_set_bit(int nr, void *addr) /* 原子地翻转addr所指对象的第nr位,并返回原先的值 */ int test_and_clear_bit(int nr, void *addr) /* 原子地清空addr所指对象的第nr位,并返回原先的值 */ int test_and_change_bit(int nr, void *addr) /* 原子地翻转addr所指对象的第nr位,并返回原先的值 */ int test_bit(int nr, void *addr) /* 原子地返回addr所指对象的第nr位 */
内核还提供了一组与上述操做对应的非原子位函数,非原子位函数与原子位函数的操做彻底相同。
可是前者不保证原子性,其名字前缀多两个下划线。 好比test_bit()和__test_bit()
内核还提供了两个例程用来指定的地址开始搜索第一个被设置的位。
int find_first_bit(unsigned long *addr, unsigned int size) int find_first_zero_bit(unsigned long *addr, unsgined int size) /* 第一参数是一个指针,第二个参数是要搜索的总位数 */
Linux内核中最多见的锁是自旋锁。自旋锁最多只能被一个可执行线程持有。
自旋锁的要点:被争用的自旋锁使得请求它的线程在等待锁从新可用时自选(特别浪费处理器时间),因此自旋锁不该该被长时间持有。
这也是自旋锁的初衷:在短期内进行轻量级加锁。持有自旋锁的时间最好小于完成两次上下文切换的耗时。
相关的体系结构代码在文件<asm/spinlock.h>中,实际须要用到的接口定义在文件<linux/spinlock.h>中。自旋锁的基本使用形式以下:
DEFINE_SPINLOCK(mr_lock); spin_lock(&mr_lock); /* 临界区 */ spin_unlock(&mr_lock);
在中断处理程序中使用自旋锁时,必定要在获取锁以前,首先禁止本地中断。不然,中断处理程序就会打断持有锁的内核代码,争用已经被持有的锁。这样就会自旋,变成双重请求死锁。
警告:自旋锁是不可递归的
内核进制中断同时请求锁的接口,以下:
DEFINE_SPINLOCK(mr_lock); unsigned long flags; /* 临界区 */ spin_lock_irqsave(&mr_lock, flags); spin_unlock_irqrestore(&mr_lock, flags);
spin_lock_irqsave()保存中断的当前状态,并禁止本地中断,而后再去获取指定的锁。反过来spin_unlock_irqrestore()对指定的锁解锁,而后让中断恢复到加锁前的状态。
因此计时中断最初是被禁止的,代码也不会错误地激活他们,相反,会继续让他们禁止。
锁的大原则:针对代码加锁会使得程序难以理解,而且容易引起竞争条件,正确的作法应该是对数据而不是代码加锁。
若是你能肯定中断在加锁前是激活的,那就不须要在解锁后恢复中断之前的状态了。 你能够无条件地在解锁时激活中断。
这时使用spin_lock_irq()和spin_unlock_irq()会更好点,
DEFINE_SPINLOCK(mr_lock); spin_lock_irq(&mr_lock); /* 关键节 */ spin_unlock_irq(&mr_lock);
可使用spin_lock_init()方法初始化动态建立的自旋锁,spin_try_lock()试图得到某个特定自旋锁,若是已被争用,那么该防范会马上返回一个非0值,而不会等待自旋锁释放。
若是成功得到了这个锁,返回0。同理spin_is_lock(),用于检查特定锁是否被占用。占用非0,不然返回0
spin_lock() /* 获取指定的自旋锁 */ spin_lock_irq() /* 进制本地中断并获取指定的锁 */ spin_lock_irqsave() /* 保存本地中断的当前状态,禁止本地中断,并获取指定的锁 */ spin_unlock() /* 释放指定的锁 */ spin_unlock_irqrestore() /* 释放指定的锁,并让本地中断恢复到之前状态 */ spin_lock_init() /* 动态初始化指定的spinlock_t */ spin_trylock() /* 试图获取指定的锁,若是未获取,则返回非0 */ spin_is_locked() /* 若是指定的锁当前正在被获取,则返回非0,不然返回0 */
与下半部配合使用时,必须当心使用锁机制。函数spin_lock_bh()用于获取指定锁,同时会禁止全部下半部的执行。
相应的spin_unlock_bh()函数执行相反的操做
有时锁的用途能够明确的分为读取和写入两个场景。
当对某个数据结构的操做能够划分为 读/写或者消费者/生产者两种类别时,相似读/写锁这样的机制就颇有帮助了。
/* 读/写自旋锁的初始化 */ DEFINE_RWLOCK(mr_rwlock); read_lock(&mr_rwlock); /* 临界区(只读)··· */ read_unlock(&mr_rwlock); write_lock(&mr_rwlock); /* 临界区(读写)··· */ write_unlock(&mr_rwlock); /* 注意:不能把一个读锁升级为写锁 */ read_lock(&mr_rwlock); write_lock(&mr_rwlock);
下面列出了针对读-写自旋锁的全部操做:
read_lock() /* 得到指定的读锁 */ read_lock_irq() /* 禁止本地中断并得到指定读锁 */ read_lock_irqsave() /* 存储本地中断的当前状态,禁止本地中断并得到指定读锁 */ read_unlock() /* 释放指定的读锁 */ read_unlock_irqrestore() /* 释放指定的读锁并将本地中断恢复到指定的前状态 */ write_lock() /* 得到指定的写锁 */ write_lock_irq() /* 禁止本地中断并得到指定写锁 */ write_lock_irqsave() /* 储存本地中断的当前状态,进制本地中断并得到指定写锁 */ write_unlock() /* 释放指定的写锁 */ write_unlock_irq() /* 释放指定的写锁并激活本地中断 */ write_unlock_irqrestore() /* 释放指定的写锁并将本地中断恢复到指定的前状态 */ write_trylock() /* 试图得到指定的写锁,若是写锁不可用,返回非0值 */ rwlock_init() /* 初始化指定的rwlock_t */
读写锁会照顾读比照顾写要多一点,因此大量读者一定会使挂起的写着处于饥饿状态
Linux信号量是一种睡眠锁,若是有一个任务试图得到一个不可用的信号量时,信号量会将其推动一个等待队列,让后让其睡眠。
这时处理器能重获自由,从而去执行其余代码。当持有信号量可用后,处于等待队列中的那个任务将被唤醒。
信号量比自旋锁提供了更好的处理器利用率,由于没有把时间花费在忙等待上,可是,信号量比自旋锁有更大的开销。
若是须要在自旋锁和信号量中作选择,应该根据锁被持有的时间长短作判断。
信号量同时容许的持有者数量能够在声明信号量时指定。这个值称为使用者数量(usage count)或简单地叫数量(count)。
一般状况下,信号量和自旋锁同样,一个时刻容许一个持有者。这样的的信号量被称为二值信号量或者称为互斥信号量。
若是初始化时为大于1的非0值,信号量被称为计数信号量,容许一个时刻至多有count个锁持有者。
信号量有两个down()和up()操做,down操做经过信号量计数减1来请求得到一个信号量,若是结果是0或大于0,得到信号量。不然就进入等待队列。
up()操做用来释放信号量,也被称做提高,由于会增长信号量计数值。若是在该信号量上的等待队列不为空,那么等待队列的任务会被唤醒同时得到信号量。
在头文件<asm/semaphore.h>中,能够经过静态地声明信号量
struct semaphore name; sema_init(&name, count); /* name是信号量变量名,count是信号量的使用数量 */ static DECLARE_MUTEX(name); /* 更为普通的信号量的建立 */ sema_inti(sem, count); /* 动态建立,sem是指针,count是信号量的使用者数量 */ init_MUTEX(sem);
函数down_interruptible()试图获取指定的信号量,若是信号量不可用,它将把调用进程置成TASK_INTERRUPTIBLE状态----进入睡眠。
sema_init(struct semaphore *, int) /* 以指定的计数值初始化动态建立的信号量 */ init_MUTEX(struct semaphore *) /* 以计数值1初始化动态建立的信号量 */ init_MUTEX_LOCKED(struct semaphore *) /* 以计数值0初始化动态建立的信号量,初始为加锁状态 */ down_interruptible(struct semaphore *) /* 以试图得到指定的信号量,若是信号量已被争用, 则进入不可中断睡眠状态 */ down(struct semaphore *) /* 以试图得到指定的信号量,若是信号量已被争用, 则进入不可中断睡眠状态 */ down_trylock(struct semaphore *) /* 以试图得到指定的信号量,若是信号量已被争用, 则当即返回非0值 */ up(struct semaphore *) /* 以释放指定的信号量,若是睡眠队列不空, 则唤醒其中一个任务 */
定义在文件<linux/rwsem.h>中,经过如下语句能够建立静态声明
static DECLARE_RWSEM(name); /* 静态建立,name是新信号量名 */ init_rwsem(struct rw_semaphore *sem) /* 动态建立, */
全部的读写信号量都是互斥信号量,在引用计数等于1,它们只对写着互斥,不对读者。例如:
static DECLARE_RWSEM(mr_rwsem); /* 试图获取信号量用于读... */ down_read(&mr_rwsem); /* 临界区(只读)... */ /* 释放信号量 */ up_read(&mr_rwsem); /* ... */ /* 试图获取信号量用于写 ... */ down_write(&mr_rwsem); /* 临界区(读和写) ... */ /* 释放信号量 */ up_write(&mr_sem);
多数时候信号量只使用计数1,信号量适合用于哪些较复杂的、未明状况下的互斥访问。
为了找到要给更简单睡眠锁,内核引入了互斥体(mutex),指的是能够睡眠的强制互斥锁。
DEFINE_MUTEX(name); /* 静态定义mutex */ mutex_init(&mutex); /* 动态初始化mutex */ /* 互斥锁锁定和解锁并不难 */ mutex_lock(&mutex); /* 临界区 */ mutex_unlock(&mutex);
下面是基本的mutex操做列表:
mutex_lock(struct mutex *) /* 为指定的mutex上锁,若是锁不可用则睡眠 */ mutex_unlock(struct mutex *) /* 为指定的mutex解锁 */ mutex_trylock(struct mutex *) /* 试图获取指定的mutex,若是成功则返回1; 不然锁被获取,返回值是0 */ mutex_is_locked(struct mutex *) /* 若是锁已被争用,则返回1;不然返回0 */
mutex更要个的要求了:
打开内核配置选项CONFIG_DEBUG_MUTEXES后,就会有多种检测来确保这些约束得以遵照。
在信号量和互斥体的选中中,首选mutex。除非mutex的某个约束妨碍你使用,不然相比信号量要有限使用mutex。
在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体
需求 | 建议的加锁方法 |
低开销加锁 | 优先自旋锁 |
短时间锁定 | 优先自旋锁 |
长期加锁 | 优先互斥体 |
中断上下文中加锁 | 使用自旋锁 |
持有锁须要睡眠 | 使用互斥体 |
若是内核中一个任务须要发出信号通知另外一个任务发生了某个特定事件,利用完成变量。
完成变量由结构completion表示,定义在<linux/completion.h>中。
DECLARE_COMPLETION(mr_comp); /* 静态建立 */ init_completion(&mr_comp); /* 动态建立 */
在一个指定的完成变量上,须要等待的任务调用wait_for_completion()来等待特定事件。
init_completions(struct completion *) /* 初始化指定的动态建立的完成变量 */ wait_for_completions(struct completion *) /* 等待指定的完成变量接收信号 */ complete(struct completion *) /* 发信号唤醒任何等待任务 */
使用完成变量的例子能够参考kernel/sched.c和kernel/fork.c。
完成变量的一般用法是:将完成变量做为数据结构中的一项动态建立,而完成数据结构初始化工做的内核代码将调用wait_for_completion()进行等待。
初始化完成后,初始化函数调用completion()唤醒在等待的内核任务。
BKL(大内核锁)是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过分到细粒度加锁机制。
BKL的使用方式和自旋锁相似,函数lock_kernel()请求锁,unlock_kernel()释放锁。
函数kernel_locker()检测锁当前是否被持有,有返回非0值,不然返回0。头文件在<linux/smp_lock.h>中。
lock_kernel(); /* 临界区,对全部其余的BLK用户进行同步······ * 注意,你能够安全地在此睡眠,锁会悄无声息的被释放 * 当你的任务被从新调度时,锁又会被悄无声息地获取 * 这意味着你不会处于死锁状态,可是,若是你须要锁保护这里的数据 * 你仍是不须要睡眠 */ unlock_kernel();
BLK函数列表
lock_kernel() /* 得到BKL */ unlock_kernel() /* 释放BKL */ kernel_locked() /* 若是锁被持有返回非0值,不然返回0 */
2.6版本引入的新型锁,简称seq锁。实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会获得一个锁,而且序列值会增长。
seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_sq_lock); /* 定义一个seq锁 */ write_seqlock(&mr_seq_lock); /* 写锁被获取 ... */ write_sequnlock(&mr_seq_lock); /* 和自旋锁相似,不一样状况发生在读时,而且与自旋锁有很大不一样 */ unsigned long seq; do { seq = read_seqbegin(&mr_seq_lock); /* 读这里的数据 ... */ } while(read_seqretry(&mr_seq_lock, seq);
seq锁有助于提供一种很是轻量级和具备可扩展性的外观。可是seq锁对写着更有利,只要没有其余写者,写锁老是可以被成功得到。读者不会影响写锁。
以下状况用seq锁比较理想:
jiffies使用的就是seq锁。
u64 get_jiffies_64(void) { unsigned long seq; u64 ret; do { seq = read_seqbegin(&xtime_lock); ret = jiffies_64; } while (read_seqretry(&xtime_lock, seq)); return ret; }
若是想要进一步了解jiffies和内核时间管理,在kernel/timer.c与kernel/time/tick-common.c文件
因为内核是抢占性的,内核中的进程在任什么时候刻均可能停下来以便另外一个具备更高优先权的进程运行。
若是一个自旋锁被持有,内核便不能进行抢占。由于内核抢占和SMP面对相同的并发问题,而且内核已是SMP安全的,因此,这种简单的变化使得内核也是抢占安全的。
有些时候咱们不须要自旋锁,可是任然须要关闭内核抢占。为了解决这个问题,能够经过preemt_disable()禁止内核抢占。每次调用都必须有一个相应的preemt_enable()调用。
当最后一次preemt_enable()被调用后,内核抢占才从新启用。例如:
preempt_disable() /* 抢占被禁止 ... */ preempt_enable();
抢占计数存放着被持有锁的数量和preempt_disable()的调用次数,若是计数是0,那么内核能够进行抢占。若是为1或更大的值,那么,内核就不会进行抢占。
他是一种对原子操做和睡眠颇有效的调试方法。
preempt_disable() /* 增长抢占计数值,从而进制内核抢占 */ preempt_enable() /* 减小抢占计数,并当该值降为0时检查和执行被挂起的需调度的任务 */ preempt_enable_no_resched() /* 激活内核抢占但再也不检查任何被挂起的需调度任务 */ preempt_count() /* 返回抢占计数 */
为了更简洁的方法解决每一个处理器上的数据访问问题,能够经过get_cpu()得到处理器编号。这个函数在返回当前处理器号前首先会关闭内核抢占。
int cpu; /* 禁止内核抢占,并将CPU设置为当前处理器 */ cpu = get_cpu(); /* 对每一个处理器的数据进行操做 */ /* 再给与内核抢占性,"CPU"可改变故它再也不有效 */ put_cpu();
当处理多处理器之间或硬件设备之间的同步问题时,有时须要在你的程序代码中以指定的顺序发出读内存(读入)和写内存(存储)指令。
编译器和处理器为了提升效率,可能对读和写从新排序,这样无疑使问题复杂化了。
一样也能够指示编译器不要对给定点周围的指令序列进行从新排序,这些确保顺序的指令称做屏障。
rmb()方法提供了一个"读"内存屏障,它确保跨越rmb()的载入动做不会发生重排序。也就是说在rmb()以前的载入操做不会被从新排序在该调用以后,一样的,在rmb()以后的载入操做不会被重排在该调用以前
wmb()方法提供了一个"写"内存屏障,和rmb()相似,区别是它针对存储而非载入
mb()方法及提供了读屏障也提供写屏障。
read_barrier_depends()是rmb()的变种,它提供了读屏障,可是仅仅针对后续读操做所依靠的哪些载入。
barrier()方法能够防止编译器跨屏障对载入或存储操做进行优化。
rmb() /* 阻止跨越屏障的载入动做发生重排序 */ read_barrier_depends() /* 组织跨越屏障的具备数据依赖关系的载入动做重排序 */ wmb() /* 组织跨越屏障的存储动做发生重排序 */ mb() /* 阻止跨越屏障的载入和存储动做从新排序 */ smp_rmb() /* 在SMP上提供rmb()功能,在UP上提供barrier()功能 */ smp_read_barrier_depends() /* 在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能 */ smp_wmb() /* 在SMP上提供wmb()功能,在UP上提供barrier()功能 */ smp_mb() /* 在SMP上提供mb()功能,在UP上提供barrier()功能 */ barrier() /* 组织编译器跨屏障对载入或存储操做进行优化 */