Kernel Locking 中文版.docx
- 文档编号:30220838
- 上传时间:2023-08-07
- 格式:DOCX
- 页数:44
- 大小:99.37KB
Kernel Locking 中文版.docx
《Kernel Locking 中文版.docx》由会员分享,可在线阅读,更多相关《Kernel Locking 中文版.docx(44页珍藏版)》请在冰豆网上搜索。
KernelLocking中文版
KernelLocking中文版
UnreliableGuideToLocking
RustyRussell
翻译:
albcamus
Copyright©2003RustyRussell
Thisdocumentationisfreesoftware;youcanredistributeitand/ormodifyitunderthetermsoftheGNUGeneralPublicLicenseaspublishedbytheFreeSoftwareFoundation;eitherversion2oftheLicense,or(atyouroption)anylaterversion.
Thisprogramisdistributedinthehopethatitwillbeuseful,butWITHOUTANYWARRANTY;withouteventheimpliedwarrantyofMERCHANTABILITYorFITNESSFORAPARTICULARPURPOSE.SeetheGNUGeneralPublicLicenseformoredetails.
YoushouldhavereceivedacopyoftheGNUGeneralPublicLicensealongwiththisprogram;ifnot,writetotheFreeSoftwareFoundation,Inc.,59TemplePlace,Suite330,Boston,MA02111-1307USA
FormoredetailsseethefileCOPYINGinthesourcedistributionofLinux.
第1章.介绍
欢迎进入Rusty优秀的《UnreliableGuidetoKernelLocking》细节。
本文档描述了Linux2.6内核的锁系统。
随着Linux内核引入了超线程与抢占,每一个Hacking内核的人都应该了解并发性与为SMP加锁的基础知识。
第2章.并发带来的问题
(如果你已知道并发是什么,请略过本章).
在一个普通的程序里,你可以这样为一个计数器增加计数:
very_important_count++;
下面是你期望的结果:
事情还可能这样发生:
Table2-2.可能的结果
2.1.竞态条件与临界区
这种情况,结果依赖于多个任务的相对执行顺序,叫做竞态条件。
包含并发问题的代码,叫做临界区。
尤其是Linux开始支持SMP机器以来,竞态条件和临界区就成为内核设计与实现的主要问题。
抢占具有相同的影响,即使只有一个CPU也是这样的:
抢占了正在临界区中执行的任务,我们就重现了上面描述的情况。
在这种情况下,抢占了其他代码的线程必须在它的临界区独自运行(排斥其它线程)。
解决的方法是:
认识到何时会发生同时访问,使用锁来保证在任意时刻只有一个实例能够进入临界区。
在Linux内核中有很多友好的原语(primitives)来帮助你做到这点──也有不友好的原语,但我将当作它们并不存在。
第3章.在Linux内核中加锁
我多么希望能给你一句忠告:
不要与比你更不理性的人同眠;而如果是关于锁的忠告,我会这样给出:
保持简单。
不要试图引入新类型的锁。
真是够奇怪的,最后一条竟然恰恰与我的忠告相反──因为这时你已经与不理性的人同眠了,你需要考虑弄只看门狗。
(译者案:
Watchdog是一种以NMI方式检查CPU死锁的机制)
3.1.两种主要类型的锁:
自旋锁与信号量
内核中主要有两种类型的锁。
最基本的类型是自旋锁(include/asm/spinlock.h),它只允许一个持有者;如果你获取自旋锁时不成功,你将一直在自旋中尝试获得它,直到成功。
自旋锁又小又快,可以在任何地方使用。
第二种类型是信号量(include/asm/spinlock.h),它可以在任何时刻被多个持有者同时持有(允许的持有者的数量是在信号量初试化时规定的),但是在通常情况下信号量只允许一个持有者(这时它也叫互斥锁,mutex)。
如果你获取信号量时不成功,任务就会把自身放到一个队列中睡眠,一直到信号量被释放时才被唤醒。
这意味着在你等待的时候,CPU将做其他的事情。
但是,很多情况是不允许睡眠的(参考第9章),这时你应该用自旋锁而不是信号量。
这两种锁的任何一种都是不允许递归的:
参考7.1小节.
3.2.单CPU内核与锁
在单CPU机器上,对于编译时既没打开CONFIG_SMP、也没打开CONFIG_PREEMPT的内核来说,自旋锁根本不存在。
这是一个出色的设计策略:
既然没有别人能在同时刻执行,就没理由加锁。
(译案:
感谢max2005兄的指正)
如果编译时同时打开了CONFIG_SMP和CONFIG_PREEMPT,自旋锁的作用就仅仅是禁止抢占,这就足以防止任何竞态了。
多数情况下,我们可以把抢占等同于SMP来考虑其可能带来的竞态问题,不必单独对付它。
当测试加锁代码时,即使你没有SMP测试环境,总应该打开CONFIG_SMP和CONFIG_PREEMPT选项,因为这会重现关于锁的某些类型的BUG。
信号量依然存在,因为它们之所以被引入,乃是为了同步用户上下文。
我们将马上看到这一点。
3.3.只在用户上下文加锁
如果你的数据结构只可能被用户上下文访问,你可以用信号量(linux/asm/semaphore.h)来保护它。
这是最简单的情况了:
把信号量初试化为可用资源的数量(通常是1),就可以调用down_interruptible()来获取信号量,调用up()来释放它。
还有一个down()函数,但应该避免使用它,因为如果有信号到来它不会返回。
例如:
linux/net/core/netfilter.c允许用nf_register_sockopt()注册新的setsockopt()和getsockopt()调用。
注册和注销只有在模块加载与卸载时才会发生(是在引导时执行的,没有并发问题),注册了的链表只有setsockopt()或getsockopt()系统调用才会查阅。
因此,nf_sockopt_mutex就可以完美的保护住这些,这是因为setsockopt和getsockopt允许睡眠。
3.4.在用户上下文与Softirqs之间加锁
如果一个softirq与用户上下文共享数据,就有两个问题:
首先,当前的用户上下文可能被softirq中断;其次,临界区可能会在别的CPU进入。
这时spin_lock_bh()(include/linux/spinlock.h)就有了用武之地。
它会在那个CPU上禁止softirqs,然后获取锁。
spin_unlock_bh()做相反的工作。
(由于历史原因,后缀‘bh’成为对各种下半部的通称,后者是softwareinterrupts的旧称。
其实spin_lock_bh本应叫作spin_lock_softirq才贴切)
注意这里你也可以用spin_lock_irq()或者spin_lock_irqsave(),这样不单会禁止softirqs,还会禁止硬件中断:
参考第4章。
这在UP上也能很好地工作:
自旋锁消失了,该宏变成了只是local_bh_disable()(include/linux/interrupt.h),它会禁止softirqs运行。
3.5.在用户上下文与Tasklets之间加锁
这种情况与上面的情况(译者案,上面“在用户上下文与softirqs之间加锁”的情况)完全一致,因为tasklets本来就是作为一种softirq运行的。
3.6.在用户上下文与Timers之间加锁
这种情况也和上面情况完全一致,因为timers本来就是一种softirq(译者案:
Timer是时钟中断的下半部)。
从加锁观点来看,tasklets和timers的地位是等同的。
3.7.在Tasklets/Timers之间加锁
有时一个tasklet或timer会与另一个tasklet或timer共享数据。
3.7.1.同一个Tasklet/Timer
由于同一时刻,一个tasklet决不会在两个CPU上执行,即使在SMP机器上,你也不必担心你的tasklet会发生重入问题(同时运行了两个实例)
3.7.2.不同的Tasklets/Timers
如果你的tasklet或timer想要同另一个tasklet或timer共享数据,你需要对它们二者都使用spin_lock()和spin_unlock()。
没必要使用spin_lock_bh(),因为你已经处在tasklet中了,而同一个CPU不会同时再执行其他的tasklet。
3.8.在Softirqs之间加锁
一个softirq经常需要与自己或其它的tasklet/timer共享数据。
3.8.1.同一个Softirq
同一个softirq可能在别的CPU上执行:
你可以使用per-CPU数据(参考8.3小节)以获得更好的性能。
如果你打算这样使用softirq,通常是为了获得可伸缩的高性能而带来额外的复杂性。
你需要用spin_lock()和spin_unlock()保护共享数据。
3.8.2.不同的Softirqs
你需要使用spin_lock()和spin_unlock()保护共享数据,不管是timer,tasklet,还是不同的/同一个/另一个的softirq:
它们中的任何一个都可以在另一个CPU上运行。
第4章.硬中断上下文
硬中断通常与一个tasklet或softirq通信。
这通常涉及到把一个任务放到某个队列中,再由softirq取出来。
4.1.在硬中断与Softirqs/Tasklets之间加锁
如果一个硬件中断服务例程与一个softirq共享数据,就有两点需要考虑。
第一,softirq的执行过程可能会被硬件中断打断;第二,临界区可能会被另一个CPU上的硬件中断进入。
这正是spin_lock_irq()派上用场的地方。
它在那个CPU上禁止中断,然后获取锁。
spin_unlock_irq()做相反的工作。
硬件中断服务例程中不需要使用spin_lock_irq(),因为当它在执行的时候softirq是不可能执行的:
它可以使用spin_lock(),这个会更快一些。
唯一的例外是另一个硬件中断服务例程使用了相同的锁:
spin_lock_irq()会禁止那个硬件中断。
这在UP机器上也能很好的工作:
自旋锁消失了,spin_lock_irq()变成了仅仅是local_irq_disable()(include/asm/smp.h),这将会禁止sofirq/tasklet/BH的运行。
spin_lock_irqsave()(include/linux/spinlock.h)是spin_lock_irq()的变体,它把当前的中断开启与否的状态保存在一个状态字中,用以将来传递给spin_unlock_restore()。
这意味着同样的代码既可以在硬件中断服务例程中(这时中断已关闭)使用,也可以在softirqs中(这时需要主动禁止中断)使用。
注意,softirqs(包括tasklets和timers)是在硬件中断返回时得到运行的,因此spin_lock_irq()同样也会禁止掉它们。
从这个意义上说,spin_lock_irqsave()是最通用和最强大的加锁函数。
4.2.在两个硬中断服务例程之间加锁
很少有两个硬件中断服务例程共享数据的情况。
如果你真的需要这样做,可以使用spin_lock_irqsave():
在进入中断服务时是否自动关闭中断,这件事是体系结构相关的。
第5章.关于锁的使用的图表
PeteZaitcev提供了这样的总结:
*
如果你处在进程上下文(任何系统调用),想把其他进程排挤出临界区,使用信号量。
你可以获得信号量之后去睡眠(例如调用copy_from_user或kmalloc(x,GFP_KERNEL)之类的函数)。
*
否则(亦即:
数据可能被中断访问),使用spin_lock_irqsave()和spin_lock_irqrestore()。
*
避免任何持有自旋锁超过5行代码的情况,避免任何跨越函数调用的只有自旋锁的情况。
(有种情况例外,比方象readb这样的原子访问)
5.1.加锁最低要求表
下面的表列出了在不同上下文中加锁时的最低要求。
在一些情况下,同一个上下文在给定时刻只能在一个CPU上执行,因此不需要锁(例如,某个线程在给定时刻只可能在一个CPU上运行,但如果它需要跟另一个线程共享数据,就需要锁了)
记住上面的建议:
你可以总是只用spin_lock_irqsave(),它是其它加锁原语的超集。
表5-1.加锁的要求
第6章.常见的例子
让我们一步步看一下一个简单的例子:
一个对映射号的缓存(译者案:
原文“acacheofnumbertonamemappings”,虚存子系统不熟,恐怕翻译不确)。
该缓存保存了对象的使用有多频繁这个数据,当它满了,抛出最少使用的那个。
6.1.都在用户上下文
在第一个例子中,我们假定所有的操作都发生在用户上下文(也就是,都在系统调用中),所以允许睡眠。
这意味着我们可以使用信号量来保护cache和其中的所有对象。
下面是代码:
1.#include
2.#include
3.#include
4.#include
5.#include
6.
7.structobject
8.{
9. structlist_headlist;
10. intid;
11. charname[32];
12. intpopularity;
13.};
14.
15.
16./*保护缓存、缓存号和其中的对象*/
17.staticDECLARE_MUTEX(cache_lock);
18.staticLIST_HEAD(cache);
19.staticunsignedintcache_num=0;
20.#defineMAX_CACHE_SIZE10
21.
22./*必须持有cache_lock信号量*/
23.staticstructobject*__cache_find(intid)
24.{
25. structobject*i;
26.
27. list_for_each_entry(i,&cache,list)
28. if(i->id==id){
29. i->popularity++;
30. returni;
31. }
32. returnNULL;
33.}
34.
35./*必须持有cache_lock信号量*/
36.staticvoid__cache_delete(structobject*obj)
37.{
38. BUG_ON(!
obj);
39. list_del(&obj->list);
40. kfree(obj);
41. cache_num--;
42.}
43.
44.
45./*必须持有cache_lock信号量*/
46.staticvoid__cache_add(structobject*obj)
47.{
48. list_add(&obj->list,&cache);
49. if(++cache_num>MAX_CACHE_SIZE){
50. structobject*i,*outcast=NULL;
51. list_for_each_entry(i,&cache,list){
52. if(!
outcast||i->popularity
53. outcast=i;
54. }
55. __cache_delete(outcast);
56. }
57.}
58.
59./*对上面函数的调用,这才是信号量发挥作用的地方──译注*/
60.intcache_add(intid,constchar*name)
61.{
62. structobject*obj;
63.
64. if((obj=kmalloc(sizeof(*obj),GFP_KERNEL))==NULL)
65. return-ENOMEM;
66.
67. strlcpy(obj->name,name,sizeof(obj->name));
68. obj->id=id;
69. obj->popularity=0;
70.
71. down(&cache_lock);
72. __cache_add(obj);
73. up(&cache_lock);
74. return0;
75.}
76.
77.voidcache_delete(intid)
78.{
79. down(&cache_lock);
80. __cache_delete(__cache_find(id));
81. up(&cache_lock);
82.}
83.
84.intcache_find(intid,char*name)
85.{
86. structobject*obj;
87. intret=-ENOENT;
88.
89. down(&cache_lock);
90. obj=__cache_find(id);
91. if(obj){
92. ret=0;
93. strcpy(name,obj->name);
94. }
95. up(&cache_lock);
96.
97.returnret;
98.
99.}
复制代码
注意我们总是保证在持有cache_lock信号量的情况下对缓存添加、删除和查找操作:
缓存结构自身与其中的对象都被该信号量保护起来。
这种情况很简单,因为我们为用户拷贝数据,而不允许用户直接访问对象。
这里有一个轻量(而且很常见)的优化:
在cache_add中,我们先设置对象的各个域,然后再获取锁。
这是安全的,因为没有人能够在我们把对象加入到cache之前就访问它。
6.2.从中断上下文中访问
现在我们来考虑一下cache_find会在中断上下文中被调用的情况:
这个“中断上下文”或者是硬件中断,或者是softirq。
我们使用一个timer来从cache中删除对象。
改写了的代码在下面,用标准的补丁形式给出:
以-开始的行是被删除了的,以+开始的行是新添加了的。
1.---cache.c.usercontext2003-12-0913:
58:
54.000000000+1100
2.+++cache.c.interrupt 2003-12-0914:
07:
49.000000000+1100
3.@@-12,7+12,7@@
4. intpopularity;
5.};
6.
7.-staticDECLARE_MUTEX(cache_lock);
8.+staticspinlock_tcache_lock=SPIN_LOCK_UNLOCKED;
9.staticLIST_HEAD(cache);
10.staticunsignedintcache_num=0;
11.#defineMAX_CACHE_SIZE10
12.@@-55,6+55,7@@
13.intcache_add(intid,constchar*name)
14.{
15. structobject*obj;
16.+ unsignedlongflags;
17.
18. if((obj=kmalloc(sizeof(*obj),GFP_KERNEL))==NULL)
19. return-ENOMEM;
20.@@-63,30+64,33@@
21. obj->id=id;
22. obj->popularity=0;
23.
24.- down(&cache_lock);
25.+ spin_lock_irqsave(&cache_lock,flags);
26. __cache_add(obj);
27.- up(&cache_lock);
28.+ spin_unlock_irqrestore(&cache_lock,flags
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Kernel Locking 中文版