Linux 内核同步机制

    本文将就自己对内核同步机制的一些简要理解,做出一份自己的总结文档。

    Linux内部,为了提供对共享资源的互斥访问,提供了一系列的方法,下面简要的一一介绍。

Technorati 标签: 互斥 Linux

    为了更加清晰的了解Linux内核中为什么需要同步机制,先来简要分析以下 在内核中 并发的来源,简要概述如下:

    1.  中断处理

         当系统正在执行A进程时,发生了中断,内核进入中断服务程序B,此时,A和B之间是并发的,对于关键资源的访问,可能会发生竞态。

     2. 调度器的可抢占性

          在单处理器上,可能因此导致进程与进程之间的并发,没有谁知道分配给该进程的时间片在什么时候用完?

          调度器一般在以下几种情况下触发运行,大前提是 内核允许 可抢占 机制。

          2.1 从中断服务程序返回时,查看待运行进程的优先级和被打断的进程优先级,如果有更高的,则转向更高优先级的进程。

          2.2 应用层调用系统调用,系统调用在返回应用层时

          2.3 分配给当前进程的时间片用光以及进行状态发生改变时

          调度器依赖于时钟中断提供精确计时,在时钟中断处理函数do_timer中会更新系统时间和进程时间片,中断返回时,调用系统调用返回函数(此函数也将用于系统调用、异常处理的返回),检测need_resched标志,如果为1,则调用调度程序 schedule(),调度程序会根据调度策略,从待运行队列中选择下一个应该运行的进程,               

      3. 多处理器的并发执行

          在多处理器上,进程和进程是严格意义上的并发,每个处理器都独自调度一个进程,也就是说,在同一时刻,有多个进程在同时运行。

     如果你的数据有可能被其他执行线程访问,那么这些数据就必须要加上适当的锁保护。给数据加锁,而不是给代码加锁。

    

   

中断屏蔽

使用方法:

local_irq_disable()  //屏蔽中断                           

critical section  //临界区

local_irq_enable()  //开中断  

原因:Linux内核的进程调度依赖时钟中断实现,如果禁止中断,内核就不会调度其他程序,因此,可以保证正在执行的内核路径不会被中断服务程序打断。

缺点:

1. local_irq_disable 和 local_irq_enable 只能禁止和使能本地CPU内中断,对于多CPU的SMP情况,无法解决此类竞争。

2. 屏蔽中断期间,内部一些异步I/O、进程调度无法进行,有可能造成数据丢失等问题。   

变体:local_irq_save(flags),在屏蔽本地CPU中断的基础上,备份禁止中断之前的CPU标志位,可通过local_irq_restore来恢复。

使用例子:

     clipboard

原子操作

     原子操作是内核实现并且保证的最小执行单元,如果需要保护的临界资源为单一变量,可以考虑这种轻量级的保护方式。

     内核提供 对于整形数的原子操作和 对于某一位的原子操作。

   

自旋锁

     自旋锁,从名字上可以知道,这个函数将会自我原地旋转,直到某一条件符合要求,如符合要求,则说明锁已经打开,可以进入,若不符合要求,则自己原地转个圈,转完后继续判断条件是否符合要求。

最简单的使用方法如下:

   spin_lock_init(&lock);  //初始化
   spin_lock (&lock);
   ...   //临界区
   spin_unlock(&lock);

自旋锁,对可抢占的内核配置起作用,对于不可抢占的内核,自动退化为空操作。

spin_lock            可以屏蔽由于抢占带来的竞争问题,无法屏蔽由于中断带来的竞争问题。

spin_lock_irq       在spin_lock的基础上,增加屏蔽中断的功能,但不保存中断标志位,

spin_lock_irqsave 在spin_lock_irq的基础上,增加保存中断时的中断标志位。

spin_lock_bh       在spin_lock的基础上,增加屏蔽软中断功能,也就禁止中断底半部的竞争问题。

小结:各种类型的自旋锁的使用,取决于你要保护的变量 可能在哪里被其他进程所修改

        如果在中断顶半部中,那可以使用spin_lock_irqsave

        如果在中断底半部中,那可以使用spin_lock_bh

        如果不在中断中,而是其他进程,那可以使用 spin_lock

    自旋锁保护的临界区代码尽可能短小精悍,如果不想一直等待,可以尝试trylock版本,此版本只检测一次就继续往下执行。

    从代码移植性角度来考虑,即使在单处理器上面,使用local_irq_disable/local_irq_enable来对共享资源进行保护是可以的,最好使用

   spin_lock_irq/spin_unlock_irq来保护,因为如果将来代码移植到多处理器上,原有的保护还是可以生效的。

    

读写锁

     考虑到读取一个变量不会改变它的值,如果A在读取时,不允许B去读取,要等A读取完后,B才能够去读取,这显得不高效,为了应对对于临界区,不会有同时的读写请求发送,一次只能发送一次读,或者一次写。读写锁对 读操作有偏好,在读的同时,可以有多个读进程同时进行读操作,写操作,最多只有一个写进程。

     因为读写是分离设置的,所以读锁和写锁也是分离的。

     最基本的读写锁函数是:

     read_lock  (read_unlock)   //获取读锁,如果不能获取,则自旋,直到获得该读锁。

     write_lock  (write_unlock) //获取写锁,如果不能获取,则自旋,直到获得该写锁

    上述读写锁基本形式,同自旋锁基本形式一样,可以防止因为抢占而带来的竞争问题,但不能防止因中断而带来的竞争问题。

    如果要防止因为中断而带来的竞争问题,可以考虑使用读写锁的禁止中断并且保存中断标志版本 read_lock_irqsave(read_lock_irqrestores),其他的几种情况都是类似分析的。

   clipboard

 

    对于读写操作比例相差悬殊的程序,有两种情况,一种是写操作远远多于读操作,另一种是读操作远远多于写操作,这些情况下的内核并发保护机制如果还是之前的一样,那也太没效率了。下面介绍的两种锁,就是应对上述这两种情况而提出的。

顺序锁

    对于写操作远远多于读操作的进程来说,内核提供了顺序锁这种互斥方式来提高效率,在保证高速写入的同时,确保读取出来的值暂时是最新的。

    我们可以想象一下,如果写操作远远多于读,如果不加任何保护措施,在“貌似”顺序执行的程序中,程序前面读到的全局变量A和程序后面读到的全局变量A,有可能值会发生改变,因为写操作频繁的出现,配合中断和抢占捣乱,这种情况是有可能出现的,因此,需要对互斥量的读操作,如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新去读数据,以便确保读到的数据是完整的。

    这种情况下,读操作在处理上就显得麻烦点,因为偏重于写操作,写操作可以强行插入任何读操作过程中,而不需要打招呼。在这种情况下,在读操作时要能检测到写操作发生了,然后重新读取一次,确保数据是最新的。

    不同的写操作之间是互斥的,写的优先级高于读。

下面以内核时钟更新代码为例子:

顺序锁锁:内核中,时钟中断频率在100HZ,而时间戳又需要定时更新,符合写操作远远都与读操作的情况

do {
    seq = read_seqbegin(&xtime_lock);       //开始读操作
    xts = current_kernel_time();
    tom = wall_to_monotonic;
} while (read_seqretry(&xtime_lock, seq));  //判断在读操作中是否有写操作发生

写操作:

write_seqlock(&xtime_lock);
....
write_sequnlock(&xtime_lock);
 
上述顺序锁的基本形式和读写锁、自旋锁一样,只能避免由于抢占而带来的竞争问题,而不能避免由于中断(硬中断和软中断)而带来的竞争问题,因此,如果要保护的对象,有可能在中断或者软中断中被修改,就需要考虑顺序锁的中断保护版本(硬中断和软中断)。
  小结:

    1.顺序锁:允许读和写操作之间的并发,也允许读与读操作之间的并发,但写与写操作之间只能是互斥的、串行的。

    2.读写自旋锁:只允许读操作与读操作之间的并发,而读与写操作,写与写操作之间只能是互斥的、串行的。

    3.自旋锁:不允许任何操作之间并发。

 

RCU锁

    顺序锁是针对写操作远远多于读操作的情况,那么针对读操作远远多于写操作的情况,比如路由表的更新,文件系统查找定位目录,内核提供了RCU锁来优化这种情况下的互斥效率。

    为了提高读取的效率,RCU采用了读操作不需要任何锁就可以访问,依次获得最高的效率,而写操作,在写入时,首先读取原始数据(Read),然后备份一个副本(Copy),然后对副本进行写入修改操作,等到合适的时候,将真实数据的指针指向副本数据中去(Update),从而完成更新操作。写操作所完成的三个步骤合起来就是RCU。

    读者在访问被RCU保护的共享数据期间不能被阻塞,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在所有读执行单元已经完成对临界区的访问进行修改操作。

 

RCU锁的读取操作:

rcu_read_lock();

…. 临界区

rcu_read_unlock();

 

RCU锁的写操作:

struct rcu_head {
    struct rcu_head *next;                //下一个RCU节点
    void (*func)(struct rcu_head *head);  //获得竞态后的处理函数
};
 
 
                  
synchronize_sched();   // 等待所有CPU都处于可抢占状态,保证所有中断(不包括软中断)处理完毕
 

RCU的写操作有两种模式:

1. 使用synchronize_rcu模式:

synchronize_rcu(void); // 阻塞写者,直到所有读者已经退出读临界区,写者才可以继续下一步操作

                               // 如果同时有多个写者调用此函数,则他们都会阻塞,

    int g_new;

    int a_new;

    a_new = 0x55  ;             // write new value to g_new

    synchronize_rcu();     // block itself until all others read finish read

   

2. 使用call_rcu模式:

void fastcall call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu))//不阻塞写者,可在中断(软中断)中使用\

struct rcu_head {
        struct rcu_head *next;
        void (*func)(struct rcu_head *head);
};

    函数call_rcu也由RCU写端调用,它不会使写者阻塞,因而可以在中断上下文或softirq使用。该函数将把函数func挂接到RCU回调函数链上,然后立即返回。一旦所有的CPU都已经完成端临界区操作,该函数将被调用来释放删除的将绝不在被应用的数据。参数head用于记录回调函数 func,一般该结构会作为被RCU保护的数据结构的一个字段,以便省去单独为该结构分配内存的操作。

 

    定义一个保护结构体:

struct protectRcu
{
    int protect;
    struct rcu_head rcu;
};
struct protectRcu *global_pr;

//一般用来释放老的数据
void callback_function(struct rcu_head *r)
{
    struct protectRcu *t;
    t=container_of(r, struct protectRcu, rcu);
    kfree(t);
}

//RCU写操作
void write_process()
{
    struct protectRcu *t, *old;
    t = kmalloc (sizeof(*t),GFP_KERNEL);//创建副本

    spin_lock(&foo_spinlock);     //创建自旋锁保护
    t->protect = xx;                  //更新副本参数
    old= global_pr;                     
    global_pr=t;                         //用副本替换
    spin_unlock(&foo_spinlock);

    //调用回调函数
    call_rcu(old->rcu, callback_function);
}

RCU部分的精华文章:RCU机制

 

信号量

    信号量与自旋锁功能类似,唯一的区别是,当获取不到时,进程不会像自旋锁那样原地旋转,而是进入休眠状态,让出CPU。

    在内核级别和应用级别,都有信号量的身影。

    主要操作函数有

     初始化信号量 init_MUTEX(&sem);

     down(&sem)      //获取信号量,当获取不到时,会进入睡眠状态,屏蔽中断与抢占,底层实现如下:

     image

     一般使用 down_interruptible(&sem)   //可中断

    if(down_interruptible(&sem)){

    return  - ERESTARTSYS;

    }

    …..  //保护区域

   up(&sem);                        //释放信号量

 

备注:信号量的变体,读写信号量,其概念和用法 ,与自旋锁和读写锁是一样的,就不再过多分析,应该能够举一反三的吧。

        读写信号量:读操作(down_read,up_read) 写操作(down_write,up_write)

        信号量的第二变体,当读写信号量只允许一个读进程和一个写进程时,退化为互斥量。mutex_lock与mutex_unlock。

互斥锁

    互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。

   Linux 2.6.32.65中的互斥锁结构体

    image

定义一个互斥锁:

    struct mutex mutex;

    mutex_init(&mutex);       //初始化互斥所,状态为未锁定,count为1,wait_lock未上锁。等待队列wait_list为空

获取互斥锁:

mutex_lock(&mutex);

mutex_lock_interruptible(&mutex);
   //和mutex_lock()一样,也是获取互斥锁。在获得了互斥锁或进入睡眠直到获得互斥锁之后会返回0。如果在等待获取锁的时候进入睡眠状态收到一个信号(被信号打断睡眠),则返回_EINIR。

mutex_trylock(struct mutex *lock);
试图获取互斥锁,如果成功获取则返回1,否则返回0,不等待。

释放互斥所:

    mutex_unlock(struct mutex *lock);
释放被当前进程获取的互斥锁。该函数不能用在中断上下文中,而且不允许去释放一个没有上锁的互斥锁。

 

完成量

    用于一个执行单元A 等待(wait_for_completion)  另一个执行单元B执行完某事(complete).意思很简单,使用方法也简单。

    先定义,再初始化,接着在一个执行单元A中执行到一定程度后,处于等待状态(wait_for_completion) 另一个执行单元B执行完某事(complete),执行单元B执行完后,执行单元A才能继续往下执行。

等待:    wait_for_completion(&completion)  //不可中断版本  wait_for_completion_interruptible 可中断版本

唤醒:  complete(&completion) //只唤醒一个等待的执行单元  complete_all()唤醒所有等待该完成量的执行单元

 

总结

image

 

参考链接:

http://www.cnblogs.com/hanyan225/archive/2010/10/09/1846628.html

posted @ 2015-01-27 07:52  浩天之家  阅读(688)  评论(0编辑  收藏  举报