Linux 内核 锁

0

在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。

在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,同步机制包括:
原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(在开发内核2.5.43中引入该技术的并正式包含在2.6内核中)和seqlock(只包含在2.6以后内核中)

对于以上每种锁都要明确其特定的应用场合,使用不当反而会影响性能甚至错误。

原子操作

原子操作简介
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。
原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。

原子操作api介绍

原子类型定义如下:
typedef struct { volatile int counter; } atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。

主要api如下:

atomic_read(atomic_t * v);
该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。

atomic_set(atomic_t * v, int i);
该函数设置原子类型的变量v的值为i。
、、、

信号量(semaphore)

信号量简介
Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干
**信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。**一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

信号量api介绍

DEFINE_MUTEX
静态定义和初始化一个互斥锁. 1

void mutex_init(struct mutex *mutex);
动态初始化一个互斥锁

void sema_init (struct semaphore *sem, int val);
该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。

void down(struct semaphore * sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数首先判断sem->count的值是否大于0,如果true则sem->count–,否者调用者将被挂起,直到别的任务释放该信号量才能继续运行。

int down_interruptible(struct semaphore * sem);
该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。

int down_trylock(struct semaphore * sem);
该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。

void up(struct semaphore * sem);
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

读写信号量(rw_semaphore)

读写信号量简介
读写信号量对访问者进行了细分,或者为读者,或者为写者,读者在保持读写信号量期间只能对该读写信号量保护的共享资源进行读访问,如果一个任务除了需要读,可能还需要写,那么它必须被归类为写者,它在对共享资源访问之前必须先获得写者身份,写者在发现自己不需要写访问的情况下可以降级为读者。读写信号量的访问规则:

  • 读写信号量同时拥有的读者数不受限制,也就说可以有任意多个读者同时拥有一个读写信号量。
  • 如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;否则,读者必须被挂起直到写者释放该信号量。
  • 如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。

因此,写者是排他性的,独占性的。
读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现

读写信号量api介绍:

DECLARE_RWSEM(name)
该宏声明一个读写信号量name并对其进行初始化。

void init_rwsem(struct rw_semaphore *sem);
该函数对读写信号量sem进行初始化。

void down_read(struct rw_semaphore *sem);
读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

int down_read_trylock(struct rw_semaphore *sem);
该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

void down_write(struct rw_semaphore *sem);
写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

int down_write_trylock(struct rw_semaphore *sem);
该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

void up_read(struct rw_semaphore *sem);
读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

void up_write(struct rw_semaphore *sem);
写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。

void downgrade_write(struct rw_semaphore *sem);
该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率

自旋锁(spinlock)

自旋锁简介
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁

信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作,在单CPU且可抢占的内核下,自旋锁实际上只进行开启和关闭内核抢占的操作。
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

自旋锁的API

spin_lock_init(x)
该宏用于初始化自旋锁x。自旋锁在真正使用前必须先初始化。该宏用于动态初始化。

DEFINE_SPINLOCK(x)
该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

SPIN_LOCK_UNLOCKED
该宏用于静态初始化一个自旋锁。
DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKED

spin_is_locked(x)
该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,返回真,否则返回假。

spin_unlock_wait(x)
该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,直到该自旋锁被保持者释放。

spin_trylock(lock)
该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。

spin_lock(lock)
该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它获得锁并返回。总之,只有它获得锁才返回。

spin_lock_irqsave(lock, flags)
该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。

spin_lock_irq(lock)
该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。

spin_lock_bh(lock)
该宏在得到自旋锁的同时失效本地软中断。

spin_unlock(lock)
该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

spin_unlock_irqrestore(lock, flags)
该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

spin_unlock_irq(lock)
该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。

spin_unlock_bh(lock)
该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。
、、、

注:被保护的共享资源只在进程上下文访问和软中断上下文访问
当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。

举例说明:spinlock用在进程上下文和中断
进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区,(有一个问题没有搞懂关中断是关闭对所有cpu的中断还是本地cpu的?从关闭中断的函数来看似乎是针对本地的),所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。如果有,那就需要使用spinlock_irq_save,该函数即会关抢占,也会关本地中断(因为不能保证打断A的中断和A不是一个cpu,因此spin_lock在多核cpu上使用还是要关中断)

注:spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。

具体代码:
(1)单核cpu
如果spin_lock不处于中断上下文,则spin_lock锁定的代码只会在内核发生抢占2的时候才会丢失CPU拥有权。所以,对于单核来说,需要在spin_lock获得锁的时候禁止抢占,释放锁的时候开放抢占。因此这不是真正意义上的锁。
内核代码实现如下:

static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
#define _raw_spin_lock(lock)            __LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
  可以看到仅禁止了内核抢占

 static inline void spin_unlock(spinlock_t *lock)
{
    raw_spin_unlock(&lock->rlock);
}
 #define raw_spin_unlock(lock)      _raw_spin_unlock(lock)
 #define _raw_spin_unlock(lock)         __UNLOCK(lock)
 #define __UNLOCK(lock) \
  do { preempt_enable(); __release(lock); (void)(lock); } while (0)
  开启内核抢占

(2)多核
存在临界区同时在多核上被执行的情况,这时候才需要一个真正的锁来宣告代码对资源的占有。几个核可能会同时access临界区,这时的spinlock是如何实现的呢?
要用到CPU提供的一些特殊指令,对lock变量进行原子操作。
SMP中spin_lock的实现
实现在include/linux/spinlock_api_smp.h

static inline void __raw_spin_lock(raw_spinlock_t*lock)

{

preempt_disable(); 

spin_acquire(&lock->dep_map, 0, 0,_RET_IP_); 

LOCK_CONTENDED(lock, do_raw_spin_trylock,do_raw_spin_lock);}

SMP上的实现被分解为三句话。

preempt_disable() 关抢占

spin_acquire()是sparse检查需要

LOCK_CONTENDED()是一个宏,如果不考虑CONFIG_LOCK_STAT(该宏是为了统计lock的操作),则:
#define LOCK_CONTENDED(_lock, try, lock) \ lock(_lock)
则第三句话等同于:
do_raw_spin_lock(lock)
而do_raw_spin_lock()则可以从spinlock.h中找到痕迹:
static inline intdo_raw_spin_trylock(raw_spinlock_t *lock){ return arch_spin_trylock(&(lock)->raw_lock);}

看到arch,我们明白这个函数是体系结构相关的。这部分代码使用汇编实现。

大内核锁(BKL–Big Kernel Lock)

大内核锁简介:
大内核锁本质上也是自旋锁,但是它又不同于自旋锁,自旋锁是不可以递归获得锁的,因为那样会导致死锁。但大内核锁可以递归获得锁大内核锁用于保护整个内核,而自旋锁用于保护非常特定的某一共享资源。进程保持大内核锁时可以发生调度,具体实现是:在执行schedule时,schedule将检查进程是否拥有大内核锁,如果有,它将被释放,以致于其它的进程能够获得该锁,而当轮到该进程运行时,再让它重新获得大内核锁。注意在保持自旋锁期间是不允许发生调度的。
需要特别指出,整个内核只有一个大内核锁,其实不难理解,内核只有一个,而大内核锁是保护整个内核的,当然有且只有一个就足够了。
还需要特别指出的是,大内核锁是历史遗留,内核中用的非常少,一般保持该锁的时间较长,因此不提倡使用它。从2.6.11内核起,大内核锁可以通过配置内核使其变得可抢占(自旋锁是不可抢占的),这时它实质上是一个互斥锁,使用信号量实现

大内核锁的API

void lock_kernel(void);
该函数用于得到大内核锁。它可以递归调用而不会导致死锁。

void unlock_kernel(void);
该函数用于释放大内核锁。当然必须与lock_kernel配对使用,调用了多少次lock_kernel,就需要调用多少次unlock_kernel。

大内核锁的API使用非常简单,按照以下方式使用就可以了:

lock_kernel();
//对被保护的共享资源的访问
…
unlock_kernel();

读写锁(rwlock)

读写锁简介
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者
在读写锁保持期间也是抢占失效的。
读写锁访问规则:

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。
如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
读写锁API
读写锁的API看上去与自旋锁很象,只是读者和写者需要不同的获得和释放锁的API。

rwlock_init(x)
该宏用于动态初始化读写锁x。

DEFINE_RWLOCK(x)
该宏声明一个读写锁并对其进行初始化。它用于静态初始化。

RW_LOCK_UNLOCKED
它用于静态初始化一个读写锁。
DEFINE_RWLOCK(x)等同于rwlock_t x = RW_LOCK_UNLOCKED

read_trylock(lock)
读者用它来尽力获得读写锁lock,如果能够立即获得读写锁,它就获得锁并返回真,否则不能获得锁,返回假。无论是否能够获得锁,它都将立即返回,绝不自旋在那里。

write_trylock(lock)
写者用它来尽力获得读写锁lock,如果能够立即获得读写锁,它就获得锁并返回真,否则不能获得锁,返回假。无论是否能够获得锁,它都将立即返回,绝不自旋在那里。

read_lock(lock)
读者要访问被读写锁lock保护的共享资源,需要使用该宏来得到读写锁lock。如果能够立即获得,它将立即获得读写锁并返回,否则,将自旋在那里,直到获得该读写锁。

write_lock(lock)
写者要想访问被读写锁lock保护的共享资源,需要使用该宏来得到读写锁lock。如果能够立即获得,它将立即获得读写锁并返回,否则,将自旋在那里,直到获得该读写锁。

read_lock_irqsave(lock, flags)
读者也可以使用该宏来获得读写锁,与read_lock不同的是,该宏还同时把标志寄存器的值保存到了变量flags中,并失效了本地中断。

write_lock_irqsave(lock, flags)
写者可以用它来获得读写锁,与write_lock不同的是,该宏还同时把标志寄存器的值保存到了变量flags中,并失效了本地中断。

read_lock_irq(lock)
读者也可以用它来获得读写锁,与read_lock不同的是,该宏还同时失效了本地中断。该宏与read_lock_irqsave的不同之处是,它没有保存标志寄存器。

write_lock_irq(lock)
写者也可以用它来获得锁,与write_lock不同的是,该宏还同时失效了本地中断。该宏与write_lock_irqsave的不同之处是,它没有保存标志寄存器。

read_lock_bh(lock)
读者也可以用它来获得读写锁,与与read_lock不同的是,该宏还同时失效了本地的软中断。

write_lock_bh(lock)
写者也可以用它来获得读写锁,与write_lock不同的是,该宏还同时失效了本地的软中断。

大读者锁(brlock-Big Reader Lock)

大读者锁简介
大读者锁是读写锁的高性能版,读者可以非常快地获得锁,但写者获得锁的开销比较大大读者锁只存在于2.4内核中,在2.6中已经没有这种锁(提醒读者特别注意)。它们的使用与读写锁的使用类似,只是所有的大读者锁都是事先已经定义好的。这种锁适合于读多写少的情况,它在这种情况下远好于读写锁。
大读者锁的实现机制是:每一个大读者锁在所有CPU上都有一个本地读者写者锁,一个读者仅需要获得本地CPU的读者锁,而写者必须获得所有CPU上的锁

RCU(Read-Copy Update)

RCU简介
RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

因此RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。
读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机的这一时期称为宽限期(grace period),而CPU发生了上下文切换称为经历一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。

注:以下以链表元素删除为例详细说明这一过程:
写者要从链表中删除元素 B,它首先遍历该链表得到指向元素 B 的指针,然后修改元素 B 的前一个元素的 next 指针指向元素 B 的 next 指针指向的元素C,修改元素 B 的 next 指针指向的元素 C 的 prep 指针指向元素 B 的 prep指针指向的元素 A,在这期间可能有读者访问该链表,修改指针指向的操作是原子的,所以不需要同步,而元素 B 的指针并没有去修改,因为读者可能正在使用 B 元素来得到下一个或前一个元素。写者完成这些操作后注册一个回调函数以便在 grace period 之后删除元素 B,然后就认为已经完成删除操作。垃圾收集器在检测到所有的CPU不在引用该链表后,即所有的 CPU 已经经历了 quiescent state,grace period 已经过去后,就调用刚才写者注册的回调函数删除了元素 B。

在这里插入图片描述
图 使用 RCU 进行链表删除操作

RCU的API
rcu_read_lock()
读者在读取由RCU保护的共享数据时使用该函数标记它进入读端临界区。

rcu_read_unlock()
该函数与rcu_read_lock配对使用,用以标记读者退出读端临界区。
夹在这两个函数之间的代码区称为”读端临界区”(read-side critical section)。读端临界区可以嵌套,如图,临界区2被嵌套在临界区1内。
在这里插入图片描述
在读端临界区发生了什么?要回答这个问题需要搞清楚rcu_read_lock和rcu_read_unlock做了什么操作,实际上即关闭内核抢占和打开内核抢占

static inline void __rcu_read_lock(void)
{
preempt_disable();
}

static inline void __rcu_read_unlock(void)
{
preempt_enable();
}

即在读端临界区中时内核是禁止抢占的。
那么这时是否度过宽限期(Grace Period)的判断就比较简单:每个CPU都经过一次抢占。因为发生抢占,就说明不在rcu_read_lock和rcu_read_unlock之间,必然已经完成访问或者还未开始访问。

synchronize_rcu()
该函数由RCU写端调用,它将阻塞写者,直到经过grace period后,即所有的读者已经完成读端临界区,写者才可以继续下一步操作。如果有多个RCU写端调用该函数,他们将在一个grace period之后全部被唤醒。注意,该函数在2.6.11及以前的2.6内核版本中为synchronize_kernel,只是在2.6.12才更名为synchronize_rcu,但在2.6.12中也提供了synchronize_kernel和一个新的函数synchronize_sched,因为以前有很多内核开发者使用synchronize_kernel用于等待所有CPU都退出不可抢占区,而在RCU设计时该函数只是用于等待所有CPU都退出读端临界区,它可能会随着RCU实现的修改而发生语意变化,因此为了预先防止这种情况发生,在新的修改中增加了专门的用于其它内核用户的synchronize_sched函数和只用于RCU使用的synchronize_rcu,现在建议非RCU内核代码部分不使用synchronize_kernel而使用synchronize_sched,RCU代码部分则使用synchronize_rcu,synchronize_kernel之所以存在是为了保证代码兼容性。

synchronize_kernel()
其他非RCU的内核代码使用该函数来等待所有CPU处在可抢占状态,目前功能等同于synchronize_rcu,但现在已经不建议使用,而使用synchronize_sched。

synchronize_sched()
该函数用于等待所有CPU都处在可抢占状态,它能保证正在运行的中断处理函数处理完毕,但不能保证正在运行的softirq处理完毕。注意,synchronize_rcu只保证所有CPU都处理完正在运行的读端临界区。

void fastcall call_rcu(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
struct rcu_head {
struct rcu_head *next;
void (*func)(struct rcu_head *head);
};
函数call_rcu也由RCU写端调用,它不会使写者阻塞,因而可以在中断上下文或softirq使用。该函数将把函数func挂接到RCU回调函数链上,然后立即返回。一旦所有的CPU都已经完成读端临界区操作,该函数将被调用来释放删除的将绝不在被应用的数据。参数head用于记录回调函数func,一般该结构会作为被RCU保护的数据结构的一个字段,以便省去单独为该结构分配内存的操作。需要指出的是,函数synchronize_rcu的实现实际上使用函数call_rcu。

void fastcall call_rcu_bh(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
函数call_ruc_bh功能几乎与call_rcu完全相同,唯一差别就是它把softirq的完成也当作经历一个quiescent state,因此如果写端使用了该函数,在进程上下文的读端必须使用rcu_read_lock_bh。

#define rcu_dereference§ ({
typeof§ _________p1 = p;
smp_read_barrier_depends();
(_________p1);
})
该宏用于在RCU读端临界区获得一个RCU保护的指针,该指针可以在以后安全地引用,内存栅只在alpha架构上才使用。

顺序锁(seqlock)

顺序锁简介
顺序锁也是对读写锁的一种优化,对于顺序锁,读者绝不会被写者阻塞,也就说,读者可以在写者对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写者完成写操作,写者也不需要等待所有读者完成读操作才去进行写操作。但是,写者与写者之间仍然是互斥的,即如果有写者在进行写操作,其他写者必须自旋在那里,直到写者释放了顺序锁。
这种锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写者可能使得指针失效,但读者如果正要访问该指针,将导致OOPs。
如果读者在读操作期间,写者已经发生了写操作,那么,读者必须重新读取数据,以便确保得到的数据是完整的。
这种锁对于读写同时进行的概率比较小的情况,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。

顺序锁的API

void write_seqlock(seqlock_t *sl);
写者在访问被顺序锁s1保护的共享资源前需要调用该函数来获得顺序锁s1。它实际功能上等同于spin_lock,只是增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。

void write_sequnlock(seqlock_t *sl);
写者在访问完被顺序锁s1保护的共享资源后需要调用该函数来释放顺序锁s1。它实际功能上等同于spin_unlock,只是增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。
写者使用顺序锁的模式如下:

write_seqlock(&seqlock_a);
//写操作代码块

write_sequnlock(&seqlock_a);

因此,对写者而言,它的使用与spinlock相同。

顺序计数的API:

unsigned read_seqcount_begin(const seqcount_t *s);
读者在对被顺序计数保护的共享资源进行读访问前需要使用该函数来获得当前的顺序号。

int read_seqcount_retry(const seqcount_t *s, unsigned iv);
读者在访问完被顺序计数s保护的共享资源后需要调用该函数来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。
因此,读者使用顺序计数的模式如下:

do {
seqnum = read_seqbegin_count(&seqcount_a);
//读操作代码块

} while (read_seqretry(&seqcount_a, seqnum));

void write_seqcount_begin(seqcount_t *s);
写者在访问被顺序计数保护的共享资源前需要调用该函数来对顺序计数的顺序号加1,以便读者能够检查出是否在读期间有写者访问过。

void write_seqcount_end(seqcount_t *s);
写者在访问完被顺序计数保护的共享资源后需要调用该函数来对顺序计数的顺序号加1,以便读者能够检查出是否在读期间有写者访问过。
写者使用顺序计数的模式为:

write_seqcount_begin(&seqcount_a);
//写操作代码块

write_seqcount_end(&seqcount_a);
需要特别提醒,顺序计数的使用必须非常谨慎,只有确定在访问共享数据时已经保持了互斥锁才可以使用。

注:禁止内核抢占就比较简单了,就是防止当前进程不会突然被另一个进程抢占。在Linux的实现就是preempt_disable()和preempt_enable()函数:
#define preempt_disable() \
do { \
//增加preempt_count
inc_preempt_count(); \
//保证先加了preempt_count才进行以后的操作
barrier(); \
} while (0)

#define preempt_enable() \
do { \
preempt_enable_no_resched(); \
barrier(); \
//检查当前进程是否可抢占
preempt_check_resched(); \
} while (0)

不管是禁止中断还是禁止内核抢占,都是为了提供内核同步,但是他们都没有提供任何保护机制来防止其它处理器的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问,而禁止中断提供保护机制,这是防止来自其他中断处理程序的并发访问

侵删
转载参考:https://blog.csdn.net/godleading/article/details/78259842

おすすめ

転載: blog.csdn.net/weixin_44576259/article/details/120353431