一、导言
在现代操作体系里,同一时刻或许有多个内核履行流在履行,因而内核其实象多进程多线程编程相同也需求一些同步机制来同步各履行单元对同享数据的拜访。尤其是在多处理器体系上,更需求一些同步机制来同步不同处理器上的履行单元对同享的数据的拜访。
在干流的Linux内核中包含了简直一切现代的操作体系具有的同步机制,这些同步机制包含:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(只包含在2.6内核中)和seqlock(只包含在2.6内核中)。
二、原子操作
所谓原子操作,便是该操作绝不会在履行结束前被任何其他使命或事情打断,也就说,它的最小的履行单位,不行能有比它更小的履行单位,因而这儿的原子实践是运用了物理学里的物质微粒的概念。
原子操作需求硬件的支撑,因而是架构相关的,其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_UNLOCKEDspin_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上软中止或中止对同享资源的非同步拜访。