Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))

【Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))】丈夫志四海,万里犹比邻。这篇文章主要讲述Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))相关的知识,希望能为你提供帮助。
一、信号量概述

  • Linux中的信号最是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量
  • 这就比自旋锁提供了更好的处理器利用率,因为没有把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销
  • 可以从信号量的睡眠特性得出一些有意思的结论:
    • 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况
    • 相反,锁被短时间持有时,使用信号量就不太适宜了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长
    • 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的
    • 你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其他进程试图获得同一信号量时不会因此而死锁(因为该进程也只是去睡眠而已,而你最终会继续执行的)
    • 在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的
二、计数信号量和二值信号量
  • 最后要讨论的是信号量的一个有用特性,它可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数或简单地叫数量
二值信号量/互斥信号量
  • 通常情况下,信号量和自旋锁一 样,在一个时刻仅允许有一个锁持有者。这时计数等于1,这样的信号量被称为二值信号量(因为它或者由一个任务持有,或者根本没有任务持有它) 或者称为互斥信号量(因为它强制进行互斥)
计数信号量
  • 另一方面,初始化时也可以把数量设置为大于1的非0值。这种情况,信号量被称为计数信号量,它允许在一个时刻至多有count个锁持有者
  • 计数信号量不能用来进行强制互斥,因为它允许多个执行线程同时访问临界区。相反,这种信号量用来对特定代码加以限制,内核中使用它的机会不多
  • 在使用信号量时,基本上用到的都是互斥信号量(计数等于1的信号量)
三、PV操作
  • 信号量在1968年由Edsger Wybe Dijkstra提出,此后它逐渐称为一种常用的锁机制
  • 信号量支持两个原子操作P()和V(),这两个名字来自荷兰语Proberen金额Vershogen。前者叫做测试操作(字面意思是探查),后者叫做增加操作
  • 后来的系统把两种操作分别叫做down()和up(),Linux也遵从这种叫法
    • down()操作通过对信号量计数减1来请求获得一个信号量。如果结果是0或大于0,获得信号量锁,任务就可以进入临界区。如果结果是负数,任务会被放入等待队列,处理器执行其他任务。该函数如同一个动词,降低(down)—个信号量就等于获取该信号量
    • 相反,当临界区中的操作完成后,up()操作用来释放信号量,该操作也被称作是提升(upping)信号量,因为它会增加信号量的计数值。如果在该信号量上的等待队列不为空,那么处于队列中等待的任务在被唤醒的同时会获得该信号量
四、创建和初始化信号量
  • 信号量的实现是与体系结构相关的,具体实现定义在文件< asm/semaphore.h>
  • struct semaphore类型用来表示信号量
  • 可以通过以下方式静态地声明信号量——其中name是信号量变量名,count是信号量的使用数量:
struct semaphore name; sema_init(& name,count);

  • 静态创建互斥信号量可以使用以下快捷方式,name仍然是互斥信号量的变量名:
static DELARE_MUTEX(name);

  • 更常见的情况是,信号量作为一个大数据结构的一部分动态创建。此时,只有指向该动态创建的信号量的间接指针,可以使用如下函数来对它进行初始化:
    • sem是指针,count是信号量的使用者数量
sema_init(sem,count);

  • 与前面类似,初始化一个动态创建的互斥信号量时使用如下函数:
init_MUTEX(sem);

五、使用信号量
  • 下表针对信号量的方法给出了完整列表
Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))

文章图片

down_interruptible()、down()
  • 函数down_interruptible()试图获取指定的信号量,如果信号量不可用,它将把调用进程置成TASK_INTERRUPTIBLE状态——进入睡眠。回忆前面的内容,这种进程状态意味着任务可以被信号唤醒,一般来说这是件好事。如果进程在等待获取信号量的时候接收到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回-EINTR
  • 另外一个函数down()让进程在TASK_UNINTERRUPTIBLE状态下睡眠。你应该不希望这种情况发生,因为这样一来,进程在等待信号景的时候就不再响应信号了
  • 因此,使用down_interruptible()比使用down()更为普遍(也更正确)。也许你会觉得这两个函数名字起得有点不恰当,的确,这些命名并不很理想
down_trylock()函数
  • 使用down_trylock()函数,你可以尝试以堵塞方式来获取指定的信号量。在信号量已被占用时,它立刻返回非0值;否则,它返回0,而且让你成功持有信号量锁
up()函数
  • 要释放指定的信号量,需要调用up()函数。如下:
Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))

文章图片

六、读写信号量
  • 与自旋锁一样,信号量也有分区读-写访问的可能。与读-写自旋锁和普通自旋锁之间的关系差不多,读-写信号量也要比普通信号量更具优势
  • 读 -写信号量在内核中是由rw_semaphore结构表示的,定义在文件< linux/rwsem.h> 中
  • 通过以下语句可以创建静态声明的读-写信号量:
    • 其中name是新信号量名
static DECLARE_RWSEM(name);

  • 动态创建的读-写信号量可以通过以下函数初始化:
init_rwsem(struct rw_semaphore *sem);

  • 所有的读-写信号量都是互斥信号量——也就是说,它们的引用计数等于1 , 虽然它们只对写者互斥,不对读者。只要没有写者,并发持有读锁的读者数不限。相反,只有唯一的写(在没有读者时)可以获得写锁
  • 所有读-写锁的睡眠都不会被信号打断,所以它只有一个版本的down()操作。例如:
Linux(内核剖析):31---内核同步之(信号量(semaphore)读写信号量(rw_semaphore))

文章图片

  • 与标准信号量一样,读-写信号量也提供了down_read_trylock()和down_write_trylock()方法。这两个方法都需要一个指向读-写信号量的指针作为参数。如果成功获得了信号量锁,它们返回非0值;如果信号量锁被争用,则返回0。要小心(不知道为什么要这样)这与普通信号量的情形完全相反
  • 读-写信号量相比读-写自旋锁多一种特有的操作:downgrade_write()。这个函数可以动态地将获取的写锁转换为读锁
  • 读-写信号量和读-写自旋锁一样,除非代码中的读和写可以明白无误地分割开来,否则最好不使用它。再强调一次,读-写机制使用是有条件的,只有在你的代码可以自然地界定出读-写时才有价值

    推荐阅读