• Linux驱动程序中的并发控制


    <临界区>
    a:对共享资源进行访问的代码称为临界区。
     

    <原子操作>

        a:原子操作用于执行轻量级,仅仅执行一次的的操作比如修改计数器,有条件的增加值,设置某一位。所谓原子操作是指该操作在执行玩之前绝对不会被打断,原子操作的代码都是用汇编实现的,因为C语言无法实现这样的操作。
       任何拥有锁的代码都必须是原子的,不能休眠。
       b:自旋锁:
            (1)获得锁是要注意不能调用会导致休眠的API。
            (2)拥有自旋锁之前必须禁止本地中断。
       c:原子变量
            原子变量不能是一个大于24位的数据。
     
    <自旋锁>
    a:旋锁是Linux中最简单的并发控制机制,一般用于处理耗时比较短的操作,自旋锁可以确保只有一个进程进入临界区。其他想要进入临界资源的线程必须原地打转,直到获得临界资源的进程释放资源(注意:这里所说的线程不是内核线程,而是执行的线程)
    b:使用步骤
    spinlock_t lock;
    void spin_lock_init(spinlock_t lock);
        加锁
    spin_lock(&lock);
    详解:
    static inline void spin_lock(spinlock_t *lock)
    {
        raw_spin_lock(&lock->rlock);
    }
    可以看出来spin_lock()函数调用的是raw_spin_lock是个一宏,其实现与处理器有一定关系,对于ARM处理器而言,最终展开为:
        static inline void raw_spin_lock(raw_spinlock_t *lock) 
        {
                preempt_disable();
                do_raw_spin_lock(lock);
        }
        解锁
    spin_unlock(&lock);
    注:一般在打开设备的时候使用
    注意事项:
    处理上的当前进程A正在对一个全局变量"a"进行操作,所以在错作之前调用spin_lock()来进入临界区,当他正处于临界区的时候。处理器发生中断,但是切好中断处理函数也要对全局变量"a"进行操作。因为是全局变量就需要使用函数spin_lock()函数来对数据进行保护。但是当中断处理函数试图去获得自旋锁的时候,发现该数据已经枷锁,这样就会导致中断处理函数不能处理完任务,这样就会导致进程死锁。为了解决该问题,可以使用spin_lock()函数改进型。
    spin_lock_irq()和函数spin_lock_irqsave(),这两个函数的共性都是在获得锁之前禁用的本地中断。
     
    c:使用自旋锁应该注意事项
    1)自旋锁实际上是忙等待,当锁不可以获得的时候将CPU不能做任何事情,一直执行"测试并设置"的操作,因此适合用于占用锁时间极短的情况使用,这时候使用才是最合理的。
    2)自旋锁可能会对导致系统死锁,引发这个问题一般是递归使用一个自旋锁(CPU已经使用了一个锁,但是CPU想第二次获得这个锁)
    3)自旋期间不能调用可能引起进程调度的函数,如果进程获得自旋锁后再阻塞,这将导致进程休眠,但是外面还有进程想要获得自旋锁,这将导致进程死锁,系统崩溃。(这些函数包含:copy_to_user(),copy_from_user(),kmolloc()和msleep())
    <信号量>
    当获取的信号量没有释放时,进程会将自身加入到一个等待队列中去睡眠,指导拥有信号量的进程释放信号量后,处于等待队列中的那个进程才被唤醒。
    注:也就是说有获取信号量会导致睡眠,也就是说只有能够睡眠的进程才能够使用信号量——可以推断出中断处理程序不能使用信号量。
    a:定义信号量
    struct semaphore{
        spinlock_t lock;
        unsigned int  count ;
        struct list_head head;
    };
    数据解析:
    lock :用来保护对count的访问
    count:
        count = 0:表示信号量正在被其他进程使用
        count <0:表示至少有一个进程在wait_list(等待队列)等待信号量杯释放
        count>0:便是信号量可以被获得
    head:用来串接获得信号量没有成功进入睡眠状态的进程,这样方便在能获得信号量的时候唤醒该链表上的进程。
    b:信号量的使用
        定义信号量
    struct semaphore sem;
       初始化信号量
    void sem_init(struct semaphore *sem,int val);
        锁定信号量
    void down(struct semaphore sem);
    int down_interruptible(struct semphore *sem);
    其中使用最多的是:
    int down_interruptible(struct semphore *sem)
    {
         unsigned int flags;
         int result = 0;
         spin_lock_irqsave(&sem->lock,flags);
         if(likely(sem->count>0))
         {
              sem->count--; //该变量决定同一时刻可以有多少个进程可以进入
         }
         else
         {
              result = _down_interruptible(sem);
              spin_unlock_irqstore(&sem->lock,flags);
              return result;
         }
    }
    其中函数:
    static noinline int __sched __down_interruptible(struct semaphore *sem)
    {
    return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
    }
    其中函数:
    static inline int __sched __down_common(struct semaphore *sem, long state,
    long timeout)
    {
    struct task_struct *task = current; //当前进程
    struct semaphore_waiter waiter;
     
    list_add_tail(&waiter.list, &sem->wait_list); //将前者加入到后者的链表中
    waiter.task = task;
    waiter.up = 0;
     
    for (;;) {
    if (signal_pending_state(state, task))
    goto interrupted;
    if (timeout <= 0)
    goto timed_out;
    __set_task_state(task, state);
    spin_unlock_irq(&sem->lock);
    timeout = schedule_timeout(timeout); //切换进程,让当前进程进入睡眠状态
    spin_lock_irq(&sem->lock);
    if (waiter.up)
    return 0;
    }
     
        释放信号量
    void up(struct semphore *sem);
    {
            unsigned int flags;
            spin_lock_irqsave(&sem->lock,flags);
            if(likely(list_empty(&sem->wait_list)))
            {
                    sem->count++;//如果信号量的wait_list为空,那么表明其他进程正在等待信号量,则将count加1.
             }
             else
            {
                    __up(sem);//如果wait_list不为空,则说明其他进程 在wait_list上等待该信号量,此时调用_up(sem)来唤醒进                                    //程 
                    spin_lock_irqsave(&sem->lock,flags);
            }
    }
    其中函数:
    static noinline void __sched  __up(struct semaphore *sem)
    {
            struct semaphore_waiter *wiater = list_first_entry(&sem->wait_list,struct semphore_waiter,list); //获得第一个节点
            list_del(&waiter->list); //将睡眠进程从链表中移除
            waiter->up = 1;
            wake_up_porcess(waiter->task);唤醒该进程
    }
     
    <互斥体>
    a:由于互斥体会在面临资源竞争的时候将未获得临界资源的进程置于睡眠状态,所以在中断处理函数中只能使用自旋锁不能使用互斥体和信号量。
    b:互斥体正在慢慢替换旧的的信号量,信号量可以被配置为允许预定数量的进程进入临界资源(但是这是一种很罕见的做法)
     
    <完成量>
    a:Linux中提供了一种机制实现一个线程发送给另一个线程完成某项工作,这种机制就叫做完成量。
    b:完成量的定义
    struct completion{
        unsigned int done;
       wait_queue_head_t wait;        
    }
    解析:
    done:首先done是一个大于unsigned 类型的数据,所有永远大于或等于零。
    done=0:会将想拥有该完成量的进程至于等待队列中
    done>0:等待队列中的进程可以获得完成量
    wait:是一个等待队列头,用来管理当前所有等待在该completion上所有的进程
    c:定义并初始化struct completion{}
    静态定义并初始化一个struct conpletion{}
       DECLARE_COMPLETION()
    动态初始化一个struct completion{}
       init_completion()
    c:完成量的使用
     
    void __sched wait_for_completion(struct completion *x)
    {
    wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
    }
    其中:
    wait_for_common(struct completion *x, long timeout, int state)
    {
    might_sleep();
    spin_lock_irq(&x->wait.lock);
    timeout = do_wait_for_common(x, timeout, state);
    spin_unlock_irq(&x->wait.lock);
    return timeout;
    }
    其中:
     
    static inline long __sched
    do_wait_for_common(struct completion *x, long timeout, int state)
    {
    if (!x->done) //首先检查有多少个完成量(有几个进程完成任务),如果是0则进程将进入睡眠状态
            {
    DECLARE_WAITQUEUE(wait, current);  //声明等待队列节点,并将当前进程的task_struct 赋值给等待队列                                                                                      //节点的private 
    __add_wait_queue_tail_exclusive(&x->wait, &wait); 
    do {
    if (signal_pending_state(state, current)) {
    timeout = -ERESTARTSYS;
    break;
    }
    __set_current_state(state);
    spin_unlock_irq(&x->wait.lock);
    timeout = schedule_timeout(timeout);  //进程睡眠
    spin_lock_irq(&x->wait.lock);
    } while (!x->done && timeout);
    __remove_wait_queue(&x->wait, &wait);
    if (!x->done)
    return timeout;
    }
    x->done--;
    return timeout ?: 1;
    }
    注意:done=0的时候才会调用该函数,调用该函数后,该函数后面的代码都不会执行,直到拥有该完成量的进程调用void completion(struct completion *x)后,才会继续执行小面的进程。
     
    唤醒等待队列:
    void completion(struct completion *x);
    void completion_all(struct completion *x);
     
    *********************************************************************************************************************************
     
    <等待队列>
    等待队列并不是一种互斥机制(是用来实现互斥机制的),等待队列是内核定义的一种数据结构,用来实现其他的内核机制,比如完成量,信号量和工作队列。
    等待队列的实现
    (1)双向列表:
    struct list_head {
    struct list_head *next, *prev;
    };
    (2)等待队列头:
    struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
    };
    typedef struct __wait_queue_head wait_queue_head_t;
    注:spinlock_t lock;
    一般某个结构体中有自旋锁,该锁一般都是为了保护该结构其中的某个成员的读写。这里的“lock”就是为了保护往等待队列头中加入等待队列。当加入完成后再释放锁。
    定义等待队列头的两种方式:
    a:DECLARE_WATI_QUEUE_HEAD()来完成等待队列头对象的静态定义和初始化。
    b:init_waitqueue_head()在运行期间初始化一个等待队列头。
     
    (3)等待队列:
    struct __wait_queue {
    unsigned int flags;
    #define WQ_FLAG_EXCLUSIVE0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
    };
    数据分析:
    flags:唤醒队列上的进程时,该标志会影响操作的行为模式。
    private:等待队列的私有数据结构,使用应用中,用来只想睡眠在该节点上的进程的task_struct (在Linux中用该结构表示一个进程)结构体。
    func:当想要唤醒该节点上是进程的使用的函数一般是默认的。
    task_list:用来将独立的等待队列节点串联起来。
    初始化等待队列节点的两种方式:
    a:DECLARE_WAITQUEUE()定义并初始化一个等待队列。
    #define DEFINE_WAIT_FUNC(name, function)                  
        wait_queue_t name = {                        
            .private    = current,                
            .func       = function,              
            .task_list  = LIST_HEAD_INIT((name).task_list),  
        }  
      
    b:int_waitqueue_entry()在程序运行期间初始化一个等待队列节点对象。
    static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)  
    {  
        q->flags = 0;  
        q->private = p;  
        q->func = default_wake_function;  
    }  
    注意:
           1. 为了使得等待进程在一个等待队列中睡眠,需要调用函数wait_event()函数。进程进入睡眠,将控制权释放给调度器。
           2. 在内核中另一处,调用wake_up()函数唤醒等待队列中的睡眠进程。
    注:使用wait_event()函数使得进程睡眠;而在内核另一处有一个对应的wake_up()函数被调用。
     
    (4)等待队列的应用
    等待队列常用的模式就是实现进程的睡眠等待,当某一进程在运行过程中所需要的资源暂时无法获得的时候,进程进入睡眠状态以让出处理器资源给其他进程。(注意:进入睡眠状态以为着从调度器的运行队列中移除,此时进程被挂载到某一等待队列的节点中,为例实现进程的睡眠机制,进程会产生一个新的等待队列节点,然后将进程的task_struct 放到等待队列的节点对象的private成员中)
     
    (5)进程睡眠
    a: 通过add_wait_queue()函数将一个进程添加到等待队列,首先获得队列的自旋锁,然后调用__add_wait_queue()实现将新的等待进程添加等待队列(添加到等待队列的头部),然后解锁;代码如下:
    static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)  
    {  
        list_add(&new->task_list, &head->task_list);  
    }  
     
    b:另一个函数add_wait_queue_exclusive()的含义与add_wait_queue()函数类似,但是将等待进程添加到等待队列的尾部,并设置WQ_EXCLUSIXE标志。
            使得进程在等待队列上睡眠的另一种方法是:prepare_to_wait(),除了有add_wait_queue()函数的参数外,还要设置进程的状态。
            另一个函数prepare_to_wait_exclusive()语义类似。        
            通常情况下,add_wait_queue()函数不会直接使用,因为add_wait_queue()函数不与具体的逻辑相管理,单纯的一个等待队列的模型是没有意义的,因此通常使用的是wait_event()函数
     
    #define wait_event(wq, condition)                  
    do {                                 
            if (condition)                       
            break;                        
               __wait_event(wq, condition);                  
    } while (0)  
    其中函数:__wait_event()
    #define __wait_event(wq, condition)                    
    do {                                
           DEFINE_WAIT(__wait);                                                     
            for (;;) 
           {                        
                        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);  
                        if (condition)                    
                                break;                    
                       schedule();                   
            }                              
          finish_wait(&wq, &__wait);                
    } while (0)  
     
    其中wq是等待进程需要加入的等待队列,而condition是通过与所等待时间有关的一个C表达式形式给出。表示,条件满足时,可以立即停止处理。
    主要工作由__wait_event()来完成:
           (1) 调用DEFINE_WAIT宏创建等待队列成员;
           (2) 使用一个无线循环,在循环体内,
                    (a) 调用prepare_to_wait()使得进程在等待队列上等待,并将进程状态置为不可中TASK_UNINTERRUPTIBLE;
                    (b) 当进程被唤醒时,检查指定的条件condition是否满足,如果满足则跳出循环,否则将控制权交给调度器,然后进程继续睡眠。
            (3) 调用函数finish_wait()将进程状态设置为TASK_RUNNING,并从等待队列的链表中移除对应的成员。
           其他与wait_event类似的函数:
           1. wait_event_interrupable()函数 ,使得进程处于可中断(TASK_INTERRUPTIBLE)状态,从而睡眠进程可以通过接收信号被唤醒;
           2. wait_event_timeout()函数,等待满足指定的条件,但是如果等待时间超过指定的超时限制则停止睡眠,可以防止进程永远睡眠;
           3. wait_event_interruptible_timeout() 使得进程睡眠,不但可以通过接收信号被唤醒,也具有超时限制。
     
    (6)进程唤醒
           内核中虽然定义了很多唤醒等待队列中进程的函数,但是最终调用的都是__wake_up()
    #define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)  
    #define wake_up_nr(x, nr)       __wake_up(x, TASK_NORMAL, nr, NULL)  
    #define wake_up_all(x)          __wake_up(x, TASK_NORMAL, 0, NULL)  
    #define wake_up_locked(x)       __wake_up_locked((x), TASK_NORMAL)  
      
    #define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)  
    #define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)  
    #define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)  
    #define wake_up_interruptible_sync(x)   __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)  
           而__wake_up()函数在加锁之后调用的是__wake_up_common()
    static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,  
                int nr_exclusive, int wake_flags, void *key)  
    {  
        wait_queue_t *curr, *next;  
      
        list_for_each_entry_safe(curr, next, &q->task_list, task_list) {  
            unsigned flags = curr->flags;  
      
            if (curr->func(curr, mode, wake_flags, key) &&  
                    (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)  
                break;  
        }  
    }  
            其中:q是等待队列,mode指定进程的状态,用于控制唤醒进程的条件,nr_exclusive表示将要唤醒的设置了WQ_FLAG_EXCLUSIVE标志的进程的数目。 
            然后扫描链表,调用func(注册的进程唤醒函数,默认为default_wake_function)唤醒每一个进程,直至队列为空,或者没有更多的进程被唤醒,或者被唤醒的的独占进程数目已经达到规定数目。
    简单的demo:
     
     
    <并发保护的用法>
    a:非抢占内核,单CPU情况下存在的于进程上下文的临界区。
        该情况不需要加锁
    b:非抢占内核,但CPU情况下存在进程和中断上下文的临界区资源。
        在进入临界区之前需要禁止中断,这样才能保护资源:
            local_irq_disable();        
            local_irq_enable();
    c:可抢占内核,单CPU情况下存在进程和中断上下文的临界区资源。
         如果使能了抢占,仅仅禁用了中断还不能很好的保护临界资源,因为另外一个处于进程上下文之间的资源可能会进入临界资源,所以解决该问题的方式是在进入临界资源之前禁止抢占和禁止中断:
            spin_lock_irqsave(&my_lock,flags)
            spin_unlock_irqrestore($&my_lock,flags)
    d:可抢占内核,SMP情况下存在于进程和中断上下文的临界资源。
            同上

    <wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

  • 相关阅读:
    算法提高 11-2删除重复元素
    Codeforces 402 D Upgrading Array
    Codeforces 351B Jeff and Furik
    湖南多校对抗赛(2015.03.28) I Inversion Sequence
    湖南多校对抗赛(2015.03.28) H SG Value
    湖南多校对抗赛(2015.03.28) G Good subsequence
    湖南多校对抗赛(2015.03.28) E Longest Increasing Subsequence Again
    湖南多校对抗赛(2015.03.28) B Design road
    湖南多校对抗赛(2015.03.28) A Rectangle
    Codeforces 515D Drazil and Tiles
  • 原文地址:https://www.cnblogs.com/big-devil/p/8589961.html
Copyright © 2020-2023  润新知