在Linux内核中,主要的静态发生于以下几种情况:
1、对称多处理器(SMP)的多个CPU:
多个CPU共同使用系统总线,可访问共同点的外设和存储器。
2、单CPU内核进程与抢占它的进程:
一个进程的执行可被另一高优先级进程打断。
3、中断(硬中断、软中断、Tasklet,底半部)与进程之间:
中断可以打断正在执行的进程,若访问该进程正在访问的空间,将引发竞态。
上述并发的发生除了SMP是真正的并行以外,其他的都是“宏观并行,微观串行”的,但其引发的实质问题与SMP相似。
访问共享字段的代码区域成为临界区(critical sections)
中断屏蔽:
使用方法
local_irq_disable() //屏蔽中断 ... critical section //临界区 ... local_irq_enable() //开中断
local_irq_disable/enable只能禁止/使能本CPU内的中断,不能解决SMP多CPU引发的竞态,故不推荐使用,其适宜于自旋锁联合使用。
其原理是让cpu本身不相应中断,比如arm处理器,其底层的实现是屏蔽cpsr的i位:static inline void arch_local_irq_disable(void)
{
asm volatile(
cpsid i @arch_local_irq_disable
:
:"memery," "cc";
}
由于linux的异步IO操作,进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屁股比中断是很危险的,这有可能造成数据丢失乃至系统崩溃等后果。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快执行完临界区的代码。
原子操作:原子操作可以保证对一个整形数据的修改是排他性的。linux内核提供了一系列的函数来实现内核的原子操作,这些函数又分为两类,分别针对位和整形变量进行原子操作。
使用方法
//设置原子变量的值 static __inline__ void atomic_set(atomic_t *v, int i); //输入原子指针,将原子变量置为i atomic_t v = ATOMIC_INIT(i); //直接将v的原子变量初始化为i //原子变量的基本操作 atomic_read(atmic_t *v); //读值 void atomic_add/sub(int i, atomic_t *v); //加/减i操作 void atomic_inc/dec(atomic_t *v); //自加/自减操作 int atomic_inc/dec_test(atomic_t *v); //自加/自减后测试,为0返回ture,否则返回false int atomic_sub_and_test(int i, atomic_t *v); //减i后测试,为0返回ture,否则返回false int atomic_add/sub_return(int i, atomic_t *v); //加/减i后return int atomic_inc/dec_return(atomic_t *v); //自加/自减后return
一个操作例程:使设备只能被一个进程打开
static atomic_t xxx_atomic = ATOMIC_INIT(1); //初始化 static int xxx_open(struct inode *inode, struct file *filp) { ... if(!atomic_dec_and_test(&xxx_atomic)){ //首次调用xxx_atomic时,其为1,则test后返回true atomic_inc(&xxx_availavle); //再次调用是执行if(!false){}的内容 return - EBUSY; } ... return 0; } static int xxx_release(struct inode *inode, struct file *filp) { atomic_inc(&xxx_atomic); //清楚调用,使其变回初值 return 0; } //这里只是举例,并非一定要先dec_test然后inc, 只要前后的操作,不互相冲突, 实现再次调用时返回忙,而释放时使原子变量回到调用前的值即可
自旋锁(spin lock):
自旋锁是一种典型的对临界资源进行互斥访问的手段,保证临界区不受别的cpu或者本cpu内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能会受到中断的影响。为了防止这种影响,就需要用到自旋锁的衍生。
为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前,其他单元无法访问这个内存变量。如果测试结果表明锁已空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个测试,即所谓的自旋,通俗的说就是原地打转。
自旋锁有四种操作:
//定义自旋锁 spinlock_t lock; //初始化自旋锁 spin_lock_init(lock); //获得自旋锁 spin_lock(lock); //若获得则返回,否则自旋 tryspin_lock(lock); //若获得返回真,否则返回假 //释放自旋锁 spin_unlock(lock); //
1、自旋锁是忙等待锁,当等待时间较长的时候将降低系统系能;
2、自旋锁可能导致系统死锁(锁陷阱);
3、自旋锁锁定器件不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞赛,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核崩溃。
接下来深入研究一下自旋锁的工作过程:
//此处的spin_lock是针对配置了SMP的内核 static inline void spin_lock(spinlock_t *lock) { raw_spin_lock(&lock->rlock); } #define raw_spin_lock(lock) _raw_spin_lock(lock) void __lockfunc _raw_spin_lock(raw_spinlock_t *lock) { __raw_spin_lock(lock); } EXPORT_SYMBOL(_raw_spin_lock); static inline void __raw_spin_lock(raw_spinlock_t *lock) //这里开始,便是自旋锁实际的执行过程了 { preempt_disable(); //禁止抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); //判断锁是否为0,lock为0则可以抢占,lock为1则不可抢占 LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); //使lock为1,即加锁 }
//接下来细看一下每个函数的内存实现 //首先是禁止抢占函数 #define preempt_disable() \ do { \ inc_preempt_count(); \ //禁止抢占函数的本质就是将preempt_count+1,具体如下(1) barrier(); \ //barrier函数是一个内存屏障函数,具体如下(2) } while (0) //(1) #define preempt_count() (current_thread_info()->preempt_count) #define inc_preempt_count() add_preempt_count(1) #define add_preempt_count(val) do { preempt_count() += (val); } while (0) //即最后返回为 current_thread_info()->preempt_count+1 //(2) //在linux/arch/cris/include/asm/system.h文件中,可以看到mb函数族的本质就是barrier #define barrier() __asm__ __volatile__("": : :"memory") //其实是一个空操作 #define mb() barrier() //读写屏障 #define rmb() mb() //读屏障 #define wmb() mb() //写屏障 //引用大师的讲解,CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。 //屏障之所以起到作用,是因为在执行空操作的时候是不允许其他进程对寄存器调用的,而这样的做法,保证了在屏障前执行的操作,和在屏障后执行的操作不互相影响,具体也可参阅《LDK》,
//接着来看一下spin_acquire()与LOCK_CONTENTDED() #ifdef CONFIG_DEBUG_LOCK_ALLOC # ifdef CONFIG_PROVE_LOCKING # define spin_acquire(l, s, t, i) lock_acquire(l, s, t, 0, 2, NULL, i) # define spin_acquire_nest(l, s, t, n, i) lock_acquire(l, s, t, 0, 2, n, i) # else //CONFIG未设置CONFIG_PROVE_LOCKING情况下 # define spin_acquire(l, s, t, i) lock_acquire(l, s, t, 0, 1, NULL, i) # define spin_acquire_nest(l, s, t, n, i) lock_acquire(l, s, t, 0, 1, NULL, i) # endif # define spin_release(l, n, i) lock_release(l, n, i) #else //CONFIG未设置CONFIG_DEBUG_LOCK_ALLOC情况下 # define spin_acquire(l, s, t, i) do { } while (0) # define spin_release(l, n, i) do { } while (0) #endif
首先来仔细看一下spin_acquire()
void lock_acquire(struct lockdep_map *lock, unsigned int subclass, int trylock, int read, int check, struct lockdep_map *nest_lock, unsigned long ip) //入口参数对应为*lock = &lock->dep_map, subclass = 0, trylock = 0, //read = 0, check = 1/2, *nest_lock = NULL, ip = _REP_IP_ { unsigned long flags; if (unlikely(current->lockdep_recursion)) //current->lock_recursion不为0则返回 return; raw_local_irq_save(flags); //save flags check_flags(flags); //暂时保留、看不是太懂 current->lockdep_recursion = 1; //这里将其置1了,可想而知,在另外一个进程调用lock_acquire来获取该dep_map的时候,将直接返回 trace_lock_acquire(lock, subclass, trylock, read, check, nest_lock, ip); __lock_acquire(lock, subclass, trylock, read, check, irqs_disabled_flags(flags), nest_lock, ip, 0); //接下来在详解该函数,(2) current->lockdep_recursion = 0; //再将其置0,使其他进程可以正常调用lock_acquire raw_local_irq_restore(flags); //接下来再详解该函数,(1) } EXPORT_SYMBOL_GPL(lock_acquire); //(1) #define raw_local_irq_restore(flags) \ do { \ typecheck(unsigned long, flags); \ arch_local_irq_restore(flags); \ //这个暂时不讨论了 } while (0)
#define typecheck(type,x) \ //一个检验类型的函数,type 与 x 类型相同则返回1 ({ type __dummy; \ typeof(x) __dummy2; \ (void)(&__dummy == &__dummy2); \ 1; \ }) //(2) static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass, int trylock, int read, int check, int hardirqs_off, struct lockdep_map *nest_lock, unsigned long ip, int references) //传入参数 *lock = &lock->dep_map, subclass = 0, trylock = 0, //read = 0, check = 1/2, hardirqs_off = irqs_disabled_flags(flags), //*nest_lock = NULL, ip = _RET_IP_ //这个函数确实难懂了些= =、姑且略过
{ struct task_struct *curr = current; struct lock_class *class = NULL; struct held_lock *hlock; unsigned int depth, id; int chain_head = 0; int class_idx; u64 chain_key; if (!prove_locking) check = 1; if (unlikely(!debug_locks)) return 0; /* * Lockdep should run with IRQs disabled, otherwise we could * get an interrupt which would want to take locks, which would * end up in lockdep and have you got a head-ache already? */ if (DEBUG_LOCKS_WARN_ON(!irqs_disabled())) return 0; if (lock->key == &__lockdep_no_validate__) check = 1; if (subclass < NR_LOCKDEP_CACHING_CLASSES) class = lock->class_cache[subclass]; /* * Not cached? */
if (unlikely(!class)) { class = register_lock_class(lock, subclass, 0); if (!class) return 0; } atomic_inc((atomic_t *)&class->ops); if (very_verbose(class)) { printk("\nacquire class [%p] %s", class->key, class->name); if (class->name_version > 1) printk("#%d", class->name_version); printk("\n"); dump_stack(); } /* * Add the lock to the list of currently held locks. * (we dont increase the depth just yet, up until the * dependency checks are done) */ depth = curr->lockdep_depth; /* * Ran out of static storage for our per-task lock stack again have we? */ if (DEBUG_LOCKS_WARN_ON(depth >= MAX_LOCK_DEPTH)) return 0; class_idx = class - lock_classes + 1; if (depth) { hlock = curr->held_locks + depth - 1; if (hlock->class_idx == class_idx && nest_lock) { if (hlock->references) hlock->references++; else hlock->references = 2; return 1; } }
hlock = curr->held_locks + depth; /* * Plain impossible, we just registered it and checked it weren't no * NULL like.. I bet this mushroom I ate was good! */ if (DEBUG_LOCKS_WARN_ON(!class)) return 0; hlock->class_idx = class_idx; hlock->acquire_ip = ip; hlock->instance = lock; hlock->nest_lock = nest_lock; hlock->trylock = trylock; hlock->read = read; hlock->check = check; hlock->hardirqs_off = !!hardirqs_off; hlock->references = references; #ifdef CONFIG_LOCK_STAT hlock->waittime_stamp = 0; hlock->holdtime_stamp = lockstat_clock(); #endif if (check == 2 && !mark_irqflags(curr, hlock)) return 0; /* mark it as used: */ if (!mark_lock(curr, hlock, LOCK_USED)) return 0; /* * Calculate the chain hash: it's the combined hash of all the * lock keys along the dependency chain. We save the hash value * at every step so that we can get the current hash easily * after unlock. The chain hash is then used to cache dependency * results. * * The 'key ID' is what is the most compact key value to drive * the hash, not class->key. */ id = class - lock_classes; /* * Whoops, we did it again.. ran straight out of our static allocation. */ if (DEBUG_LOCKS_WARN_ON(id >= MAX_LOCKDEP_KEYS)) return 0;
chain_key = curr->curr_chain_key; if (!depth) { /* * How can we have a chain hash when we ain't got no keys?! */ if (DEBUG_LOCKS_WARN_ON(chain_key != 0)) return 0; chain_head = 1; } hlock->prev_chain_key = chain_key; if (separate_irq_context(curr, hlock)) { chain_key = 0; chain_head = 1; } chain_key = iterate_chain_key(chain_key, id); if (!validate_chain(curr, lock, hlock, chain_head, chain_key)) return 0; curr->curr_chain_key = chain_key; curr->lockdep_depth++; check_chain_key(curr); #ifdef CONFIG_DEBUG_LOCKDEP if (unlikely(!debug_locks)) return 0; #endif if (unlikely(curr->lockdep_depth >= MAX_LOCK_DEPTH)) { debug_locks_off(); printk("BUG: MAX_LOCK_DEPTH too low!\n"); printk("turning off the locking correctness validator.\n"); dump_stack(); return 0; } if (unlikely(curr->lockdep_depth > max_lockdep_depth)) max_lockdep_depth = curr->lockdep_depth; return 1; }继续往下看一下LOCK_CONTENDED()
//入口参数为(lock, do_raw_spin_trylock, do_raw_spin_lock) #define LOCK_CONTENDED(_lock, try, lock) \ do { \ if (!try(_lock)) { \ //这里实际为 do_raw_spin_trylock(lock), 具体如下(1) lock_contended(&(_lock)->dep_map, _RET_IP_); \ //详解如下(2) lock(_lock); \ //这里实际为 do_raw_spin_lock(lock), 具体如下(3) } \ lock_acquired(&(_lock)->dep_map, _RET_IP_); \ //详解如下(4) } while (0) //(1) static inline int do_raw_spin_trylock(raw_spinlock_t *lock) { return arch_spin_trylock(&(lock)->raw_lock); } # define arch_spin_trylock(lock) ({ (void)(lock); 1; }) //实际返回值都是为1,写个小程序验证下就明白了 //在锁机制中, 若上锁了则lock 为1, 否则 为0 //(2) //好吧、到此为止先了……这里的函数真的比较深、能力所限看不太懂 void lock_contended(struct lockdep_map *lock, unsigned long ip) { unsigned long flags; if (unlikely(!lock_stat)) return; if (unlikely(current->lockdep_recursion)) return; raw_local_irq_save(flags); check_flags(flags); current->lockdep_recursion = 1; trace_lock_contended(lock, ip); __lock_contended(lock, ip); current->lockdep_recursion = 0; raw_local_irq_restore(flags); }
EXPORT_SYMBOL_GPL(lock_contended);
一个自旋锁机制的内核实现确实是博大精深。
若只挑简单的地方来理解,可以只理解总结为如下步奏来实现一个自旋锁
1、声明锁变量
2、上锁
3、临界区
4、解锁
顺序锁(seqlock):
顺序锁是对读写锁的一种优化,若使用顺序锁,读与写操作不阻塞,只阻塞同种操作,即读与读/写与写操作。
写执行单元的操作顺序如下:
//获得顺序锁 void write_seqlock(seqlock_t *s1); int write_tryseqlock(seqlock_t *s1); write_seqlock_irqsave(lock, flags) write_seqlock_irq(lock) write_seqlock_bh(lock) //释放顺序锁 void write_sequnlock(seqlock_t *s1); write_sequnlock_irqrestore(lock, flags) write_sequnlock_irq(lock) write_sequnlock_bh(lock)
//读开始 unsinged read_seqbegin(const seqlock_t *s1); read_seqbegin_irqsave(lock, flags) //重读,读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查在读操作器件是否有写操作,如果有,读执行单元需要从新读一次。 int reead_seqretry(const seqlock_t *s1, unsigned iv); read_seqretry_irqrestore(lock, iv, flags)
对于被RCU保护的功效数据结构,读执行单元不需要获得任何锁就可以访问它,不使用原子指令,而且在除alpha的所有架构上也不需要内存屏障(Memory Barrier),因此不会导致锁竞争、内存延迟以及流水线停滞。使用RCU的写执行单元在访问它前需要首先拷贝一个副本,然后对副本进行修改,最后使用一个回调机制在适当的实际把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据的操作的时候。
RCU可以看作读写锁的高性能版本,相比读写锁,RCU的优点在于既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。但是RCU不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。
具体操作:略
信号量(semaphore):
信号量是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。
相同点:只有得到信号量的进程才能执行临界区的代码。
(linux自旋锁和信号量锁采用的都是“获得锁-访问临界区-释放锁”,可以称为“互斥三部曲”,实际存在于几乎所有多任务操作系统中)
不同点:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
//信号量的结构 struct semaphore sem; //初始化信号量 void sema_init(struct semaphore *sem, int val) //常用下面两种形式 #define init_MUTEX(sem) sema_init(sem, 1) #define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //以下是初始化信号量的快捷方式,最常用的 DECLARE_MUTEX(name) //初始化name的信号量为1 DECLARE_MUTEX_LOCKED(name) //初始化信号量为0 //常用操作 DECLARE_MUTEX(mount_sem); down(&mount_sem); //获取信号量 ... critical section //临界区 ... up(&mount_sem); //释放信号量信号量用于同步时只能唤醒一个执行单元,而完成量(completion)用于同步时可以唤醒所有等待的执行单元。
自旋锁与互斥锁的选择
1、当锁 不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开始是等待获取自旋锁的时间Tcs,若Tcs比较小,则应使用自旋锁,否则应使用信号量
2、信号量锁保护的临界区可以包含引起阻塞的代码,而自旋锁则却对要避免使用包含阻塞的临界区代码,否则很可能引发锁陷阱
3、信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。
读写信号量:与读写信号锁相似,是一种放宽粒度的实现机制。
小结一下并发控制这一部分:
现在的处理器基本上都是SMP类型的,而且在新的内核版本中,基本上都支持抢占式的操作,在linux中很多程序都是可重入的,要保护这些数据,就得使用不同的锁机制。
而锁机制的基本操作过程其实大同小异的,声明变量,上锁,执行临界区代码,然后再解锁。
不同点在于,可以重入的限制不同,有的可以无限制重入,有的只允许异种操作重入,而有的是不允许重入操作的。
而在考虑不同的锁机制的使用时,也要考虑CPU处理的效率问题,对于不同的代码长度,不同的代码执行时间,选择一个好的锁对CPU的良好使用有很大的影响,否则将造成浪费。