1. 自旋锁

Linux内核中最常见的锁是自旋锁。一个自旋锁就是一个互斥设备,它只能有两个值:"锁定"和"解锁"。如果锁可用,则"锁定"位被设置,而代码继续进入临界区;相反,如果锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到锁可用为止。这个循环就是自旋锁的"自旋"。自旋锁最多只能被一个可执行的线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。注意,同一个锁可以用在多个位置。缺点:一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间)。所以,自旋锁不应该被长时间持有。当然,可以采用另外的方式处理对锁的争用:让请求线程睡眠,直到锁重新可用时在唤醒它。但是,这里有两次明显的上下文切换,被阻塞的线程要换入或换出。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。

注意:

1) 如果禁止内核被抢占,那么在编译时自旋锁会被完成剔除出内核。

2) Linux内核实现的自旋锁是不可递归的。小心自加锁。

3) 调试自旋锁, 加上配置选项CONFIG_DEBUG_SPINLOCK。

4)自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。

5) 加锁是对数据不是对代码。

6) 所有自旋锁的等待在本质上都是不可中断的。

1.1. 自旋锁API介绍自旋锁实现与体系结构密切相关,定义在<linux/spinlock.h>中。在编译时对自旋锁的初始化:

spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

或者在运行时:

void spin_lock_init(spinlock_t *lock);

进入临界区:

spin_lock(&my_lock);

/*访问数据*/

spin_unlock(&my_lock);

内核提供禁止中断同时请求锁的接口:

spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

unsigned long flags;

spin_lock_irqsave(&my_lock, flags);

/*访问数据*/

spin_unlock_irqrestore(&my_lock, flags);函数spin_lock_irqsave保存了中断的当前状态,并禁止了本地中断,然后在获取指定的锁;函数spin_unlock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。

内核提供的自旋锁的接口:

void spin_lock_irq (spinlock_t *lock);

void spin_unlock_irq (spinlock_t *lock);

void spin_lock_bh (spinlock_t *lock);

void spin_unlock_bh (spinlock_t *lock);

如果能够确保没有任何其他代码禁止本地处理器的中断,也就是说,能够确保在释放自旋锁时应该启用中断,这可以使用spin_lock_irq函数,而无需跟踪标志。函数spin_lock_bh在获得锁之前禁止软件中断,但是会让硬件中断保持打开。

int spin_trylock (spinlock_t *lock);

int spin_trylock_bh (spinlock_t *lock);

这两个函数是非阻塞性的,成功获取自旋锁返回非零值,否则返回零。

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就 为多处理器提供了防止并发访问所需的保护机制,但是在单处理器上,编译的时候不会加入自旋锁。它仅仅被当作一个设置内核抢占机制是否被启用的开关。注意, Linux内核实现的自旋锁是不可递归的,这一点不同于自旋锁在其他操作系统中的实现,如果你想得到一个你正持有的锁,你必须自旋,等待你自己释放这个 锁,但是你处于自旋忙等待中,所以永远没有机会释放锁,于是你就被自己锁死了,一定要注意!

自旋锁可以用在中断处理程序中,但是在使用时一定要在获取锁之前,首先禁止本地 中断(当前处理器上的中断),否则中断处理程序就可能打断正持有锁的内核代码,有可能会试图支争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自 旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就会造成双重请求死锁。

自旋锁与下半部:由于下半部(中断程序下半部)可以抢占进程上下文中的代码,所 以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半 部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它 必须得到锁的保护,因为同种类型的两个软中断也可以同进运行在一个系统的多个处理器上。但是,同一个处理器上的一个软中断绝不会抢占另一个软中断,因此, 根本不需要禁止下半部。

1.3. 读-写自旋锁当对某个数据结构的操作可以清晰化为读/写两种类型时,读/写自旋锁就派上了用场。一个或多个读任务可以并发的持有读锁;相反,写锁只能被一个写任务所持有,而且此时不能有并发的读操作。多个读任务可以安全地获得同一个读锁,事实上,即使一个线程递归地获取同一读锁也是安全的。

初始化读/写锁:

rwlock_t my_rwlock=RW_LOCK_UNLOCKED;

在读任务的代码分支上:

read_lock(&my_rwlock);

/*读数据*/

read_unlock(&my_rwlock);

在写任务的代码分支上:

write_lock(&my_rwlock);

/*读数据*/

write_unlock(&my_rwlock);

注意:不能把一个读锁升级为一个写锁:

read_lock(&my_rwlock);

write_lock(&my_rwlock);

这将会带来死锁,因为写锁会不断自旋,等待所有的读锁被释放,其中包括自己。

内核实现的读锁和写锁接口在<linux/spinlock.h>中有定义。

在使用Linux读-写自旋锁时,需要考虑一点事这种机制照顾读比照顾写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是读任务却可以继续成功的占用读锁,而且自旋等待的写任务在所有读任务释放锁之前是无法获得写锁的。

2. 信号量

Linux中的信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。这就比自旋锁提供更好的处理器利用率,因为把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。

以下是信号量的睡眠特性:

1) 由于争用信号量的进程在等待锁重新变得可用时会睡眠,所以信号量适合用于锁被长时间持有的情况

2)因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间还有长

3) 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,在中断上下文中是不能进行调度的4) 持有信号量的进程可以睡眠,因为其他进程试图获取同一信号量锁时不会因此而死锁,而持有信号量的进程最终会继续执行的5) 持有信号量的进程不能占用自旋锁。因为等待信号量时会睡眠,而在持有自旋锁时是不允许睡眠的往往在需要和用户空间同步时,你的代码会需要睡眠,此时使用信号量是唯一的选择。信号量不同于自旋锁,它不会禁止内核抢占,所以持有信号量的代码可以被抢占。这就意味着信号量不会对调度的等待时间带来负面影响。

信号量的一个重要特性,它可以同时允许任意数量的所持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量实在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻只允许一个持有者。这时计数等于1,这样的信号量称为二值信号量或互斥信号量。

信号量支持两个原子操作P()和V(),这两个名字来之荷兰语Proberen(探查)和Vershogen(增加)。后来系统把这两种操作称为down()和up()。down操作是对信号量计数减一来请求获取一个信号量。如果结果是0或大于0,获得信号量锁,任务就进入临界区。如果是负数,任务就会被放入等待队列,处理器执行其它任务。up操作是释放信号量,增加信号量的计数值。

2.1. 创建和初始化信号量信号量的实现与体系有关,具体实现 (在<asm/semaphore.h>中定义)。

静态声明信号量:

static DECLARE_SEMAPHORE(name,count);

互斥信号量的静态声明:

static DECLARE_MUTEX(name);

运行时动态初始化信号量:

sema_init(sem,count);

init_MUTEX(sem);

2.2. 使用信号量函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠,也就是说,该任务可以被信号唤醒。如果进程在等待获取信号量的时候接受到了信号,那么该进程就会被唤醒。而函数down_ interruptible()会返回-EINTR。

函数down()会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,进程在等待信号量的时候就不再响应信号了。

函数down_trylock()尝试以阻塞方式来获取指定的信号量。在信号量已被占用时,他立刻返回非0值,否则返回0。

要释放指定的信号量,需要调用up()函数。

实例如下:

static DECLARE_MUTEX(my_sem);

if(down_interruptible(&my_sem))

{

/* 信号被接受,信号量还未获取 */

}

/*访问共享数据*/

up(my_sem);

2.3. 读-写信号量读-写信号量在内核中是由rw_semaphore结构表示的,定义在<linux/rwsem.h>中。

静态声明读-写信号量:

static DECLARE_RWSEM(name);

运行时动态初始化读-写信号量:

init_rwsem(struct rw_semaphore *sem);

所有的读-写信号量都是互斥信号量。只要没有写者,并发持有读锁的读者数不限。只有唯一的写者可以获取写锁。所有读-写锁的睡眠都不会被信号打断,所以他只有一个版本down操作。

实例如下:

static DECLARE_ RWSEM (my_rwsem);

down_read(&my_rwsem);

/*访问共享数据*/

up_read(my_sem);

down_write(&my_rwsem);

/*访问共享数据*/

up_write(my_sem);

与标准信号量一样,读-写信号量耶提供了down_read_trylock()和down_write_trylock()方法。读-写信号量比读-写自旋锁多一种特有的操作:downgrade_writer()。这个函数可以动态地将获取的写锁转为读锁。

2.4. 自旋锁与信号量

在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用信号量。比较如下:

建议的加锁方法

低开销加锁

优先使用自旋锁

短期锁定

优先使用自旋锁

长期锁定

优先使用信号量

中断上下文中加锁

使用自旋锁

持有锁需要睡眠

使用信号量

自旋锁&信号量的更多相关文章

  1. 漫画|Linux 并发、竞态、互斥锁、自旋锁、信号量都是什么鬼?(转)

    知乎链接:https://zhuanlan.zhihu.com/p/57354304 1. 锁的由来? 学习linux的时候,肯定会遇到各种和锁相关的知识,有时候自己学好了一点,感觉半桶水的自己已经可 ...

  2. linux自旋锁、互斥锁、信号量

    为了避免并发,防止竞争.内核提供了一组同步方法来提供对共享数据的保护. 我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别. Linux 使用的同步机制可以说从2.0到2 ...

  3. Linux 内核同步之自旋锁与信号量的异同【转】

    转自:http://blog.csdn.net/liuxd3000/article/details/8567070 Linux 设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导 ...

  4. linux 自旋锁和信号量【转】

    转自:http://blog.csdn.net/xu_guo/article/details/6072823 版权声明:本文为博主原创文章,未经博主允许不得转载. 自旋锁最多只能被一个可执行线程持有( ...

  5. 【APUE】信号量、互斥体和自旋锁

    http://www.cnblogs.com/biyeymyhjob/archive/2012/07/21/2602015.html http://blog.chinaunix.net/uid-205 ...

  6. 本地自旋锁与信号量/多服务台自旋队列-spin wait风格的信号量

    周日傍晚,我去家附近的超市(...)买苏打水,准备自制青柠苏打.我感觉我做的比买的那个巴黎水要更爽口.由于天气太热,非常多人都去超市避暑去了,超市也不撵人,这仿佛是他们的策略.人过来避暑了,走的时候难 ...

  7. Linux——临界段,信号量,互斥锁,自旋锁,原子操作

    一. linux为什么需要临界段,信号量,互斥锁,自旋锁,原子操作? 1.1. linux内核后期版本是支持多核CPU以及抢占式调度.这里就存在一个并发,竞争状态(简称竟态). 1.2. 竞态条件 发 ...

  8. LINUX内核笔记:自旋锁

    目录 自旋锁作用与基本使用方法? 在SMP和UP上的不同表现? 自旋锁与上下文 使用spin_lock()后为什么不能睡眠? 强调:锁什么? 参考   1.自旋锁作用与基本使用方法? 与其他锁一样,自 ...

  9. [内核同步]自旋锁spin_lock、spin_lock_irq 和 spin_lock_irqsave 分析

    转自:http://blog.csdn.net/wh_19910525/article/details/11536279 自旋锁的初衷:在短期间内进行轻量级的锁定.一个被争用的自旋锁使得请求它的线程在 ...

随机推荐

  1. Java的代理模式

    最近在学习Spring,关于Spring AOP的代理模式不是很了解,看了一篇博文就懂了. https://www.cnblogs.com/cenyu/p/6289209.html Java的三种代理 ...

  2. noip模拟26[肾炎黄·酱累黄·换莫黄]

    \(noip模拟26\;solutions\) 这个题我做的确实是得心应手,为啥呢,因为前两次考试太难了 T1非常的简单,只不过我忘记了一个定理, T2就是一个小小的线段树,虽然吧我曾经说过我再也不写 ...

  3. mysqli_fetch_row()函数返回结果的理解

    在PHP处理对数据库查询返回的结果集,即mysqli_query()函数返回的结果集,我们可以把它处理为数组形式以便于处理. 我们一般会用下面四个函数: 1.array mysqli_fetch_ar ...

  4. [考试总结]noip模拟13

    因为最近考试频繁,所以咕掉了好长时间... 淦,刚说完又来一场... 先咕了,等以后有时间再写.... 回来了... 首先看到这个题目们,感觉就不存好意... 然后开始开 \(T1\). 只能蒻蒻地按 ...

  5. PGSQL存储过程学习

    一.存储过程定义:   存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参 ...

  6. python 管理工具

    pip 包管理工具 virtualenv 虚拟环境管理工具 切换目录 virtualenvwrapper 虚拟环境管理工具加强版 pyenv python版本管理工具 修改环境变量 pyenv-vir ...

  7. Go通关03:控制结构,if、for、switch逻辑语句

    if 条件语句 func main() { i:=6 if i >10 { fmt.Println("i>10") } else if i>5 && ...

  8. 接口管理效率神器Apifox

    前言 你是一个测试,你们团队目前开发模式是前后端分离. 某一天,版本V1.0接口评审完,发布在了swagger上,前后端各自进行开发.此时你根据接口文档将新接口迁移到JMeter上,然后开始编写接口测 ...

  9. XCTF-Web进阶-upload1

    显然是让我们上传文件,思路当然是上传一个木马文件,然后通过蚁剑连接查看目录获取flag. 但是当我们想要上传php文件的时候会出现弹窗,并且连"上传"按钮都被禁用了. ext = ...

  10. 走心的中级Android工程师跳槽经验分享

    这些经验是我最近四个月,从准备面试到找到合适工作的汗水和泪水,希望对你们能有帮助! define 跳槽 跳槽前要思考的问题 钱不到位怎么办 心委屈怎么办 离职前的思考 确定要走时需要做的准备 行情怎么 ...