Linux内核中的同步与互斥

系统 Linux
Linux是一个内核。“内核”指的是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。一个内核不是一套完整的操作系统。一套基于Linux内核的完整操作系统叫作Linux操作系统,或是GNU/Linux。内核中进程也存在着同步和互斥,让我们看看是如何分析的。

    先看进程间的互斥。在linux内核中主要通过semaphore机制和spin_lock机制实现。主要的区别是在semaphore机制中,进不了临界区时会进行进程的切换,而spin_lock刚执行忙等(在SMP中)。

    先看内核中的semaphore机制。前提是对引用计数count增减的原子性操作。内核用ato

     mic_t的数据结构和在它上面的一系列操作如atomic_add()、atomic_sub()等等实现。(定义在atomic.h中)

  semaphone机制主要通过up()和down()两个操作实现。

  semaphone的结构为

 

  1.   struct semaphore {  
  2.  
  3.   atomic_t count;  
  4.  
  5.   int sleepers;  
  6.  
  7.   wait_queue_head_t wait;  
  8.  
  9.   };  

 

  相应的down()函数为

 

  1.   static inline void down(struct semaphore*sem)  
  2.  
  3.   {  

 

  /* 1 */sem->count--; //为原子操作

 

  1.   if(sem->count<0)  
  2.  
  3.   {  
  4.  
  5.   struct task_struct *tsk = current;  
  6.  
  7.   DECLARE_WAITQUEUE(wait, tsk);  
  8.  
  9.   tsk->state = TASK_UNINTERRUPTIBLE;  
  10.  
  11.   add_wait_queue_exclusive(&sem->wait, &wait);  
  12.  
  13.   spin_lock_irq(&semaphore_lock);  

 

  /* 2 */ sem->sleepers++;

 

  1.   for (;;) {  
  2.  
  3.   int sleepers = sem->sleepers;  
  4.  
  5.   /*  
  6.  
  7.   * Add "everybody else" into it. They aren't  
  8.  
  9.   * playing, because we own the spinlock.  
  10.  
  11.   */ 

 

  /* 3 */ if (!atomic_add_negative(sleepers - 1, &sem->count)) {

  /* 4 */ sem->sleepers = 0; //这时sem->count=0

 

  1.   break;  
  2.  
  3.   }  

 

  /* 4 */ sem->sleepers = 1; /* us - see -1 above */ // 这时sem

 

  1.   ->count  
  2.  
  3.   =-1  
  4.  
  5.   spin_unlock_irq(&semaphore_lock);  
  6.  
  7.   schedule();  
  8.  
  9.   tsk->state = TASK_UNINTERRUPTIBLE;  
  10.  
  11.   spin_lock_irq(&semaphore_lock);  
  12.  
  13.   }  
  14.  
  15.   spin_unlock_irq(&semaphore_lock);  
  16.  
  17.   remove_wait_queue(&sem->wait, &wait);  
  18.  
  19.   tsk->state = TASK_RUNNING;  
  20.  
  21.   wake_up(&sem->wait);  
  22.  
  23.   }  
  24.  
  25.   }  

 

  相应的up()函数为

 

  1.   void up(struct semaphore*sem)  
  2.  
  3.   {  

 

  sem->count++; //为原子操作

 

  1.   if(sem->count<=0)  
  2.  
  3.   {  

 

  //唤醒等待队列中的一个符合条件的进程(因为每个进程都加了TASK_EXCLUSIVE标志)

  。

  };

  假设开始时,count=1;sleepers=0

#p#

     当进程A执行down()时,引用计数count--,如果这时它的值大于等于0,则从down()中直接返回。如果count少于0,则A的state改为TASK_INTERRUPTIBLE后进入这个信号量的等待队列中,同时使sleepers++;然后重新计算count=sleepers - 1 + count,若这时引用计数仍小于0(一般情况下应为-1,因为count = - sleepers,不过在SMP结构中,期间别的进程可能执行了up()和down()从而使得引用计数的值可能变化),则执行进程切换。

     当进程A又获得机会运行时,它先执行wake_up(&sem->wait)操作,唤醒等待队列里的一个进程,接着它进入临界区,从临界区出来时执行up()操作,使sem>count++,(如果进程A是从down()中直接返回,因为这时等待队列一定为空,所以它不用执行wake_up()操作,直接进入临界区,在从临界区出来时一样执行up()操作,使 sem->count++)。这时如果count的值小于等于0,这表明在它在临界区期间又有一个进程(可能就是它进入临界区时唤醒的那个进程)进入睡眠了,则执行wake_up()操作,反之,如果count的值已经大于0,这表明在它在临界区期间没有别的进程(包括在它进入临界区时被它唤醒过的那个进程)进入睡眠,那么它就可以直接返回了。

     从被唤醒的那个进程看看,如果在唤醒它的进程没执行up()之前它就得到了运行机会,这时它又重新计算count=sleepers - 1 + count=-1;从而sleepers被赋值1;这时它又必须进行调度让出运行的机会给别的进程,自己去睡眠。这正是发生在唤醒它的进程在临界区时运行的时候。

     如果是在唤醒它的进程执行了up()操作后它才得到了运行机会,而且在唤醒它的进程在临界区期间时没别的进程执行down(),则count的值在进程执行up()之前依然为0,这时在up()里面就不必要再执行wake_up()函数了。

     可以通过一个例子来说明具体的实现。设开始sem->count=sem->sleepers=0。也就是有锁但无等待队列 (一个进程已经在运行中)。先后分别进行3个down()操作,和3个up()操作,如下:

     为了阐述方便,只保留了一些会改变sleepers和count值的步骤,并且遵循从左到右依次进行的原则。

  down1:

  count(0->-1),sleepers(0->1),sleepers-1+count(-1),count(-1),sleepers(1),调度

  down2:

  count(-1->-2),sleepers(1->2),sleepers-1+count(-1),count(-1),sleepers(1),调度

  down3:

  count(-1->-2),sleepers(1->2),sleepers-1+count(-1),count(-1),sleepers(1),调度

  up1:

  count(-1->0),唤醒一个睡眠进程(设为1),(进程1得到机会运行)sleepers-1+count

  (0),count(0),sleepers(0),break,

  唤醒另一个睡眠进程(设为2),

  (进程2得到机会运行)sleepers-1+count(-1),count(-1),sleepers(1),调度(没达到

  条件,又得睡觉)

  也可能是这样的:

  up1`:

  count(-1->0),唤醒一个睡眠进程(设为1),(进程1得到机会运行)sleepers-1+count

  (0),count(0),sleepers(0),break,

  唤醒另一个睡眠进程(设为2),

  进程2在以后才得到机会运行)

  up2:

  count(-1->0),(因为count<=0)唤醒一个睡眠进程(设为2),

  进程2得到机会运行)sleepers-+count(0) , count(0) , sleepers(0) ,break,

  唤醒另一个睡眠进程(设为3),

  进程3得到机会运行)sleepers-1+count(-1),count(-1),sleepers(1),调度(没达到条

  件,又得睡觉)

  对应上面的1`:

  up2`:

  count(0->1),(因为count>0,所以直接返回)

  进程2得到机会运行)sleepers-1+count(0),count(0),sleepers(0),break,

  唤醒另一个睡眠进程,(设为3)

  up3:

  count(-1->0),(因为count<=0)唤醒一个睡眠进程(设为3),

  进程3得到机会运行)sleepers-1+count(0),count(0),sleepers(0),break,

  唤醒另一个睡眠进程(这时队列里没进程了)

  进程3运行结束,执行up(), 使count =1 ,这时变成没锁状态 )

  对应上边的2`:

  up3`:

  count(0->1),(因为count>0,所以直接返回)

  进程3得到机会运行)sleepers-1+count(0),count(0),sleepers(0),break,

  唤醒另一个睡眠进程(这时队列里没进程了)

  进程3运行结束,执行up(), 使count =1 ,这时变成没锁状态 )

  当然,还有另一种情况,就是up()操作和down()操作是交*出现的,

  一般的规律就是,如果进程在临界区期间又有进程(无论是哪个进程,新来的还是刚被唤醒的那个)进入睡眠,就会令count的值从0变为-1,从而进程在从临界区出来执行up()里就必须执行一次wake_up(),以确保所有的进程都能被唤醒,因为多唤醒几个是没关系的。如果进程在临界区期间没有别的进程进入睡眠,则从临界区出来执行up()时就用不着去执行wake_up()了(当然,执行了也没什么影响,不过多余罢了)。

     而为什么要把wake_up()和count++分开呢,可以从上面的up1看出来,例如,进程2第一次得到机会运行时,本来这时唤醒它的进程还没执行up()的,但有可能其它进程执行了 up()了,所以真有可能会发现count==1的情况,这时它就真的不用睡觉了,令count=sl  eepers=0,就可以接着往下执行了。

  还可看出一点,一般的,( count ,sleepers)的值的取值范围为(n ,0)[n>0] 和(0

  ,0

  )和 (1 ,-1)。

#p#

  下边看看spin_lock机制。

  Spin_lock采用的方式是让一个进程运行,另外的进程忙等待,由于在只有一个cpu

  的机

  器(UP)上微观上只有一个进程在运行。所以在UP中,spin_lock和spin_unlock就都是空

  的了。

  在SMP中,spin_lock()和spin_unlock()定义如下。

 

  1.   typedef struct {  
  2.  
  3.   volatile unsigned int lock;  
  4.  
  5.   } spinlock_t;  

 

  static inline void spin_lock(spinlock_t *lock)

 

  1.   {  
  2.  
  3.   __asm__ __volatile__(  
  4.  
  5.   "\n1:\t" 
  6.  
  7.   "lock ; decb %0\n\t" 
  8.  
  9.   "js 2f\n" //lock->lock< 0 ,jmp 2 forward  
  10.  
  11.   ".section .text.lock,\"ax\"\n" 
  12.  
  13.   "2:\t" 
  14.  
  15.   "cmpb $0,%0\n\t" //wait lock->lock==1  
  16.  
  17.   "rep;nop\n\t" 
  18.  
  19.   "jle 2b\n\t" 
  20.  
  21.   "jmp 1b\n" 
  22.  
  23.   ".previous" 
  24.  
  25.   :"=m" (lock->lock) : : "memory");  
  26.  
  27.   }  

 

  static inline void spin_unlock(spinlock_t *lock)

 

  1.   {  
  2.  
  3.   __asm__ __volatile__(  
  4.  
  5.   "movb $1,%0" 
  6.  
  7.   :"=m" (lock->lock) : : "memory"); //lock->lock=1  
  8.  
  9.   }  

 

  一般是如此使用:

 

  1.   #define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 }  
  2.  
  3.   spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED;  
  4.  
  5.   spin_lock_(&xxx_lock)  
  6.  
  7.   ...  
  8.  
  9.   critical section  
  10.  
  11.   ...  
  12.  
  13.   spin_unlock (&xxx_lock)  

 

     可以看出,它和semaphore机制解决的都是两个进程的互斥问题,都是让一个进程退出临界区后另一个进程才进入的方法,不过sempahore机制实行的是让进程暂时让出CPU,进入等待队列等待的策略,而spin_lock实行的却是却进程在原地空转,等着另一个进程结束的策略。

     下边考虑中断对临界区的影响。要互斥的还有进程和中断服务程序之间。当一个进程在执行一个临界区的代码时,可能发生中断,而中断函数可能就会调用这个临界区的代码,不让它进入的话就会产生死锁。这时一个有效的方法就是关中断了。

 

  1.   #define local_irq_save(x) __asm__ __volatile__("pushfl ; popl %0 ;  
  2.  
  3.   cli":  
  4.  
  5.   "=g" (x): /* no input */ :"memory")  
  6.  
  7.   #define local_irq_restore(x) __asm__ __volatile__("pushl %0 ; popfl"/*  
  8.  
  9.   no  
  10.  
  11.   output */ :"g" (x):"memory")  
  12.  
  13.   #define local_irq_disable() __asm__ __volatile__("cli": : :"memory")  
  14.  
  15.   #define local_irq_enable() __asm__ __volatile__("sti": : :"memory")  
  16.  
  17.   #define cpu_bh_disable(cpu) do { local_bh_count(cpu)++; barrier(); } while (  
  18.  
  19.   0)  
  20.  
  21.   #define cpu_bh_enable(cpu) do { barrier(); local_bh_count(cpu)--; } while 
  22.  
  23.   (0  
  24.  
  25.   )  
  26.  
  27.   #define local_bh_disable() cpu_bh_disable(smp_processor_id())  
  28.  
  29.   #define local_bh_enable() cpu_bh_enable(smp_processor_id())  

 

  对于UP来说,上面已经是足够了,不过对于SMP来说,还要防止来自其它cpu的影响,这

  时解决的方法就可以把上面的spin_lock机制也综合进来了。

 

  1.   #define spin_lock_irqsave(lock, flags) do {  
  2.  
  3.   local_irq_save(flags); sp  
  4.  
  5.   in_lock(lock); } while (0)  
  6.  
  7.   #define spin_lock_irq(lockdo { local_irq_disable();  
  8.  
  9.   spin_lock(lo  
  10.  
  11.   ck); } while (0)  
  12.  
  13.   #define spin_lock_bh(lockdo { local_bh_disable();  
  14.  
  15.   spin_lock(loc  
  16.  
  17.   k); } while (0)  
  18.  
  19.   #define spin_unlock_irqrestore(lock, flags) do { spin_unlock(lock); local_i  
  20.  
  21.   rq_restore(flags); } while (0)  
  22.  
  23.   #define spin_unlock_irq(lockdo { spin_unlock(lock);  
  24.  
  25.   local_irq_enable();  
  26.  
  27.   } while (0)  
  28.  
  29.   #define spin_unlock_bh(lockdo { spin_unlock(lock);  
  30.  
  31.   local_bh_enable();  
  32.  
  33.   } while (0)  

 

  前面说过,对于UP来说,spin_lock()是空的,所以以上的定义就一起适用于UP 和SM

  P的情形了。

  而read_lock_irqsave(lock, flags) , read_lock_irq(lock),

  read_lock_bh(lock) 和

  write_lock_irqsave(lock, flags) , write_lock_irq(lock),

  write_lock_bh(lock

  ) 就是spin_lock的一个小小的变型而己了。

一口能看完的都是大虾,希望本文能对你们有很大的帮助。

【编辑推荐】

  1. Linux就这样被黑客入侵
  2. Linux Kernel 2.6.37发布 Ext4性能提升
  3. 如何学好Linux 十一大建议
  4. 从头学习Linux基础的8大建议
  5. 眼见为实 近看Sandy Bridge内核真身
  6. 2010年度报告:是谁在编写Linux内核?
  7. Linux内核编译之高手教程

 

 

 

 

 

责任编辑:赵鹏 来源: 网络转载
相关推荐

2009-11-28 20:24:13

Linux互斥锁同步移植

2010-06-28 10:57:23

nohzhres

2021-06-03 08:03:13

网络

2020-09-04 10:14:02

Linux驱动7内核

2020-08-26 08:59:58

Linux线程互斥锁

2009-10-29 09:41:01

Linux内核DeviceMappe

2012-03-09 10:44:11

Java

2010-04-21 12:54:46

Unix内核

2018-11-13 12:52:50

Linux内核栈回溯

2017-03-27 18:05:49

Linux内核编译与开发

2018-05-18 09:07:43

Linux内核内存

2009-09-28 10:09:09

Linux内核Linux循环链表

2023-05-15 08:58:41

块设备驱动Linux

2023-06-02 08:29:24

https://wwMutex

2011-07-28 18:24:15

Linux 3.0内核

2022-04-11 08:42:09

TypeScript子类型定义

2016-09-20 15:21:35

LinuxInnoDBMysql

2012-02-07 16:01:35

Linux内核Android

2010-01-06 16:47:53

Linux内核

2009-12-29 10:24:51

Linux内核循环链表
点赞
收藏

51CTO技术栈公众号