Linux内核中用于同步的几种机制集中起来分析

Linux内核中用于同步的几种机制集中起来分析,强调了它们之间在实现和使用上的不同。
  同步通常是为了达到多线程协同的目的而设计的一种机制,通常包含异步信号机制和互斥机制作为其实现的底层。在Linux 2.4内核中也有相应的技术实现,包括信号量、自旋锁、原子操作和等待队列,其中原子操作和等待队列又是实现信号量的底层。
  
  一. 等待队列和异步信号
  
  wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。我们从它的使用范例着手,看看等待队列是如何实现异步信号功能的。
  
  在核心运行过程中,经常会因为某些条件不满足而需要挂起当前线程,直至条件满足了才继续执行。在2.4内核中提供了一组新接口来实现这样的功能,下面的代码节选自kernel/printk.c:
  
   unsigned long log_size;
  1: DECLARE_WAIT_QUEUE_HEAD(log_wait);...
  4: spinlock_t console_lock = SPIN_LOCK_UNLOCKED;...
   int do_syslog(int type,char *buf,int len){
   ...
  2: error=wait_event_interruptible(log_wait,log_size);
   if(error)
   goto out;
   ...
  5: spin_lock_irq(
   ...
   log_size--;
   ...
  6: spin_unlock_irq(
   ...
   }
   asmlinkage int printk(const char *fmt,...){
   ...
  7: spin_lock_irqsave(console_lock,flags);
   ...
   log_size++;...
  8: spin_unlock_irqrestore(
  3: wake_up_interruptible(log_wait);
   ...
   }
  
  这段代码实现了printk调用和syslog之间的同步,syslog需要等待printk送数据到缓冲区,因此,在2:处等待log_size非0;而printk一边传送数据,一边增加log_size的值,完成后唤醒在log_wait上等待的所有线程(这个线程不是用户空间的线程概念,而是核内的一个执行序列)。执行了3:的wake_up_interruptible()后,2:处的wait_event_interruptible()返回0,从而进入syslog的实际动作。
  
  1:是定义log_wait全局变量的宏调用。
  
  在实际操作log_size全局变量的时候,还使用了spin_lock自旋锁来实现互斥,关于自旋锁,这里暂不作解释,但从这段代码中已经可以清楚的知道它的使用方法了。
  
  所有wait queue使用上的技巧体现在wait_event_interruptible()的实现上,代码位于include/linux/sched.h中,前置数字表示行号:
  
  779 #define __wait_event_interruptible(wq, condition, ret) 780 do { 781 wait_queue_t __wait; 782 init_waitqueue_entry( 783 784 add_wait_queue( 785 for (;;) { 786 set_current_state(TASK_INTERRUPTIBLE); 787 if (condition) 788 break; 789 if (!signal_pending(current)) { 790 schedule(); 791 continue; 792 } 793 ret = -ERESTARTSYS; 794 break; 795 } 796 current->state = TASK_RUNNING; 797 remove_wait_queue( 798 } while (0)
  799
  800 #define wait_event_interruptible(wq, condition) 801 ({ 802 int __ret = 0; 803 if (!(condition)) 804 __wait_event_interruptible(wq, condition, __ret); 805 __ret; 806 })
  
  在wait_event_interruptible()中首先判断condition是不是已经满足,如果是则直接返回0,否则调用__wait_event_interruptible(),并用__ret来存放返回值。__wait_event_interruptible()首先定义并初始化一个wait_queue_t变量__wait,其中数据为当前进程结构current(struct task_struct),并把__wait入队。在无限循环中,__wait_event_interruptible()将本进程置为可中断的挂起状态,反复检查condition是否成立,如果成立则退出,如果不成立则继续休眠;条件满足后,即把本进程运行状态置为运行态,并将__wait从等待队列中清除掉,从而进程能够调度运行。如果进程当前有异步信号(POSIX的),则返回-ERESTARTSYS。
  
  挂起的进程并不会自动转入运行的,因此,还需要一个唤醒动作,这个动作由wake_up_interruptible()完成,它将遍历作为参数传入的log_wait等待队列,将其中所有的元素(通常都是task_struct)置为运行态,从而可被调度到,执行__wait_event_interruptible()中的代码。
  
  DECLARE_WAIT_QUEUE_HEAD(log_wait)经过宏展开后就是定义了一个log_wait等待队列头变量:
  
  struct __wait_queue_head log_wait = {
   lock: SPIN_LOCK_UNLOCKED,
   task_list: { log_wait.task_list,
   if(condition)
   break;
  }while(1);
  
  相对而言,这种操作序列有反复的入队、出队动作,更加耗时,而很大一部分等待操作的确是需要判断一个条件是否满足的,因此2.4才推荐使用wait_event接口。
  
  在wake_up系列接口中,还有一类wake_up_sync()和wake_up_interruptible_sync()接口,保证调度在wake_up返回之后进行。
  
  二. 原子操作和信号量
  
  POSIX有信号量,SysV IPC有信号量,核内也有信号量,接口很简单,一个down(),一个up(),分别对应P操作和V操作,down()调用可能引起线程挂起,因此和sleep_on类似,也有interruptible系列接口。down意味着信号量减1,up意味着信号量加1,这两个操作显然需要互斥。在Linux 2.4中,并没有如想象中的用锁实现,而是使用了原子操作。

 


  
  在include/asm/atomic.h中定义了一系列原子操作,包括原子读、原子写、原子加等等,大多是直接用汇编语句来实现的,这里就不详细解释。
  
  我们从信号量数据结构开始,它定义在include/asm/semaphore.h中:
  
  struct semaphore {
   atomic_t count;
   int sleepers;
   wait_queue_head_t wait;
  }
  
  down()操作可以理解为申请资源,up()操作可以理解为释放资源,因此,信号量实际表示的是资源的数量以及是否有进程正在等待。在semaphore结构中,count相当于资源计数,为正数或0时表示可用资源数,-1则表示没有空闲资源且有等待进程。而等待进程的数量并不关心。这种设计主要是考虑与信号量的原语相一致,当某个进程执行up()函数释放资源,点亮信号灯时,如果count恢复到0,则表示尚有进程在等待该资源,因此执行唤醒操作。一个典型的down()-up()流程是这样

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值