linux同步之相关工具

=================================
本文系本站原创,欢迎转载!
转载请注明出处:http://blog.csdn.net/gdt_a20
=================================

摘要:

     以前文为基础,看一下kernel中相关的同步工具;

1.原子操作

           原子操作可以保证指令以原子方式执行,执行过程不会被打断,要么不执行,要么执行完,两个原子操作不可能并发地访问同一变量,

   因此不会引起竞争。

           内核提供了两组院子操作接口---一组针对整数,一组针对单独的位,在linux支持的所有体系结构上都实现了这两组接口,通过用特殊指令或者锁总线的

   方法。

           针对整数的院子操作至鞥年对atomic_t类型数据进行,防止优化,防止传给非原子操作。

          整数相关函数:  

           ATOMIC_INIT(int i)                              //声明一个atomic_t变量并且初始化为i

           int atomic_read(atomic_t *v)             //原子地读取整数变量v

           void atomic_set(atomic_t *v, int i)    //原子地设置v为i

           void atomic_add(int i, atomic_t *v)   //v+i

           void atomic_sub(int i, atomic_t *v)   //v-i

           void atomic_inc(atomic_t *v)             //v+1

           void atomic_dec(atomic_t *v)            //v-1

           int atomic_sub_and_test(int i, atomic_t *v)       //v-i, 结果等于0,返回真,否则返回假 

           int atomic_add_negative(int i, atomic_t *v)       //原子地给v+i,结果是负数,返回真,否则返回假

           int atomic_dec_and_test(atomic_t *v)               //v-1, 结果是0,返回真,否则返回假

           int atomic_inc_and_test(atomic_t *v)                //v+1,如果是0,返回真,否则返回假

        原则:能使用原子就不使用锁,原子系统开销小,对高速缓存影响小。

        位操作相关函数:

           void set_bit(int nr, void *addr)                          //原子地设置addr所指对象的第nr位

           void clear_bit(int nr, void *addr)                       //清空addr第nr位

           void change_bit(int nr, void *addr)                  //反转addr第nr位

           int test_and _set_bit(int nr, void *addr)          //设置addr第nr位,并返回原先的位的值

           int test_and_clear_bit(int nr, void *addr)        //清除addr第nr位,并返回原先的值

           int test_and_change_bit(int nr, void *addr)   //反转addr第nr,并返回原先的值

           int test_bit(int nr, void *addr)                            //原子地返回addr的第nr位

        对应上面的非原子操作,在前面名字前加入 __两个下划线。

           int find_first_bit(unsigned long *addr, unsigned int size)            //找到第一个为1的bit 

           int find_first_zero_bit(unsigned long *addr, unsigned int size)  //找到第一个为0的bit


2.自旋锁

    自旋锁最多只能被一个可执行线程持有,如果一个线程试图获得一个已经被占用的自旋锁,那么该线程
就会一直进行忙循环等待锁重新可用(特别浪费处理器时间).所以自旋锁不应该被长期持有。这个也正式自旋锁设计的要点:在短期内进行轻量级枷锁。

   自旋锁不可递归,自旋锁可以使用在中断处理程序中,但一定要在获取锁之前禁止本地中断,不然可能中断处理程序就会打断正持有锁的内核代码,

   导致再次去争用这个锁,导致死锁,

   禁止中断请求锁的接口:

   spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;

   unsigned long flags;

   spin_lock_irqsave(&mr_lock, flags);

   /* 临界区*/

   spin_lock_irqrestore(&mr_lock, flags);

  无条件地解锁时激活中断(不提倡):

   spinlock_t  mr_lock = SPIN_LOCK_UNLOCKED;

   spin_lock_irq(&mr_lock);

   /* 临界区*/

   spin_lock_irq(&mr_lock);

a.自旋锁和下半部

   同类的tasklet不可能同时进行,所以对于同类tasklet中的共享数据不需要保护,但是当数据被两个不同种类的tasklet共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁。这里不需要禁止下半部,因为在同一个处理器上绝不会有tasklet相互抢占的情况。

  对于软中断,无论是否是同一种类型,如果数据被软中断共享,那么它必须得到锁的保护,因为即使是同种类型的软中断也可以同时运行在一个系统的多个处理器上,但是同一个处理器上的一个软中断绝不会抢占另一个软中断,因此根本没必要禁止下半部。

b.读写自旋锁

  一个或多个任务可以并发的持有读者锁,相反用于写的锁最多只能被一个写任务所持有,而且此时不能有并发的读操作。

  初始化以及使用:

  rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;

  read_lock(&mr_rwlock );

 ...

   read_unlock(&mr_rwlock );

   对于写: 

    write_lock(&mr_rwlock );

     ...

    write_unlock(&mr_rwlock );

  读写锁的相关函数:

   read_lock()                                        //获得指定的读锁

   read_lock_irq()                                 //禁止本地中断并获得指定的读锁

   read_lock_irqsave()                        //保存当前状态,禁止本地中断并且获得指定读锁

   read_unlock()                                   //释放指定的读锁

   read_unlock_irq()                            //释放读锁,激活中断

   read_unlock_irqrestore()               //释放指定的读锁并回复中断状态

   write_lock()                                       //获得指定的写锁

   write_lock_irq()                                //获得指定的写锁并禁止中断

   write_lock_irqsave()                       //保存当前状态,禁止中断,获得写锁

   write_unlock()                                  //释放写锁

   write_unlock_irq()                           //释放指定的写锁并激活本地中断

   write_unlock_irqrestore()              //释放指定的写锁并将本地中断回复到指定的当前状态

   write_trylock()                                  //试图获得指定的写锁,如果不可用,立即返回非0

   rw_lock_init()                                   //初始化指定rwlock_t

   rw_is_locked()                                //如果指定的锁当前已被持有,返回非0,否则返回0

2.信号量

    信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,如果是长期状态,可以选择让请求线程睡眠,知道锁重新可以用时再唤醒它。这样就不会太浪费处理器周期。但这样的另外一个缺点是会带来一定的开销,有两次明显的上下文切换,被阻塞的线程要换出和换入,与实现自选锁少数的几行代码相比,上下文切换当然有较多的代码,因此持有自旋锁的时间最好小于完成两次上下文切换的时间,当然我们大多数人都不会无聊的去测量上下文切换的时间,所以我们让持有自选锁的时间尽可能短就可以了。
    自旋锁和信号量在使用上的一些差异:

    a.争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会长期时间持有的情况

    b.锁被短期持有时,不适宜用信号量,因为睡眠,维护等待队列以及唤醒锁话费的开销要比争用锁的全部时间还要长

    c.由于执行线程在锁被争用时睡眠,所以只能用在进程上下文,因为中断上下文中是不可能进行调度的,其实从理论上讲,中断上下文完全可以使能睡眠,           这样也不会有什么问题,但这样违背了中断的意义,中断的意义在于有一个优先级更高,更紧急的事件发生,内核要放弃当前稍低优先级的进程,进行更       紧急情况的处理,而这时中断又自己去睡眠很不合情理,况且在中断上下文中,中断是随机发生挂接在某个进程中的,连当前进程都不知道到底是一个什         么进程就随性的加入睡眠,调度另外一个进程,这显然也不是很合理的。

    d.你可以在持有信号量的时候睡眠,其它进程在试图获得同一个信号量时不会因此而死锁。

    e.在你占用信号量的同事不能占用自旋锁,因为信号量可以睡眠自旋锁不能睡眠。

   信号量的另一个有用的特性是可以同时允许任意数量的锁持有者,自旋锁在一个时刻至多允许一个持有者,count计数器如果等于1,这样的信号量就叫做

   二值信号量或者互斥,大于1的叫做计数信号量。

   信号量的使用:

               static DECLARE)SEMAPHORE_GENERIC(name, count);

    创建互斥信号量

               static DECLARE_MUTEX(name);

   动态创建信号量

               sema_init(sem, count);

               init_MUTEX(sem);

   down_interruptible()试图获得指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠,这也意味着任务可以被信号唤醒,down ()会  让进程在TASK _uninterruptible 状态下睡眠,这样就不会再响应信号,一次前者更常见。

                static DECLARE_MUTEX(mr_sem);

                if (down_interruptible(&mr_sem)              //获得信号量

                {

                   //信号被接受,但是还没有被获取

                }

                 //临界区

               up(&mr_sem);                                                //释放指定的信号量

     信号量相关函数:

    sema_init(struct semaphore *, init)                    //以指定的计数值初始化动态创建的信号量

    init_MUTEX(struct semaphore *)                        //以计数1初始化动态创建的信号量

    init_MUTEX_LOCKED(struct semaphore *)     //以计数值0初始化动态创建的信号量,即初始化为加锁

    down_interruptible(struct semaphore *)           //一试图获得指定的信号量,如果信号量已被争用,则进入可中断睡眠状态

    down(struct semaphore*)                                    //试图获得指定的信号量,如果信号量已被争用,则进入不可中断睡眠状态

    down_trylock(struct semaphore *)                     //如果已被争用,则立刻返回非0值

    up(struct semaphore *)                                        //释放指定的信号量,如果睡眠队列不为空,则唤醒其中的一个任务

  信号量同样也有读写的对应函数,定义在linux/rwwem.h.

3.完成量

  如果在内核一个任务需要发出信号量通知另一个任务发生了某个特定事件,利用完成量比较简单。

  完成量是信号量的一个简单的解决方法,一个任务要执行一些工作,另一个任务就会在完成变量上等待。

  linux/completion.h

 DECLARE_COMPLETION(mr_comp);

 init_completion()动态的创建并初始化完成变量。

 在指定的完成变来那个上,需要等待的任务调用wait_for_completion()来等待特性时间,特定时间发生活,乘胜时间的任务调用complete来发送信号

  唤醒正在等待的任务。可以参考kernel/sched.c和kernel/forck.c

4.seq锁

   2.6的新型锁,用于共享数据;

5.内核抢占

     内核中的进程在任何时刻都可以停下来以便另一个具有更优先权的进程运行,这意味着一个任务预备抢占的任务可能会在同一个临界去内运行,为了避免这个情况发生,内核抢占代码使用自选锁作为非抢占区域的标记,如果一个自选锁被持有,内核便不能进行抢占。
preempt_disable();禁止抢占
preempt_enable();使能抢占

preempt_enable_no_resched();激活内核抢占但不检查任何被挂起的调度任务

preempt_count();返回抢占计数
抢占计数存放这被持有锁的数量和preempt_disable调用次数,如果是0,那么内核可以进行抢占,如果为1活更大,那么不能抢占.

6.顺序和屏障

   编译器和处理器有时候可能为了提高效率会对指令代码进行重新排序,这样会使问题复杂化,确保顺序的指令叫做屏障。

   rmb()确保在rmb之前的载入操作不会被重新排在该调用之后,rmb之后的也不会排在他的前面。

   wmb()是针对存储。

   rmb()                                              //防止跨越屏障的载入动作发生重新排序

   read_barrier_depends()             //防止跨越屏障的具有数据依赖关系的载入动作重新排序

   wmb()                                             //组织跨越屏障的存储动作发生重新排序

   mb()                                                //祖师跨越屏障的载入和存储发生重排序

   smp_rmb()                                    //同下

   smp_read_barrier_depends()

   smp_wmb()

   smp_mb()                                    //smp上提供mb()功能,在up上提供barrier()功能

   barrier()                                         //阻止编译器括约屏障对载入或存储操作进行优化



7.总结

    概括介绍了内核中用的的同步工具,使用情形,注意要点,重点还是要实际的多运用,加深理解^.^!。


-------------参考<linux内核设计与实现>



  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值