1.atomic 原子操作
就是执行起来不能被打断的操作。构成物质的最小颗粒。需要硬件支持。汇编实现。用volatile修饰优化器在用到这个变量时必须每次都小心地从内存重新读取这个变量的值,而不是使用保存在寄存器里的备份。
包括各种 atomic_xxx 操作的对象是atomic *v。
通常用来实现引用计数
2.spanlock 自旋锁
如果有人占用了锁就不行地询问所有没有被释放。适合于保有锁很短的进程。不会引起睡眠。另外自旋锁适用于任何(进程、中断)上下文(信号量只适用于进程上下文)。自旋锁无法被抢占(和信号量相比)。只能有一个单元获得锁。
DEFINE_SPINLOCK(x)等同于spinlock_tx = SPIN_LOCK_UNLOCKED
spin_lock 获得自旋锁 spin_trylock不做等待
spin_lock_irqsave 失效本地中断,并保存寄存器的值到flags
spin_lock_bh(lock) 该宏在得到自旋锁的同时失效本地软中断
被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。
如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行
spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访。
3.mutex(互斥锁)
内核互斥锁是在原子API之上实现的,并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快。
数据结构的字段:
1、atomic_t count; --指示互斥锁的状态:1没有上锁,可以获得;0被锁定,不能获得;
2、spinlock_t wait_lock;--等待获取互斥锁中使用的自旋锁。在获取互斥锁的过程中,操作会在自旋锁的保护中进行。初始化为为锁定。
3、struct list_head wait_list;--等待互斥锁的进程队列。
获取互斥锁。实际上是先给count做自减操作,然后使用自旋锁进入临界区。首先取得count的值,再将count置为-1,判断如果原来count的值为1,也即互斥锁可以获得,则直接获取,跳出。否则进入循环反复测试互斥锁的状态。在循环中,也是先取得互斥锁原来的状态,再将其置为-1,判断如果可以获取(等于1),则退出循环,否则设置当前进程的状态为不可中断状态,解锁自身的自旋锁,进入睡眠状态,待被在调度唤醒时,再获得自身的自旋锁,进入新一次的查询其自身状态(该互斥锁的状态)的循环。
释放被当前进程获取的互斥锁。该函数不能用在中断上下文中,而且不允许去释放一个没有上锁的互斥锁。
4.semaphore 信号量
和互斥锁类似,但是支持多个任务访问共享资源
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
5.rw_semaphore
读者在保持读写信号量期间只能对该读写信号量保护的共享资源进行读访问, 写要单独获得写身份。读者数目不受限制
写是排他的,独占的
6.seqlock
写进临界区+1,出临界区+1
只是互斥写写和写先发生的写读。
如此一来奇数时是在进行写操作。读要在偶数时进行。 读时如果当前值和退出值不一样就需要重新读。
可以通过自旋锁实现
seqlock的实现非常简单:
写操作进入临界区时:
voidwrite_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock); //上写写互斥锁
++sl->sequence; //sequence++
}
写操作退出临界区时:
voidwrite_sequnlock(seqlock_t *sl)
{
sl->sequence++; //sequence再++
spin_unlock(&sl->lock);// 释放写写互斥锁
}
读操作进入临界区时:
unsignedread_seqbegin(const seqlock_t *sl)
{
unsigned ret;
repeat:
ret=sl->sequence; // 读sequence值
if(unlikely(ret & 1)) { // 如果sequence为奇数自旋等待
gotorepeat;
}
return ret;
}
读操作尝试退出临界区时:
intread_seqretry(const seqlock_t *sl,unsigned start)
{
return(sl->sequence != start); //看看sequence与进入临界区时是否发生过改变
}
而读操作一般会这样进行:
do{
seq =read_seqbegin(&seq_lock); // 进入临界区
do_something();
}while (read_seqretry(&seq_lock, seq));//尝试退出临界区,存在冲突则重试
7.rcu(read-compy-update)
RCU也是用于能够区分读与写的场合,并且也是读多写少,但是读操作的优先权大于写操作(与seqlock相反)。
RCU也是用于能够区分读与写的场合,并且也是读多写少,但是读操作的优先权大于写操作(与seqlock相反)。
RCU的实现思路是,读操作不需要互斥、不需要阻塞、也不需要原子指令,直接读就行了。而写操作在进行之前需要把被写的对象copy一份,写完之后再更新回去。其实RCU所能保护的并不是任意的临界区,它只能保护由指针指向的对象(而不保护指针本身)。读操作通过这个指针来访问对象(这个对象就是临界区);写操作把对象复制一份,然后更新,最后修改指针使其指向新的对象。由于指针总是一个字长的,对它的读写对于CPU来说总是原子的,所以不用担心更新指针只更新到一半就被读取的情况(指针的值为0x11111111,要更新为0x22222222,不会出现类似0x11112222这样的中间状态)。所以,当读写操作同时发生时,读操作要么读到指针的旧值,引用了更新前的对象、要么读到了指针的新值,引用了更新后的对象。即使同时有多个写操作发生也没关系(是否需要写写互斥跟写操作本身的场景相关)。
为解决旧对象释放的问题,RCU提供了四个函数(另外还有一些它们的变形):
rcu_read_lock(void)、rcu_read_unlock(void)
synchronize_rcu(void)、call_rcu(struct rcu_head *head, void (*func)(structrcu_head*head))。
当读操作要调用rcu_dereference访问对象之前,需要先调用rcu_read_lock;当不再需要访问对象时,调用rcu_read_unlock。
当写操作调用rcu_assign_pointer完成对对象的更新之后,需要调用synchronize_rcu或call_rcu。其中synchronize_rcu会阻塞等待在此之前所有调用了rcu_read_lock的读操作都已经调用rcu_read_unlock,synchronize_rcu返回后写操作一方就可以将被它替换掉的旧对象释放了;而call_rcu则是通过注册回调函数的方式,由回调函数来释放旧对象,写操作一方将不需要阻塞等待。同样,等到在此之前所有调用了rcu_read_lock的读操作都调用rcu_read_unlock之后,回调函数将被调用。
如果你足够细心,可能已经注意到了这样一个问题。synchronize_rcu和call_rcu会等待的是“在此之前所有调用了rcu_read_lock的读操作都已经调用了rcu_read_unlock”,然而在rcu_assign_pointer与synchronize_rcu或call_rcu之间,可能也有读操作发生(调用了rcu_read_lock),它们引用到的是写操作rcu_assign_pointer后的新对象。按理说写操作一方想要释放旧对象时,是不需要等待这样的读操作的。但是由于这些读操作发生在synchronize_rcu或call_rcu之前,按照RCU的机制,还真得等它们都rcu_read_unlock。这岂不是多等了一些时日?
8.读写锁
详细内容见:https://blog.csdn.net/ygm_linux/article/details/50358739