`
lxc63lxc
  • 浏览: 28399 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

《Linux内核设计与实现》ch09

 
阅读更多

《Linux内核设计与实现》ch09
2010年11月01日
  内核同步方法
  Linux提供了一组相当完备的同步方法。
  9.1原子操作
  保证指令以原子方式执行,即执行过程不被打断。
  内核提供了两组原子操作接口,一组针对整数进行操作,另一组针对单独的位进行操作。在Linux支持的所有体系结构上都实现了这两组接口。大多数体系结构要么本来就支持简单的原子操作,要么就为单步执行提供了锁内存总线的指令。
  9.1.1 原子整数操作
  针对整数的原子操作只能对atomic_t类型的数据进行处理。
  在这里之所以引入了一个特殊数据类型,而没有直接使用C语言的int类型,主要出于以下原因:
  1.让原子函数只接受atomic_t类型的操作数,可以确保原子操作数只与这种特殊类型数据一起使用。同时也保证了该类型数据不会被传递给其它任何非原子函数;
  2.使用atomic_t类型确保编译器不对相应的值访问优化--这点使得原子操作最终接收到正确的内存地址,而不只是一个别名;
  3.在不同结构体上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
  使用原子整型操作需要的声明都在文件中。有些体系结构会提供一些只能在该体系结构上使用的额外原子操作方法,但所有的体系结构都能保证内核使用到的所有操作的最小集。
  定义一个atomic_t类型的数据:
  atomic_t   v;         /* 定义 v */
  atomic_t u = ATOMIC_INIT(0);   /*定义u并初始化为0 */
  操作也很简单:
  atomic_set(&v,4);      /* v=4(原子的) */
  atomic_add(2, &v)    /* v = v+2 = 6 (原子的) */
  atomic_inc(&v)        /* v=v+1=7(原子的)   */
  当需要把atomic_t转换成int型时,可以使用atomic_read()来完成:
  printk("%d\n", atomic_read(&v));              /* 将打印 "7" */
  原子整数操作最常见的用途就是实现计数器,atomic_inc()和atomic_dec()。
  原子操作通常都是内联函数(http://blog.csdn.net/northplayboy/archive/2005/12/ 12/550413.aspx),往往是通过内嵌汇编指令来实现的。
  原子性确保指令执行期间不被打断,要么全部执行,要么根本不执行。
  顺序性确保即使两条或多条指令出现在独立的执行线程甚至是独立的处理器上,它们仍然要保持特定的执行顺序。
  9.1.2原子位操作
  除了原子整数操作外,内核还提供了一组针对位这一级数据进行操作的函数。它们是与体系结构相关的操作,定义在中。
  位操作函数是对普通的内存地址进行操作的。它的参数是一个指针和一个位号。
  unsigned long word = 0;
  set_bit(0, &word);          /* 第0位被设置 */
  set_bit(1, &word);
  printk("%ul\n" , word); /* 打印3 */
  非原子位操作的意义?
  例如:假定给出2个原子位操作:先对某位置1,然后清0。如果没有原子操作,那么这一位可能的确清0了,但可能根本没有置位。置位操作可能与清除操作同时发生,但没成功。原子操作能保证置位真正发生。
  9.2自旋锁
  Linux中最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个线程试图获得一个被争用的自旋锁,那么该线程会一直忙循环(特别浪费处理器时间),等待锁重新可用,所以自旋锁不应该被长时间持有。这也是自旋锁的初衷:在短期内进行轻量级加锁。
  另外,还可以采用别的方式处理对锁的争用:让请求线程睡眠,直到重新可用时再唤醒它。这样处理器就不必等待,可以去执行其他代码。但这会带来一定的开销(2次线程切换)。
  自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。这些与体系结构相关的代码定义在文件中,实际上需要用到的接口定义在文件中。基本使用如下:
  spinlock_t   mr_lock = SPIN_LOCK_UNLOCKED;
  spin_lock(&mr_lock);
  /* 临界区 */
  spin_unlock(&mr_lock);
  警告:Linux内核实现的自旋锁是不可递归的,这点不同于自旋锁在其他操作系统上的实现。 
  自旋锁可以用在中断处理程序中。此时,一定要先禁止本地(当前处理器)中断。否则可能出现:中断处理程序打断正持有锁的内核代码,并争用锁,而持有锁者在等待中断程序结束,发生死锁。
  内核为我们提供了禁止中断同时申请锁的接口,使用起来很方便:
  spinlock_t   mr_lock = SPIN_LOCK_UNLOCKED;
  unsigned   long   flags;
  spin_lock_irqsave(&mr_lock, flags);
  /* 临界区   */
  spin_unlock_irqrestore(&mr_lock, flags);
  9.2.1其他对自旋锁的操作
  spin_lock()                     获取指定的自旋锁
  spin_lock_irq()                禁止本地中断并获取指定的锁(不提倡使用)
  spin_lock_irqsave()          保存本地中断的当前状态,禁止本地中断,并获取指定的锁
  spin_unlock()                  释放指定的锁
  spin_unlock_irq()             释放指定的锁,并激活本地中断
  spin_unlock_irqrestore      释放指定的锁,并让本地中断恢复到以前的状态
  spin_lock_init()                 动态初始化指定的spinlock_t
  spin_try_lock()                 试图获得锁,未获得返回非0
  spin_is_locked()               如果指定的锁当前正在被获取,则返回非0,否则,返回0
  9.2.2自旋锁和下半部 函数spin_lock_bh()用于获取指定锁,同时它会禁止所有下半部的执行。相应的spin_unlock_bh()函数执行相反的操作。
  1.由于下半部分可以抢占进程上下文中的代码,所以当两者共享数据时,必须对进程上下文中的共享数据保护,加锁的同时还要禁止下半部执行;
  2.由于中断处理程序可以抢占下半部分,所以当两者共享数据时,必须对下半部分中的共享数据保护,在获得恰当锁的同时还要禁止中断;
  3.同类的tasklet不可能同时运行,所以同类的tasklet中的共享数据不需要保护;对于不同类的tasklet中的共享数据,需要在访问下半部中的数据前先获得一个普通的自旋锁;
  4.对于软中断,无论是否同类型,如果数据被软中断共享,那么它必须得到锁的保护。因为同种类型的软中断可以同时运行在一个系统的多个处理器上。但同一处理器上的一个软中断不会抢占另一个软中断,因此不需要禁止下半部。
  9.3读-写自旋锁
  有时,锁的用途可以明确地分为读取和写入。当对某个数据结构的操作可以被划分为读/写两种类别时,类似读/写锁这样的机制就很有用了。为此,Linux提供了专门的读-写自旋锁。
  这种自旋锁为读和写分别提供了不同的锁。一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有;而且此时不能有并发的读操作。
  有时把读/写锁叫做共享/排斥锁,或者并发/排斥锁,应为这种锁以共享(对读者而言)和排斥(对写者而言)的形式使用。
  例如:
  rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;
  读者代码 写者代码
  read_lock(&mr_rwlock); write_lock(&mr_rwlock);
  /*临界区*/    /*临界区*/
  read_unlock(&mr_rwlock); write_unlock(&mr_rwlock);
  注意:不能把读锁"升级"为写锁。下面的代码会造成死锁:
  read_lock(&mr_rwlock);
  write_lock(&mr_rwlock);
  因此写操作时,要在一开始就请求写锁。
  多个读者可以安全的获得同一个读锁,这个特性使得读-写锁成为一种有用并且常用的优化手段。
  当读锁被持有时,写操作为了互斥访问只能等待,但是,读者可以继续成功的占用锁。而等待的写者在所有读者释放之前无法获得锁。设计锁时要注意这点。
  如果加锁时间可能很长或者代码持有锁时可能睡眠,那么最后使用信号量来完成加锁功能。
  9.4信号量
  Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。所以信号量比自旋锁的开销大。
  从信号量的睡眠特性可以得出一些结论:
  1.由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的;
  2.由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为中断上下文不能进行调度;
  3.占用信号量时,不能占用自旋锁。因为持有自旋锁时不能睡眠。
  其它特点:
  4.信号量不同于自旋锁,它不会禁止内核抢占;
  5.信号量可以同时允许任意数量的锁持有者。实际使用时,基本上用到的都是互斥信号量(计数等于1的信号量)。
  信号量支持两个原子操作P()和V(),后来系统把两种操作分别叫做down()和up()。
  down()通过对信号量减1来请求获得一个信号量,如果结果是0或大于0,获得信号量,任务就可进入临界区。up()用来释放信号量,增加计数值。
  9.4.1创建和初始化信号量
  信号量的实现是与体系结构相关的,具体实现定义在文件中。struct semaphore类型用来表示信号量。
  1.静态声明信号量:
  static DECLARE_SEMAPHORE_GENERIC(name, count)
  name是信号量变量名,count是信号量的使用者数量。
  创建互斥信号量:static DECLARE_MUTEX(name);
  2.更为常见的情况是,信号量作为一个大数据结构的一部分被动态的创建
  此时,你只有指向该动态创建的信号量的间接指针,可以使用如下函数对其初始化:
  sema_init(sem, count);   sem是一个指针;或init_MUTEX(sem);此时count=1。
  9.4.2 使用信号量
  1.函数down_interruptible()试图获取信号量,如果获取失败,他将以TASK_INTERRUPTIBLE(这个状态表示任务是可以被信号唤醒的)状态进入睡眠。如果进程在等待获取信号量时接收到了信号,那么该进程就会唤醒,函数返回-EINTR。
  2.另一个函数down()会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,这种情况当然不是我们想看到的。因此使用down_interruptible()比使用down()更为普遍。
  例子:
  static DECLARE_MUTEX(mr_sem);   /* 定义并声明一个信号量,名字为mr_sem */
  if (down_interruptible(&mr_sem)) {     /*   信号被接收,信号量还未获取   */    }
  /* 临界区 */
  up(&mr_sem);             /* 释放信号量 */
  信号量方法:
  sema_init(struct   semaphore *, int)         以指定的计数值初始化动态创建的信号量
  init_MUTEX(struct semaphore *)             以计数值1初始化动态创建的信号量
  init_MUTEX_LOCKED(struct semaphore *)    以计数值0初始化动态创建的信函量(初始为加锁状态)
  ]down_interruptible(struct semaphore *)    试图获得指定的信号量,如果已被征用,进入可中断睡眠
  down(struct semaphore *)             试图获得指定的信号量,如果被争用,进入不可打断睡眠状态
  down_trylock(struct   semaphore *)        试图获得指定的信号量,被征用则返回非0值
  up(struct   semaphore *)          释放指定的信号量,如果睡眠队列不为空,则唤醒其中一个
  9.5读-写信号量
  在内核中是由rw_semaphore结构表示的,定义在文件中。
  1.静态创建 static DECLARE_RWSEM(name); 2.动态创建的信号量初始化 init_rwsem(struct rw_semaphore *sem)
  所有的读-写信号量都是互斥信号量。只要没有写者,并发持有读锁的读者数不限。只有惟一的写者可以获得锁。所有写锁的睡眠都不会被信号打断,所以它只有一个版本的down操作。
  例子:
  static DECLARE_RWSEM(mr_rwsem);    /* 静态声明一个读--写锁 *?
  down_read(&mr_rwsem);             /*    试图获取信号量并用于读操作    */
  /* 临界区 */
  up_read(&mr_rwsem);       /* 释放信号量   */
  down_write(&mr_rwsem);    /* 试图获取信号量用于写    */
  /*   临界区 */
  up_write(&mr_sem);
  与读-写自旋锁相比,读-写信号量有一个特有的操作:downgrade_writer()。该函数可以将写锁转换为读锁。
  9.6自旋锁与信号量使用的场合,前面已经叙述
  9.7完成变量
  如果内核中一个任务需要发出信号通知另一任务发生了某个特定事件,此时可以利用完成变量。完成变量时使两个任务得以同步的简单方法。
  有结构completion表示,定义在中。
  9.8 seq锁
  seq锁是2.6内核才引入的一种新型锁。这种锁提供了一种很简单的机制,用于读写共享数据。实现这种锁主要依靠一个序列计数器。
  在读数据之前,读取序列号,然后开始读,若读的过程中曾被写打断,则会使序列号增加,读完后,再次读取该序列号。通过比较前后连个序列号,来确定读操作是否曾被写打断。此外,如果读取的是偶数(初值是0),那么就表明写操作没有发生 。
  在多个读者和少数写者共享一把锁(这种情况似乎很少能够发生)的时候,seq锁对写者更有利。只要没有其它写者,写锁总是可以被成功获得。读者不会影响写锁,这点和读者--写者自旋锁及信号量一样。
  9.9内核抢占
  可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。
  9.10顺序和屏障
  当处理多处理器之间或硬件设备之间的同步问题时,有时需要在你的程序代码中以指定的顺序发出读内存和写内存的指令。
  1.在和硬件交互式,时常需要确保给定的读操作发生在其它读或写操作之前;
  2.多处理器上,可能需要按写数据的顺序读数据。但编译器和处理器为了提高效率,可能对读和写重新排序;
  因此需要机器指令来确保顺序要求,这些指令称为屏障。
  rmb()方法提供了一个"读"内存屏障,它确保跨越rmb()的载入动作不会发生重排序。
  wmb()方法提供了一个"写"内存屏障,功能与rmb()类似,区别仅仅在于它是针对存储而非载入。
  mb()方法既提供了读屏障也提供了写屏障。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics