前面介绍了同步的概念以及使用的场合。接下来要介绍的是我们为了实现内核的同步,linux里面所提供的一些方法,当然从另外一个角度我们也可以看出linux的系统实现相当的完备。
开头大致的罗列一下我们系统里面的同步方法吧:
1.
原子操作
2.
位操作
3.
自选锁
4.
读写锁
5.
信号量
6.
完成变量
7.
Seq锁
8.
禁止抢占
9.
BKL大内核锁
10.
。。。。。
上面我们列举了那么多,不知道你常用那些来完成你的代码。反正我常用的主要就是原子操作、位操作、自旋锁、信号量等。我们挨个在说明一下每个方法的使用要点吧。
首先是原子操作,这个和你们前面那个银行取款的例子可以结合在一起来说。但是出问题时因为对于银行钱的操作可以被中断,假如我的这个操作不能够被打断,即要么扣款成功,要么不能扣款,那么银行就不会判决我们盗窃国家财富了。
更具体的说,在linux系统里面,我们专门定义了atomic类型的原子变量,主要完成对于当个数字的操作。它的操作也比较简单,我们可以举一个例子。
Atomic_t
v;
Atomic_t u =
ATOMIC_INIT(0);
上面两种方式都完成了变量的赋值,接着我们说明一下如何操作这个原子变量。
例如
atomic_set(&v,4)这样就对于V赋值为4,记住我们传递的是地址。
Atomic_add(2,&v),这样就完成了对于这个变量的加二操作。
Atomic_inc(&v)
这个是完成变量的自加。
另外我们还有很多测试的操作,例如
Atomic_sub_and_test(int I, atomic_t *
v) 这个操作就是首先减去i,如果结果等于0就返回真。
更多的函数列表请参考linux的相关头文件。
第二个我们想看的是原子位操作。上面的操作是对于整个数字的,那么接下来我们就针对位进行了操作,在很多时候位图应用的相当广泛,例如linux的进程调度就是一个大的位图,通过它达到基于优先级的O(1)调度,另外就是我们的标志位,常常用bit来设置,特别在netfilter等模块里面,位操作更是司空见惯。
位操作就比较简单了,主要就是对某一位设置,要么为0,要么为1。这样我们常见的主要就是
set_bit(0,&word)
这个例子就死把word的0为置上。
Clear_bit(0,&word)这个操作就把0为的变量清空了。
还有更常用的test_and_set_bit(0,&word),这个操作时设置第0位置并且返回之前的值。但是新的内核不再推荐我们用这种方式,而是用自选锁替代这种方式。
第三个要说的就是自选锁了,自旋锁主要用于核间信息同步使用。自选锁只能被一个执行线程锁持有,如果这个时候有另外一个执行线程还来夺取这把锁,新的执行线程就会一直忙循环等待,直到之前的执行线程释放自选锁。通过上面的简单描述我们也可以知道,单CPU是不能开启自选锁的,不然肯定死锁,另外自选锁也是不能递归的,不然也是死锁。
自选锁可以在中断处理程序中使用,但是有一点特别重要,就是获取自旋锁之前,请禁止本地中断,不然一旦这个中断处理程序也要争用这把锁,系统就会死锁。另外还是我们之前强调过很多遍的观点,锁主要用于数据的保护,而不是代码的保护。
接下来介绍的就是读写锁,主要为了提高一个或者几个写着,但是大量读者的情况的操作效率。我们记住其中一点就是了,不要升级锁的类型。例如本来是读锁保护的,千万不要用写锁来开启。
另外就是比较新的代码,已经用RCU来替换这种操作了,Read copy
update。简单的说就是替换整块数据,延迟释放。更多的信息,请参考2.6.16之后的linux内核。
接着出场的就是信号量了,这儿大家都比较熟悉,信号量创建的时候指名这个信号量可以被几个人共享,超过这个限额了,更多的操作就要等待。假如只容许1个用户访问,就变成了临界区或者二进制信号量。他的操作主要就是down和up,分别表示减少和增加一个引用。信号量在应用程序中应用的更为广泛一些,因为信号量可以睡眠。通过这句话我们也大致能够看出他们的差异,自选锁主要用在实际苛刻的场合,不能睡眠,而信号量则相反。
在后面要说的就是完成变量了,他主要是用于异步通告。首先注册一个异步方法,在等待条件完成后,通过触发完成变量,这样注册的回调函数就会被唤醒。
我们最后介绍的就是顺序和屏障,之前介绍的这些操作都是为了完成原则操作,它关注的是存储的操作。而在另外一些场合,要求我们的严格指令顺序,这一点锁就起不到做了,linux他哦共了barrier操作来确保我们的指令顺序。但是我们需要明白的是内存屏障对于cache的影响比较大,所以使用之前,要考虑清楚是不是必须的。