06. 并发与竞态相关概念

之前的scull字符设备驱动实例中,我说过还有许多不完善的地方。考虑如下状态:当读取的数据量比较大或者比较耗时,此时有其他的线程在我们的数据区域中写入新的数据,那此时我们读到的数据会是什么情况呢?

本节以介绍一些概念为主,主要学习以下内容:

  • 竞态和并发的概念;
  • 避免出现竞态的一些手段介绍(信号量、互斥体、自旋锁、completion等)

1. 并发与竞态的概念

并发是指系统试图一次完成多个任务。

竞态是指对于共享数据的非控制访问。

为什么会产生并发和竞态呢?主要有以下几个原因:

  • 当前的系统是多任务操作系统,用户空间进程可能会以任何组合来访问我们的代码,在SMP(对称多处理器)中,可能在多个CPU上同时执行我们的代码;
  • Linux内核是可抢占的,当前操作可能在未完成的情况下,丢失了对CPU的占有,而新的获取CPU的进程可能会去调用我们的代码;
  • 设备中断是异步事件,也可能导致代码并发执行;
  • Linux内核中提供了延时机制(例如:workqueue、tasklet、timer等),随时都有可能开始执行我们的代码;

那么如何避免并发和竞态的产生呢,主要有以下几个原则:

  • 只要有可能,应该尽量避免资源的共享。在代码中的明显体现是少用全局变量,使得方法是可重入的;
  • 必须显式地管理对共享资源的访问。常用的技术称为“锁定”或者“互斥”,确保一次只有一个线程操作共享资源;

2. 信号量和互斥体

信号量本质上是一个数值,用于标记资源的可用数量,它和一对函数联合使用,这对函数称为 PV

当需要使用某个资源的时候,在相应的信号量上调用P函数,如果信号量的数值大于0,则其数值会减1,同时,当前的操作会继续下去,称为获取信号量;如果信号量的数值为0,则当前操作会进入等待状态。

使用完成后,使用 V 函数来释放资源,此时信号量的数值会增加1,并唤醒其他等待线程,称为释放信号量。

如果信号量的数值为1,则表示当前只能有一个线程拥有资源,其他线程都需要等待,只有等其中拥有资源的权限释放后,才能去获取资源,此时的信号量也称为“互斥体”(mutex)。

在Linux中,用于信号量的函数有以下:

#include <linux/semaphore.h>  //书中写的是<asm/semaphore.h>,我实践中发现是<linux/semaphore.h>

void sema_init(struct semaphore *sem, int val); //初始化信号量,设定信号量初值为val
DECLARE_MUTEX(name); //初始化信号量,初始值为1
DECLARE_MUTEX_LOCKED(name); //初始化信号量,初始值为0
void init_MUTEX(struct semaphore *sem); //运行中初始化信号量,初始值为1
void init_MUTEX_LOCKED(struct semaphore *sem); //运行中初始化信号量,初始值为0
void down(struct semaphore *sem); //获取信号量(即上面的P函数),未获取到时一直等待,不可中断
int down_interruptible(struct semaphore *sem); //同down,会等待,但可被中断,返回0表示获取成功,返回非0表示获取失败
int down_trylock(struct semaphore *sem); //同down,但不会等待,而是立即返回,返回0表示获取成功,返回非0表示获取失败
void up(struct semaphore *sem); // 释放信号量(即上面的V函数)

需要注意的是,获取到信号量后,一定别忘了释放信号量。

3. completion机制

在内核编程中,经常遇到这种状态:当前线程需要等待另一个线程完成某个操作或初始化后才能继续运行。

这种情况我们可以使用信号量来完成:初始化一个初值为0的信号量,当前线程去获取这个信号量,而在另一个线程中完成操作后释放信号量,然后让当前线程继续执行下去。这种方法并不是很好的方法,为此,linux提供了completion接口,它是一种轻量级的机制,用于一个线程告诉另外一个线程某个工作已经完成 。

在Linux中,completion机制的常用函数有以下:

#include <linux/completion.h>

DECLARE_COMPLETION(my_completion); //创建一个completion
init_completion(struct completion *c); // 在运行中创建并初始化completion
void wait_for_completion(struct completion *c); // 等待completion,是不可中断的
void complete(struct completion *c); // 触发completion,使得上面的等待线程继续,只能唤醒一个等待线程
void complete_all(struct completion *c); // 触发completion,使得上面等待线程继续,唤醒所有等待线程

4. 自旋锁

信号量对于互斥来讲是一个非常有用的工具,但并不是唯一的工具。在许多情况下,更多的是使用自旋锁(spinlock)机制来实现互斥。

在信号量中,获取锁的等待会产生休眠,因此,不能用于不能休眠的代码中,比如中断线程。而自旋锁的获取不会产生休眠,因此可以用于不能休眠的代码中,且通常能够提供更高的性能。

自旋锁的工作过程可以简单理解为就是一直检测当前锁的状态,然后获取,不断循环,所以称为自旋。

Linux中自旋锁的常用函数如下:

#include <linux/spinlock.h>

spinlock_t my_lock = SPIN_LOCK_UNLOCKED; // 编译时初始化自旋锁
void spin_lock_init(spinlock_t *lock); //运行中初始化自旋锁
void spin_lock(spinlock_t *lock); //自旋等待获取锁,不可中断
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); // 获取自旋锁前禁止中断,并将中断状态保存到flags中
void spin_lock_irq(spinlock_t *lock); //获取自旋锁前禁止中断,不跟踪中断状态,释放自旋锁时需要自己保证启用中断
void spin_lock_bh(spinlock_t *lock); // 获取自旋锁前禁止软件中断,而硬件中断保存打开
void spin_unlock(spinlock_t *lock); //释放获取的锁,对应spin_lock
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); //释放获取的锁,对应spin_lock_irqsave
void spin_unlock_irq(spinlock_t *lock); //释放获取的锁,对应spin_lock_irq
void spin_unlock_bh(spinlock_t *lock); //释放获取的锁,对应spin_lock_bh

int spin_trylock(spinlock_t *lock); //不会等待,直接返回,0表示获取成功,非0表示获取失败
int spin_trylock_bh(spinlock_t *lock); //不会等待,直接返回,0表示成功,非0表示获取失败

在使用自旋锁时,也要注意以下问题:

  • 拥有自旋锁的代码必须是原子的,即不能休眠,不能被打断;
  • 拥有自旋锁时,需要禁止中断,防止中断中获取锁,造成死锁;
  • 拥有自旋锁的时间需要尽可能短,越短越好;

5. 原子变量

如果我们共享的变量是一个简单的整数值,用一个锁机制对于一个简单的整数值来说显得十分浪费。针对这种情况,Linux提供了一种原子整数类型,称为 atomic_t

atomic_t 中不能记录大于24位的整数。其常用的函数如下:

#include <asm/atomic.h>

void atomic_set(atomic_t *v, int i); //运行时初始化原子变量为i
atomic_t v = ATOMIC_INIT(0); //编译时初始化原子变量为0
int atomic_read(atomic_t *v); //返回原子变量的值
void atomic_add(int i, atomic_t *v); // 原子变量加上i
void atomic_sub(int i, atomic_t *v) // 原子变量减去i
void atomic_inc(atomic_t *v); // 原子变量自加1
void atomic_decatomic_t *v);

以上是常用的一些函数,还有一些结果检测、返回等操作其实都可以用上述的操作来完成。需要说明的是,原子变量只能通过指定的函数来访问,不能直接使用数学运算法进行赋值和运算。

以上是对并发和竞态的一些介绍,以及工作和学习中常用到的一些避免竞态的手段。当然,还有一些其他手段用于避免竞态的发生,我这边没有介绍,有兴趣的可以自己去了解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容