Linux下锁的使用

  1. 简介

Welcome, to Rusty's Remarkably Unreliable Guide to Kernel Locking issues. This document describes the locking systems in the Linux Kernel in 2.6.

With the wide availability of HyperThreading, and preemption in the Linux Kernel, everyone hacking on the kernel needs to know the fundamentals of concurrency and locking for SMP.

ß preemption: processes in user context inside the kernel would preempt each other

  也就是说,抢占意味着在内核态中的用户上下文是可以被抢占的

  1. 并发带来的问题

在一个普通的程序中,你可以通过如下方法增加一个计数器:

very_important_count++;

下面是你期望的结果:

Table 2.1. Expected Results

Instance 1

Instance 2

read very_important_count (5)

 

add 1 (6)

 

write very_important_count (6)

 

 

read very_important_count (6)

 

add 1 (7)

 

write very_important_count (7)

下面是可能产生的结果:

Table 2.2. Possible Results

Instance 1

Instance 2

read very_important_count (5)

 

 

read very_important_count (5)

add 1 (6)

 

 

add 1 (6)

write very_important_count (6)

 

 

write very_important_count (6)

竞态条件与临界区

这种情况,结果依赖于多个任务执行的相对时序,叫做竞态条件(race condtion)。包含并发问题的代码,叫做临界区。尤其是Linux开始支持SMP机器以来,竞态条件和临界区就成为内核设计与实现的主要问题。

抢占具有相同的影响,即使只有一个CPU也是这样的:抢占了正在临界区中执行的任务,就有产生竞态条件。在这种情况下,抢占了其他代码的线程必须在它的临界区独自运行(排斥其它线程)。

解决的方式是:认识到何时会发生同时访问,使用锁来保证在任意时刻只有一个实例能够进入临界区。在Linux内核中有很多友好的原语(primitives)来帮助你做到这点。也有不友好的原语,我们可将它们当做不存在。

  1. 在Linux内核中加锁

关于锁的忠告,我会这样给出:保持简单(keep it simple)。不要试图引入新类型的锁。

  1. 两种主要类型的锁:自旋锁与信号量

内核中主要两种类型的锁。最基本的类型是自旋锁(include/asm/spinlock.h),它只允许一个持有者;如果你获取自旋锁时不成功,你将一直在自旋中尝试获得它,直到成功。自旋锁又小又快,可以在任何地方使用。

第二种类型的锁是互斥锁(include/linux/mutex.h),它有点像自旋锁,但是你有可能在获取互斥锁前被阻塞住。如果你获取互斥锁不成功,任务就会把自身放在一个队列中睡眠,一直到互斥锁被释放时才被唤醒。这就意味着在你等待的时候,CPU将做其它的事情。但是很多情况是不允许睡眠的(如中断上下文),这时你应该用自旋锁而不是信号量。

ß 注:为啥中断上下文不允许睡眠呢?因为在中断上下文中,current的值为被中断时正在运行的进程,如果此时睡眠的话,把被中断的进程加入等待队列,也去调度执行新的进程,这下完了,中断就再也无法被调度回来执行了!!!

这两种锁的任何一种都是不允许递归的。

  1. 单CPU内核与锁

对于没有打开CONFIG_SMP和CONFIG_PREEMPT选项编译的内核,自旋锁根本不存在。这是一个出色的设计策略:既然没有别人能在同时刻执行,就没理由加锁。

如果内核编译时没有打开CONFIG_SMP但是打开了CONFIG_PREEMPT选项的话,自旋锁的作用就仅仅禁止抢占,这就足够防止竞态发生。多数情况下,我们可以把抢占等同于SMP来考虑其可能带来的竞态问题,不必单独对付它。

当测试加锁代码时,即使你没有SMP测试环境,总是应该打开CONFIG_SMP和CONFIG_PREEMPT选项,因为这会重现关于锁的某些类型的BUG。

互斥锁依然存在,因为它们之所以被引入,及是为了同步用户上下文。

 

  1. 只在用户上下文加锁

如果你有一个数据结构只能被用户上下文访问的话,你可以用一个简单的互斥锁(include/linux/mutex.h)来保护它。这是最简单的情况:你初始化这个互斥锁,然后你调用mutex_lock_interruptible()来获取这个互斥锁,通过mutex_unlock()来释放它。还有一个mutex_lock()函数,但应避免使用,因为它在收到一个信号时不会返回。

例如:net/netfilter/nf_sockopt.c允许nf_register_sockopt()注册新的setsockopt()和getsockopt()调用。注册和注销只在模块加载和卸载的时才会发生(在引导的时候执行,没有并发问题),注册了的链表只有setsockopt()或getsockopt()系统调用才会查阅。因此,nf_sockopt_mutex就可以完美的保护这些,这是因为setsockopt和getsockopt允许睡眠。

  1. 在用户上下文与softirqs之间加锁

如果一个软中断与用户上下文共享数据的话,你会遇到两个问题。首先,当前用户上下文能被一个软中断打断,其次,别的CPU也可以进入临界区。这时spin_lock_bh()(include/linux/spinlock.h)就有了用武之地。它会禁止当前CPU的软中断,然后获取自旋锁。spin_unlock_bh()做相反的工作。(由于历史原因,后缀’bh’成为各种下半部的通称。其实spin_lock_bh本应该叫作spin_lock_softirq才贴切)

注:为啥用spin_lock_bh()呢?如果软中断与用户上下文不在同一个CPU上的话,则需要用自旋锁,但软中断又有可能和用户上下文在同一个CPU上执行,如果只用自旋锁的话,则会产生死锁,通过禁止软中断就可以避免这种情况。

注意这里你也可以使用spin_lock_irq()或者spin_lock_irqsave(),这样不单会禁止软中断还会禁止中断。

注:软中断触发的时候是在中断返回的时候!

这在单处理器上也能很好的工作:自旋锁消失了,该宏变成了只有local_bh_disable()(include/linux/interrupt.h),这会禁止软中断运行。

  1. 在用户上下文与tasklets之间的加锁

这种情况与上面的情况完全一致,因为tasklets本来就是作为一种softirq运行的。

  1. 在用户上下文与Timers之间加锁

这种情况也和上面情况完全一致,因为timers本来就是一种软中断。从加锁观点来看,tasklets和timers的地位是等同的。

  1. 在tasklets/Timers之间加锁

有时一个tasklet或timer会与另一个tasklet或者timer共享数据。

  1. 同一个tasklet/timer

由于同一时刻,一个tasklet决不会在两个CPU上执行,即使在SMP机器上,你也不必担心你的tasklet会发生重入问题。

注:tasklet只会被在被调度的CPU上执行。

  1. 不同的tasklets/timers

如果你的tasklet或timer想要同另一个tasklet或timer共享数据,你需要对它们二者都使用spin_lock()和spin_unlock()。没有必要使用spin_lock_bh(),因为你已经处在tasklet中了,而同一个CPU不会同时再执行其它的tasklet了。也就不存在一个CPU上同时执行tasklet而引发spinlock死锁的问题。

 

  1. 在softirqs之间加锁

一个softirq经常需要与自己或其它的tasklet/timer共享数据。

  1. 同一个softirq

同一个softirq可能在别的CPU上执行:你可以使用percpu数据以获得更好的性能。如果你打算这样使用softirq,通常是为了获得可伸缩的高性能而带来额外的复杂性。

你需要用spin_lock()和spin_unlock()保护共享数据。

  1. 不同的softirqs

你需要使用spin_lock()和spin_unlock()保护共享数据,不管是timer,tasklet,还是不同的/同一个/另一个的softirq:它们中的任何一个都可以在另一个CPU上运行。

  1. 硬件中断上下文

硬件中断通常与一个tasklet或softirq通信。这通常涉及到把一个任务放到某个队列中,再由softirq取出来。

  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),这将会禁止softirq/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()是最通用和最强大的加锁函数。

  1. 在两个硬件中断服务例程之间加锁

很少有两个硬件中断服务例程共享数据的情况。如果你真的需要这样做,可以使用spin_lock_irqsave():在进入中断服务时是否自动关闭所有中断,这个是与体系结构相关的。

  1. 关于锁的使用图表

Pete Zaitcev提供了这样的总结:

  1. 如果你处在进程上下文(任何系统调用),想把其它进程排挤出临界区,可以使用互斥锁。你可以获得互斥锁之后去睡眠(例如调用copy_from_user或kmalloc(x, GFP_KERENL)之类的函数)。
  2. 否则(也就是数据会被中断访问),使用spin_lock_irqsave()和spin_lock_irqrestore()。
  3. 避免任何持有自旋锁超过5行代码的情况,避免任何跨越函数调用的只有自旋锁的情况。

加锁的最低要求表

下面的表列出了在不同上下文中加锁时的最低要求。在一些情况下,同一个上下文在给定的时候只能在一个CPU上执行,因此不需要锁(例如,某一线程在给定时刻只可能在一个CPU上运行,但如果它需要跟另一个线程共享数据,就需要锁了)

记住上面的建议:你可以总是只用spin_lock_irqsave(),它是其它加锁原语的超集。

Table 5.1. Table of Locking Requirements

 

IRQ Handler A

IRQ Handler B

Softirq A

Softirq B

Tasklet A

Tasklet B

Timer A

Timer B

User Context A

User Context B

IRQ Handler A

None

 

 

 

 

 

 

 

 

 

IRQ Handler B

SLIS

None

 

 

 

 

 

 

 

 

Softirq A

SLI

SLI

SL

 

 

 

 

 

 

 

Softirq B

SLI

SLI

SL

SL

 

 

 

 

 

 

Tasklet A

SLI

SLI

SL

SL

None

 

 

 

 

 

Tasklet B

SLI

SLI

SL

SL

SL

None

 

 

 

 

Timer A

SLI

SLI

SL

SL

SL

SL

None

 

 

 

Timer B

SLI

SLI

SL

SL

SL

SL

SL

None

 

 

User Context A

SLI

SLI

SLBH

SLBH

SLBH

SLBH

SLBH

SLBH

None

 

User Context B

SLI

SLI

SLBH

SLBH

SLBH

SLBH

SLBH

SLBH

MLI

None

Table 5.2. Legend for Locking Requirements Table

SLIS

spin_lock_irqsave

SLI

spin_lock_irq

SL

spin_lock

SLBH

spin_lock_bh

MLI

mutex_lock_interruptible

 

  1. trylock相关的函数

有这么一些函数试图一次得到锁并且马上返回一个值来表示获取这个锁是成功还是失败。当某个其它线程持有一把锁时,这些函数可以被使用。你可以稍后再获取这把锁来访问被该锁保护的数据。

spin_trylock()不会自旋等待而是如果第一次尝试获取自旋锁成功的话,就会返回非0值,否则返回0。该函数跟spin_lock一样,能在所有的上下文中使用:你必须关闭能够中断你的上下文然后获取自旋锁。

mutex_trylock不会挂起你的任务而是如果第一次尝试获取互斥锁成功的话,就会返回非0值,否则返回0。该函数不能在硬件中断或者软中断上下文中安全的使用,尽管它不会导致睡眠。

  1. 常见的例子

让我们一步步看一下一个简单的例子:number到name的映射cache(相当于通过number来查找name)。该缓存保存了对象使用的有多频繁的数据,当它满时,就抛出最小使用的那个。

  1. 都在用户上下文

在第一个例子中,我们假定所有的操作都发生在用户上下文(比如,都在系统调用中),所以可以允许睡眠。这意味首我们可以使用互斥量来保护cache和其中的所有对象。下面是代码:

#include <linux/list.h>

#include <linux/slab.h>

#include <linux/string.h>

#include <linux/mutex.h>

#include <asm/errno.h>

 

struct object

{

        struct list_head list;

        int id;

        char name[32];

        int popularity;                         ß 表示对象被操作的次数

};

 

/* Protects the cache, cache_num, and the objects within it */

static DEFINE_MUTEX(cache_lock);

static LIST_HEAD(cache);

static unsigned int cache_num = 0;

#define MAX_CACHE_SIZE 10

 

/* Must be holding cache_lock */

static struct object *__cache_find(int id)

{

        struct object *i;

 

        list_for_each_entry(i, &cache, list)

                if (i->id == id) {

                        i->popularity++;

                        return i;

                }

        return NULL;

}

 

/* Must be holding cache_lock */

static void __cache_delete(struct object *obj)

{

        BUG_ON(!obj);

        list_del(&obj->list);

        kfree(obj);

        cache_num--;

}

 

/* Must be holding cache_lock */

static void __cache_add(struct object *obj)

{

        list_add(&obj->list, &cache);

        if (++cache_num > MAX_CACHE_SIZE) {

                struct object *i, *outcast = NULL;

                list_for_each_entry(i, &cache, list) {

                        if (!outcast || i->popularity < outcast->popularity)

                                outcast = i;

                }

                __cache_delete(outcast);

        }

}

 

int cache_add(int id, const char *name)

{

        struct object *obj;

 

        if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)

                return -ENOMEM;

 

        strlcpy(obj->name, name, sizeof(obj->name));

        obj->id = id;

        obj->popularity = 0;

 

        mutex_lock(&cache_lock);

        __cache_add(obj);

        mutex_unlock(&cache_lock);

        return 0;

}

 

void cache_delete(int id)

{

        mutex_lock(&cache_lock);

        __cache_delete(__cache_find(id));

        mutex_unlock(&cache_lock);

}

 

int cache_find(int id, char *name)

{

        struct object *obj;

        int ret = -ENOENT;

 

        mutex_lock(&cache_lock);

        obj = __cache_find(id);

        if (obj) {

                ret = 0;

                strcpy(name, obj->name);

        }

        mutex_unlock(&cache_lock);

        return ret;

}

注意我们总是保证在持有cache_lock互斥锁的情况下对缓存进入添加、删除和查找操作:缓存结构自身与其中的对象都被该互斥锁保护起来。这种情况很简单,因为我们为用户拷贝数据,而不允许用户直接访问对象。

这里有一个轻量(而且很常见)的优化:在cache_add中,我们先设置对象的各个域,然后再获取锁。这是安全的,因为没有人能够在我们把对象加入到cache之前访问它。

  1. 从中断上下文中访问

现在我们来考虑一下cache_find会在中断上下文中被调用的情况:可以是硬件中断或者是软中断。我们使用一个timer来从cache中删除对象。

改写了的代码在下面,用标准的补丁的形式给出:以-开始的行是被删除了的,以+开始的行是新添加了的。

--- cache.c.usercontext      2003-12-09 13:58:54.000000000 +1100

+++ cache.c.interrupt         2003-12-09 14:07:49.000000000 +1100

@@ -12,7 +12,7 @@

         int popularity;

 };

 

-static DEFINE_MUTEX(cache_lock);

+static DEFINE_SPINLOCK(cache_lock);

 static LIST_HEAD(cache);

 static unsigned int cache_num = 0;

 #define MAX_CACHE_SIZE 10

@@ -55,6 +55,7 @@

 int cache_add(int id, const char *name)

 {

         struct object *obj;

+        unsigned long flags;

 

         if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)

                 return -ENOMEM;

@@ -63,30 +64,33 @@

         obj->id = id;

         obj->popularity = 0;

 

-        mutex_lock(&cache_lock);

+        spin_lock_irqsave(&cache_lock, flags);

         __cache_add(obj);

-        mutex_unlock(&cache_lock);

+        spin_unlock_irqrestore(&cache_lock, flags);

         return 0;

 }

 

 void cache_delete(int id)

 {

-        mutex_lock(&cache_lock);

+        unsigned long flags;

+

+        spin_lock_irqsave(&cache_lock, flags);

         __cache_delete(__cache_find(id));

-        mutex_unlock(&cache_lock);

+        spin_unlock_irqrestore(&cache_lock, flags);

 }

 

 int cache_find(int id, char *name)

 {

         struct object *obj;

         int ret = -ENOENT;

+        unsigned long flags;

 

-        mutex_lock(&cache_lock);

+        spin_lock_irqsave(&cache_lock, flags);

         obj = __cache_find(id);

         if (obj) {

                 ret = 0;

                 strcpy(name, obj->name);

         }

-        mutex_unlock(&cache_lock);

+        spin_unlock_irqrestore(&cache_lock, flags);

         return ret;

 }

注意一下,如果中断原来是开启着的,spin_lock_irqsave会关掉它们;否则就什么也不做。这些函数可以安全的在任何上下文中调用。

不幸的是,cache_add调用了带有GFP_KERNEL标志的kmalloc,这只是在用户上下文才是合法的。我们假定cache_add仍然只会在用户上下文中被调用,否则我们就得给cache_add添加参数了。(注:如果打算让cache_add能在中断和用户两种上下文中使用,应该添加一个参数来表达究竟是在哪种上下文中调用的。)

  1. 把对象暴露在文件之外

如果对象包含更多的信息,仅仅提供拷贝函数是不够的:其它代码可能会需要保持一个指向对象的指针,例如,不想每次访问它都要先查找。这就产生了两个问题。

第一个问题是,我们采用cache_lock来保护对象,我们必须把该锁定义为非static的,这样其它文件中的代码才能使用。这使得锁的使用更加技巧化,再也不是在同一个地方了。

第二个问题是生命期问题。如果其它结构保留了一个指向我们的对象的指针,它多半期望该指针总是有效的。很不幸,这只在你持有锁时才会得到保证,否则其它人可能调用cache_delete来删除对象,还可能更糟糕,在删除之后又添加了另一个对象,还在原来的地址上。

但是由于只有一把锁,你也不能总持有它,否则其它人就完全无法工作了。

该问题的解决是使用引用计数:每个持有指向对象的指针的人,在他们第一次获得该指针时,递增一次引用计数;当他们使用完毕,递减一次引用计数。谁把引用计数减到了零,就知道可以删除了,于是执行删除操作。下面是代码:

--- cache.c.interrupt  2003-12-09 14:25:43.000000000 +1100

+++ cache.c.refcnt    2003-12-09 14:33:05.000000000 +1100

@@ -7,6 +7,7 @@

 struct object

 {

         struct list_head list;

+        unsigned int refcnt;

         int id;

         char name[32];

         int popularity;

@@ -17,6 +18,35 @@

 static unsigned int cache_num = 0;

 #define MAX_CACHE_SIZE 10

 

+static void __object_put(struct object *obj)

+{

+        if (--obj->refcnt == 0)

+                kfree(obj);

+}

+

+static void __object_get(struct object *obj)

+{

+        obj->refcnt++;

+}

+

+void object_put(struct object *obj)

+{

+        unsigned long flags;

+

+        spin_lock_irqsave(&cache_lock, flags);

+        __object_put(obj);

+        spin_unlock_irqrestore(&cache_lock, flags);

+}

+

+void object_get(struct object *obj)

+{

+        unsigned long flags;

+

+        spin_lock_irqsave(&cache_lock, flags);

+        __object_get(obj);

+        spin_unlock_irqrestore(&cache_lock, flags);

+}

+

 /* Must be holding cache_lock */

 static struct object *__cache_find(int id)

 {

@@ -35,6 +65,7 @@

 {

         BUG_ON(!obj);

         list_del(&obj->list);

+        __object_put(obj);

         cache_num--;

 }

 

@@ -63,6 +94,7 @@

         strlcpy(obj->name, name, sizeof(obj->name));

         obj->id = id;

         obj->popularity = 0;

+        obj->refcnt = 1; /* The cache holds a reference */

 

         spin_lock_irqsave(&cache_lock, flags);

         __cache_add(obj);

@@ -79,18 +111,15 @@

         spin_unlock_irqrestore(&cache_lock, flags);

 }

 

-int cache_find(int id, char *name)

+struct object *cache_find(int id)

 {

         struct object *obj;

-        int ret = -ENOENT;

         unsigned long flags;

 

         spin_lock_irqsave(&cache_lock, flags);

         obj = __cache_find(id);

-        if (obj) {

-                ret = 0;

-                strcpy(name, obj->name);

-        }

+        if (obj)

+                __object_get(obj);

         spin_unlock_irqrestore(&cache_lock, flags);

-        return ret;

+        return obj;

 }

我们把引用计数操作封装进标准的’get’和’put’函数中。现在我们可以用cache_find返回对象的指针了,它具有引用计数功能。这样,持有对象指针的用户不必像以前那样为了保证指针的合法性就一直持用锁了。(例如,调用copy_to_user把名字拷贝到用户空间)

为引用计数使用原子操作

实践中,refcnt的类型应该是atomic_t。在include/asm/atomic.h中定义了几个原子操作:这些操作保证了从系统的所有CPU的角度看,都是原子的。因此不需要锁。这种情况下,原子操作要比自旋锁简单的多,尽管复杂情况下使用自旋锁要来得清晰。使用atomic_inc和atomic_dec_and_test来取代标准的加减操作符,再也不用为引用计数上锁了。

--- cache.c.refcnt       2003-12-09 15:00:35.000000000 +1100

+++ cache.c.refcnt-atomic         2003-12-11 15:49:42.000000000 +1100

@@ -7,7 +7,7 @@

 struct object

 {

         struct list_head list;

-        unsigned int refcnt;

+        atomic_t refcnt;

         int id;

         char name[32];

         int popularity;

@@ -18,33 +18,15 @@

 static unsigned int cache_num = 0;

 #define MAX_CACHE_SIZE 10

 

-static void __object_put(struct object *obj)

-{

-        if (--obj->refcnt == 0)

-                kfree(obj);

-}

-

-static void __object_get(struct object *obj)

-{

-        obj->refcnt++;

-}

-

 void object_put(struct object *obj)

 {

-        unsigned long flags;

-

-        spin_lock_irqsave(&cache_lock, flags);

-        __object_put(obj);

-        spin_unlock_irqrestore(&cache_lock, flags);

+        if (atomic_dec_and_test(&obj->refcnt))

+                kfree(obj);

 }

 

 void object_get(struct object *obj)

 {

-        unsigned long flags;

-

-        spin_lock_irqsave(&cache_lock, flags);

-        __object_get(obj);

-        spin_unlock_irqrestore(&cache_lock, flags);

+        atomic_inc(&obj->refcnt);

 }

 

 /* Must be holding cache_lock */

@@ -65,7 +47,7 @@

 {

         BUG_ON(!obj);

         list_del(&obj->list);

-        __object_put(obj);

+        object_put(obj);

         cache_num--;

 }

 

@@ -94,7 +76,7 @@

         strlcpy(obj->name, name, sizeof(obj->name));

         obj->id = id;

         obj->popularity = 0;

-        obj->refcnt = 1; /* The cache holds a reference */

+        atomic_set(&obj->refcnt, 1); /* The cache holds a reference */

 

         spin_lock_irqsave(&cache_lock, flags);

         __cache_add(obj);

@@ -119,7 +101,7 @@

         spin_lock_irqsave(&cache_lock, flags);

         obj = __cache_find(id);

         if (obj)

-                __object_get(obj);

+                object_get(obj);

         spin_unlock_irqrestore(&cache_lock, flags);

         return obj;

 }

  1. 保护对象自身

在上面的例子中,我们都假定对象(除了引用计数)自从创建之后就不会被更改。如果我们希望允许改变name,有三种可能:

  1. 你可以把cache_lock变成非static的,告诉人们改变name之前先获得锁。
  2. 你也可以提供一个cache_obj_rename函数,该函数先获得锁,然后为调用者改变name。你需要告诉每一个需要改变name的人使用这个函数。
  3. 你也可以用cache_lock只保护cache自身,而使用另一把锁来保护name域。

理论上,你可以把锁的使用细粒度化(fine-grained)到这样的程度:为每个域维持一把锁。实践中,最通用的变体是:

  1. 一把用来保护基础设施(infrasturcture)和所有对象的锁。至今为止我们在示例中看到的便是这种情况。
  2. 一把用来保护基础设施(包括对象结构中的链表指针)的锁,另一把放在对象内部,用来保护对象的其它域。
  3. 多把锁来保护基础设施(例如,Hash表的每一个链都用一把锁保护),可能配合使用每个对象一把的锁。

下面是“每个对象一把的锁”的实现:

--- cache.c.refcnt-atomic   2003-12-11 15:50:54.000000000 +1100

+++ cache.c.perobjectlock         2003-12-11 17:15:03.000000000 +1100

@@ -6,11 +6,17 @@

 

 struct object

 {

+        /* These two protected by cache_lock. */

         struct list_head list;

+        int popularity;

+

         atomic_t refcnt;

+

+        /* Doesn't change once created. */

         int id;

+

+        spinlock_t lock; /* Protects the name */

         char name[32];

-        int popularity;

 };

 

 static DEFINE_SPINLOCK(cache_lock);

@@ -77,6 +84,7 @@

         obj->id = id;

         obj->popularity = 0;

         atomic_set(&obj->refcnt, 1); /* The cache holds a reference */

+        spin_lock_init(&obj->lock);

 

         spin_lock_irqsave(&cache_lock, flags);

         __cache_add(obj);

注意我的策略是popularity域由cache_lock来保护,而不是每个对象一把的锁。这是因为它(就象对象内部的struct list_head域一样)在逻辑上是属于基础设施的一部分的。这样,当在__cache_add查找最少使用的对象时,我就不必去获取每个对象一把的锁了。

这个策略还有一处要注意:id域是不可更改的,这样我就不必在__cache_find()时去获取每个对象一把的锁来保护它。每个对象一把的锁,在这里,只是被调用者使用来保护name域的读和写。

另外,注意我在注释里说明了哪些数据由哪些锁来保护。这是非常重要的,因为它描述了代码运行时的行为,(倘若不加注释)仅仅靠阅读代码是很难获得认识的。这正如Alan Cox所说,“锁是用来保护数据的,而非代码。”

  1. 常见的问题
  2. 死锁:简单与高级

当一段代码试图两次获取同一把自旋锁时,就出现了BUG:它将永远自旋,等待锁被释放。

试想一个稍微复杂一点的情况,假设你有一个softirq和用户上下文共享一片区域。如果你使用了spin_lock()来保护它,很可能用户上下文持有锁时被这个softirq打断,然后softirq尝试获得该锁-但它永远也不会成功。

这些情形称为死锁。如同上面展示的那样,它甚至会在单CPU机器上发生。

这种死锁容易诊断:在SMP机器上,看门狗定时器或者编译时加入DEBUG_SPINLOCKS(include/linux/spinlock.h)会在发生死锁时立即显示它。

更复杂的问题是一种叫做“致命拥抱”的死锁,它涉及到两把或更多的锁。比方你有一个Hash表:表中的每一个项是一个自旋锁和一个对象链表。在softirq处理函数中,你可能会想更改某个对象在Hash表中的位置,从某个位置到另一个:你获得了旧的Hash链表的自旋锁和新的链表的自旋锁,然后从旧的链表中删除对象,插入新的链表中。

这里有两个问题。第一,如果你的代码试图把对象移动到相同的链表中,它就会因两次尝试获得同一把锁而死锁;第二,如果在别的CPU上运行的同一个softirq试图把另一个对象从你的链表移动到你的源链表中(AB-BA死锁),下面的事情就发生了:

Table 8.1. Consequences

CPU 1

CPU 2

Grab lock A -> OK

Grab lock B -> OK

Grab lock B -> spin

Grab lock A -> spin

这两个CPU将永远自旋,等待着对方释放自旋锁。它会使系统看起来像崩溃一样。

  1. 防备死锁

教科书告诉你说:总是以相同的顺序获取锁,你就不会造成死锁了。而实践则会告诉你,这种方法不具有扩展性:当我们创建一把锁的时候,我对内核的理解还真不足以计算出在5000种锁的层次中,哪一种合适。

最好的锁是被封装了的:它们不会暴露在头文件中,不会被它所在文件之外的函数持有。你可以读一下这种代码,来看看为什么它永远不会死锁。因为它持有一把锁时,是决不会试图去获取另一把的。使用它的代码的人甚至不需要知道你曾经使用了锁。

一个经典的问题是当你提供了回调函数或钩子函数:如果你在持有锁的情况下调用它们,你将冒死锁的危险:简单死锁或致命拥抱。

  1. 对死锁的防备过当

死锁诚然会带来问题,然而不如数据冲突之甚。试想这样的一段代码,它获取一把读锁,搜索一个链表,如果没有找到想要的数据,就释放掉读锁,然后获取一把写锁,把对象插入到链表:这样的代码存在竞态问题。

  1. 竞态timers:一次内核游戏

Timers会造成它们独有的竞态条件。考虑一个对象集合(链表,Hash表等),每个对象都有一个timer,该timer负责销毁它所属的对象。

如果你打算销毁整个对象集合(例如模块卸载的时候),你可能会这样做:

          /* THIS CODE BAD BAD BAD BAD: IF IT WAS ANY WORSE IT WOULD USE

           HUNGARIAN NOTATION */

        spin_lock_bh(&list_lock);

        while (list) {

                struct foo *next = list->next;

                del_timer(&list->timer);

                kfree(list);

                list = next;

        }

        spin_unlock_bh(&list_lock);

在SMP机器上,这段代码早晚要导致崩溃。因为一个timer可能在spin_lock_bh()之前已经gone off了,这将只有在我们调用spin_unlock_bh()之后才能成功的获得锁,然后试图去释放掉响应的元素(而元素已经释放掉了!)

这个问题可以通过检查del_timer()的返回值来预防:如果返回1,说明timer已经被删除了;如果返回0,说明它正在运行。所以我们应当这样:

retry:

                spin_lock_bh(&list_lock);

                while (list) {

                        struct foo *next = list->next;

                        if (!del_timer(&list->timer)) {

                                /* Give timer a chance to delete this */

                                spin_unlock_bh(&list_lock);

                                goto retry;

                        }

                        kfree(list);

                        list = next;

                }

                spin_unlock_bh(&list_lock);

另一个常见问题是那些自启动(通过在timer的函数的最后调用add_timer()就会实现定时器的自启动)timers的删除操作。这是一个很常见的易导致竞态的情形,你该使用del_timer_sync()(include/linux/timer.h)来处理这种情况。该函数的返回值是:我们要删除的timer最终被停止启动之前,删除动作应该执行的次数。

  1. 加锁速度

在考虑使用锁的代码的速度问题上,主要有三件事需要关注。首先是并发性:当锁被别人持有时,有多少事情是我们需要等待的。其次是,需要花多少时间来 获取与释放一把无争议的锁。第三呢,是尽量使用更少的或者更灵活的锁。当然,这里我已经假定锁的使用非常频繁,否则你都不用担心效率问题的。

并发性依赖于锁被持有多长时间:你持有锁的时间,就应该同确实需要持有它的时间一样长,不要更长。在上面那个关于cache的例子中,我们在创建对象时是不持有锁的,只有在已经准备好把它插入到链表中时才去获取锁。

获取锁的时间依赖于加锁操作对于管道线(pipeline stalls)做了多少破坏性工作,以及本CPU是最后一个试图获取该锁的CPU(亦即:对本CPU来说,锁是否为缓存密集的(cache-hot))的可能性有多大:在具有很多CPU的机器上,这种可能性迅速降低。考虑一个700MHz的Intel Pentium III处理器:一条指令大约花费0.7纳秒,一次原子增加操作大约花费58纳秒,一次缓存密集的的加锁操作大约花费160纳秒,而从其他CPU上的一次缓存行转换还要额外花费170到360纳秒。

这两个目标会发生冲突:通过把锁分成不同的的部分,可以减少持有锁的时间(例如上面例子中“每个对象一把的锁”),但是这增加了获取锁这一操作的数量,因而结果一般是比只有一把锁的情况要慢一些。这也是推崇加锁简洁性的一个理由。

第三个考虑点放到了下面:的确存在一些方法能够减少锁的使用。

  1. 读/写锁变体

自旋锁与互斥锁都具有读/写变体:rwlock_t和struct rw_semaphore。它们把使用者分成了两类:读者和写者。如果你只需要读数据,你该获取读锁;但如果你需要写数据,你需要获取写锁。可以有很多使用者同时获取读锁,但写锁却只能有单个使用者持有。如果你的代码可以简洁的划分出读和写的界限(就象我们上面的cache代码那样),而且读者通常需要很长时间的持有读锁,使用读写锁会更好。它们要比普通的锁要稍微慢点儿,所以在实践中rwlock_t通常并不很值得使用。

  1. 避免锁的使用:RCU

有一种特殊的读/写锁的方法叫做RCU(Read Copy Update),读者可以完全避免使用锁:由于我们希望我们的cache被读取远比被更新来的频繁(否则的话,还引入cache干什么?),使用RCU就是一个可选择的优化。

如何移除读锁呢?移除读锁意味着写者可能在存在读者的情况下更新链表。这很简单:要向链表中添加元素的代码足够小心,我们就可以在它添加的同时读取链表。例如,把new元素添加到list链表中:

new->next = list->next;

wmb();

list->next = new;

wmb()是一个写内存屏障。它保证了第一个操作(设置新元素的next指针)是完整的,而且立即为其它CPU所见,这发生在第二个操作(把新元素添加到链表中)之前。这是很重要的,因为现代CPU和编译器可能会改变指令的执行顺序,除非明确告诉它们不要这样:我们希望一个读者要么完全看不到新元素的插入,要么能够看到新元素插入之后的next指针正确指向了链表的剩余部分。

幸运的是,有一个函数专门添加标准的struct list_head链表:list_add_rcu()(include/linux/list.h)。

从链表中删除元素更简单:假设我们要删除的元素为A,我们让指向A的指针重新指向A的后继者,读者要么完全看到了它,要么完全忽略了它。

list->next = old->next;

有一个list_del_rcu()函数(include/linux/list.h)来做该工作(而普通版本的删除操作会毒害(poison)要移除的对象,这是我们不愿看到的)。

读者也一定要小心:有些CPU会观察next指针以便预取下一个元素,但这样的话,如果next指针恰恰在这时候发生了变化,那么这些CPU预取的(pre-fetched)内容就是错误的。还好,有一个list_for_each_entry_rcu()函数(include/linux/list.h)可以帮助你。

当然,写者可以只使用list_for_each_entry() (译者案:这里的“只使用”,是说不用需要一个XXX_rcu()这样的函数),因为不可能同时有两个写者。

我们的困难最终是:什么时候我们可以真正的删除元素?记住,一个读者可能正在访问要被删除的元素:如果我们释放掉这个元素,而让next指针指向新元素,读者将立即访问到垃圾内存并崩溃。我们必须等待到所有正在遍历链表的读者都结束了遍历,才能真正释放掉元素。我们用call_rcu()函数注册一个回调函数,当所有读者结束遍历时,它会真正销毁对象。

但是,RCU如何得知什么时候读者结束遍历了呢?首先,读者总是在rcu_read_lock()/rcu_read_unlock()之间执行遍历:这会禁止抢占(而且只是禁止抢占,其它什么都不做),因此读者在遍历链表期间不会睡眠

RCU一直等待到其他的CPU至少睡眠了一次:因为读者不会在读取过程中睡眠,此时我们就知道我们等待其结束的那些读者终于都结束了。于是回调函数被调用。真实的RCU代码要比这里讲述的更优化些,但这里讲的是它的基础。

--- cache.c.perobjectlock  2003-12-11 17:15:03.000000000 +1100

+++ cache.c.rcupdate        2003-12-11 17:55:14.000000000 +1100

@@ -1,15 +1,18 @@

 #include <linux/list.h>

 #include <linux/slab.h>

 #include <linux/string.h>

+#include <linux/rcupdate.h>

 #include <linux/mutex.h>

 #include <asm/errno.h>

 

 struct object

 {

-        /* These two protected by cache_lock. */

+        /* This is protected by RCU */

         struct list_head list;

         int popularity;

 

+        struct rcu_head rcu;

+

         atomic_t refcnt;

 

         /* Doesn't change once created. */

@@ -40,7 +43,7 @@

 {

         struct object *i;

 

-        list_for_each_entry(i, &cache, list) {

+        list_for_each_entry_rcu(i, &cache, list) {

                 if (i->id == id) {

                         i->popularity++;

                         return i;

@@ -49,19 +52,25 @@

         return NULL;

 }

 

+/* Final discard done once we know no readers are looking. */

+static void cache_delete_rcu(void *arg)

+{

+        object_put(arg);

+}

+

 /* Must be holding cache_lock */

 static void __cache_delete(struct object *obj)

 {

         BUG_ON(!obj);

-        list_del(&obj->list);

-        object_put(obj);

+        list_del_rcu(&obj->list);

         cache_num--;

+        call_rcu(&obj->rcu, cache_delete_rcu);

 }

 

 /* Must be holding cache_lock */

 static void __cache_add(struct object *obj)

 {

-        list_add(&obj->list, &cache);

+        list_add_rcu(&obj->list, &cache);

         if (++cache_num > MAX_CACHE_SIZE) {

                 struct object *i, *outcast = NULL;

                 list_for_each_entry(i, &cache, list) {

@@ -104,12 +114,11 @@

 struct object *cache_find(int id)

 {

         struct object *obj;

-        unsigned long flags;

 

-        spin_lock_irqsave(&cache_lock, flags);

+        rcu_read_lock();

         obj = __cache_find(id);

         if (obj)

                 object_get(obj);

-        spin_unlock_irqrestore(&cache_lock, flags);

+        rcu_read_unlock();

         return obj;

 }

注意读者会在__cache_find()中改变popularity域的值,但是没有使用锁。我们的方案应该把它变成atomic_t类型的,但是对本例来说我们不会导致竞态条件:近似的结果就已经不错了,所以我没做改变。

结果是,cache_find()函数并不需要与其它函数同步,因此它在SMP上几乎跟在UP上一样快。

此处还存在一个更纵深的可能的优化:想一想我们最初的cache代码,那时没有引用计数,无论何时调用者想访问对象,就要持有锁。这仍然是可能的:如果你持有锁,就没人能够删掉对象,因此你就不必采用put和get操作引用计数了。

由于RCU中的“读锁”会禁止抢占(而且只是禁止抢占,其它什么都不做),调用者如果调用cache_find()或object_put()之前已经禁止抢占了,就并不需要读取并写回引用计数:我们可以把__cache_find()变成非static的从而暴露给其它文件,这时候调用者就可以直接调用这些函数了。

此处的优势是:引用计数并不被写入,对象并不被更改,这在SMP机器上会非常快──由于CPU的caching的原因。

  1. Per-CPU数据

另一种使用相当广泛的避免锁的技术是,为每个CPU使用数据的一份拷贝。例如,如果你想为一个普通的条件保持一个计数,你可能会使用自旋锁和一个计数器,这简单而漂亮。

如果它表现的太慢了(通常不会,但是如果你的确测试到这种情况发生了),你就该改变一下策略:为每个CPU保持一个计数器,这样,没有一个计数器需要互斥的锁来保护。参考DEFINE_PER_CPU(),get_cpu_var()和put_cpu_var() (include/linux/percpu.h)。

Per-CPU计数器的另一个用场是local_t数据类型,和cpu_local_inc()以及其它的相关函数。这在某些体系结构上是比简单的加锁代码更高效的。(include/asm/local.h)

注意,对计数器来说,根本不存在简单而可靠的方法可以精确的得到它的值而不需要引入锁。只不过某些情况下这不是问题罢了。

  1. 中断服务例程通常使用哪些数据

如果数据只是在中断服务例程内部访问,你根本不需要锁:内核本身已经保证了中断服务例程不会同时在多个CPU上运行。

Manfred Spraul指出,即使数据会偶尔被用户上下文或softirqs/tasklets访问,你仍然可以这样。中断服务例程自身不需要锁,其它所有的访问者这样做:

spin_lock(&lock);

disable_irq(irq);

...

enable_irq(irq);s

spin_unlock(&lock);

disable_irq()禁止了中断服务例程的运行(如果此刻已经在别的CPU上运行了,那就等待它的结束)。自旋锁禁止了同时刻其它任何可能的访问。自然,这比单个spin_lock_irq()调用要慢些,所以只有这种访问很少发生时这种用法才是合理的。

  1. 中断里调用哪些函数是安全的?

内核中许多函数会导致睡眠(亦即,直接或间接地调用了scheduler()):当持有自旋锁或禁止抢占时,你不可以调用它们。这同样意味着只有在用户上下文才可以调用:在中断里调用它们是非法的。

  1. 一些可能睡眠的函数

下面列出了最常见的几个,但通常来讲,你需要阅读代码来判断其它的函数是否可以在中断里安全的调用。如果每一个人都是在可睡眠环境下调用某个函数的,那么多半你也要保证可睡眠的环境(译者案:原文是“If everyone else who calls it can sleep, you probably need to be able to sleep, too.”) 特别地,注册与注销函数通常需要在用户

上下文中调用,它们可能睡眠。

  1. 访问用户空间:
  1. copy_from_user()
  2. copy_to_user()
  3. get_user()
  4. put_user()
  1. kmalloc(GFP_KERNEL)
  2. mutex_lock_interruptible() and mutex_lock()

There is a mutex_trylock() which does not sleep. Still, it must not be used inside interrupt context since its implementation is not safe for that. mutex_unlock() will also never sleep. It cannot be used in interrupt context either since a mutex must be released by the same task that acquired it.

  1. 一些不会睡眠的函数

有些函数可以在任何上下文中调用,可以持有任何的锁。

  1. printk()
  2. kfree()
  3. add_timer() 和del_timer()

 

--end--

猜你喜欢

转载自blog.csdn.net/b0207191/article/details/88535197