Linux 内核的同步机制,第 1 部分 + 第二部分

Linux 内核的同步机制,第 1 部分

一、 引言

在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(只包含在2.6内核中)和seqlock(只包含在2.6内核中)。

本文的下面各章节将详细讲述每一种同步机制的原理、用途、API以及典型应用示例。

二、原子操作

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。

原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。

原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。

原子类型定义如下:


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。


void atomic_add(int i, atomic_t *v);

该函数给原子类型的变量v增加值i。


atomic_sub(int i, atomic_t *v);

该函数从原子类型的变量v中减去i。


int atomic_sub_and_test(int i, atomic_t *v);

该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。


void atomic_inc(atomic_t *v);

该函数对原子类型变量v原子地增加1。


void atomic_dec(atomic_t *v);

该函数对原子类型的变量v原子地减1。


int atomic_dec_and_test(atomic_t *v);

该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。


int atomic_inc_and_test(atomic_t *v);

该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。


int atomic_add_negative(int i, atomic_t *v);

该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。


int atomic_add_return(int i, atomic_t *v);

该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。


int atomic_sub_return(int i, atomic_t *v);

该函数从原子类型的变量v中减去i,并且返回指向v的指针。


int atomic_inc_return(atomic_t * v);

该函数对原子类型的变量v原子地增加1并且返回指向v的指针。


int atomic_dec_return(atomic_t * v);

该函数对原子类型的变量v原子地减1并且返回指向v的指针。

原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1,当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放IP碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。

三、信号量(semaphore)

Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。

信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

信号量的API有:


DECLARE_MUTEX(name)

该宏声明一个信号量name并初始化它的值为0,即声明一个互斥锁。


DECLARE_MUTEX_LOCKED(name)

该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。


void sema_init (struct semaphore *sem, int val);

该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。


void init_MUTEX (struct semaphore *sem);

该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。


void init_MUTEX_LOCKED (struct semaphore *sem);

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。


void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。


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的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

信号量在绝大部分情况下作为互斥锁使用,下面以console驱动系统为例说明信号量的使用。

在内核源码树的kernel/printk.c中,使用宏DECLARE_MUTEX声明了一个互斥锁console_sem,它用于保护console驱动列表console_drivers以及同步对整个console驱动系统的访问,其中定义了函数acquire_console_sem来获得互斥锁console_sem,定义了release_console_sem来释放互斥锁console_sem,定义了函数try_acquire_console_sem来尽力得到互斥锁console_sem。这三个函数实际上是分别对函数down,up和down_trylock的简单包装。需要访问console_drivers驱动列表时就需要使用acquire_console_sem来保护console_drivers列表,当访问完该列表后,就调用release_console_sem释放信号量console_sem。函数console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要访问console_drivers,因此它们都使用函数对acquire_console_sem和release_console_sem来对console_drivers进行保护。

四、读写信号量(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);

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

读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。在Linux中,每一个进程都用一个类型为task_t或struct task_struct的结构来描述,该结构的类型为struct mm_struct的字段mm描述了进程的内存映像,特别是mm_struct结构的mmap字段维护了整个进程的内存块列表,该列表将在进程生存期间被大量地遍利或修改,因此mm_struct结构就有一个字段mmap_sem来对mmap的访问进行保护,mmap_sem就是一个读写信号量,在proc文件系统里有很多进程内存使用情况的接口,通过它们能够查看某一进程的内存使用情况,命令free、ps和top都是通过proc来得到内存使用信息的,proc接口就使用down_read和up_read来读取进程的mmap信息。当进程动态地分配或释放内存时,需要修改mmap来反映分配或释放后的内存映像,因此动态内存分配或释放操作需要以写者身份获得读写信号量mmap_sem来对mmap进行更新。系统调用brk和munmap就使用了down_write和up_write来保护对mmap的访问。

五、自旋锁(spinlock)

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

信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。

自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单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_trylock_irqsave(lock, flags)

该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失效本地中断,如果没有获得锁,它什么也不做。因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。


spin_trylock_irq(lock)

该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。


spin_trylock_bh(lock)

该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。


spin_can_lock(lock)

该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。

如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。 如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。

如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些,因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

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

本系列文章的第二部分将详细介绍Linux内核中的其他一些同步机制,包括大内核锁、读写锁、大读者锁、RCU和顺序锁。

==========================================================================================================

Linux 内核的同步机制,第 2 部分

六、大内核锁(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不同的是,该宏还同时失效了本地的软中断。


read_unlock(lock)

读者使用该宏来释放读写锁lock。它必须与read_lock配对使用。


write_unlock(lock)

写者使用该宏来释放读写锁lock。它必须与write_lock配对使用。


read_unlock_irqrestore(lock, flags)

读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时把标志寄存器的值恢复为变量flags的值。它必须与read_lock_irqsave配对使用。


write_unlock_irqrestore(lock, flags)

写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时把标志寄存器的值恢复为变量flags的值,并使能本地中断。它必须与write_lock_irqsave配对使用。


read_unlock_irq(lock)

读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时使能本地中断。它必须与read_lock_irq配对使用。


write_unlock_irq(lock)

写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时使能本地中断。它必须与write_lock_irq配对使用。


read_unlock_bh(lock)

读者也可以使用该宏来释放读写锁,与read_unlock不同的是,该宏还同时使能本地软中断。它必须与read_lock_bh配对使用。


write_unlock_bh(lock)

写者也可以使用该宏来释放读写锁,与write_unlock不同的是,该宏还同时使能本地软中断。它必须与write_lock_bh配对使用。

读写锁的获得和释放锁的方法也有许多版本,具体用哪个与自旋锁一样,因此参考自旋锁部分就可以了。只是需要区分读者与写者,读者要用读者版本,而写者必须用写者版本。

八、大读者锁(brlock-Big Reader Lock)

大读者锁是读写锁的高性能版,读者可以非常快地获得锁,但写者获得锁的开销比较大。大读者锁只存在于2.4内核中,在2.6中已经没有这种锁(提醒读者特别注意)。它们的使用与读写锁的使用类似,只是所有的大读者锁都是事先已经定义好的。这种锁适合于读多写少的情况,它在这种情况下远好于读写锁。

大读者锁的实现机制是:每一个大读者锁在所有CPU上都有一个本地读者写者锁,一个读者仅需要获得本地CPU的读者锁,而写者必须获得所有CPU上的锁。

大读者锁的API非常类似于读写锁,只是锁变量为预定义的锁ID。


void br_read_lock (enum brlock_indices idx);

读者使用该函数来获得大读者锁idx,在2.4内核中,预定义的idx允许的值有两个:BR_GLOBALIRQ_LOCK和BR_NETPROTO_LOCK,当然,用户可以添加自己定义的大读者锁ID 。


void br_read_unlock (enum brlock_indices idx);

读者使用该函数释放大读者锁idx。


void br_write_lock (enum brlock_indices idx);

写者使用它来获得大读者锁idx。


void br_write_unlock (enum brlock_indices idx);

写者使用它来释放大读者锁idx。


br_read_lock_irqsave(idx, flags)

读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时把寄存器的值保存到变量flags中,并且失效本地中断。


br_read_lock_irq(idx)

读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时失效本地中断。与br_read_lock_irqsave不同的是,该宏不保存标志寄存器。


br_read_lock_bh(idx)

读者也可以使用该宏来获得大读者锁idx,与br_read_lock不同的是,该宏还同时失效本地软中断。


br_write_lock_irqsave(idx, flags)

写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时把寄存器的值保存到变量flags中,并且失效本地中断。


br_write_lock_irq(idx)

写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时失效本地中断。与br_write_lock_irqsave不同的是,该宏不保存标志寄存器。


br_write_lock_bh(idx)

写者也可以使用该宏来获得大读者锁idx,与br_write_lock不同的是,该宏还同时失效本地软中断。


br_read_unlock_irqrestore(idx, flags)

读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时把变量flags的值恢复到标志寄存器。


br_read_unlock_irq(idx)

读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时使能本地中断。


br_read_unlock_bh(idx)

读者也使用该宏来释放大读者锁idx,它与br_read_unlock不同之处是,该宏还同时使能本地软中断。


br_write_unlock_irqrestore(idx, flags)

写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时把变量flags的值恢复到标志寄存器。


br_write_unlock_irq(idx)

写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时使能本地中断。


br_write_unlock_bh(idx)

写者也使用该宏来释放大读者锁idx,它与br_write_unlock不同之处是,该宏还同时使能本地软中断。

这些API的使用与读写锁完全一致。

九、RCU(Read-Copy Update)

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

RCU也是读写锁的高性能版本,但是它比大读者锁具有更好的扩展性和性能。 RCU既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代读写锁,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。

RCU的API如下;


rcu_read_lock()

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


rcu_read_unlock()

该函数与rcu_read_lock配对使用,用以标记读者退出读端临界区。


synchronize_rcu()

该函数由RCU写端调用,它将阻塞写者,直到经过grace period后,即所有的读者已经完成读端临界区,写者才可以继续下一步操作。如果有多个RCU写端调用该函数,他们将在一个grace period之后全部被唤醒。


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(p)     ({ \
                                typeof(p) _________p1 = p; \
                                smp_read_barrier_depends(); \
                                (_________p1); \
                                })

该宏用于在RCU读端临界区获得一个RCU保护的指针,该指针可以在以后安全地引用,内存栅只在alpha架构上才使用。

除了这些API,RCU还增加了链表操作的RCU版本,因为对于RCU,对共享数据的操作必须保证能够被没有使用同步机制的读者看到,所以内存栅是非常必要的。


static inline void list_add_rcu(struct list_head *new, struct list_head *head)

该函数把链表项new插入到RCU保护的链表head的开头。使用内存栅保证了在引用这个新插入的链表项之前,新链表项的链接指针的修改对所有读者是可见的。


static inline void list_add_tail_rcu(struct list_head *new,
                                        struct list_head *head)

该函数类似于list_add_rcu,它将把新的链表项new添加到被RCU保护的链表的末尾。


static inline void list_del_rcu(struct list_head *entry)

该函数从RCU保护的链表中移走指定的链表项entry,并且把entry的prev指针设置为LIST_POISON2,但是并没有把entry的next指针设置为LIST_POISON1,因为该指针可能仍然在被读者用于便利该链表。


static inline void list_replace_rcu(struct list_head *old, struct list_head *new)

该函数是RCU新添加的函数,并不存在非RCU版本。它使用新的链表项new取代旧的链表项old,内存栅保证在引用新的链表项之前,它的链接指针的修正对所有读者可见。


list_for_each_rcu(pos, head)

该宏用于遍历由RCU保护的链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu链表操作函数(如list_add_rcu)并发运行。


list_for_each_safe_rcu(pos, n, head)

该宏类似于list_for_each_rcu,但不同之处在于它允许安全地删除当前链表项pos。


list_for_each_entry_rcu(pos, head, member)

该宏类似于list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。


list_for_each_continue_rcu(pos, head)

该宏用于在退出点之后继续遍历由RCU保护的链表head。


static inline void hlist_del_rcu(struct hlist_node *n)

它从由RCU保护的哈希链表中移走链表项n,并设置n的ppre指针为LIST_POISON2,但并没有设置next为LIST_POISON1,因为该指针可能被读者使用用于遍利链表。


static inline void hlist_add_head_rcu(struct hlist_node *n,
                                        struct hlist_head *h)

该函数用于把链表项n插入到被RCU保护的哈希链表的开头,但同时允许读者对该哈希链表的遍历。内存栅确保在引用新链表项之前,它的指针修正对所有读者可见。


hlist_for_each_rcu(pos, head)

该宏用于遍历由RCU保护的哈希链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu哈希链表操作函数(如hlist_add_rcu)并发运行。


hlist_for_each_entry_rcu(tpos, pos, head, member)

类似于hlist_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。

对于RCU更详细的原理、实现机制以及应用请参看作者专门针对RCU发表的一篇文章,"Linux 2.6内核中新的锁机制--RCU(Read-Copy Update)"。

十、顺序锁(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相同。


int write_tryseqlock(seqlock_t *sl);

写者在访问被顺序锁s1保护的共享资源前也可以调用该函数来获得顺序锁s1。它实际功能上等同于spin_trylock,只是如果成功获得锁后,该函数增加了一个对顺序锁顺序号的加1操作,以便读者能够检查出是否在读期间有写者访问过。


unsigned read_seqbegin(const seqlock_t *sl);

读者在对被顺序锁s1保护的共享资源进行访问前需要调用该函数。读者实际没有任何得到锁和释放锁的开销,该函数只是返回顺序锁s1的当前顺序号。


int read_seqretry(const seqlock_t *sl, unsigned iv);

读者在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。

因此,读者使用顺序锁的模式如下:


do {
   seqnum = read_seqbegin(&seqlock_a);
//读操作代码块
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)

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


write_seqlock_irq(lock)

写者也可以用该宏来获得顺序锁lock,与write_seqlock不同的是,该宏同时还失效了本地中断。与write_seqlock_irqsave不同的是,该宏不保存标志寄存器。


write_seqlock_bh(lock)

写者也可以用该宏来获得顺序锁lock,与write_seqlock不同的是,该宏同时还失效了本地软中断。


write_sequnlock_irqrestore(lock, flags)

写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还把标志寄存器的值恢复为变量flags的值。它必须与write_seqlock_irqsave配对使用。


write_sequnlock_irq(lock)

写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还使能本地中断。它必须与write_seqlock_irq配对使用。


write_sequnlock_bh(lock)

写者也可以用该宏来释放顺序锁lock,与write_sequnlock不同的是,该宏同时还使能本地软中断。它必须与write_seqlock_bh配对使用。


read_seqbegin_irqsave(lock, flags)

读者在对被顺序锁lock保护的共享资源进行访问前也可以使用该宏来获得顺序锁lock的当前顺序号,与read_seqbegin不同的是,它同时还把标志寄存器的值保存到变量flags中,并且失效了本地中断。注意,它必须与read_seqretry_irqrestore配对使用。


read_seqretry_irqrestore(lock, iv, flags)

读者在访问完被顺序锁lock保护的共享资源进行访问后也可以使用该宏来检查,在读访问期间是否有写者访问了该共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。它与read_seqretry不同的是,该宏同时还把标志寄存器的值恢复为变量flags的值。注意,它必须与read_seqbegin_irqsave配对使用。

因此,读者使用顺序锁的模式也可以为:


do {
   seqnum = read_seqbegin_irqsave(&seqlock_a, flags);
//读操作代码块
...
} while (read_seqretry_irqrestore(&seqlock_a, seqnum, flags));

读者和写者所使用的API的几个版本应该如何使用与自旋锁的类似。

如果写者在操作被顺序锁保护的共享资源时已经保持了互斥锁保护对共享数据的写操作,即写者与写者之间已经是互斥的,但读者仍然可以与写者同时访问,那么这种情况仅需要使用顺序计数(seqcount),而不必要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 2.4以来,内核对SMP的支持越来越好,很大程度上,对SMP的支持,这些锁机制是非常必要和重要的。基本上,内核开发者在开发中都会需要使用一些同步机制,本文通过详细地讲解内核中所有的同步机制,使得读者能够对内核锁机制有全面的了解和把握。


参考资料

  1. Kernel Locking Techniques,http://www.linuxjournal.com/article/5833
  2. Redhat 9.0 kernel source tree
  3. kernel.org 2.6.12 source tree
  4. Linux 2.6内核中新的锁机制--RCU(Read-Copy Update), http://www.ibm.com/developerworks/cn/linux/l-rcu/
  5. Unreliable Guide To Locking.

关于作者

杨燚,计算机科学硕士,毕业于中科院计算技术研究所,有4年的Linux内核编程经验,目前从事嵌入式实时Linux的开发与性能测试。您可以通过yang.yi@bmrtech.comyyang@ch.mvista.com与作者联系。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值