spinlock

spinlock介绍

spinlock又称自旋锁,线程通过busy-wait-loop的方式来获取锁,任时刻只有一个线程能够获得锁,其他线程忙等待直到获得锁。spinlock在多处理器多线程环境的场景中有很广泛的使用,一般要求使用spinlock的临界区尽量简短,这样获取的锁可以尽快释放,以满足其他忙等的线程。Spinlock和mutex不同,spinlock不会导致线程的状态切换(用户态->内核态),但是spinlock使用不当(如临界区执行时间过长)会导致cpu busy飙高。

spinlock与mutex对比

优缺点比较

spinlock不会使线程状态发生切换,mutex在获取不到锁的时候会选择sleep。

mutex获取锁分为两阶段,第一阶段在用户态采用spinlock锁总线的方式获取一次锁,如果成功立即返回;否则进入第二阶段,调用系统的futex锁去sleep,当锁可用后被唤醒,继续竞争锁。

Spinlock优点:没有昂贵的系统调用,一直处于用户态,执行速度快。

Spinlock缺点:一直占用cpu,而且在执行过程中还会锁bus总线,锁总线时其他处理器不能使用总线。

Mutex优点:不会忙等,得不到锁会sleep。

Mutex缺点:sleep时会陷入到内核态,需要昂贵的系统调用。

使用准则

Spinlock使用准则:临界区尽量简短,控制在100行代码以内,不要有显式或者隐式的系统调用,调用的函数也尽量简短。例如,不要在临界区中调用read,write,open等会产生系统调用的函数,也不要去sleep;strcpy,memcpy等函数慎用,依赖于数据的大小。

 

那些情况该使用它们spin_lock到spin_lock_irqsave

Spinlock的目的是用来同步SMP中会被多个CPU同时存取的变量。在Linux中,普通的spinlock由于不带额外的语义,是用起来反而要非常小心。

在Linux kernel中执行的代码大体分normal和interrupt context两种。tasklet/softirq可以归为normal因为他们可以进入等待;nested interrupt是interrupt context的一种特殊情况,当然也是interrupt context。Normal级别可以被interrupt抢断,interrupt会被另一个interrupt抢断,但不会被normal中断。各个 interrupt之间没有优先级关系,只要有可能,每个interrupt都会被其他interrupt中断。

我们先考虑单CPU的情况。在这样情况下,不管在什么执行级别,我们只要简单地把CPU的中断关掉就可以达到独占处理的目的。从这个角度来说,spinlock的实现简单地令人乍舌:cli/sti。只要这样,我们就关闭了preemption带来的复杂之门。

单CPU的情况很简单,多CPU就不那么简单了。单纯地关掉当前CPU的中断并不会给我们带来好运。当我们的代码存取一个shared variable时,另一颗CPU随时会把数据改得面目全非。我们需要有手段通知它(或它们,你知道我的意思)——spinlock正为此设。这个例子是我们的第一次尝试:

extern spinlock_t lock;
// ...
spin_lock(&lock);
// do something
spin_unlock(&lock);

他能正常工作吗?答案是有可能。在某些情况下,这段代码可以正常工作,但想一想会不会发生这样的事:

// in normal run level
extern spinlock_t lock;
// ...
spin_lock(&lock);
// do something
                                                                           // interrupted by IRQ ...
                                                                          // in IRQ
                                                                         extern spinlock_t lock;
                                                                         spin_lock(&lock);

喔,我们在normal级别下获得了一个spinlock,正当我们想做什么的时候,我们被interrupt打断了,CPU转而执行interrupt level的代码,它也想获得这个lock,于是“死锁”发生了!解决方法很简单,看看我们第二次尝试:

extern spinlock_t lock;
// ...
cli; // disable interrupt on current CPU
spin_lock(&lock);
// do something
spin_unlock(&lock);
sti; // enable interrupt on current CPU

在获得spinlock之前,我们先把当前CPU的中断禁止掉,然后获得一个lock;在释放lock之后再把中断打开。这样,我们就防止了死锁。事实上,Linux提供了一个更为快捷的方式来实现这个功能:

extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);

如果没有nested interrupt,所有这一切都很好。加上nested interrupt,我们再来看看这个例子:

// code 1
extern spinlock_t lock1;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);
// code 2
extern spinlock_t lock2;
// ...
spin_lock_irq(&lock2);
// do something
spin_unlock_irq(&lock2);

Code 1和code 2都可运行在interrupt下,我们很容易就可以想到这样的运行次序():

Code 1                                                                        Code 2

extern spinlock_t lock1;

// ...

spin_lock_irq(&lock1);

                                                                                   extern spinlock_t lock2;

// ...
spin_lock_irq(&lock1);

// do something

                                                                                   spin_unlock_irq(&lock2);

                                                                                   // do something

spin_unlock_irq(&lock1);

问题是在第二个spin_unlock_irq后这个CPU的中断已经被打开,“死锁”的问题又会回到我们身边!

解决方法是我们在每次关闭中断前纪录当前中断的状态,然后恢复它而不是直接把中断打开。

unsigned long flags;
local_irq_save(flags);
spin_lock(&lock);
// do something
spin_unlock(&lock);
local_irq_restore(flags);

Linux同样提供了更为简便的方式:

unsigned long flags;
spin_lock_irqsave(&lock, flags);
// do something

spin_unlock_irqrestore(&lock, flags);

总结:

如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。

当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。

如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。

timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。

如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。

因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。

因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。

当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问

 

阅读笔记:

spinlock是为了解决多CPU访问时的一致性问题。但是简单的使用spin_lock和spin_unlock 解决了多CPU访问的一致性问题。但是却可能会被中断导致锁不能释放而导致的死锁问题。因此,就需要加入关闭中断。使用spin_lock_irq 和 spin_unlock_irq将执行的代码包起来。但由于该函数是控制当前cpu的中断。可能出现的情况是两个同时执行的代码。另一个又把中断打开了。从而前面说的死锁现象又会发生。因此我们不能简单的忽略中断,而需要保存之前的中断状态,执行后恢复到之前的中断状态(而不是直接打开中断)。

这时就需要使用spin_lock_irqsave 和spin_unlock_irqrestore 回复中断标志。

      总体来说,spin_lock是为了阻止多CPU对共享资源的访问的同步问题。而后面的阻止中断是为了阻止同一CPU上面软终端或中断对共享资源的非同步访问。

 

Linux信号量工具
现在我们已经了解了什么是信号量以及他们在理论上是如何工作的,现在我们可以来了解一下这些特性在Linux中是如何实现的。信号量函数接口设计十分精 细,并且提供了比通常所需要的更多的实用性能。所有的Linux信号量函数在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。乍看 起来,这似乎使得事情变得更为复杂,但是在一个进程需要锁住多个资源的复杂情况下,在信号量数组上进行操作将是一个极大的优点。在这一章,我们将会关注于 使用单一信号量,因为在大多数情况下,这正是我们需要使用的。
信号量函数定义如下:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
事实上,为了获得我们特定操作所需要的#define定义,我们需要在包含sys/sem.h文件之前通常需要包含sys/types.h与sys/ipc.h文件。而在某些情况下,这并不是必须的。
因为我们会依次了解每一个函数,记住,这些函数的设计是用于操作信号量值数组的,从而会使用其操作向比单个信号量所需要的操作更为复杂。
注意,key的作用类似于一个文件名,因为他表示程序也许会使用或是合作所用的资源。相类似的,由semget所返回的并且为其他的共享内存函数所用的标 识符与由fopen函数所返回 的FILE *十分相似,因为他被进程用来访问共享文件。而且与文件类似,不同的进程会有不同的信号量标识符,尽管他们指向相同的信号量。key与标识符的用法对于在 这里所讨论的所有IPC程序都是通用的,尽管每一个程序会使用独立的key与标识符。
semget
semget函数创建一个新的信号量或是获得一个已存在的信号量键值。
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是一个用来允许不相关的进程访问相同信号量的整数值。所有的信号量是为不同的程序通过提供一个key来间接访问的,对于每一个信号量系统 生成一个信号量标识符。信号量键值只可以由semget获得,所有其他的信号量函数所用的信号量标识符都是由semget所返回的。
还有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是创建一个只有创建进程可以访问的信号量。这通常并没有有用的目的,而幸运的是,因为在某些Linux系统上,手册页将IPC_PRIVATE并没有阻止其他的进程访问信号量作为一个bug列出。
num_sems参数是所需要的信号量数目。这个值通常总是1。
sem_flags参数是一个标记集合,与open函数的标记十分类似。低九位是信号的权限,其作用与文件权限类似。另外,这些标记可以与 IPC_CREAT进行或操作来创建新的信号量。设置IPC_CREAT标记并且指定一个已经存在的信号量键值并不是一个错误。如果不需 要,IPC_CREAT标记只是被简单的忽略。我们可以使用IPC_CREAT与IPC_EXCL的组合来保证我们可以获得一个新的,唯一的信号量。如果 这个信号量已经存在,则会返回一个错误。
如果成功,semget函数会返回一个正数;这是用于其他信号量函数的标识符。如果失败,则会返回-1。
semop
函数semop用来改变信号量的值:
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数,sem_id,是由semget函数所返回的信号量标识符。第二个参数,sem_ops,是一个指向结构数组的指针,其中的每一个结构至少包含下列成员:
struct sembuf {
    short sem_num;
    short sem_op;
    short sem_flg;
}
第一个成员,sem_num,是信号量数目,通常为0,除非我们正在使用一个信号量数组。sem_op成员是信号量的变化量值。(我们可以以任何量改变信 号量值,而不只是1)通常情况下中使用两个值,-1是我们的P操作,用来等待一个信号量变得可用,而+1是我们的V操作,用来通知一个信号量可用。
最后一个成员,sem_flg,通常设置为SEM_UNDO。这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没有释放这个信号量, 如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。将sem_flg设置为SEM_UNDO是一个好习惯,除非我们需要不同的行 为。如果我们确实变我们需要一个不同的值而不是SEM_UNDO,一致性是十分重要的,否则我们就会变得十分迷惑,当我们的进程退出时,内核是否会尝试清 理我们的信号量。
semop的所用动作会同时作用,从而避免多个信号量的使用所引起的竞争条件。我们可以在手册页中了解关于semop处理更为详细的信息。
semctl
semctl函数允许信号量信息的直接控制:
int semctl(int sem_id, int sem_num, int command, ...);
第一个参数,sem_id,是由semget所获得的信号量标识符。sem_num参数是信号量数目。当我们使用信号量数组时会用到这个参数。通常,如果 这是第一个且是唯一的一个信号量,这个值为0。command参数是要执行的动作,而如果提供了额外的参数,则是union semun,根据X/OPEN规范,这个参数至少包括下列参数:
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
}
许多版本的Linux在头文件(通常为sem.h)中定义了semun联合,尽管X/Open确认说我们必须定义我们自己的联合。如果我们发现我们确实需 要定义我们自己的联合,我们可以查看semctl手册页了解定义。如果有这样的情况,建议使用手册页中提供的定义,尽管这个定义与上面的有区别。
有多个不同的command值可以用于semctl。在这里我们描述两个会经常用到的值。要了解semctl功能的详细信息,我们应该查看手册页。
这两个通常的command值为:
SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:当信号量不再需要时用于删除一个信号量标识。
semctl函数依据command参数会返回不同的值。对于SETVAL与IPC_RMID,如果成功则会返回0,否则会返回-1。
使用信号量
正如我们在前面部分的描述中所看到的,信号量操作是相当复杂的。这是最不幸的,因为使用临界区进行多进程或是多线程编程是一个十分困难的问题,而其拥有其自己复杂的编程接口也增加了编程负担。
幸运的是,我们可以使用最简单的二值信号量来解决大多数需要信号量的问题。在我们的例子中,我们会使用所有的编程接口来创建一个非常简单的用于二值信号量的P
与V类型接口。然后,我们会使用这个简单的接口来演示信号量如何工作。
要试验信号量,我们将会使用一个简单的程序,sem1.c,这个程序我们可以多次调用。我们将会使用一个可选的参数来标识这个程序是负责创建信号量还是销毁信号量。
我们使用两个不同字符的输出来标识进入与离开临界区。使用参数调用的程序会在进入与离开其临界区时输出一个X,而另一个程序调用会在进入与离开其临界区时输出一个O。因为在任何指定的时间内只有一个进程能够进入其临界区,所以所有X与O字符都是成对出现的。
试验--信号量

1 在#include语句之后,我们定义函数原型与全局变量,然后我们进入main函数。在这里使用semget函数调用创建信号量,这会返回一个信号量 ID。如果程序是第一次调用(例如,使用一个参数并且argc > 1来调用),程序就会调用set_semvalue来初始化信号量并且将op_char设置为X。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include "semun.h"
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
int main(int argc, char **argv)
{
    int i;
    int pause_time;
    char op_char = 'O';
    srand((unsigned int)getpid());
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
    if(argc > 1)
    {
        if(!set_semvalue())
        {
            fprintf(stderr, "Failed to initialize semaphore/n");
            exit(EXIT_FAILURE);
        }
        op_char = 'X';
        sleep(2);
    }
2 然后我们使用一个循环代码进入并且离开临界区10次。此时会调用semaphore_p函数,这个函数会设置信号量并且等待程序进入临界区。
    for(i=0;i<10;i++)
    {
        if(!semaphore_p()) exit(EXIT_FAILURE);
        printf("%c", op_char); fflush(stdout);
        pause_time = rand() % 3;
        sleep(pause_time);
        printf("%c", op_char); fflush(stdout);
3 在临界区之后,我们调用semaphore_v函数,在随机的等待之后再次进入for循环之后,将信号量设置为可用。在循环之后,调用del_semvalue来清理代码。
        if(!semaphore_v()) exit(EXIT_FAILURE);
        pause_time = rand() % 2;
        sleep(pause_time);
    }
    printf("/n%d - finished/n", getpid());
    if(argc > 1)
    {
        sleep(10);
        del_semvalue();
    }
    exit(EXIT_SUCCESS);
    }
4 函数set_semvalue在一个semctl调用中使用SETVAL命令来初始化信号量。在我们使用信号量之前,我们需要这样做。
static int set_semvalue(void)
{
    union semun sem_union;
    sem_union.val = 1;
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0;
    return 1;
}
5 del_semvalue函数几乎具有相同的格式,所不同的是semctl调用使用IPC_RMID命令来移除信号量ID:
static void del_semvalue(void)
{
    union semun sem_union;
    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore/n");
}
6 semaphore_p函数将信号量减1(等待):
static int semaphore_p(void)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flag = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_p failed/n");
        return 0;
    }
    return 1;
}
7 semaphore_v函数将sembuf结构的sem_op部分设置为1,从而信号量变得可用。
static int semaphore_v(void)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1;
    sem_b.sem_flag = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_v failed/n");
        return 0;
    }
    return 1;
}
注意,这个简单的程序只有每个程序有一个二值信号量,尽管如果我们需要多个信号量,我们可以扩展这个程序来传递多个信号量变量。通常,一个简单的二值信号量就足够了。
我们可以通过多次调用这个程序来测试我们的程序。第一次,我们传递一个参数来通知程序他并不负责创建与删除信号量。另一次调用没有传递参数。
下面是两次调用的示例输出结果:
$ ./sem1 1 &
[1] 1082
$ ./sem1
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
1083 - finished
1082 - finished
$
正如我们所看到了,O与X是成对出现的,表明临界区部分被正确的处理了。如果这个程序在我们的系统上不能正常运行,也许我们需要在调用程序之前使用命令stty -tostop来保证生成tty输出的后台程序不会引起信号生成。
工作原理
这个程序由我们选择使用semget函数所获得的键生成一个信号量标识开始。IPC_CREAT标记会使得如果需要的时候创建一个信号量。
如果这个程序有参数,他负责使用我们的set_semvalue函数来初始化信号量,这是更为通用的semctl函数的一个简化接口。同时,他也使用所提 供的参数来决定要输出哪一个字符。sleep只是简单的使得我们在这个程序执行多次之前有时间调用程序的另一个拷贝。在程序中我们使用srand与 rand来引入一些伪随机计数。
这个程序循环十次,在其临界区与非临界区等待一段随机的时间。临界区代码是通过调用我们的semaphore_p与semaphore_v函数来进行保护的,这两个函数是更为通用的semop函数的简化接口。
在删除信号量之前,使用参数调用的程序拷贝会等待其他的调用结束。如果信号量没有删除,他就会继续存在于系统中,尽管已经没有程序再使用他。在实际的程序 中,保证我们没有遗留信号是十分重要的。在我们下一次运行程序时,遗留的信号量会引起问题,而且信号量是限制资源,我们必须小心使用。

转自:http://blog.chinaunix.net/space.php?uid=13670711&do=blog&cuid=2034149

 

refer:自旋锁spinlock剖析与改进

        那些情况该使用它们spin_lock到spin_lock_irqsave

        linux c 信号量编程

最新文章

  1. HTML5 progress和meter控件
  2. ajax优点与缺点
  3. 天津政府应急系统之GIS一张图(arcgis api for flex)讲解(九)地图定位模块
  4. .NET项目开发—浅谈面向对象的纵横向关系、多态入口,单元测试(项目小结)
  5. Java生成与解析二维码
  6. [C] C++对C的部分扩充
  7. HDU 2819 隐式二分图匹配
  8. Android Fragment 生命周期及其API使用(建议使用自定义View替换Fragment)
  9. (C# Binary Tree) 基本概念和算法
  10. MVC的异步,Entity Framework的异步,ADO.NET的异步,
  11. zoj 3165 (最小割,最大点权独立集)
  12. 转载:DIV+CSS有可能遇到的问题
  13. jquery自调用匿名函数解析
  14. Java IO详解(转)
  15. 从 0 到 1,Java Web 网站架构搭建的技术演进
  16. UE4 内容示例网络同步Learn
  17. Hadoop与分布式数据处理 Spark VS Hadoop有哪些异同点?
  18. 史上最污技术解读,让你秒懂IT术语(转载)
  19. laravel框架入门
  20. vue-8-组件

热门文章

  1. Sql 将A表数据插入到B表
  2. github 配置ssh
  3. 【leetcode】997. Find the Town Judge
  4. java中 抽象类和接口的区别
  5. selenium2-java环境搭建 示例为chrome浏览器
  6. 测试过程中bug分类
  7. 16. Django基础数据访问
  8. msf generate exec payload
  9. interleave two text files with specified lines
  10. vue自定义指令clickoutside实现点击其他元素才会触发