Linux内核中的互斥与同步机制

7300阅读 0评论2013-03-12 wibnmo
分类:LINUX

共有这么几大类
1.自旋锁
2.信号量
3.互斥锁
4.RCU
5.原子变量
6.完成量

文章列举了各个互斥机制所要用的api以及在什么情况下用哪种互斥,并未对内核中的互斥和同步机制详细分析,
只供今后写代码时查阅,如果想了解详细机制可参考LKD或<<深入Linux设备驱动程序内核机制>>等书.

自旋锁
    spin_lock/spin_unlock
    因为只禁止抢占,并未对中断做处理,所以不能在中断上下文用,所以便有了以下变体

    spin_lock_irq/spin_unlock_irq
    禁止抢占,禁止中断,可以在中断上下文用

    spin_lock_irqsave/spin_unlock_irqrestore
    禁止抢占,禁止中断的同时保存中断前处理器FLAGS寄存器的状态,在ARM上是保存CPSR寄存器

    spin_lock_bh/spin_unlock_bh
    相对于spin_lock_irq来说,spin_lock_bh关闭的是softirq

    非阻塞
    spin_trylock
    spin_trylock_irq
    spin_trylock_irqsave
    spin_trylock_bh

    读写者自旋锁
        如果系统中有大量对共享资源的读操作,但并不会改写其内容,那么用spin_lock就会大大降低系统性能
        所以便有了读写者自旋锁rwlock.唯一与自旋锁不同的是可以允许多个读者同时访问,如果有写操作参与那么得互斥

        读取者
        read_lock/read_unlock
        read_lock_irq/read_unlock_irq
        read_lock_irqsave/read_unlock_irqrestore

        写入者    
        write_lock/write_unlock
        write_lock_irq/write_unlock_irq
        write_lock_irqsave/write_unlock_irqrestore

    顺序锁
        typedef struct {
            unsigned sequence;
            spinlock_t lock;
        } seqlock_t;

        顺序锁seqlock的设计思想是写加锁,读不加
        为了保证读取数据的过程中不会由写入者参与,便设置了一个sequence值,读取者在开始读取前读取该值,
        读取操作完成后再读取该值,看两值是否一致,如果不一致,说明数据被更新,读取操作无效.因此写入者在
        开始写入时要更新sequence值

        同样,静态初始化
        #define DEFINE_SEQLOCK(x) \
                seqlock_t x = __SEQLOCK_UNLOCKED(x)

        动态
        seqlock_init

        例子
        //定义一个顺序锁变量demo_seqlock
        DEFINE_SEQLOCK(demo_seqlock)

        //写入者代码
        write_seqlock(&demo_seqlock);    //实际写之前调用write_seqlock获取自旋锁,同时更新sequence的值
        do_write();            //实际的写入操作
        write_unseqlock(&demo_seqlock);    //写入结束,释放自旋锁

        //读取者代码
        unsigned start;
        do {
            //读取操作前先得到sequence的值赋给start,用以在读操作结束后判断是否发生更新
            //注意读操作无需获取锁,但是如果有写操作在进行那么会一直循环读取sequence的值,直到写操作结束
            //read_seqbegin是通过判断sequence的最低位实现的,写操作完成那么sequence&0返回0,否则返回1
            start = read_seqbegin(&demo_seqlock);

            do_read();    //实际的读操作

        } while (read_seqretry(&demo_seqlock, start));    //如果有数据更新,再重新读取

        如果考虑到中断安全问题,可以用
        write_seqlock_irq/write_sequnlock_irq
        write_seqlock_irqsave/write_sequnlock_irqrestore
        write_seqlock_bh/write_sequnlock_bh

        read_seqbegin_irqsave
        read_seqretry_irqrestore

        顺序锁seqlock和之前的读写者自旋锁rwlock其实是相同的,者是读写互斥,写写互斥,读读不互斥

信号量
    struct semaphore {
        raw_spinlock_t          lock;
        unsigned int            count;
        struct list_head        wait_list;
    };
    相对于自旋锁来讲,信号量最大的特点就是允许调用它的线程睡眠
    定义信号量    struct semaphore
    初始化信号量    sema_init(struct semaphore *sem, int val)

    信号量主要是DOWN/UP操作,DOWN操作有,不过驱动使用最频繁的是down_interruptible
    down
    down_interruptible
    down_killable
    down_trylock
    down_timeout

    UP操作只有一个
    up

    即使不是信号量的拥有者也可以调用up函数来释放一个信号量,这一点是与mutex不同的

    其实信号量常见的用途就是实现互斥机制,也就是信号量的count为1,也就是任意时刻只允许一个进程进入临界区,用法是
    #define DECLARE_MUTEX(name)

    以免与mutex产生混淆,Thomas Gleixner在2010年9月7号改为了
    #define DEFINE_SEMAPHORE(name)  \
        struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

    所以现在我们用DEFINE_SEMAPHORE

    读写者信号量
        与读写自旋锁一个意思,者是为了提高系统性能
        /*
         * the rw-semaphore definition
         * - if activity is 0 then there are no active readers or writers
         * - if activity is +ve then that is the number of active readers
         * - if activity is -1 then there is one active writer
         * - if wait_list is not empty, then there are processes waiting for the semaphore
         */
        struct rw_semaphore {
            __s32                   activity;
            raw_spinlock_t          wait_lock;
            struct list_head        wait_list;
        #ifdef CONFIG_DEBUG_LOCK_ALLOC
            struct lockdep_map dep_map;
        #endif
        };

        定义
        #define DECLARE_RWSEM(name) \
            struct rw_semaphore name = __RWSEM_INITIALIZER(name)

        初始化
        init_rwsem

        DOWN操作
        down_read
        down_read_trylock
        down_write
        down_write_trylock

        UP操作
        up_read
        up_write

互斥锁
    用信号量实现互斥不是Linux中最经典的用法,于是便有了mutex.
    struct mutex {
        /* 1: unlocked, 0: locked, negative: locked, possible waiters */
        atomic_t        count;
        spinlock_t        wait_lock;
        struct list_head    wait_list;
    #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
        struct task_struct    *owner;
    #endif
    #ifdef CONFIG_DEBUG_MUTEXES
        const char         *name;
        void            *magic;
    #endif
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map    dep_map;
    #endif
    };

    定义并初始化,静态
    #define DEFINE_MUTEX(mutexname) \
        struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)

    动态
    struct mutex
    mutex_init

    DOWN/UP操作,得不到锁就去等待队列睡眠
    mutex_lock/mutex_unlock

RCU
    Read-Copy_Update,即读/写-复制-更新.Linux提供了很多互斥机制,RCU与其他不同的是它是免锁的.
    RCU的应用场景也是读取者/写入者,不同的是RCU不用考虑读/写的互斥问题.

    简单原理是,将读取者和写入者要访问的共享数据放在指针p指向的区域,读取者通过p来访问数据,而写入者通过
    修改这个区域来更新数据.在具体实现上读取者没有太多的事要做,大量的工作都在写入者一方.免锁的实现双方
    必须同时遵守一定的规则.

    读取者所做的工作是
    禁止抢占,对p指针的引用只能在临界区中

    写入者做做的工作是
    1.分配新的空间ptr
    2.更新数据
    3.用新指针ptr替换老指针p
    4.调用call_rcu释放老指针所指向的空间

    注意第4步释放空间时必须确保没有读取者对老指针的引用,内核是通过判断处理器是否发生进程切换来实现的.因为
    读取者是禁止抢占的,所以在临界区不会发生进程切换(就单核而言),如果发生进程切换那么就说明不在临界区了

    例子
    //假设struct shared_data是读者和写者要共同访问的共享数据
    struct shared_data {
        int a;
        int b;
        struct rcu_head rcu;
    };

    //读取者代码
    //读取者调用rcu_read_lock/rcu_read_unlock构建它的读取临界区,所有对指向被保护资源指针的引用都应该只出现在临界区中,
    //而且临界区中的代码不能睡眠
    static void demo_reader(struct shared_data *ptr)
    {
        struct shared_data *p = NULL;

        rcu_read_lock();

        p = rcu_dereference(ptr);    //调用rcu_dereference获得指向共享数据的指针
        if (p)
            do_something...

        rcu_read_unlock();
    }

    //写入者代码
    //写入者提供的回调函数,用于释放老指针
    static void demo_del_oldptr(struct rcu_head *rh)
    {
        struct shared_data *p = container_of(rh, struct shared_data, rcu);
    
        kfree(p);
    }

    static void demo_writer(struct shared_data *ptr)
    {
        struct shared_data *new_ptr = kmalloc(...);
        ...
        new_ptr->a = 30;
        new_ptr->b = 40;

        rcu_assign_pointer(ptr, new_ptr);    //用新指针更新老指针

        call_rcu(ptr->rcu, demo_del_oldptr);    //调用call_rcu让内核在确保所有对老指针ptr的引用都结束后回调demo_del_oldptr释放老指针所指向的区域
    }

    和call_rcu类似的还有一个synchronize_rcu,不过后者会阻塞,它会等待所有对老指针的引用都消失后才执行,所以在中断上下文要用call_rcu

原子变量
    如果需要保护的数据只是一个简单的整型变量,那么可以用原子变量

    typedef struct {
        int counter;
    } atomic_t;

    例子
    atomic_t flag = ATOMIC_INIT(0);

    //Task A
    void add_flag()
    {
        atomic_inc(&flag);
    }

    //Task B
    void add_flag()
    {
        atomic_inc(&flag);
    }

完成量
    struct completion {
        unsigned int done;
        wait_queue_head_t wait;
    };

    静态初始化
    #define DECLARE_COMPLETION(work) \
        struct completion work = COMPLETION_INITIALIZER(work)

    动态
    init_completion

    等待完成所调用的API
    wait_for_completion
    wait_for_completion_interruptible
    wait_for_completion_timeout
    wait_for_completion_interruptible_timeout

    完成
    complete
    complete_all
上一篇:中断-电平/边沿触发时要做的具体工作
下一篇:Android中的Intent详细讲解