在上一篇博文中笔者分析了关于内存屏障、读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量、读写信号量的内容。
六、信号量
关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码;不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态。它的定义是includelinuxsemaphore.h文件中,结构体如图6.1所示。其中的count变量是计数作用,通过使用lock变量实现对count变量的保护,而wait_list则是对申请信号量的进程维护的等待队列。
图6.1 信号量的结构体定义
我们首先看下它是如何使用的,首先定义一个信号量,然后初始化信号量,它包括两种方法。如图6.2所示。方法1:简单的初始化,定义信号量的个数由val决定,实际上val值即是赋给信号量结构体中的count变量;方法2是直接将结构体中count值设置成1,此时信号量可用于实现进程间的互斥量。注意:对于信号量的初始化函数Linux最新版本存在变化,本文所采用的Linux版本已不存在如init_MUTEX和init_MUTEX_LOCKED等初始化函数,同时也更换了名字等,这点读者在阅读的时候需要下,因此笔者建议以后在编程中遇到需要使用信号量的时候尽量采用sema_init(struct semaphore *sem, int val)函数,因为这个函数就目前为止从未发生变化。
图6.2 信号量的初始函数
下面我们讨论如何获得信号量,它主要包括三个函数,第一个函数表示当信号申请不到时会进程会休眠;对于第二个函数来说,它表示如果当进程因申请不到信号量而进入睡眠后,能被信号打断,这里所说的信号是指进程间通信的信号,比如我们的Ctrl+C,但这时候这个函数的返回值不为0;第三个函数表示信号量无论是否获得,都将立即返回,但返回值会根据是否申请成功而定,同时这个函数也不会导致睡眠。最后的up函数,这个还是很好理解的,就是释放信号量,进而唤醒队列中的等待信号量的进程。函数如图6.3所展示一般。
图6.3 信号量的获得和释放函数
接下来笔者将举几个例子,依次如图6.4,图6.5所示。图6.4的例子仍是实现一个设备只能被一个进程打开。例子很简单,这里便不再细说,主要体现到底如何使用信号量。如图6.4所展示。
图6.4 信号量的使用示例
如图6.5实现的是进程间的同步问题。实际上,当把信号量的初始值为0,则可以实现同步了。正如图6.5所展示一般,对于执行单元A而言,如果执行单元B不执行up函数,执行单元A就因为申请不到进程而睡眠,直至up函数被调用,所以执行代码b前必须等到执行单元B执行完代码c。相信这个内容在操作系统课程都均有提及。
图6.5 信号量实现同步示例
讨论过示例后,相信读者对于信号量的使用有了比较好的了解。下面让我们来简单的讨论下它的实现机制。通过了解信号量的结构体,可以发现结构体中除了使用自旋锁机制外,就是count值的变化(这一点先前已提及)。它的源码如图6.6,图6.7所示。
图6.6 信号量down函数内核源码 图6.7 信号量up函数内核源码
从源码中可以看到信号量利用自旋锁的相关函数实现了对count变量的保护,通过判断变量是否大于0以及自增减来实现信号量的申请和释放。也是因为这一点,所以信号量能够实现同步机制。
另外,关于源码中的__down和__up函数的实现内容,它们其实是维护申请信号量的进程的链表队列(增加、删除操作),以及进程调度方面的一些信息(超时机制),从而让进程实现休眠。由于其中的源码量较为庞大,涉及的内容也较多,故这里不再深入讨论,感兴趣的可以查阅相关资料。
关于信号量的内容还有一点需要提及,我们知道信号量时进程级的,因此对于它的使用必然是当占用资源较长时间的时候。这点在使用信号量的使用需要重点考虑,反之,则容易影响程序的性能等。OK,至此,关于信号量的内容即讨论到此。
七、读写信号量
接下来笔者将讨论有关读写信号量的内容,这部分是较难的一部分,需分析较多的源码。同样,我们首先看下它主要能够做些什么:读写信号量与信号量的关系如同自旋锁与读写自旋锁的关系,它允许N个读操作同时访问共享资源,但最多只能有一个写操作。它的定义位置是在archx86includeasm wsem.h以及kernel wsem.c中。关于它的结构体的定义,如图7.1所示。显然,它的结构体定义和信号量的结构体定义如出一辙。
图7.1 读写信号量的结构体实现
而后了解下它的具体使用方法,如图7.2所展示的函数。同其它锁机制类似,它所提供的接口函数也是较为简单,包括这些函数能实现的功能都差不了多少。
图7.2 读写信号量的接口函数
在了解读写信号量的定义和使用后,接下来读者将讨论的具体源码实现。它的具体实现是采用汇编实现,比较绕,需配合源码来一一说明,源码次序依次如图7.3至图7.8所示。为便于分析,下面源码只给出最为关键的内容。
对于图7.3所展示的内容主要是读写信号量中需要使用的一些宏定义,具体为何取这个值笔者就不大了解了,后续研究深入了可能会有所体会。
图7.3 读写自旋锁的内核源码
图7.5 读写自旋锁的内核源码
图7.4和同7.5所示源码主要体现了读锁和写锁的实现内容。实际上还是利用test指令检测count变量的值来实现。申请成功的内容好理解,其中的xadd指令表示将原操作数和目的操作数值相交换,而后相加保存入目的操作数。一旦读写信号量申请失败,则需要跳转到如图7.6,图7.7所示的源码中。其中图7.6,图7.7中所示的源码没多少内容,最后还是跳转到rwsem_down_read_failed和rwsem_down_write_failed函数中,通过汇编源码保存改变相应存储变量的寄存器,从而再次返回到C代码中,如图7.8所示的源码中。由于笔者能力有限,对于图7.8所示的源码内容中调用的rwsem_down_write_failed函数被没有研究透,故这里并未进一步深入研究。
至此,关于读写信号量的内容讨论基本结束,确实由于笔者能力有限,关于读写信号量的内容以及本博文的系列内容,读者可进一步深入研究,并欢迎讨论。共同进步。
图7.6 读写自旋锁的内核源码 图7.7 读写自旋锁的内核源码
图7.8 读写自旋锁的内核源码
出于文章篇幅的限制,本篇博文到此结束,后续将会给出《大话Linux内核中锁机制之完成量、互斥量》,感兴趣的读者可继续阅读后一篇博文。由于笔者水平所限,博文中难免有出错之处,欢迎读者指出,大家相互讨论,共同进步。