linux内核:Linux内核的同步机制

本文详细介绍了Linux内核中同步机制:原子操作、信号量、读写信号量和自旋锁API使用要求以及些典型举例

  、引言

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

  在主流Linux内核中包含了几乎所有现代操作系统具有同步机制这些同步机制包括:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(只包含在2.6内核中)和seqlock(只包含在2.6内核中)

   2、原子操作

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

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

  原子操作主要用于实现资源计数很多引用计数(refcnt)就是通过原子操作实现原子类型定义如下:

typedef struct { volatile counter; } atomic_t;




  volatile修饰字段告诉gcc不要对该类型数据做优化处理对它访问都是对内存访问而不是对寄存器访问

  原子操作API包括:
atomic_read(atomic_t * v);




  该对原子类型变量进行原子读操作它返回原子类型变量v

atomic_(atomic_t * v, i);




  该设置原子类型变量v值为i

void atomic_add( i, atomic_t *v);




  该给原子类型变量v增加值i

atomic_sub( i, atomic_t *v);




  该从原子类型变量v中减去i

atomic_sub_and_test( i, atomic_t *v);




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

void atomic_inc(atomic_t *v);




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

void atomic_dec(atomic_t *v);




  该对原子类型变量v原子地减1

atomic_dec_and_test(atomic_t *v);




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

atomic_inc_and_test(atomic_t *v);




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

atomic_add_negative( i, atomic_t *v);




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

atomic_add_( i, atomic_t *v);




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

atomic_sub_( i, atomic_t *v);




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

atomic_inc_(atomic_t * v);




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

atomic_dec_(atomic_t * v);



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

  原子操作通常用于实现资源引用计数在TCP/IP协议栈IP碎片处理中就使用了引用计数碎片队列结构struct ipq描述了个IP碎片字段refcnt就是引用计数器类型为atomic_t当创建IP碎片时(在ip_frag_create中)使用atomic_把它设置为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实现)




3、信号量(semaphore)

  Linux内核信号量在概念和原理上和用户态 VIPC机制信号量是但是它绝不可能在内核的外使用因此它和 VIPC机制信号量毫不相干

  信号量在创建时需要设置表示同时可以有几个任务可以访问该信号量保护共享资源值为1就变成互斥锁(Mutex)即同时只能有个任务可以访问信号量保护共享资源

  个任务要想访问共享资源首先必须得到信号量获取信号量操作将把信号量值减1若当前信号量值为负数表明无法获得信号量该任务必须挂起在该信号量等待队列等待该信号量可用;若当前信号量值为非负数表示可以获得信号量因而可以立刻访问被该信号量保护共享资源

  当任务访问完被信号量保护共享资源后必须释放信号量释放信号量通过把信号量值加1实现如果信号量值为非正数表明有任务等待当前信号量因此它也唤醒所有等待该信号量任务

  信号量API有:



DECLARE_MUTEX(name)



  该宏声明个信号量name并化它值为0即声明个互斥锁

DECLARE_MUTEX_LOCKED(name)



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

void sema_init (struct semaphore *sem, 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值非负就直接返回否则者将被挂起直到别任务释放该信号量才能继续运行

down_erruptible(struct semaphore * sem);



  该功能和down类似区别的处为down不会被信号(signal)打断但down_erruptible能被信号打断因此该有返回值来区分是正常返回还是被信号中断如果返回0表示获得信号量正常返回如果被信号打断返回-EINTR

down_trylock(struct semaphore * sem);



  该试着获得信号量sem如果能够立刻获得它就获得该信号量并返回0否则表示不能获得信号量sem返回值为非0值因此它不会导致者睡眠可以在中断上下文使用

void up(struct semaphore * sem);



  该释放信号量sem即把sem值加1如果sem值为非正数表明有任务等待该信号量因此唤醒这些等待者

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

  在内核源码树kernel/prk.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这 3个实际上是分别对downup和down_trylock简单包装

  需要访问console_drivers驱动列表时就需要使用acquire_console_sem来保护console_drivers列表当访问完该列表后release_console_sem释放信号量console_sem

  console_unblankconsole_deviceconsole_stopconsole_startregister_console和unregister_console都需要访问console_drivers因此它们都使用对acquire_console_sem和release_console_sem来对console_drivers进行保护




4、读写信号量(rw_semaphore)

  读写信号量对访问者进行了细分或者为读者或者为写者读者在保持读写信号量期间只能对该读写信号量保护共享资源进行读访问如果个任务除了需要读可能还需要写那么它必须被归类为写者它在对共享资源访问的前必须先获得写者身份写者在发现自己不需要写访问情况下可以降级为读者读写信号量同时拥有读者数不受限制也就说可以有任意多个读者同时拥有个读写信号量

  如果个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量那么任何读者都可以成功获得该读写信号量;否则读者必须被挂起直到写者释放该信号量如果个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量那么个写者可以成功获得该读写信号量否则写者将被挂起直到没有任何访问者因此写者是排他性独占性

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

  读写信号量相关API有:





DECLARE_RWSEM(name)



  该宏声明个读写信号量name并对其进行

void init_rwsem(struct rw_semaphore *sem);



  该对读写信号量sem进行

void down_read(struct rw_semaphore *sem);



  读者来得到读写信号量sem会导致者睡眠因此只能在进程上下文使用

down_read_trylock(struct rw_semaphore *sem);



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

void down_write(struct rw_semaphore *sem);



  写者使用该来得到读写信号量sem它也会导致者睡眠因此只能在进程上下文使用

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访问




5、自旋锁(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上软中断或中断对共享资源非同步访问

  参考资料

Kernel Locking Techniques,http://www.linuxjournal.com/article/5833

Redhat 9.0 kernel source tree

kernel.org 2.6.12 source tree

Linux 2.6内核中新锁机制--RCU(Read-Copy Update),

http://www.ibm.com/developerworks/cn/linux/l-rcu/

Unreliable Guide To Locking.

Tags:  linux内核完全注释 linux内核编译 深入理解linux内核 linux内核

延伸阅读

最新评论

发表评论